Skip to main content

bitwarden_crypto/enc_string/
symmetric.rs

1use std::{borrow::Cow, str::FromStr};
2
3use bitwarden_encoding::{B64, FromStrVisitor};
4use coset::{CborSerializable, iana::KeyOperation};
5use rand::RngExt;
6use serde::Deserialize;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
9
10use super::{check_length, from_b64, from_b64_vec, split_enc_string};
11use crate::{
12    Aes256CbcHmacKey, ContentFormat, CoseEncrypt0Bytes, KeyDecryptable, KeyEncryptable,
13    KeyEncryptableWithContentType, SymmetricCryptoKey, SymmetricKeyAlgorithm, Utf8Bytes,
14    XAes256GcmKey, XChaCha20Poly1305Key,
15    cose::{XAES_256_GCM, XCHACHA20_POLY1305, symmetric::CoseContentEncryptionAlgorithm},
16    error::{CryptoError, EncStringParseError, Result, UnsupportedOperationError},
17    hazmat::symmetric_encryption::Aes256CbcHmacSha256,
18    keys::KeyId,
19};
20
21#[cfg(feature = "wasm")]
22#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
23const TS_CUSTOM_TYPES: &'static str = r#"
24export type EncString = Tagged<string, "EncString">;
25"#;
26
27/// Numerical representation of each [EncString] variant, as used in both the string and the binary
28/// format.
29const AES256_CBC_TYPE: u8 = 0;
30const AES256_CBC_HMAC_TYPE: u8 = 2;
31const COSE_ENCRYPT0_TYPE: u8 = 7;
32
33/// # Encrypted string primitive
34///
35/// [EncString] is a Bitwarden specific primitive that represents a symmetrically encrypted piece of
36/// data, encoded as a string. They are are used together with the [KeyDecryptable] and
37/// [KeyEncryptable] traits to encrypt and decrypt data using [SymmetricCryptoKey]s.
38///
39/// The flexibility of the [EncString] type allows for different encryption algorithms to be used
40/// which is represented by the different variants of the enum.
41///
42/// ## Note
43///
44/// For backwards compatibility we will rarely if ever be able to remove support for decrypting old
45/// variants, but we should be opinionated in which variants are used for encrypting.
46///
47/// ## Variants
48/// - [Aes256Cbc_B64](EncString::Aes256Cbc_B64) - Deprecated and MUST NOT be used for encrypting as
49///   it is not authenticated
50/// - [Aes256Cbc_HmacSha256_B64](EncString::Aes256Cbc_HmacSha256_B64)
51/// - [Cose_Encrypt0_B64](EncString::Cose_Encrypt0_B64) - The preferred variant for encrypting data.
52///
53/// ## Serialization
54///
55/// [EncString] implements [ToString] and [FromStr] to allow for easy serialization and uses a
56/// custom scheme to represent the different variants.
57///
58/// The scheme is one of the following schemes:
59/// - `[type].[iv]|[data]`
60/// - `[type].[iv]|[data]|[mac]`
61/// - `[type].[cose_encrypt0_bytes]`
62///
63/// Where:
64/// - `[type]`: is a digit number representing the variant.
65/// - `[iv]`: (optional) is the initialization vector used for encryption.
66/// - `[data]`: is the encrypted data.
67/// - `[mac]`: (optional) is the MAC used to validate the integrity of the data.
68/// - `[cose_encrypt0_bytes]`: is the COSE Encrypt0 message, serialized to bytes
69#[allow(missing_docs)]
70#[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
71#[allow(unused, non_camel_case_types)]
72pub enum EncString {
73    /// 0
74    Aes256Cbc_B64 {
75        iv: [u8; 16],
76        data: Vec<u8>,
77    },
78    /// 1 was the now removed `AesCbc128_HmacSha256_B64`.
79    /// 2
80    Aes256Cbc_HmacSha256_B64 {
81        iv: [u8; 16],
82        mac: [u8; 32],
83        data: Vec<u8>,
84    },
85    // 7 The actual enc type is contained in the cose struct
86    Cose_Encrypt0_B64 {
87        data: Vec<u8>,
88    },
89    /// A string that matches no known [EncString] forma. This could be an unimplemented
90    /// variant, or a malformed variant such as an encstring with invalid length of iv/mac.
91    Unparseable {
92        raw: String,
93    },
94}
95
96#[cfg(feature = "wasm")]
97impl wasm_bindgen::describe::WasmDescribe for EncString {
98    fn describe() {
99        <String as wasm_bindgen::describe::WasmDescribe>::describe();
100    }
101}
102
103#[cfg(feature = "wasm")]
104impl FromWasmAbi for EncString {
105    type Abi = <String as FromWasmAbi>::Abi;
106
107    unsafe fn from_abi(abi: Self::Abi) -> Self {
108        use wasm_bindgen::UnwrapThrowExt;
109
110        let s = unsafe { String::from_abi(abi) };
111        Self::from_str(&s).unwrap_throw()
112    }
113}
114
115#[cfg(feature = "wasm")]
116impl OptionFromWasmAbi for EncString {
117    fn is_none(abi: &Self::Abi) -> bool {
118        <String as OptionFromWasmAbi>::is_none(abi)
119    }
120}
121
122#[cfg(feature = "wasm")]
123impl IntoWasmAbi for EncString {
124    type Abi = <String as IntoWasmAbi>::Abi;
125
126    fn into_abi(self) -> Self::Abi {
127        self.to_string().into_abi()
128    }
129}
130
131#[cfg(feature = "wasm")]
132impl TryFrom<wasm_bindgen::JsValue> for EncString {
133    type Error = CryptoError;
134
135    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
136        let string = value
137            .as_string()
138            .ok_or(EncStringParseError::NoType)
139            .map_err(CryptoError::from)?;
140        Self::from_str(&string)
141    }
142}
143
144/// Deserializes an [EncString] from a string.
145impl FromStr for EncString {
146    type Err = CryptoError;
147
148    /// Never returns an error: anything that does not match a known format becomes
149    /// [EncString::Unparseable]. Use [EncString::parse_strict] to reject those instead.
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        Ok(Self::parse_known_format(s)
152            .unwrap_or_else(|| EncString::Unparseable { raw: s.to_owned() }))
153    }
154}
155
156/// Infallible conversion, since parsing never fails: unknown formats become
157/// [EncString::Unparseable].
158impl From<&str> for EncString {
159    fn from(s: &str) -> Self {
160        Self::parse_known_format(s).unwrap_or_else(|| EncString::Unparseable { raw: s.to_owned() })
161    }
162}
163
164/// Infallible conversion, since parsing never fails: unknown formats become
165/// [EncString::Unparseable].
166impl From<String> for EncString {
167    fn from(s: String) -> Self {
168        Self::parse_known_format(&s).unwrap_or(EncString::Unparseable { raw: s })
169    }
170}
171
172impl EncString {
173    /// Parses one of the known [EncString] formats, or [None] if the string matches none of them.
174    fn parse_known_format(s: &str) -> Option<Self> {
175        let (enc_type, parts) = split_enc_string(s);
176        match (enc_type, parts.len()) {
177            ("0", 2) => Some(EncString::Aes256Cbc_B64 {
178                iv: from_b64(parts[0]).ok()?,
179                data: from_b64_vec(parts[1]).ok()?,
180            }),
181            ("2", 3) => Some(EncString::Aes256Cbc_HmacSha256_B64 {
182                iv: from_b64(parts[0]).ok()?,
183                data: from_b64_vec(parts[1]).ok()?,
184                mac: from_b64(parts[2]).ok()?,
185            }),
186            ("7", 1) => Some(EncString::Cose_Encrypt0_B64 {
187                data: from_b64_vec(parts[0]).ok()?,
188            }),
189            _ => None,
190        }
191    }
192
193    /// Parses an [EncString], rejecting invalid values
194    pub fn parse_strict(s: &str) -> Result<Self, CryptoError> {
195        Self::parse_known_format(s).ok_or(CryptoError::UnparseableEncString)
196    }
197
198    /// The id of the key this [EncString] was encrypted with, read from the COSE protected header.
199    /// [None] for the legacy types, which carry no key id, or if the header holds none.
200    pub fn key_id(&self) -> Option<KeyId> {
201        let EncString::Cose_Encrypt0_B64 { data } = self else {
202            return None;
203        };
204
205        let msg = coset::CoseEncrypt0::from_slice(data.as_slice()).ok()?;
206        KeyId::try_from(msg.protected.header.key_id.as_slice()).ok()
207    }
208
209    /// The algorithm of the key this [EncString] was encrypted with. [None] for the unauthenticated
210    /// legacy type, an [EncString::Unparseable], or a COSE message with an unknown algorithm.
211    pub fn algorithm(&self) -> Option<SymmetricKeyAlgorithm> {
212        let data = match self {
213            EncString::Aes256Cbc_HmacSha256_B64 { .. } => {
214                return Some(SymmetricKeyAlgorithm::Aes256CbcHmac);
215            }
216            EncString::Cose_Encrypt0_B64 { data } => data,
217            EncString::Aes256Cbc_B64 { .. } | EncString::Unparseable { .. } => return None,
218        };
219
220        let msg = coset::CoseEncrypt0::from_slice(data.as_slice()).ok()?;
221        let algorithm = msg.protected.header.alg.as_ref()?;
222        let algorithm = match CoseContentEncryptionAlgorithm::try_from(algorithm).ok()? {
223            CoseContentEncryptionAlgorithm::Aes256Gcm => SymmetricKeyAlgorithm::Aes256Gcm,
224            CoseContentEncryptionAlgorithm::XAes256Gcm => SymmetricKeyAlgorithm::XAes256Gcm,
225            CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
226                SymmetricKeyAlgorithm::XChaCha20Poly1305
227            }
228            CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256 => {
229                SymmetricKeyAlgorithm::Aes256CbcHmac
230            }
231        };
232        Some(algorithm)
233    }
234
235    /// Synthetic sugar for mapping `Option<String>` to `Result<Option<EncString>>`
236    pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
237        s.map(|s| s.parse()).transpose()
238    }
239
240    /// Parses the binary [EncString] representation. Unlike [FromStr], this stays strict and
241    /// errors on malformed input
242    pub fn from_buffer(buf: &[u8]) -> Result<Self> {
243        if buf.is_empty() {
244            return Err(EncStringParseError::NoType.into());
245        }
246        let enc_type = buf[0];
247
248        match enc_type {
249            AES256_CBC_TYPE => {
250                check_length(buf, 18)?;
251                let iv = buf[1..17].try_into().expect("Valid length");
252                let data = buf[17..].to_vec();
253
254                Ok(EncString::Aes256Cbc_B64 { iv, data })
255            }
256            AES256_CBC_HMAC_TYPE => {
257                check_length(buf, 50)?;
258                let iv = buf[1..17].try_into().expect("Valid length");
259                let mac = buf[17..49].try_into().expect("Valid length");
260                let data = buf[49..].to_vec();
261
262                Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
263            }
264            COSE_ENCRYPT0_TYPE => Ok(EncString::Cose_Encrypt0_B64 {
265                data: buf[1..].to_vec(),
266            }),
267            _ => Err(EncStringParseError::InvalidTypeSymm {
268                enc_type: enc_type.to_string(),
269                parts: 1,
270            }
271            .into()),
272        }
273    }
274
275    #[allow(missing_docs)]
276    pub fn to_buffer(&self) -> Result<Vec<u8>> {
277        let mut buf;
278        let enc_type = self.enc_type().ok_or(CryptoError::UnparseableEncString)?;
279
280        match self {
281            EncString::Aes256Cbc_B64 { iv, data } => {
282                buf = Vec::with_capacity(1 + 16 + data.len());
283                buf.push(enc_type);
284                buf.extend_from_slice(iv);
285                buf.extend_from_slice(data);
286            }
287            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
288                buf = Vec::with_capacity(1 + 16 + 32 + data.len());
289                buf.push(enc_type);
290                buf.extend_from_slice(iv);
291                buf.extend_from_slice(mac);
292                buf.extend_from_slice(data);
293            }
294            EncString::Cose_Encrypt0_B64 { data } => {
295                buf = Vec::with_capacity(1 + data.len());
296                buf.push(enc_type);
297                buf.extend_from_slice(data);
298            }
299            EncString::Unparseable { .. } => return Err(CryptoError::UnparseableEncString),
300        }
301
302        Ok(buf)
303    }
304}
305
306// `Display` is not implemented here because printing for debug purposes should be different
307// from serializing to a string. For Aes256_Cbc, or Aes256_Cbc_Hmac, `ToString` and `Debug`
308// are the same. For `Cose_Encrypt0`, `Debug` will print the decoded COSE message, while
309// `ToString` will print the Cose_Encrypt0 bytes, encoded in base64.
310#[allow(clippy::to_string_trait_impl)]
311impl ToString for EncString {
312    fn to_string(&self) -> String {
313        fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
314            let encoded_parts: Vec<String> = parts
315                .iter()
316                .map(|part| B64::from(*part).to_string())
317                .collect();
318            format!("{}.{}", enc_type, encoded_parts.join("|"))
319        }
320
321        match &self {
322            EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(AES256_CBC_TYPE, &[iv, data]),
323            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
324                fmt_parts(AES256_CBC_HMAC_TYPE, &[iv, data, mac])
325            }
326            EncString::Cose_Encrypt0_B64 { data } => fmt_parts(COSE_ENCRYPT0_TYPE, &[data]),
327            EncString::Unparseable { raw } => raw.clone(),
328        }
329    }
330}
331
332impl std::fmt::Debug for EncString {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        match self {
335            EncString::Aes256Cbc_B64 { iv, data } => {
336                let mut debug_struct = f.debug_struct("EncString::Aes256Cbc");
337                #[cfg(feature = "dangerous-crypto-debug")]
338                {
339                    debug_struct.field("iv", &hex::encode(iv));
340                    debug_struct.field("data", &hex::encode(data));
341                }
342                #[cfg(not(feature = "dangerous-crypto-debug"))]
343                {
344                    _ = iv;
345                    _ = data;
346                }
347                debug_struct.finish()
348            }
349            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
350                let mut debug_struct = f.debug_struct("EncString::Aes256CbcHmacSha256");
351                #[cfg(feature = "dangerous-crypto-debug")]
352                {
353                    debug_struct.field("iv", &hex::encode(iv));
354                    debug_struct.field("data", &hex::encode(data));
355                    debug_struct.field("mac", &hex::encode(mac));
356                }
357                #[cfg(not(feature = "dangerous-crypto-debug"))]
358                {
359                    _ = iv;
360                    _ = data;
361                    _ = mac;
362                }
363                debug_struct.finish()
364            }
365            EncString::Unparseable { raw } => {
366                let mut debug_struct = f.debug_struct("EncString::Unparseable");
367                #[cfg(feature = "dangerous-crypto-debug")]
368                debug_struct.field("raw", raw);
369                #[cfg(not(feature = "dangerous-crypto-debug"))]
370                {
371                    _ = raw;
372                }
373                debug_struct.finish()
374            }
375            EncString::Cose_Encrypt0_B64 { data } => {
376                let mut debug_struct = f.debug_struct("EncString::CoseEncrypt0");
377
378                match coset::CoseEncrypt0::from_slice(data.as_slice()) {
379                    Ok(msg) => {
380                        if let Some(ref alg) = msg.protected.header.alg {
381                            let alg_name = match alg {
382                                coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
383                                    "XChaCha20-Poly1305"
384                                }
385                                coset::Algorithm::PrivateUse(XAES_256_GCM) => "XAES-256-GCM",
386                                other => return debug_struct.field("algorithm", other).finish(),
387                            };
388                            debug_struct.field("algorithm", &alg_name);
389                        }
390
391                        let key_id = &msg.protected.header.key_id;
392                        if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
393                            debug_struct.field("key_id", &key_id);
394                        }
395                        debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
396                        if let Some(ref content_type) = msg.protected.header.content_type {
397                            debug_struct.field("content_type", content_type);
398                        }
399
400                        #[cfg(feature = "dangerous-crypto-debug")]
401                        if let Some(ref ciphertext) = msg.ciphertext {
402                            debug_struct.field("ciphertext", &hex::encode(ciphertext));
403                        }
404                    }
405                    Err(_) => {
406                        debug_struct.field("error", &"INVALID_COSE");
407                    }
408                }
409
410                debug_struct.finish()
411            }
412        }
413    }
414}
415
416impl<'de> Deserialize<'de> for EncString {
417    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
418    where
419        D: serde::Deserializer<'de>,
420    {
421        deserializer.deserialize_str(FromStrVisitor::new())
422    }
423}
424
425impl serde::Serialize for EncString {
426    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
427    where
428        S: serde::Serializer,
429    {
430        serializer.serialize_str(&self.to_string())
431    }
432}
433
434impl EncString {
435    pub(crate) fn encrypt_aes256_hmac(
436        data_dec: &[u8],
437        key: &Aes256CbcHmacKey,
438    ) -> Result<EncString> {
439        let mut iv = [0u8; 16];
440        bitwarden_random::rng().fill(&mut iv);
441        let (mac, data) = Aes256CbcHmacSha256::encrypt(&iv, data_dec, key.as_composite_key());
442        Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
443    }
444
445    pub(crate) fn encrypt_xchacha20_poly1305(
446        data_dec: &[u8],
447        key: &XChaCha20Poly1305Key,
448        content_format: ContentFormat,
449    ) -> Result<EncString> {
450        let data =
451            crate::cose::symmetric::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
452        Ok(EncString::Cose_Encrypt0_B64 {
453            data: data.to_vec(),
454        })
455    }
456
457    pub(crate) fn encrypt_xaes256_gcm(
458        data_dec: &[u8],
459        key: &XAes256GcmKey,
460        content_format: ContentFormat,
461    ) -> Result<EncString> {
462        let data = crate::cose::symmetric::encrypt_xaes256_gcm(data_dec, key, content_format)?;
463        Ok(EncString::Cose_Encrypt0_B64 {
464            data: data.to_vec(),
465        })
466    }
467
468    /// The numerical representation of the encryption type of the [EncString], or [None] for an
469    /// [EncString::Unparseable], which has no known type.
470    const fn enc_type(&self) -> Option<u8> {
471        match self {
472            EncString::Aes256Cbc_B64 { .. } => Some(AES256_CBC_TYPE),
473            EncString::Aes256Cbc_HmacSha256_B64 { .. } => Some(AES256_CBC_HMAC_TYPE),
474            EncString::Cose_Encrypt0_B64 { .. } => Some(COSE_ENCRYPT0_TYPE),
475            EncString::Unparseable { .. } => None,
476        }
477    }
478}
479
480impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
481    fn encrypt_with_key(
482        self,
483        key: &SymmetricCryptoKey,
484        content_format: ContentFormat,
485    ) -> Result<EncString> {
486        match key {
487            SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
488            SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
489                if !inner_key
490                    .supported_operations
491                    .contains(&KeyOperation::Encrypt)
492                {
493                    return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
494                }
495                EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
496            }
497            SymmetricCryptoKey::XAes256GcmKey(key) => {
498                if !key.supported_operations.contains(&KeyOperation::Encrypt) {
499                    return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
500                }
501                EncString::encrypt_xaes256_gcm(self, key, content_format)
502            }
503            SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
504                UnsupportedOperationError::EncryptionNotImplementedForKey,
505            )),
506            SymmetricCryptoKey::Aes256GcmKey(_) => Err(CryptoError::OperationNotSupported(
507                UnsupportedOperationError::EncryptionNotImplementedForKey,
508            )),
509        }
510    }
511}
512
513impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
514    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
515        match (self, key) {
516            (EncString::Aes256Cbc_B64 { .. }, SymmetricCryptoKey::Aes256CbcKey(_)) => {
517                Err(CryptoError::OperationNotSupported(
518                    UnsupportedOperationError::DecryptionNotImplementedForKey,
519                ))
520            }
521            (
522                EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
523                SymmetricCryptoKey::Aes256CbcHmacKey(key),
524            ) => Aes256CbcHmacSha256::decrypt(iv, data, mac, key.as_composite_key())
525                .map_err(|_| CryptoError::Decrypt),
526            (
527                EncString::Cose_Encrypt0_B64 { data },
528                SymmetricCryptoKey::XChaCha20Poly1305Key(key),
529            ) => {
530                let (decrypted_message, _) = crate::cose::symmetric::decrypt_xchacha20_poly1305(
531                    &CoseEncrypt0Bytes::from(data.as_slice()),
532                    key,
533                )?;
534                Ok(decrypted_message)
535            }
536            (EncString::Cose_Encrypt0_B64 { data }, SymmetricCryptoKey::XAes256GcmKey(key)) => {
537                let (decrypted, _) = crate::cose::symmetric::decrypt_xaes256_gcm(
538                    &CoseEncrypt0Bytes::from(data.as_slice()),
539                    key,
540                )?;
541                Ok(decrypted)
542            }
543            (EncString::Unparseable { .. }, _) => Err(CryptoError::UnparseableEncString),
544            (_, SymmetricCryptoKey::XAes256GcmKey(_)) => Err(CryptoError::WrongKeyType),
545            _ => Err(CryptoError::WrongKeyType),
546        }
547    }
548}
549
550impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
551    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
552        Utf8Bytes::from(self).encrypt_with_key(key)
553    }
554}
555
556impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
557    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
558        Utf8Bytes::from(self).encrypt_with_key(key)
559    }
560}
561
562impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
563    #[bitwarden_logging::instrument(err)]
564    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
565        let dec: Vec<u8> = self.decrypt_with_key(key)?;
566        String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
567    }
568}
569
570/// Usually we wouldn't want to expose EncStrings in the API or the schemas.
571/// But during the transition phase we will expose endpoints using the EncString type.
572impl schemars::JsonSchema for EncString {
573    fn schema_name() -> Cow<'static, str> {
574        "EncString".into()
575    }
576
577    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
578        generator.subschema_for::<String>()
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use coset::iana::KeyOperation;
585    use schemars::schema_for;
586
587    use super::EncString;
588    use crate::{
589        CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
590        SymmetricKeyAlgorithm, derive_symmetric_key,
591    };
592
593    fn xaes_key(operations: Vec<KeyOperation>) -> SymmetricCryptoKey {
594        SymmetricCryptoKey::XAes256GcmKey(crate::XAes256GcmKey {
595            key_id: [0u8; KEY_ID_SIZE].into(),
596            enc_key: Box::pin([0u8; 32].into()),
597            supported_operations: operations,
598        })
599    }
600
601    fn encrypt_with_xaes(plaintext: &str) -> EncString {
602        plaintext
603            .to_owned()
604            .encrypt_with_key(&xaes_key(vec![
605                coset::iana::KeyOperation::Decrypt,
606                coset::iana::KeyOperation::Encrypt,
607                coset::iana::KeyOperation::WrapKey,
608                coset::iana::KeyOperation::UnwrapKey,
609            ]))
610            .expect("encryption works")
611    }
612
613    fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
614        let key_id = [0u8; KEY_ID_SIZE];
615        let enc_key = [0u8; 32];
616        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
617            key_id: key_id.into(),
618            enc_key: Box::pin(enc_key.into()),
619            supported_operations: vec![
620                coset::iana::KeyOperation::Decrypt,
621                coset::iana::KeyOperation::Encrypt,
622                coset::iana::KeyOperation::WrapKey,
623                coset::iana::KeyOperation::UnwrapKey,
624            ],
625        });
626
627        plaintext.encrypt_with_key(&key).expect("encryption works")
628    }
629
630    #[test]
631    fn test_key_id_of_cose_enc_string() {
632        let enc_string = encrypt_with_xaes("Test key id");
633        assert_eq!(enc_string.key_id(), Some([0u8; KEY_ID_SIZE].into()));
634    }
635
636    #[test]
637    fn test_algorithm_of_enc_string() {
638        let v1 = EncString::encrypt_aes256_hmac(b"Test", &derive_symmetric_key("test")).unwrap();
639        assert_eq!(v1.algorithm(), Some(SymmetricKeyAlgorithm::Aes256CbcHmac));
640        assert_eq!(
641            encrypt_with_xaes("Test").algorithm(),
642            Some(SymmetricKeyAlgorithm::XAes256Gcm)
643        );
644        assert_eq!(
645            encrypt_with_xchacha20("Test").algorithm(),
646            Some(SymmetricKeyAlgorithm::XChaCha20Poly1305)
647        );
648    }
649
650    #[test]
651    fn test_key_id_of_legacy_enc_string_is_none() {
652        let enc_string =
653            EncString::encrypt_aes256_hmac(b"Test key id", &derive_symmetric_key("test")).unwrap();
654        assert_eq!(enc_string.key_id(), None);
655    }
656
657    #[test]
658    #[ignore = "Manual test to verify debug format"]
659    fn test_debug() {
660        let enc_string = encrypt_with_xchacha20("Test debug string");
661        println!("{:?}", enc_string);
662        let enc_string_aes =
663            EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
664                .unwrap();
665        println!("{:?}", enc_string_aes);
666    }
667
668    /// XChaCha20Poly1305 encstrings should be padded in blocks of 32 bytes. This ensures that the
669    /// encstring length does not reveal more than the 32-byte range of lengths that the contained
670    /// string falls into.
671    #[test]
672    fn test_xchacha20_encstring_string_padding_block_sizes() {
673        let cases = [
674            ("", 32),              // empty string, padded to 32
675            (&"a".repeat(31), 32), // largest in first block
676            (&"a".repeat(32), 64), // smallest in second block
677            (&"a".repeat(63), 64), // largest in second block
678            (&"a".repeat(64), 96), // smallest in third block
679        ];
680
681        let ciphertext_lengths: Vec<_> = cases
682            .iter()
683            .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
684            .collect();
685
686        // Block 1: 0-31 (same length)
687        assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
688        // Block 2: 32-63 (same length, different from block 1)
689        assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
690        assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
691        // Block 3: 64+ (different from block 2)
692        assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
693    }
694
695    #[test]
696    fn test_enc_roundtrip_xchacha20() {
697        let key_id = [0u8; KEY_ID_SIZE];
698        let enc_key = [0u8; 32];
699        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
700            key_id: key_id.into(),
701            enc_key: Box::pin(enc_key.into()),
702            supported_operations: vec![
703                coset::iana::KeyOperation::Decrypt,
704                coset::iana::KeyOperation::Encrypt,
705                coset::iana::KeyOperation::WrapKey,
706                coset::iana::KeyOperation::UnwrapKey,
707            ],
708        });
709
710        let test_string = "encrypted_test_string";
711        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
712        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
713        assert_eq!(decrypted_str, test_string);
714    }
715
716    #[test]
717    fn test_xaes_encstring_string_roundtrips() {
718        let key = xaes_key(vec![
719            coset::iana::KeyOperation::Decrypt,
720            coset::iana::KeyOperation::Encrypt,
721            coset::iana::KeyOperation::WrapKey,
722            coset::iana::KeyOperation::UnwrapKey,
723        ]);
724        for plaintext in ["", "encrypted_test_string"] {
725            let encrypted = plaintext.to_owned().encrypt_with_key(&key).unwrap();
726            let decrypted: String = encrypted.decrypt_with_key(&key).unwrap();
727            assert_eq!(decrypted, plaintext);
728        }
729    }
730
731    #[test]
732    fn test_xaes_encstring_string_padding_block_sizes() {
733        // xaes-256-gcm encstrings should be padded into blocks of size 32 bytes.
734        // This test checks that the expected padding happens
735        // Input plaintext size => Expected plaintext size
736        // 0 => 32
737        // 31 => 32
738        // 32 => 64
739        // 63 => 64
740        // 64 => 96
741        let lengths = [0, 31, 32, 63, 64]
742            .map(|length| encrypt_with_xaes(&"a".repeat(length)).to_string().len());
743
744        assert_eq!(lengths[0], lengths[1]); // 32 == 32
745        assert_ne!(lengths[1], lengths[2]); // 32 != 64
746        assert_eq!(lengths[2], lengths[3]); // 64 == 64
747        assert_ne!(lengths[3], lengths[4]); // 64 != 96
748    }
749
750    #[test]
751    fn test_xaes_encryption_requires_encrypt_operation() {
752        assert!(matches!(
753            "plaintext"
754                .to_owned()
755                .encrypt_with_key(&xaes_key(vec![KeyOperation::Decrypt])),
756            Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
757        ));
758    }
759
760    #[test]
761    fn test_xaes_rejects_unsupported_encstring_variant() {
762        let encrypted =
763            EncString::encrypt_aes256_hmac(b"plaintext", &derive_symmetric_key("wrapping key"))
764                .unwrap();
765        let result: Result<Vec<u8>, CryptoError> =
766            encrypted.decrypt_with_key(&xaes_key(vec![KeyOperation::Encrypt]));
767        assert!(matches!(result, Err(CryptoError::WrongKeyType)));
768    }
769
770    #[test]
771    fn test_xaes_encstring_debug_is_readable() {
772        let debug = format!("{:?}", encrypt_with_xaes("plaintext"));
773        assert!(debug.contains("EncString::CoseEncrypt0"));
774        assert!(debug.contains("XAES-256-GCM"));
775        assert!(debug.contains("KeyId(00000000000000000000000000000000)"));
776        assert!(debug.contains("nonce"));
777        assert!(debug.contains("content_type"));
778    }
779
780    #[test]
781    fn test_enc_string_roundtrip() {
782        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
783
784        let test_string = "encrypted_test_string";
785        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
786
787        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
788        assert_eq!(decrypted_str, test_string);
789    }
790
791    #[test]
792    fn test_enc_roundtrip_xchacha20_empty() {
793        let key_id = [0u8; KEY_ID_SIZE];
794        let enc_key = [0u8; 32];
795        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
796            key_id: key_id.into(),
797            enc_key: Box::pin(enc_key.into()),
798            supported_operations: vec![
799                coset::iana::KeyOperation::Decrypt,
800                coset::iana::KeyOperation::Encrypt,
801                coset::iana::KeyOperation::WrapKey,
802                coset::iana::KeyOperation::UnwrapKey,
803            ],
804        });
805
806        let test_string = "";
807        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
808        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
809        assert_eq!(decrypted_str, test_string);
810    }
811
812    #[test]
813    fn test_enc_string_roundtrip_empty() {
814        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
815
816        let test_string = "";
817        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
818
819        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
820        assert_eq!(decrypted_str, test_string);
821    }
822
823    #[test]
824    fn test_enc_string_ref_roundtrip() {
825        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
826
827        let test_string: &'static str = "encrypted_test_string";
828        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
829
830        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
831        assert_eq!(decrypted_str, test_string);
832    }
833
834    #[test]
835    fn test_enc_string_serialization() {
836        #[derive(serde::Serialize, serde::Deserialize)]
837        struct Test {
838            key: EncString,
839        }
840
841        let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
842        let serialized = format!("{{\"key\":\"{cipher}\"}}");
843
844        let t = serde_json::from_str::<Test>(&serialized).unwrap();
845        assert_eq!(t.key.enc_type(), Some(2));
846        assert_eq!(t.key.to_string(), cipher);
847        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
848    }
849
850    #[test]
851    fn test_enc_from_to_buffer() {
852        let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
853        let enc_string: EncString = enc_str.parse().unwrap();
854
855        let enc_buf = enc_string.to_buffer().unwrap();
856
857        assert_eq!(
858            enc_buf,
859            vec![
860                2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
861                163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
862                220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
863                135, 233, 150, 136, 221, 71, 140, 125, 141, 215
864            ]
865        );
866
867        let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
868
869        assert_eq!(enc_string_new.to_string(), enc_str)
870    }
871
872    #[test]
873    fn test_from_str_cbc256() {
874        let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
875        let enc_string: EncString = enc_str.parse().unwrap();
876
877        assert_eq!(enc_string.enc_type(), Some(0));
878        if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
879            assert_eq!(
880                iv,
881                &[
882                    164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
883                ]
884            );
885            assert_eq!(
886                data,
887                &[
888                    93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
889                ]
890            );
891        } else {
892            panic!("Invalid variant")
893        };
894    }
895
896    #[test]
897    fn test_decrypt_fails_for_cbc256_keys() {
898        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
899        let key = SymmetricCryptoKey::try_from(key).unwrap();
900
901        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
902        let enc_string: EncString = enc_str.parse().unwrap();
903        assert_eq!(enc_string.enc_type(), Some(0));
904
905        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
906        assert!(
907            matches!(
908                result,
909                Err(CryptoError::OperationNotSupported(
910                    crate::error::UnsupportedOperationError::DecryptionNotImplementedForKey
911                )),
912            ),
913            "Expected decrypt to fail when using deprecated type 0 key",
914        );
915    }
916
917    #[test]
918    fn test_decrypt_downgrade_encstring_prevention() {
919        // Simulate a potential downgrade attack by removing the mac portion of the `EncString` and
920        // attempt to decrypt it using a `SymmetricCryptoKey` with a mac key.
921        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
922        let key = SymmetricCryptoKey::try_from(key).unwrap();
923
924        // A "downgraded" `EncString` from `EncString::Aes256Cbc_HmacSha256_B64` (2) to
925        // `EncString::Aes256Cbc_B64` (0), with the mac portion removed.
926        // <enc_string>
927        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
928        let enc_string: EncString = enc_str.parse().unwrap();
929        assert_eq!(enc_string.enc_type(), Some(0));
930
931        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
932        assert!(matches!(result, Err(CryptoError::WrongKeyType)));
933    }
934
935    #[test]
936    fn test_encrypt_fails_when_operation_not_allowed() {
937        // Key with only Decrypt allowed
938        let key_id = [0u8; KEY_ID_SIZE];
939        let enc_key = [0u8; 32];
940        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
941            key_id: key_id.into(),
942            enc_key: Box::pin(enc_key.into()),
943            supported_operations: vec![KeyOperation::Decrypt],
944        });
945
946        let plaintext = "should fail";
947        let result = plaintext.encrypt_with_key(&key);
948        assert!(
949            matches!(
950                result,
951                Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
952            ),
953            "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
954        );
955    }
956
957    /// Malformed strings must not fail while parsing, so that the failure surfaces at decryption
958    /// time instead of at the FFI boundary. The raw value must round-trip unchanged.
959    #[test]
960    fn test_from_str_unparseable_roundtrips() {
961        let cases = [
962            "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", // 4 byte iv
963            "2.AAECAwQFBgcICQoLDA0ODw==|Y3Q=|AAECAw==",                     // 4 byte mac
964            "0.AAECAw==|Y3Q=",                                              // 4 byte iv
965            "2.!!!!|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",     // invalid base64
966            "8.ABC",                                                        // unknown type
967            "7.ABC|DEF",                                                    // wrong part count
968        ];
969
970        for enc_str in cases {
971            let enc_string: EncString = enc_str.parse().expect("parsing never fails");
972            assert!(
973                matches!(enc_string, EncString::Unparseable { .. }),
974                "Expected {enc_str} to parse as unparseable",
975            );
976            assert_eq!(enc_string.to_string(), enc_str);
977            assert_eq!(enc_string.enc_type(), None);
978        }
979    }
980
981    #[test]
982    fn test_from_string_and_str() {
983        let known = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
984        let unknown = "8.ABC";
985
986        assert_eq!(EncString::from(known).enc_type(), Some(2));
987        assert_eq!(EncString::from(known.to_owned()).enc_type(), Some(2));
988
989        assert!(matches!(
990            EncString::from(unknown),
991            EncString::Unparseable { .. }
992        ));
993        assert_eq!(EncString::from(unknown.to_owned()).to_string(), unknown);
994    }
995
996    #[test]
997    fn test_parse_strict_rejects_unparseable() {
998        let cases = [
999            "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", // 4 byte iv
1000            "2.AAECAwQFBgcICQoLDA0ODw==|Y3Q=|AAECAw==",                     // 4 byte mac
1001            "8.ABC",                                                        // unknown type
1002        ];
1003
1004        for enc_str in cases {
1005            assert!(
1006                matches!(
1007                    EncString::parse_strict(enc_str),
1008                    Err(CryptoError::UnparseableEncString)
1009                ),
1010                "Expected {enc_str} to be rejected",
1011            );
1012        }
1013    }
1014
1015    #[test]
1016    fn test_parse_strict_accepts_known_formats() {
1017        let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
1018        let enc_string = EncString::parse_strict(enc_str).unwrap();
1019
1020        assert_eq!(enc_string.enc_type(), Some(2));
1021        assert_eq!(enc_string.to_string(), enc_str);
1022    }
1023
1024    #[test]
1025    fn test_unparseable_serde_roundtrip() {
1026        #[derive(serde::Serialize, serde::Deserialize)]
1027        struct Test {
1028            key: EncString,
1029        }
1030
1031        let cipher = "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
1032        let serialized = format!("{{\"key\":\"{cipher}\"}}");
1033
1034        let t = serde_json::from_str::<Test>(&serialized).unwrap();
1035        assert!(matches!(t.key, EncString::Unparseable { .. }));
1036        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
1037    }
1038
1039    #[test]
1040    fn test_unparseable_decrypt_fails() {
1041        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
1042        let enc_string: EncString = "2.AAECAw==|Y3Q=|AAECAw==".parse().unwrap();
1043
1044        let result: Result<Vec<u8>, CryptoError> = enc_string.decrypt_with_key(&key);
1045        assert!(matches!(result, Err(CryptoError::UnparseableEncString)));
1046    }
1047
1048    #[test]
1049    fn test_unparseable_to_buffer_fails() {
1050        let enc_string: EncString = "2.AAECAw==|Y3Q=|AAECAw==".parse().unwrap();
1051
1052        assert!(matches!(
1053            enc_string.to_buffer(),
1054            Err(CryptoError::UnparseableEncString)
1055        ));
1056    }
1057
1058    #[test]
1059    fn test_from_buffer_stays_strict() {
1060        assert!(EncString::from_buffer(&[]).is_err());
1061        assert!(EncString::from_buffer(&[2, 0, 0]).is_err());
1062        assert!(EncString::from_buffer(&[8, 0, 0]).is_err());
1063    }
1064
1065    #[test]
1066    #[cfg(not(feature = "dangerous-crypto-debug"))]
1067    fn test_debug_format() {
1068        let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
1069        let enc_string: EncString = enc_str.parse().unwrap();
1070        assert_eq!(
1071            "EncString::Aes256CbcHmacSha256".to_string(),
1072            format!("{:?}", enc_string)
1073        );
1074    }
1075
1076    #[test]
1077    fn test_json_schema() {
1078        let schema = schema_for!(EncString);
1079
1080        assert_eq!(
1081            serde_json::to_string(&schema).unwrap(),
1082            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
1083        );
1084    }
1085}