bitwarden_crypto/enc_string/
symmetric.rs

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