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