bitwarden_crypto/enc_string/
symmetric.rs

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