Skip to main content

bitwarden_crypto/enc_string/
symmetric.rs

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