Skip to main content

bwx/
cipherstring.rs

1use crate::prelude::*;
2
3use aes::cipher::{
4    generic_array::GenericArray, BlockDecryptMut as _, BlockEncryptMut as _,
5    KeyIvInit as _,
6};
7use hmac::Mac as _;
8use pkcs8::DecodePrivateKey as _;
9use rand::RngCore as _;
10use zeroize::Zeroize as _;
11
12pub enum CipherString {
13    Symmetric {
14        // ty: 2 (AES_256_CBC_HMAC_SHA256)
15        iv: Vec<u8>,
16        ciphertext: Vec<u8>,
17        mac: Option<Vec<u8>>,
18    },
19    Asymmetric {
20        // ty: 4 (RSA_2048_OAEP_SHA1)
21        ciphertext: Vec<u8>,
22    },
23}
24
25impl CipherString {
26    pub fn new(s: &str) -> Result<Self> {
27        let parts: Vec<&str> = s.split('.').collect();
28        if parts.len() != 2 {
29            return Err(Error::InvalidCipherString {
30                reason: "couldn't find type".to_string(),
31            });
32        }
33
34        let ty = parts[0].as_bytes();
35        if ty.len() != 1 {
36            return Err(Error::UnimplementedCipherStringType {
37                ty: parts[0].to_string(),
38            });
39        }
40
41        let ty = ty[0] - b'0';
42        let contents = parts[1];
43
44        match ty {
45            2 => {
46                let parts: Vec<&str> = contents.split('|').collect();
47                if parts.len() < 2 || parts.len() > 3 {
48                    return Err(Error::InvalidCipherString {
49                        reason: format!(
50                            "type 2 cipherstring with {} parts",
51                            parts.len()
52                        ),
53                    });
54                }
55
56                let iv = crate::base64::decode(parts[0])
57                    .map_err(|source| Error::InvalidBase64 { source })?;
58                let ciphertext = crate::base64::decode(parts[1])
59                    .map_err(|source| Error::InvalidBase64 { source })?;
60                let mac =
61                    if parts.len() > 2 {
62                        Some(crate::base64::decode(parts[2]).map_err(
63                            |source| Error::InvalidBase64 { source },
64                        )?)
65                    } else {
66                        None
67                    };
68
69                Ok(Self::Symmetric {
70                    iv,
71                    ciphertext,
72                    mac,
73                })
74            }
75            4 | 6 => {
76                // the only difference between 4 and 6 is the HMAC256
77                // signature appended at the end
78                // https://github.com/bitwarden/jslib/blob/785b681f61f81690de6df55159ab07ae710bcfad/src/enums/encryptionType.ts#L8
79                // format is: <cipher_text_b64>|<hmac_sig>
80                let contents = contents.split('|').next().unwrap();
81                let ciphertext = crate::base64::decode(contents)
82                    .map_err(|source| Error::InvalidBase64 { source })?;
83                Ok(Self::Asymmetric { ciphertext })
84            }
85            _ => {
86                if ty < 6 {
87                    Err(Error::TooOldCipherStringType { ty: ty.to_string() })
88                } else {
89                    Err(Error::UnimplementedCipherStringType {
90                        ty: ty.to_string(),
91                    })
92                }
93            }
94        }
95    }
96
97    pub fn encrypt_symmetric(
98        keys: &crate::locked::Keys,
99        plaintext: &[u8],
100    ) -> Result<Self> {
101        let iv = random_iv();
102
103        let mut cipher = cbc::Encryptor::<aes::Aes256>::new(
104            keys.enc_key().into(),
105            iv.as_slice().into(),
106        );
107        let mut ciphertext = plaintext.to_vec();
108        pkcs7_pad(&mut ciphertext, 16);
109        for chunk in ciphertext.chunks_exact_mut(16) {
110            cipher.encrypt_block_mut(GenericArray::from_mut_slice(chunk));
111        }
112
113        let mut digest =
114            hmac::Hmac::<sha2::Sha256>::new_from_slice(keys.mac_key())
115                .map_err(|source| Error::CreateHmac { source })?;
116        digest.update(&iv);
117        digest.update(&ciphertext);
118        let mac = digest.finalize().into_bytes().as_slice().to_vec();
119
120        Ok(Self::Symmetric {
121            iv,
122            ciphertext,
123            mac: Some(mac),
124        })
125    }
126
127    pub fn decrypt_symmetric(
128        &self,
129        keys: &crate::locked::Keys,
130        entry_key: Option<&crate::locked::Keys>,
131    ) -> Result<Vec<u8>> {
132        if let Self::Symmetric {
133            iv,
134            ciphertext,
135            mac,
136        } = self
137        {
138            let mut cipher = decrypt_common_symmetric(
139                entry_key.unwrap_or(keys),
140                iv,
141                ciphertext,
142                mac.as_deref(),
143            )?;
144            let mut buf = ciphertext.clone();
145            if buf.is_empty() || buf.len() % 16 != 0 {
146                return Err(Error::Padding);
147            }
148            for chunk in buf.chunks_exact_mut(16) {
149                cipher.decrypt_block_mut(GenericArray::from_mut_slice(chunk));
150            }
151            let unpadded_len = pkcs7_unpad(&buf).ok_or(Error::Padding)?.len();
152            buf.truncate(unpadded_len);
153            Ok(buf)
154        } else {
155            Err(Error::InvalidCipherString {
156                reason:
157                    "found an asymmetric cipherstring, expecting symmetric"
158                        .to_string(),
159            })
160        }
161    }
162
163    pub fn decrypt_locked_symmetric(
164        &self,
165        keys: &crate::locked::Keys,
166    ) -> Result<crate::locked::Vec> {
167        if let Self::Symmetric {
168            iv,
169            ciphertext,
170            mac,
171        } = self
172        {
173            let mut res = crate::locked::Vec::new();
174            res.extend(ciphertext.iter().copied());
175            let mut cipher = decrypt_common_symmetric(
176                keys,
177                iv,
178                ciphertext,
179                mac.as_deref(),
180            )?;
181            let data = res.data_mut();
182            if data.is_empty() || data.len() % 16 != 0 {
183                return Err(Error::Padding);
184            }
185            for chunk in data.chunks_exact_mut(16) {
186                cipher.decrypt_block_mut(GenericArray::from_mut_slice(chunk));
187            }
188            let unpadded_len = pkcs7_unpad(data).ok_or(Error::Padding)?.len();
189            res.truncate(unpadded_len);
190            Ok(res)
191        } else {
192            Err(Error::InvalidCipherString {
193                reason:
194                    "found an asymmetric cipherstring, expecting symmetric"
195                        .to_string(),
196            })
197        }
198    }
199
200    pub fn decrypt_locked_asymmetric(
201        &self,
202        private_key: &crate::locked::PrivateKey,
203    ) -> Result<crate::locked::Vec> {
204        if let Self::Asymmetric { ciphertext } = self {
205            // Padding was already stripped by decrypt_locked_symmetric when
206            // the private key was unwrapped; stored bytes are raw PKCS#8
207            // DER.
208            let privkey_data = private_key.private_key();
209            let pkey = rsa::RsaPrivateKey::from_pkcs8_der(privkey_data)
210                .map_err(|source| Error::RsaPkcs8 { source })?;
211            let mut bytes = pkey
212                .decrypt(rsa::Oaep::new::<sha1::Sha1>(), ciphertext)
213                .map_err(|source| Error::Rsa { source })?;
214
215            // XXX it'd be great if the rsa crate would let us decrypt
216            // into a preallocated buffer directly to avoid the
217            // intermediate vec that needs to be manually zeroized
218            let mut res = crate::locked::Vec::new();
219            res.extend(bytes.iter().copied());
220            bytes.zeroize();
221
222            Ok(res)
223        } else {
224            Err(Error::InvalidCipherString {
225                reason:
226                    "found a symmetric cipherstring, expecting asymmetric"
227                        .to_string(),
228            })
229        }
230    }
231}
232
233fn decrypt_common_symmetric(
234    keys: &crate::locked::Keys,
235    iv: &[u8],
236    ciphertext: &[u8],
237    mac: Option<&[u8]>,
238) -> Result<cbc::Decryptor<aes::Aes256>> {
239    if let Some(mac) = mac {
240        let mut key =
241            hmac::Hmac::<sha2::Sha256>::new_from_slice(keys.mac_key())
242                .map_err(|source| Error::CreateHmac { source })?;
243        key.update(iv);
244        key.update(ciphertext);
245
246        // `verify_slice` uses `subtle::ConstantTimeEq` internally, which
247        // compares byte-by-byte without short-circuiting. Using it
248        // explicitly (rather than `==`) blocks MAC-verification timing
249        // oracles on crafted ciphertexts.
250        key.verify_slice(mac).map_err(|_| Error::InvalidMac)?;
251    }
252
253    cbc::Decryptor::<aes::Aes256>::new_from_slices(keys.enc_key(), iv)
254        .map_err(|source| Error::CreateBlockMode { source })
255}
256
257impl std::fmt::Display for CipherString {
258    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
259        match self {
260            Self::Symmetric {
261                iv,
262                ciphertext,
263                mac,
264            } => {
265                let iv = crate::base64::encode(iv);
266                let ciphertext = crate::base64::encode(ciphertext);
267                if let Some(mac) = &mac {
268                    let mac = crate::base64::encode(mac);
269                    write!(f, "2.{iv}|{ciphertext}|{mac}")
270                } else {
271                    write!(f, "2.{iv}|{ciphertext}")
272                }
273            }
274            Self::Asymmetric { ciphertext } => {
275                let ciphertext = crate::base64::encode(ciphertext);
276                write!(f, "4.{ciphertext}")
277            }
278        }
279    }
280}
281
282fn random_iv() -> Vec<u8> {
283    let mut iv = vec![0_u8; 16];
284    let mut rng = rand::rng();
285    rng.fill_bytes(&mut iv);
286    iv
287}
288
289// PKCS#7: always append n bytes of value n, so block length is a multiple
290// of block_size and padding is unambiguously strippable.
291fn pkcs7_pad(buf: &mut Vec<u8>, block_size: usize) {
292    let pad_len = block_size - (buf.len() % block_size);
293    let pad_val = u8::try_from(pad_len).unwrap();
294    buf.resize(buf.len() + pad_len, pad_val);
295}
296
297fn pkcs7_unpad(b: &[u8]) -> Option<&[u8]> {
298    if b.is_empty() {
299        return None;
300    }
301
302    let padding_val = b[b.len() - 1];
303    if padding_val == 0 {
304        return None;
305    }
306
307    let padding_len = usize::from(padding_val);
308    if padding_len > b.len() {
309        return None;
310    }
311
312    for c in b.iter().copied().skip(b.len() - padding_len) {
313        if c != padding_val {
314            return None;
315        }
316    }
317
318    Some(&b[..b.len() - padding_len])
319}
320
321#[test]
322fn test_pkcs7_unpad() {
323    let tests = [
324        (&[][..], None),
325        (&[0x01][..], Some(&[][..])),
326        (&[0x02, 0x02][..], Some(&[][..])),
327        (&[0x03, 0x03, 0x03][..], Some(&[][..])),
328        (&[0x69, 0x01][..], Some(&[0x69][..])),
329        (&[0x69, 0x02, 0x02][..], Some(&[0x69][..])),
330        (&[0x69, 0x03, 0x03, 0x03][..], Some(&[0x69][..])),
331        (&[0x02][..], None),
332        (&[0x03][..], None),
333        (&[0x69, 0x69, 0x03, 0x03][..], None),
334        (&[0x00][..], None),
335        (&[0x02, 0x00][..], None),
336    ];
337    for (input, expected) in tests {
338        let got = pkcs7_unpad(input);
339        assert_eq!(got, expected);
340    }
341}
342
343#[test]
344fn test_pkcs7_pad() {
345    let tests: &[(&[u8], &[u8])] = &[
346        (
347            &[],
348            &[
349                16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
350                16,
351            ],
352        ),
353        (
354            &[0x69],
355            &[
356                0x69, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
357                15,
358            ],
359        ),
360        (
361            &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
362            &[
363                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16,
364                16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
365            ],
366        ),
367    ];
368    for (input, expected) in tests {
369        let mut buf = input.to_vec();
370        pkcs7_pad(&mut buf, 16);
371        assert_eq!(&buf[..], *expected);
372        assert_eq!(pkcs7_unpad(&buf).unwrap(), *input);
373    }
374}
375
376#[cfg(test)]
377fn test_keys() -> crate::locked::Keys {
378    let mut v = crate::locked::Vec::new();
379    v.extend(0u8..64);
380    crate::locked::Keys::new(v)
381}
382
383#[test]
384fn test_encrypt_decrypt_roundtrip() {
385    let keys = test_keys();
386    let plaintext = b"hello world, this is a test!";
387    let cs = CipherString::encrypt_symmetric(&keys, plaintext).unwrap();
388    let decrypted = cs.decrypt_symmetric(&keys, None).unwrap();
389    assert_eq!(&decrypted[..], plaintext);
390}
391
392#[test]
393fn test_encrypt_decrypt_block_boundary() {
394    let keys = test_keys();
395    // exactly one AES block; PKCS#7 must append a full block of 0x10 bytes.
396    let plaintext = b"0123456789abcdef";
397    assert_eq!(plaintext.len(), 16);
398    let cs = CipherString::encrypt_symmetric(&keys, plaintext).unwrap();
399    if let CipherString::Symmetric { ciphertext, .. } = &cs {
400        assert_eq!(ciphertext.len(), 32);
401    } else {
402        panic!("expected symmetric");
403    }
404    let decrypted = cs.decrypt_symmetric(&keys, None).unwrap();
405    assert_eq!(&decrypted[..], plaintext);
406}