bitwarden_crypto/enc_string/
symmetric.rs

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