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            }
339            (
340                EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
341                SymmetricCryptoKey::Aes256CbcHmacKey(key),
342            ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key),
343            (
344                EncString::Cose_Encrypt0_B64 { data },
345                SymmetricCryptoKey::XChaCha20Poly1305Key(key),
346            ) => {
347                let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
348                    &CoseEncrypt0Bytes::from(data.as_slice()),
349                    key,
350                )?;
351                Ok(decrypted_message)
352            }
353            _ => Err(CryptoError::WrongKeyType),
354        }
355    }
356}
357
358impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
359    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
360        Utf8Bytes::from(self).encrypt_with_key(key)
361    }
362}
363
364impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
365    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
366        Utf8Bytes::from(self).encrypt_with_key(key)
367    }
368}
369
370impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
371    #[instrument(err, skip_all)]
372    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
373        let dec: Vec<u8> = self.decrypt_with_key(key)?;
374        String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
375    }
376}
377
378/// Usually we wouldn't want to expose EncStrings in the API or the schemas.
379/// But during the transition phase we will expose endpoints using the EncString type.
380impl schemars::JsonSchema for EncString {
381    fn schema_name() -> Cow<'static, str> {
382        "EncString".into()
383    }
384
385    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
386        generator.subschema_for::<String>()
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use coset::iana::KeyOperation;
393    use schemars::schema_for;
394
395    use super::EncString;
396    use crate::{
397        CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
398        derive_symmetric_key,
399    };
400
401    fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
402        let key_id = [0u8; KEY_ID_SIZE];
403        let enc_key = [0u8; 32];
404        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
405            key_id,
406            enc_key: Box::pin(enc_key.into()),
407            supported_operations: vec![
408                coset::iana::KeyOperation::Decrypt,
409                coset::iana::KeyOperation::Encrypt,
410                coset::iana::KeyOperation::WrapKey,
411                coset::iana::KeyOperation::UnwrapKey,
412            ],
413        });
414
415        plaintext.encrypt_with_key(&key).expect("encryption works")
416    }
417
418    /// XChaCha20Poly1305 encstrings should be padded in blocks of 32 bytes. This ensures that the
419    /// encstring length does not reveal more than the 32-byte range of lengths that the contained
420    /// string falls into.
421    #[test]
422    fn test_xchacha20_encstring_string_padding_block_sizes() {
423        let cases = [
424            ("", 32),              // empty string, padded to 32
425            (&"a".repeat(31), 32), // largest in first block
426            (&"a".repeat(32), 64), // smallest in second block
427            (&"a".repeat(63), 64), // largest in second block
428            (&"a".repeat(64), 96), // smallest in third block
429        ];
430
431        let ciphertext_lengths: Vec<_> = cases
432            .iter()
433            .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
434            .collect();
435
436        // Block 1: 0-31 (same length)
437        assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
438        // Block 2: 32-63 (same length, different from block 1)
439        assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
440        assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
441        // Block 3: 64+ (different from block 2)
442        assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
443    }
444
445    #[test]
446    fn test_enc_roundtrip_xchacha20() {
447        let key_id = [0u8; KEY_ID_SIZE];
448        let enc_key = [0u8; 32];
449        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
450            key_id,
451            enc_key: Box::pin(enc_key.into()),
452            supported_operations: vec![
453                coset::iana::KeyOperation::Decrypt,
454                coset::iana::KeyOperation::Encrypt,
455                coset::iana::KeyOperation::WrapKey,
456                coset::iana::KeyOperation::UnwrapKey,
457            ],
458        });
459
460        let test_string = "encrypted_test_string";
461        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
462        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
463        assert_eq!(decrypted_str, test_string);
464    }
465
466    #[test]
467    fn test_enc_string_roundtrip() {
468        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
469
470        let test_string = "encrypted_test_string";
471        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
472
473        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
474        assert_eq!(decrypted_str, test_string);
475    }
476
477    #[test]
478    fn test_enc_roundtrip_xchacha20_empty() {
479        let key_id = [0u8; KEY_ID_SIZE];
480        let enc_key = [0u8; 32];
481        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
482            key_id,
483            enc_key: Box::pin(enc_key.into()),
484            supported_operations: vec![
485                coset::iana::KeyOperation::Decrypt,
486                coset::iana::KeyOperation::Encrypt,
487                coset::iana::KeyOperation::WrapKey,
488                coset::iana::KeyOperation::UnwrapKey,
489            ],
490        });
491
492        let test_string = "";
493        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
494        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
495        assert_eq!(decrypted_str, test_string);
496    }
497
498    #[test]
499    fn test_enc_string_roundtrip_empty() {
500        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
501
502        let test_string = "";
503        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
504
505        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
506        assert_eq!(decrypted_str, test_string);
507    }
508
509    #[test]
510    fn test_enc_string_ref_roundtrip() {
511        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
512
513        let test_string: &'static str = "encrypted_test_string";
514        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
515
516        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
517        assert_eq!(decrypted_str, test_string);
518    }
519
520    #[test]
521    fn test_enc_string_serialization() {
522        #[derive(serde::Serialize, serde::Deserialize)]
523        struct Test {
524            key: EncString,
525        }
526
527        let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
528        let serialized = format!("{{\"key\":\"{cipher}\"}}");
529
530        let t = serde_json::from_str::<Test>(&serialized).unwrap();
531        assert_eq!(t.key.enc_type(), 2);
532        assert_eq!(t.key.to_string(), cipher);
533        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
534    }
535
536    #[test]
537    fn test_enc_from_to_buffer() {
538        let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
539        let enc_string: EncString = enc_str.parse().unwrap();
540
541        let enc_buf = enc_string.to_buffer().unwrap();
542
543        assert_eq!(
544            enc_buf,
545            vec![
546                2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
547                163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
548                220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
549                135, 233, 150, 136, 221, 71, 140, 125, 141, 215
550            ]
551        );
552
553        let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
554
555        assert_eq!(enc_string_new.to_string(), enc_str)
556    }
557
558    #[test]
559    fn test_from_str_cbc256() {
560        let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
561        let enc_string: EncString = enc_str.parse().unwrap();
562
563        assert_eq!(enc_string.enc_type(), 0);
564        if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
565            assert_eq!(
566                iv,
567                &[
568                    164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
569                ]
570            );
571            assert_eq!(
572                data,
573                &[
574                    93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
575                ]
576            );
577        } else {
578            panic!("Invalid variant")
579        };
580    }
581
582    #[test]
583    fn test_decrypt_cbc256() {
584        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
585        let key = SymmetricCryptoKey::try_from(key).unwrap();
586
587        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
588        let enc_string: EncString = enc_str.parse().unwrap();
589        assert_eq!(enc_string.enc_type(), 0);
590
591        let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
592        assert_eq!(dec_str, "EncryptMe!");
593    }
594
595    #[test]
596    fn test_decrypt_downgrade_encstring_prevention() {
597        // Simulate a potential downgrade attack by removing the mac portion of the `EncString` and
598        // attempt to decrypt it using a `SymmetricCryptoKey` with a mac key.
599        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
600        let key = SymmetricCryptoKey::try_from(key).unwrap();
601
602        // A "downgraded" `EncString` from `EncString::Aes256Cbc_HmacSha256_B64` (2) to
603        // `EncString::Aes256Cbc_B64` (0), with the mac portion removed.
604        // <enc_string>
605        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
606        let enc_string: EncString = enc_str.parse().unwrap();
607        assert_eq!(enc_string.enc_type(), 0);
608
609        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
610        assert!(matches!(result, Err(CryptoError::WrongKeyType)));
611    }
612
613    #[test]
614    fn test_encrypt_fails_when_operation_not_allowed() {
615        // Key with only Decrypt allowed
616        let key_id = [0u8; KEY_ID_SIZE];
617        let enc_key = [0u8; 32];
618        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
619            key_id,
620            enc_key: Box::pin(enc_key.into()),
621            supported_operations: vec![KeyOperation::Decrypt],
622        });
623
624        let plaintext = "should fail";
625        let result = plaintext.encrypt_with_key(&key);
626        assert!(
627            matches!(
628                result,
629                Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
630            ),
631            "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
632        );
633    }
634
635    #[test]
636    fn test_from_str_invalid() {
637        let enc_str = "8.ABC";
638        let enc_string: Result<EncString, _> = enc_str.parse();
639
640        let err = enc_string.unwrap_err();
641        assert_eq!(
642            err.to_string(),
643            "EncString error, Invalid symmetric type, got type 8 with 1 parts"
644        );
645    }
646
647    #[test]
648    fn test_debug_format() {
649        let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
650        let enc_string: EncString = enc_str.parse().unwrap();
651
652        let debug_string = format!("{enc_string:?}");
653        assert_eq!(debug_string, enc_str);
654    }
655
656    #[test]
657    fn test_json_schema() {
658        let schema = schema_for!(EncString);
659
660        assert_eq!(
661            serde_json::to_string(&schema).unwrap(),
662            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
663        );
664    }
665}