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