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; the stored bytes are raw PKCS#8
207            // DER at this point.
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, etc
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 a naive `==`) blocks MAC-verification
249        // timing 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
297// XXX this should ideally just be block_padding::Pkcs7::unpad, but i can't
298// figure out how to get the generic types to work out
299fn pkcs7_unpad(b: &[u8]) -> Option<&[u8]> {
300    if b.is_empty() {
301        return None;
302    }
303
304    let padding_val = b[b.len() - 1];
305    if padding_val == 0 {
306        return None;
307    }
308
309    let padding_len = usize::from(padding_val);
310    if padding_len > b.len() {
311        return None;
312    }
313
314    for c in b.iter().copied().skip(b.len() - padding_len) {
315        if c != padding_val {
316            return None;
317        }
318    }
319
320    Some(&b[..b.len() - padding_len])
321}
322
323#[test]
324fn test_pkcs7_unpad() {
325    let tests = [
326        (&[][..], None),
327        (&[0x01][..], Some(&[][..])),
328        (&[0x02, 0x02][..], Some(&[][..])),
329        (&[0x03, 0x03, 0x03][..], Some(&[][..])),
330        (&[0x69, 0x01][..], Some(&[0x69][..])),
331        (&[0x69, 0x02, 0x02][..], Some(&[0x69][..])),
332        (&[0x69, 0x03, 0x03, 0x03][..], Some(&[0x69][..])),
333        (&[0x02][..], None),
334        (&[0x03][..], None),
335        (&[0x69, 0x69, 0x03, 0x03][..], None),
336        (&[0x00][..], None),
337        (&[0x02, 0x00][..], None),
338    ];
339    for (input, expected) in tests {
340        let got = pkcs7_unpad(input);
341        assert_eq!(got, expected);
342    }
343}
344
345#[test]
346fn test_pkcs7_pad() {
347    let tests: &[(&[u8], &[u8])] = &[
348        (
349            &[],
350            &[
351                16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
352                16,
353            ],
354        ),
355        (
356            &[0x69],
357            &[
358                0x69, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
359                15,
360            ],
361        ),
362        (
363            &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
364            &[
365                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16,
366                16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
367            ],
368        ),
369    ];
370    for (input, expected) in tests {
371        let mut buf = input.to_vec();
372        pkcs7_pad(&mut buf, 16);
373        assert_eq!(&buf[..], *expected);
374        assert_eq!(pkcs7_unpad(&buf).unwrap(), *input);
375    }
376}
377
378#[cfg(test)]
379fn test_keys() -> crate::locked::Keys {
380    let mut v = crate::locked::Vec::new();
381    v.extend(0u8..64);
382    crate::locked::Keys::new(v)
383}
384
385#[test]
386fn test_encrypt_decrypt_roundtrip() {
387    let keys = test_keys();
388    let plaintext = b"hello world, this is a test!";
389    let cs = CipherString::encrypt_symmetric(&keys, plaintext).unwrap();
390    let decrypted = cs.decrypt_symmetric(&keys, None).unwrap();
391    assert_eq!(&decrypted[..], plaintext);
392}
393
394#[test]
395fn test_encrypt_decrypt_block_boundary() {
396    let keys = test_keys();
397    // exactly one AES block; PKCS#7 must append a full block of 0x10 bytes.
398    let plaintext = b"0123456789abcdef";
399    assert_eq!(plaintext.len(), 16);
400    let cs = CipherString::encrypt_symmetric(&keys, plaintext).unwrap();
401    if let CipherString::Symmetric { ciphertext, .. } = &cs {
402        assert_eq!(ciphertext.len(), 32);
403    } else {
404        panic!("expected symmetric");
405    }
406    let decrypted = cs.decrypt_symmetric(&keys, None).unwrap();
407    assert_eq!(&decrypted[..], plaintext);
408}