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