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;
6use tracing::instrument;
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, Utf8Bytes, XChaCha20Poly1305Key,
14    cose::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                                other => return debug_struct.field("algorithm", other).finish(),
304                            };
305                            debug_struct.field("algorithm", &alg_name);
306                        }
307
308                        let key_id = &msg.protected.header.key_id;
309                        if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
310                            debug_struct.field("key_id", &key_id);
311                        }
312                        debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
313                        if let Some(ref content_type) = msg.protected.header.content_type {
314                            debug_struct.field("content_type", content_type);
315                        }
316
317                        #[cfg(feature = "dangerous-crypto-debug")]
318                        if let Some(ref ciphertext) = msg.ciphertext {
319                            debug_struct.field("ciphertext", &hex::encode(ciphertext));
320                        }
321                    }
322                    Err(_) => {
323                        debug_struct.field("error", &"INVALID_COSE");
324                    }
325                }
326
327                debug_struct.finish()
328            }
329        }
330    }
331}
332
333impl<'de> Deserialize<'de> for EncString {
334    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
335    where
336        D: serde::Deserializer<'de>,
337    {
338        deserializer.deserialize_str(FromStrVisitor::new())
339    }
340}
341
342impl serde::Serialize for EncString {
343    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
344    where
345        S: serde::Serializer,
346    {
347        serializer.serialize_str(&self.to_string())
348    }
349}
350
351impl EncString {
352    pub(crate) fn encrypt_aes256_hmac(
353        data_dec: &[u8],
354        key: &Aes256CbcHmacKey,
355    ) -> Result<EncString> {
356        let (iv, mac, data) =
357            crate::aes::encrypt_aes256_hmac(data_dec, &key.mac_key, &key.enc_key)?;
358        Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
359    }
360
361    pub(crate) fn encrypt_xchacha20_poly1305(
362        data_dec: &[u8],
363        key: &XChaCha20Poly1305Key,
364        content_format: ContentFormat,
365    ) -> Result<EncString> {
366        let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
367        Ok(EncString::Cose_Encrypt0_B64 {
368            data: data.to_vec(),
369        })
370    }
371
372    /// The numerical representation of the encryption type of the [EncString].
373    const fn enc_type(&self) -> u8 {
374        match self {
375            EncString::Aes256Cbc_B64 { .. } => 0,
376            EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
377            EncString::Cose_Encrypt0_B64 { .. } => 7,
378        }
379    }
380}
381
382impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
383    fn encrypt_with_key(
384        self,
385        key: &SymmetricCryptoKey,
386        content_format: ContentFormat,
387    ) -> Result<EncString> {
388        match key {
389            SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
390            SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
391                if !inner_key
392                    .supported_operations
393                    .contains(&KeyOperation::Encrypt)
394                {
395                    return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
396                }
397                EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
398            }
399            SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
400                UnsupportedOperationError::EncryptionNotImplementedForKey,
401            )),
402        }
403    }
404}
405
406impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
407    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
408        match (self, key) {
409            (EncString::Aes256Cbc_B64 { .. }, SymmetricCryptoKey::Aes256CbcKey(_)) => {
410                Err(CryptoError::OperationNotSupported(
411                    UnsupportedOperationError::DecryptionNotImplementedForKey,
412                ))
413            }
414            (
415                EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
416                SymmetricCryptoKey::Aes256CbcHmacKey(key),
417            ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key)
418                .map_err(|_| CryptoError::Decrypt),
419            (
420                EncString::Cose_Encrypt0_B64 { data },
421                SymmetricCryptoKey::XChaCha20Poly1305Key(key),
422            ) => {
423                let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
424                    &CoseEncrypt0Bytes::from(data.as_slice()),
425                    key,
426                )?;
427                Ok(decrypted_message)
428            }
429            _ => Err(CryptoError::WrongKeyType),
430        }
431    }
432}
433
434impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
435    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
436        Utf8Bytes::from(self).encrypt_with_key(key)
437    }
438}
439
440impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
441    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
442        Utf8Bytes::from(self).encrypt_with_key(key)
443    }
444}
445
446impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
447    #[instrument(err, skip_all)]
448    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
449        let dec: Vec<u8> = self.decrypt_with_key(key)?;
450        String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
451    }
452}
453
454/// Usually we wouldn't want to expose EncStrings in the API or the schemas.
455/// But during the transition phase we will expose endpoints using the EncString type.
456impl schemars::JsonSchema for EncString {
457    fn schema_name() -> Cow<'static, str> {
458        "EncString".into()
459    }
460
461    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
462        generator.subschema_for::<String>()
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use coset::iana::KeyOperation;
469    use schemars::schema_for;
470
471    use super::EncString;
472    use crate::{
473        CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
474        derive_symmetric_key,
475    };
476
477    fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
478        let key_id = [0u8; KEY_ID_SIZE];
479        let enc_key = [0u8; 32];
480        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
481            key_id: key_id.into(),
482            enc_key: Box::pin(enc_key.into()),
483            supported_operations: vec![
484                coset::iana::KeyOperation::Decrypt,
485                coset::iana::KeyOperation::Encrypt,
486                coset::iana::KeyOperation::WrapKey,
487                coset::iana::KeyOperation::UnwrapKey,
488            ],
489        });
490
491        plaintext.encrypt_with_key(&key).expect("encryption works")
492    }
493
494    #[test]
495    #[ignore = "Manual test to verify debug format"]
496    fn test_debug() {
497        let enc_string = encrypt_with_xchacha20("Test debug string");
498        println!("{:?}", enc_string);
499        let enc_string_aes =
500            EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
501                .unwrap();
502        println!("{:?}", enc_string_aes);
503    }
504
505    /// XChaCha20Poly1305 encstrings should be padded in blocks of 32 bytes. This ensures that the
506    /// encstring length does not reveal more than the 32-byte range of lengths that the contained
507    /// string falls into.
508    #[test]
509    fn test_xchacha20_encstring_string_padding_block_sizes() {
510        let cases = [
511            ("", 32),              // empty string, padded to 32
512            (&"a".repeat(31), 32), // largest in first block
513            (&"a".repeat(32), 64), // smallest in second block
514            (&"a".repeat(63), 64), // largest in second block
515            (&"a".repeat(64), 96), // smallest in third block
516        ];
517
518        let ciphertext_lengths: Vec<_> = cases
519            .iter()
520            .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
521            .collect();
522
523        // Block 1: 0-31 (same length)
524        assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
525        // Block 2: 32-63 (same length, different from block 1)
526        assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
527        assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
528        // Block 3: 64+ (different from block 2)
529        assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
530    }
531
532    #[test]
533    fn test_enc_roundtrip_xchacha20() {
534        let key_id = [0u8; KEY_ID_SIZE];
535        let enc_key = [0u8; 32];
536        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
537            key_id: key_id.into(),
538            enc_key: Box::pin(enc_key.into()),
539            supported_operations: vec![
540                coset::iana::KeyOperation::Decrypt,
541                coset::iana::KeyOperation::Encrypt,
542                coset::iana::KeyOperation::WrapKey,
543                coset::iana::KeyOperation::UnwrapKey,
544            ],
545        });
546
547        let test_string = "encrypted_test_string";
548        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
549        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
550        assert_eq!(decrypted_str, test_string);
551    }
552
553    #[test]
554    fn test_enc_string_roundtrip() {
555        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
556
557        let test_string = "encrypted_test_string";
558        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
559
560        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
561        assert_eq!(decrypted_str, test_string);
562    }
563
564    #[test]
565    fn test_enc_roundtrip_xchacha20_empty() {
566        let key_id = [0u8; KEY_ID_SIZE];
567        let enc_key = [0u8; 32];
568        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
569            key_id: key_id.into(),
570            enc_key: Box::pin(enc_key.into()),
571            supported_operations: vec![
572                coset::iana::KeyOperation::Decrypt,
573                coset::iana::KeyOperation::Encrypt,
574                coset::iana::KeyOperation::WrapKey,
575                coset::iana::KeyOperation::UnwrapKey,
576            ],
577        });
578
579        let test_string = "";
580        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
581        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
582        assert_eq!(decrypted_str, test_string);
583    }
584
585    #[test]
586    fn test_enc_string_roundtrip_empty() {
587        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
588
589        let test_string = "";
590        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
591
592        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
593        assert_eq!(decrypted_str, test_string);
594    }
595
596    #[test]
597    fn test_enc_string_ref_roundtrip() {
598        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
599
600        let test_string: &'static str = "encrypted_test_string";
601        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
602
603        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
604        assert_eq!(decrypted_str, test_string);
605    }
606
607    #[test]
608    fn test_enc_string_serialization() {
609        #[derive(serde::Serialize, serde::Deserialize)]
610        struct Test {
611            key: EncString,
612        }
613
614        let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
615        let serialized = format!("{{\"key\":\"{cipher}\"}}");
616
617        let t = serde_json::from_str::<Test>(&serialized).unwrap();
618        assert_eq!(t.key.enc_type(), 2);
619        assert_eq!(t.key.to_string(), cipher);
620        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
621    }
622
623    #[test]
624    fn test_enc_from_to_buffer() {
625        let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
626        let enc_string: EncString = enc_str.parse().unwrap();
627
628        let enc_buf = enc_string.to_buffer().unwrap();
629
630        assert_eq!(
631            enc_buf,
632            vec![
633                2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
634                163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
635                220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
636                135, 233, 150, 136, 221, 71, 140, 125, 141, 215
637            ]
638        );
639
640        let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
641
642        assert_eq!(enc_string_new.to_string(), enc_str)
643    }
644
645    #[test]
646    fn test_from_str_cbc256() {
647        let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
648        let enc_string: EncString = enc_str.parse().unwrap();
649
650        assert_eq!(enc_string.enc_type(), 0);
651        if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
652            assert_eq!(
653                iv,
654                &[
655                    164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
656                ]
657            );
658            assert_eq!(
659                data,
660                &[
661                    93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
662                ]
663            );
664        } else {
665            panic!("Invalid variant")
666        };
667    }
668
669    #[test]
670    fn test_decrypt_fails_for_cbc256_keys() {
671        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
672        let key = SymmetricCryptoKey::try_from(key).unwrap();
673
674        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
675        let enc_string: EncString = enc_str.parse().unwrap();
676        assert_eq!(enc_string.enc_type(), 0);
677
678        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
679        assert!(
680            matches!(
681                result,
682                Err(CryptoError::OperationNotSupported(
683                    crate::error::UnsupportedOperationError::DecryptionNotImplementedForKey
684                )),
685            ),
686            "Expected decrypt to fail when using deprecated type 0 key",
687        );
688    }
689
690    #[test]
691    fn test_decrypt_downgrade_encstring_prevention() {
692        // Simulate a potential downgrade attack by removing the mac portion of the `EncString` and
693        // attempt to decrypt it using a `SymmetricCryptoKey` with a mac key.
694        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
695        let key = SymmetricCryptoKey::try_from(key).unwrap();
696
697        // A "downgraded" `EncString` from `EncString::Aes256Cbc_HmacSha256_B64` (2) to
698        // `EncString::Aes256Cbc_B64` (0), with the mac portion removed.
699        // <enc_string>
700        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
701        let enc_string: EncString = enc_str.parse().unwrap();
702        assert_eq!(enc_string.enc_type(), 0);
703
704        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
705        assert!(matches!(result, Err(CryptoError::WrongKeyType)));
706    }
707
708    #[test]
709    fn test_encrypt_fails_when_operation_not_allowed() {
710        // Key with only Decrypt allowed
711        let key_id = [0u8; KEY_ID_SIZE];
712        let enc_key = [0u8; 32];
713        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
714            key_id: key_id.into(),
715            enc_key: Box::pin(enc_key.into()),
716            supported_operations: vec![KeyOperation::Decrypt],
717        });
718
719        let plaintext = "should fail";
720        let result = plaintext.encrypt_with_key(&key);
721        assert!(
722            matches!(
723                result,
724                Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
725            ),
726            "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
727        );
728    }
729
730    #[test]
731    fn test_from_str_invalid() {
732        let enc_str = "8.ABC";
733        let enc_string: Result<EncString, _> = enc_str.parse();
734
735        let err = enc_string.unwrap_err();
736        assert_eq!(
737            err.to_string(),
738            "EncString error, Invalid symmetric type, got type 8 with 1 parts"
739        );
740    }
741
742    #[test]
743    #[cfg(not(feature = "dangerous-crypto-debug"))]
744    fn test_debug_format() {
745        let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
746        let enc_string: EncString = enc_str.parse().unwrap();
747        assert_eq!(
748            "EncString::Aes256CbcHmacSha256".to_string(),
749            format!("{:?}", enc_string)
750        );
751    }
752
753    #[test]
754    fn test_json_schema() {
755        let schema = schema_for!(EncString);
756
757        assert_eq!(
758            serde_json::to_string(&schema).unwrap(),
759            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
760        );
761    }
762}