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