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;
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/// Deserializes an [EncString] from a string.
103impl FromStr for EncString {
104    type Err = CryptoError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        let (enc_type, parts) = split_enc_string(s);
108        match (enc_type, parts.len()) {
109            ("0", 2) => {
110                let iv = from_b64(parts[0])?;
111                let data = from_b64_vec(parts[1])?;
112
113                Ok(EncString::Aes256Cbc_B64 { iv, data })
114            }
115            ("2", 3) => {
116                let iv = from_b64(parts[0])?;
117                let data = from_b64_vec(parts[1])?;
118                let mac = from_b64(parts[2])?;
119
120                Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
121            }
122            ("7", 1) => {
123                let buffer = from_b64_vec(parts[0])?;
124
125                Ok(EncString::Cose_Encrypt0_B64 { data: buffer })
126            }
127            (enc_type, parts) => Err(EncStringParseError::InvalidTypeSymm {
128                enc_type: enc_type.to_string(),
129                parts,
130            }
131            .into()),
132        }
133    }
134}
135
136impl EncString {
137    /// Synthetic sugar for mapping `Option<String>` to `Result<Option<EncString>>`
138    pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
139        s.map(|s| s.parse()).transpose()
140    }
141
142    #[allow(missing_docs)]
143    pub fn from_buffer(buf: &[u8]) -> Result<Self> {
144        if buf.is_empty() {
145            return Err(EncStringParseError::NoType.into());
146        }
147        let enc_type = buf[0];
148
149        match enc_type {
150            0 => {
151                check_length(buf, 18)?;
152                let iv = buf[1..17].try_into().expect("Valid length");
153                let data = buf[17..].to_vec();
154
155                Ok(EncString::Aes256Cbc_B64 { iv, data })
156            }
157            2 => {
158                check_length(buf, 50)?;
159                let iv = buf[1..17].try_into().expect("Valid length");
160                let mac = buf[17..49].try_into().expect("Valid length");
161                let data = buf[49..].to_vec();
162
163                Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
164            }
165            7 => Ok(EncString::Cose_Encrypt0_B64 {
166                data: buf[1..].to_vec(),
167            }),
168            _ => Err(EncStringParseError::InvalidTypeSymm {
169                enc_type: enc_type.to_string(),
170                parts: 1,
171            }
172            .into()),
173        }
174    }
175
176    #[allow(missing_docs)]
177    pub fn to_buffer(&self) -> Result<Vec<u8>> {
178        let mut buf;
179
180        match self {
181            EncString::Aes256Cbc_B64 { iv, data } => {
182                buf = Vec::with_capacity(1 + 16 + data.len());
183                buf.push(self.enc_type());
184                buf.extend_from_slice(iv);
185                buf.extend_from_slice(data);
186            }
187            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
188                buf = Vec::with_capacity(1 + 16 + 32 + data.len());
189                buf.push(self.enc_type());
190                buf.extend_from_slice(iv);
191                buf.extend_from_slice(mac);
192                buf.extend_from_slice(data);
193            }
194            EncString::Cose_Encrypt0_B64 { data } => {
195                buf = Vec::with_capacity(1 + data.len());
196                buf.push(self.enc_type());
197                buf.extend_from_slice(data);
198            }
199        }
200
201        Ok(buf)
202    }
203}
204
205// `Display` is not implemented here because printing for debug purposes should be different
206// from serializing to a string. For Aes256_Cbc, or Aes256_Cbc_Hmac, `ToString` and `Debug`
207// are the same. For `Cose_Encrypt0`, `Debug` will print the decoded COSE message, while
208// `ToString` will print the Cose_Encrypt0 bytes, encoded in base64.
209#[allow(clippy::to_string_trait_impl)]
210impl ToString for EncString {
211    fn to_string(&self) -> String {
212        fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
213            let encoded_parts: Vec<String> = parts
214                .iter()
215                .map(|part| B64::from(*part).to_string())
216                .collect();
217            format!("{}.{}", enc_type, encoded_parts.join("|"))
218        }
219
220        let enc_type = self.enc_type();
221        match &self {
222            EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(enc_type, &[iv, data]),
223            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
224                fmt_parts(enc_type, &[iv, data, mac])
225            }
226            EncString::Cose_Encrypt0_B64 { data } => fmt_parts(enc_type, &[data]),
227        }
228    }
229}
230
231impl std::fmt::Debug for EncString {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        match self {
234            EncString::Aes256Cbc_B64 { iv, data } => {
235                let mut debug_struct = f.debug_struct("EncString::Aes256Cbc");
236                #[cfg(feature = "dangerous-crypto-debug")]
237                {
238                    debug_struct.field("iv", &hex::encode(iv));
239                    debug_struct.field("data", &hex::encode(data));
240                }
241                #[cfg(not(feature = "dangerous-crypto-debug"))]
242                {
243                    _ = iv;
244                    _ = data;
245                }
246                debug_struct.finish()
247            }
248            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
249                let mut debug_struct = f.debug_struct("EncString::Aes256CbcHmacSha256");
250                #[cfg(feature = "dangerous-crypto-debug")]
251                {
252                    debug_struct.field("iv", &hex::encode(iv));
253                    debug_struct.field("data", &hex::encode(data));
254                    debug_struct.field("mac", &hex::encode(mac));
255                }
256                #[cfg(not(feature = "dangerous-crypto-debug"))]
257                {
258                    _ = iv;
259                    _ = data;
260                    _ = mac;
261                }
262                debug_struct.finish()
263            }
264            EncString::Cose_Encrypt0_B64 { data } => {
265                let mut debug_struct = f.debug_struct("EncString::CoseEncrypt0");
266
267                match coset::CoseEncrypt0::from_slice(data.as_slice()) {
268                    Ok(msg) => {
269                        if let Some(ref alg) = msg.protected.header.alg {
270                            let alg_name = match alg {
271                                coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
272                                    "XChaCha20-Poly1305"
273                                }
274                                other => return debug_struct.field("algorithm", other).finish(),
275                            };
276                            debug_struct.field("algorithm", &alg_name);
277                        }
278
279                        let key_id = &msg.protected.header.key_id;
280                        if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
281                            debug_struct.field("key_id", &key_id);
282                        }
283                        debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
284                        if let Some(ref content_type) = msg.protected.header.content_type {
285                            debug_struct.field("content_type", content_type);
286                        }
287
288                        #[cfg(feature = "dangerous-crypto-debug")]
289                        if let Some(ref ciphertext) = msg.ciphertext {
290                            debug_struct.field("ciphertext", &hex::encode(ciphertext));
291                        }
292                    }
293                    Err(_) => {
294                        debug_struct.field("error", &"INVALID_COSE");
295                    }
296                }
297
298                debug_struct.finish()
299            }
300        }
301    }
302}
303
304impl<'de> Deserialize<'de> for EncString {
305    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
306    where
307        D: serde::Deserializer<'de>,
308    {
309        deserializer.deserialize_str(FromStrVisitor::new())
310    }
311}
312
313impl serde::Serialize for EncString {
314    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
315    where
316        S: serde::Serializer,
317    {
318        serializer.serialize_str(&self.to_string())
319    }
320}
321
322impl EncString {
323    pub(crate) fn encrypt_aes256_hmac(
324        data_dec: &[u8],
325        key: &Aes256CbcHmacKey,
326    ) -> Result<EncString> {
327        let (iv, mac, data) =
328            crate::aes::encrypt_aes256_hmac(data_dec, &key.mac_key, &key.enc_key)?;
329        Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
330    }
331
332    pub(crate) fn encrypt_xchacha20_poly1305(
333        data_dec: &[u8],
334        key: &XChaCha20Poly1305Key,
335        content_format: ContentFormat,
336    ) -> Result<EncString> {
337        let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
338        Ok(EncString::Cose_Encrypt0_B64 {
339            data: data.to_vec(),
340        })
341    }
342
343    /// The numerical representation of the encryption type of the [EncString].
344    const fn enc_type(&self) -> u8 {
345        match self {
346            EncString::Aes256Cbc_B64 { .. } => 0,
347            EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
348            EncString::Cose_Encrypt0_B64 { .. } => 7,
349        }
350    }
351}
352
353impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
354    fn encrypt_with_key(
355        self,
356        key: &SymmetricCryptoKey,
357        content_format: ContentFormat,
358    ) -> Result<EncString> {
359        match key {
360            SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
361            SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
362                if !inner_key
363                    .supported_operations
364                    .contains(&KeyOperation::Encrypt)
365                {
366                    return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
367                }
368                EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
369            }
370            SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
371                UnsupportedOperationError::EncryptionNotImplementedForKey,
372            )),
373        }
374    }
375}
376
377impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
378    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
379        match (self, key) {
380            (EncString::Aes256Cbc_B64 { iv, data }, SymmetricCryptoKey::Aes256CbcKey(key)) => {
381                crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)
382                    .map_err(|_| CryptoError::Decrypt)
383            }
384            (
385                EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
386                SymmetricCryptoKey::Aes256CbcHmacKey(key),
387            ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key)
388                .map_err(|_| CryptoError::Decrypt),
389            (
390                EncString::Cose_Encrypt0_B64 { data },
391                SymmetricCryptoKey::XChaCha20Poly1305Key(key),
392            ) => {
393                let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
394                    &CoseEncrypt0Bytes::from(data.as_slice()),
395                    key,
396                )?;
397                Ok(decrypted_message)
398            }
399            _ => Err(CryptoError::WrongKeyType),
400        }
401    }
402}
403
404impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
405    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
406        Utf8Bytes::from(self).encrypt_with_key(key)
407    }
408}
409
410impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
411    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
412        Utf8Bytes::from(self).encrypt_with_key(key)
413    }
414}
415
416impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
417    #[instrument(err, skip_all)]
418    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
419        let dec: Vec<u8> = self.decrypt_with_key(key)?;
420        String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
421    }
422}
423
424/// Usually we wouldn't want to expose EncStrings in the API or the schemas.
425/// But during the transition phase we will expose endpoints using the EncString type.
426impl schemars::JsonSchema for EncString {
427    fn schema_name() -> Cow<'static, str> {
428        "EncString".into()
429    }
430
431    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
432        generator.subschema_for::<String>()
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use coset::iana::KeyOperation;
439    use schemars::schema_for;
440
441    use super::EncString;
442    use crate::{
443        CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
444        derive_symmetric_key,
445    };
446
447    fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
448        let key_id = [0u8; KEY_ID_SIZE];
449        let enc_key = [0u8; 32];
450        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
451            key_id: key_id.into(),
452            enc_key: Box::pin(enc_key.into()),
453            supported_operations: vec![
454                coset::iana::KeyOperation::Decrypt,
455                coset::iana::KeyOperation::Encrypt,
456                coset::iana::KeyOperation::WrapKey,
457                coset::iana::KeyOperation::UnwrapKey,
458            ],
459        });
460
461        plaintext.encrypt_with_key(&key).expect("encryption works")
462    }
463
464    #[test]
465    #[ignore = "Manual test to verify debug format"]
466    fn test_debug() {
467        let enc_string = encrypt_with_xchacha20("Test debug string");
468        println!("{:?}", enc_string);
469        let enc_string_aes =
470            EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
471                .unwrap();
472        println!("{:?}", enc_string_aes);
473    }
474
475    /// XChaCha20Poly1305 encstrings should be padded in blocks of 32 bytes. This ensures that the
476    /// encstring length does not reveal more than the 32-byte range of lengths that the contained
477    /// string falls into.
478    #[test]
479    fn test_xchacha20_encstring_string_padding_block_sizes() {
480        let cases = [
481            ("", 32),              // empty string, padded to 32
482            (&"a".repeat(31), 32), // largest in first block
483            (&"a".repeat(32), 64), // smallest in second block
484            (&"a".repeat(63), 64), // largest in second block
485            (&"a".repeat(64), 96), // smallest in third block
486        ];
487
488        let ciphertext_lengths: Vec<_> = cases
489            .iter()
490            .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
491            .collect();
492
493        // Block 1: 0-31 (same length)
494        assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
495        // Block 2: 32-63 (same length, different from block 1)
496        assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
497        assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
498        // Block 3: 64+ (different from block 2)
499        assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
500    }
501
502    #[test]
503    fn test_enc_roundtrip_xchacha20() {
504        let key_id = [0u8; KEY_ID_SIZE];
505        let enc_key = [0u8; 32];
506        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
507            key_id: key_id.into(),
508            enc_key: Box::pin(enc_key.into()),
509            supported_operations: vec![
510                coset::iana::KeyOperation::Decrypt,
511                coset::iana::KeyOperation::Encrypt,
512                coset::iana::KeyOperation::WrapKey,
513                coset::iana::KeyOperation::UnwrapKey,
514            ],
515        });
516
517        let test_string = "encrypted_test_string";
518        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
519        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
520        assert_eq!(decrypted_str, test_string);
521    }
522
523    #[test]
524    fn test_enc_string_roundtrip() {
525        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
526
527        let test_string = "encrypted_test_string";
528        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
529
530        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
531        assert_eq!(decrypted_str, test_string);
532    }
533
534    #[test]
535    fn test_enc_roundtrip_xchacha20_empty() {
536        let key_id = [0u8; KEY_ID_SIZE];
537        let enc_key = [0u8; 32];
538        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
539            key_id: key_id.into(),
540            enc_key: Box::pin(enc_key.into()),
541            supported_operations: vec![
542                coset::iana::KeyOperation::Decrypt,
543                coset::iana::KeyOperation::Encrypt,
544                coset::iana::KeyOperation::WrapKey,
545                coset::iana::KeyOperation::UnwrapKey,
546            ],
547        });
548
549        let test_string = "";
550        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
551        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
552        assert_eq!(decrypted_str, test_string);
553    }
554
555    #[test]
556    fn test_enc_string_roundtrip_empty() {
557        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
558
559        let test_string = "";
560        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
561
562        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
563        assert_eq!(decrypted_str, test_string);
564    }
565
566    #[test]
567    fn test_enc_string_ref_roundtrip() {
568        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
569
570        let test_string: &'static str = "encrypted_test_string";
571        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
572
573        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
574        assert_eq!(decrypted_str, test_string);
575    }
576
577    #[test]
578    fn test_enc_string_serialization() {
579        #[derive(serde::Serialize, serde::Deserialize)]
580        struct Test {
581            key: EncString,
582        }
583
584        let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
585        let serialized = format!("{{\"key\":\"{cipher}\"}}");
586
587        let t = serde_json::from_str::<Test>(&serialized).unwrap();
588        assert_eq!(t.key.enc_type(), 2);
589        assert_eq!(t.key.to_string(), cipher);
590        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
591    }
592
593    #[test]
594    fn test_enc_from_to_buffer() {
595        let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
596        let enc_string: EncString = enc_str.parse().unwrap();
597
598        let enc_buf = enc_string.to_buffer().unwrap();
599
600        assert_eq!(
601            enc_buf,
602            vec![
603                2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
604                163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
605                220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
606                135, 233, 150, 136, 221, 71, 140, 125, 141, 215
607            ]
608        );
609
610        let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
611
612        assert_eq!(enc_string_new.to_string(), enc_str)
613    }
614
615    #[test]
616    fn test_from_str_cbc256() {
617        let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
618        let enc_string: EncString = enc_str.parse().unwrap();
619
620        assert_eq!(enc_string.enc_type(), 0);
621        if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
622            assert_eq!(
623                iv,
624                &[
625                    164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
626                ]
627            );
628            assert_eq!(
629                data,
630                &[
631                    93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
632                ]
633            );
634        } else {
635            panic!("Invalid variant")
636        };
637    }
638
639    #[test]
640    fn test_decrypt_cbc256() {
641        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
642        let key = SymmetricCryptoKey::try_from(key).unwrap();
643
644        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
645        let enc_string: EncString = enc_str.parse().unwrap();
646        assert_eq!(enc_string.enc_type(), 0);
647
648        let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
649        assert_eq!(dec_str, "EncryptMe!");
650    }
651
652    #[test]
653    fn test_decrypt_downgrade_encstring_prevention() {
654        // Simulate a potential downgrade attack by removing the mac portion of the `EncString` and
655        // attempt to decrypt it using a `SymmetricCryptoKey` with a mac key.
656        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
657        let key = SymmetricCryptoKey::try_from(key).unwrap();
658
659        // A "downgraded" `EncString` from `EncString::Aes256Cbc_HmacSha256_B64` (2) to
660        // `EncString::Aes256Cbc_B64` (0), with the mac portion removed.
661        // <enc_string>
662        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
663        let enc_string: EncString = enc_str.parse().unwrap();
664        assert_eq!(enc_string.enc_type(), 0);
665
666        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
667        assert!(matches!(result, Err(CryptoError::WrongKeyType)));
668    }
669
670    #[test]
671    fn test_encrypt_fails_when_operation_not_allowed() {
672        // Key with only Decrypt allowed
673        let key_id = [0u8; KEY_ID_SIZE];
674        let enc_key = [0u8; 32];
675        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
676            key_id: key_id.into(),
677            enc_key: Box::pin(enc_key.into()),
678            supported_operations: vec![KeyOperation::Decrypt],
679        });
680
681        let plaintext = "should fail";
682        let result = plaintext.encrypt_with_key(&key);
683        assert!(
684            matches!(
685                result,
686                Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
687            ),
688            "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
689        );
690    }
691
692    #[test]
693    fn test_from_str_invalid() {
694        let enc_str = "8.ABC";
695        let enc_string: Result<EncString, _> = enc_str.parse();
696
697        let err = enc_string.unwrap_err();
698        assert_eq!(
699            err.to_string(),
700            "EncString error, Invalid symmetric type, got type 8 with 1 parts"
701        );
702    }
703
704    #[test]
705    #[cfg(not(feature = "dangerous-crypto-debug"))]
706    fn test_debug_format() {
707        let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
708        let enc_string: EncString = enc_str.parse().unwrap();
709        assert_eq!(
710            "EncString::Aes256CbcHmacSha256".to_string(),
711            format!("{:?}", enc_string)
712        );
713    }
714
715    #[test]
716    fn test_json_schema() {
717        let schema = schema_for!(EncString);
718
719        assert_eq!(
720            serde_json::to_string(&schema).unwrap(),
721            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
722        );
723    }
724}