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