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