Skip to main content

bitwarden_crypto/hazmat/symmetric_encryption/
aes256_cbc_hmac_sha256_aead.rs

1//! # AES-256-CBC-HMAC-SHA256 operations
2//!
3//! An extended construction of AES-256-CBC-HMAC-SHA256. It is used when an AES256-CBC-HMAC-SHA256
4//! key is used for authenticated encryption in the context of COSE.
5
6use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7};
7use hmac::{KeyInit, Mac};
8use rand::RngExt;
9use subtle::ConstantTimeEq;
10
11use super::Aead;
12use crate::CryptoError;
13
14type HmacSha256 = hmac::Hmac<sha2::Sha256>;
15
16pub(crate) const NONCE_SIZE: usize = 16;
17pub(crate) const ENC_KEY_SIZE: usize = 32;
18pub(crate) const MAC_KEY_SIZE: usize = 32;
19pub(crate) const KEY_SIZE: usize = ENC_KEY_SIZE + MAC_KEY_SIZE;
20pub(crate) const MAC_SIZE: usize = 32;
21
22/// Domain separation label prefixed to the MAC input. It distinguishes this construction from the
23/// legacy [`Aes256CbcHmacSha256`](super::Aes256CbcHmacSha256) Encrypt-then-MAC construction, which
24/// MACs `iv || ciphertext` under the same MAC sub-key, so that a tag produced by one construction
25/// can never be a valid tag for the other.
26const MAC_DOMAIN_SEPARATOR: &[u8] = b"bitwarden.aes256-cbc-hmac-sha256-aead.v1";
27
28/// AES-256-CBC-HMAC-SHA256 authenticated encryption with associated data.
29pub(crate) struct Aes256CbcHmacSha256Aead;
30
31impl Aead for Aes256CbcHmacSha256Aead {
32    type Key = [u8; KEY_SIZE];
33    type Ciphertext = Aes256CbcHmacSha256AeadCiphertext;
34    type Nonce = Aes256CbcHmacSha256AeadNonce;
35
36    fn encrypt(
37        key: &Self::Key,
38        nonce: &Self::Nonce,
39        plaintext: &[u8],
40        associated_data: &[u8],
41    ) -> Self::Ciphertext {
42        let (enc_key, mac_key) = split_to_subkeys(key);
43
44        let mut encrypted_bytes =
45            cbc::Encryptor::<aes::Aes256>::new(enc_key.into(), nonce.as_array().into())
46                .encrypt_padded_vec::<Pkcs7>(plaintext);
47        let mac =
48            calculate_mac_with_aad(nonce.as_array(), &encrypted_bytes, associated_data, mac_key);
49        encrypted_bytes.extend_from_slice(&mac);
50
51        Aes256CbcHmacSha256AeadCiphertext { encrypted_bytes }
52    }
53
54    fn decrypt(
55        key: &Self::Key,
56        nonce: &Self::Nonce,
57        ciphertext: &Self::Ciphertext,
58        associated_data: &[u8],
59    ) -> Result<Vec<u8>, CryptoError> {
60        let (enc_key, mac_key) = split_to_subkeys(key);
61
62        let encrypted_bytes = ciphertext.encrypted_bytes();
63        if encrypted_bytes.len() < MAC_SIZE {
64            return Err(CryptoError::KeyDecrypt);
65        }
66        let (data, received_mac) = encrypted_bytes.split_at(encrypted_bytes.len() - MAC_SIZE);
67
68        let expected_mac = calculate_mac_with_aad(nonce.as_array(), data, associated_data, mac_key);
69        if expected_mac.ct_ne(received_mac).into() {
70            return Err(CryptoError::KeyDecrypt);
71        }
72
73        let mut buf = data.to_vec();
74        cbc::Decryptor::<aes::Aes256>::new(enc_key.into(), nonce.as_array().into())
75            .decrypt_padded::<Pkcs7>(&mut buf)
76            .map(|slice| slice.to_vec())
77            .map_err(|_| CryptoError::KeyDecrypt)
78    }
79}
80
81/// Splits the 64-byte composite key into zero-copy views over its encryption and MAC halves.
82fn split_to_subkeys(key: &[u8; KEY_SIZE]) -> (&[u8; ENC_KEY_SIZE], &[u8; MAC_KEY_SIZE]) {
83    let (enc_key, mac_key) = key.split_at(ENC_KEY_SIZE);
84    let enc_key = enc_key
85        .try_into()
86        .expect("first half of a 64-byte key is always 32 bytes");
87    let mac_key = mac_key
88        .try_into()
89        .expect("second half of a 64-byte key is always 32 bytes");
90    (enc_key, mac_key)
91}
92
93/// Computes the HMAC-SHA256 MAC over `MAC_DOMAIN_SEPARATOR || iv || len(data) || data ||
94/// len(associated_data) || associated_data`, where lengths are encoded as 8-byte big-endian `u64`s.
95fn calculate_mac_with_aad(
96    iv: &[u8; NONCE_SIZE],
97    data: &[u8],
98    associated_data: &[u8],
99    mac_key: &[u8; MAC_KEY_SIZE],
100) -> [u8; MAC_SIZE] {
101    let mut hmac =
102        HmacSha256::new_from_slice(mac_key).expect("hmac new_from_slice should not fail");
103    hmac.update(MAC_DOMAIN_SEPARATOR);
104    hmac.update(iv);
105    hmac.update(&(data.len() as u64).to_be_bytes());
106    hmac.update(data);
107    hmac.update(&(associated_data.len() as u64).to_be_bytes());
108    hmac.update(associated_data);
109    (*hmac.finalize().into_bytes())
110        .try_into()
111        // This is safe because HMAC-SHA256 output size is always 32 bytes
112        .expect("HMAC output size to be correct")
113}
114
115/// A 128-bit AES-256-CBC-HMAC-SHA256 IV.
116///
117/// A fresh, cryptographically random IV must be generated for every message encrypted under a
118/// given key; use [`Aes256CbcHmacSha256AeadNonce::make`] for this.
119pub(crate) struct Aes256CbcHmacSha256AeadNonce([u8; NONCE_SIZE]);
120
121impl Aes256CbcHmacSha256AeadNonce {
122    /// Generates a fresh, cryptographically random IV.
123    pub(crate) fn make() -> Self {
124        let mut rng = bitwarden_random::rng();
125        let mut iv = [0u8; NONCE_SIZE];
126        rng.fill(&mut iv);
127        Aes256CbcHmacSha256AeadNonce(iv)
128    }
129
130    /// Returns the raw IV bytes.
131    pub(crate) fn as_bytes(&self) -> &[u8] {
132        &self.0
133    }
134
135    /// Returns the IV as a fixed-size array, as required by the underlying CBC primitives.
136    fn as_array(&self) -> &[u8; NONCE_SIZE] {
137        &self.0
138    }
139
140    /// Parses the IV from a COSE message's unprotected `iv` header bytes.
141    fn from_cose_iv(iv: &[u8]) -> Result<Self, CryptoError> {
142        let iv: [u8; NONCE_SIZE] = iv.try_into().map_err(|_| CryptoError::InvalidNonceLength)?;
143        Ok(Aes256CbcHmacSha256AeadNonce(iv))
144    }
145}
146
147/// Parses the IV from the unprotected `iv` header of a [`coset::CoseEncrypt`] message.
148impl TryFrom<&coset::CoseEncrypt> for Aes256CbcHmacSha256AeadNonce {
149    type Error = CryptoError;
150
151    fn try_from(cose_encrypt: &coset::CoseEncrypt) -> Result<Self, Self::Error> {
152        Self::from_cose_iv(cose_encrypt.unprotected.iv.as_slice())
153    }
154}
155
156/// Parses the IV from the unprotected `iv` header of a [`coset::CoseEncrypt0`] message.
157impl TryFrom<&coset::CoseEncrypt0> for Aes256CbcHmacSha256AeadNonce {
158    type Error = CryptoError;
159
160    fn try_from(cose_encrypt0: &coset::CoseEncrypt0) -> Result<Self, Self::Error> {
161        Self::from_cose_iv(cose_encrypt0.unprotected.iv.as_slice())
162    }
163}
164
165pub(crate) struct Aes256CbcHmacSha256AeadCiphertext {
166    encrypted_bytes: Vec<u8>,
167}
168
169impl Aes256CbcHmacSha256AeadCiphertext {
170    pub(crate) fn encrypted_bytes(&self) -> &[u8] {
171        &self.encrypted_bytes
172    }
173}
174
175/// Wraps already-encrypted bytes (e.g. read from a COSE message) so they can be passed to
176/// [`Aes256CbcHmacSha256Aead::decrypt`](Aead::decrypt).
177impl From<Vec<u8>> for Aes256CbcHmacSha256AeadCiphertext {
178    fn from(encrypted_bytes: Vec<u8>) -> Self {
179        Aes256CbcHmacSha256AeadCiphertext { encrypted_bytes }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    const TESTVECTOR_PLAINTEXT: &[u8] = b"Bitwarden SDK test vector";
188    const TESTVECTOR_ASSOCIATED_DATA: &[u8] = b"Bitwarden SDK test vector associated data";
189    const TESTVECTOR_ENCRYPTED: &[u8] = &[
190        111, 16, 33, 143, 207, 253, 106, 89, 58, 52, 145, 240, 140, 213, 149, 83, 168, 188, 122,
191        57, 130, 28, 35, 182, 114, 227, 215, 250, 175, 182, 169, 102, 33, 159, 62, 224, 104, 214,
192        248, 153, 248, 63, 219, 206, 228, 17, 93, 110, 14, 150, 81, 139, 74, 235, 115, 206, 113,
193        115, 220, 197, 18, 206, 252, 198,
194    ];
195
196    const TESTVECTOR_KEY: [u8; KEY_SIZE] = [
197        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
198        25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
199        48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
200    ];
201    const TESTVECTOR_NONCE: Aes256CbcHmacSha256AeadNonce =
202        Aes256CbcHmacSha256AeadNonce([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
203
204    #[test]
205    #[ignore = "Generates test vectors; run manually"]
206    fn generate_test_vectors() {
207        let encrypted = Aes256CbcHmacSha256Aead::encrypt(
208            &TESTVECTOR_KEY,
209            &TESTVECTOR_NONCE,
210            TESTVECTOR_PLAINTEXT,
211            TESTVECTOR_ASSOCIATED_DATA,
212        );
213        let decrypted = Aes256CbcHmacSha256Aead::decrypt(
214            &TESTVECTOR_KEY,
215            &TESTVECTOR_NONCE,
216            &encrypted,
217            TESTVECTOR_ASSOCIATED_DATA,
218        )
219        .unwrap();
220        assert_eq!(decrypted, TESTVECTOR_PLAINTEXT);
221
222        println!(
223            "const TESTVECTOR_ENCRYPTED: &[u8] = &{:?};",
224            encrypted.encrypted_bytes()
225        );
226    }
227
228    /// Locks in the serialized format of the AEAD ciphertext (CBC ciphertext followed by the MAC
229    /// over the length-prefixed input); must never break, or existing data will no longer
230    /// decrypt.
231    #[test]
232    fn test_decrypt_testvector() {
233        let encrypted = Aes256CbcHmacSha256AeadCiphertext::from(TESTVECTOR_ENCRYPTED.to_vec());
234
235        let decrypted = Aes256CbcHmacSha256Aead::decrypt(
236            &TESTVECTOR_KEY,
237            &TESTVECTOR_NONCE,
238            &encrypted,
239            TESTVECTOR_ASSOCIATED_DATA,
240        )
241        .unwrap();
242
243        assert_eq!(decrypted, TESTVECTOR_PLAINTEXT);
244    }
245
246    #[test]
247    fn test_encrypt_decrypt_aes256_cbc_hmac() {
248        let key = TESTVECTOR_KEY;
249        let nonce = Aes256CbcHmacSha256AeadNonce::make();
250        let plaintext_secret_data = b"My secret data";
251        let authenticated_data = b"My authenticated data";
252        let encrypted = Aes256CbcHmacSha256Aead::encrypt(
253            &key,
254            &nonce,
255            plaintext_secret_data,
256            authenticated_data,
257        );
258        let decrypted =
259            Aes256CbcHmacSha256Aead::decrypt(&key, &nonce, &encrypted, authenticated_data).unwrap();
260        assert_eq!(plaintext_secret_data, decrypted.as_slice());
261    }
262
263    #[test]
264    fn test_make_nonce_has_correct_length() {
265        let nonce = Aes256CbcHmacSha256AeadNonce::make();
266        assert_eq!(nonce.as_bytes().len(), NONCE_SIZE);
267    }
268
269    #[test]
270    fn test_fails_when_ciphertext_changed() {
271        let key = TESTVECTOR_KEY;
272        let nonce = Aes256CbcHmacSha256AeadNonce::make();
273        let plaintext_secret_data = b"My secret data";
274        let authenticated_data = b"My authenticated data";
275
276        let mut encrypted = Aes256CbcHmacSha256Aead::encrypt(
277            &key,
278            &nonce,
279            plaintext_secret_data,
280            authenticated_data,
281        );
282        encrypted.encrypted_bytes[0] = encrypted.encrypted_bytes[0].wrapping_add(1);
283        let result = Aes256CbcHmacSha256Aead::decrypt(&key, &nonce, &encrypted, authenticated_data);
284        assert!(result.is_err());
285    }
286
287    #[test]
288    fn test_fails_when_associated_data_changed() {
289        let key = TESTVECTOR_KEY;
290        let nonce = Aes256CbcHmacSha256AeadNonce::make();
291        let plaintext_secret_data = b"My secret data";
292        let mut authenticated_data = b"My authenticated data".to_vec();
293
294        let encrypted = Aes256CbcHmacSha256Aead::encrypt(
295            &key,
296            &nonce,
297            plaintext_secret_data,
298            authenticated_data.as_slice(),
299        );
300        authenticated_data[0] = authenticated_data[0].wrapping_add(1);
301        let result = Aes256CbcHmacSha256Aead::decrypt(
302            &key,
303            &nonce,
304            &encrypted,
305            authenticated_data.as_slice(),
306        );
307        assert!(result.is_err());
308    }
309
310    #[test]
311    fn test_fails_when_nonce_changed() {
312        let key = TESTVECTOR_KEY;
313        let nonce = Aes256CbcHmacSha256AeadNonce::make();
314        let plaintext_secret_data = b"My secret data";
315        let authenticated_data = b"My authenticated data";
316
317        let encrypted = Aes256CbcHmacSha256Aead::encrypt(
318            &key,
319            &nonce,
320            plaintext_secret_data,
321            authenticated_data,
322        );
323        // Decrypting with a different (freshly generated) nonce must fail.
324        let other_nonce = Aes256CbcHmacSha256AeadNonce::make();
325        let result =
326            Aes256CbcHmacSha256Aead::decrypt(&key, &other_nonce, &encrypted, authenticated_data);
327        assert!(result.is_err());
328    }
329
330    #[test]
331    fn test_fails_when_ciphertext_too_short() {
332        let key = TESTVECTOR_KEY;
333        let nonce = Aes256CbcHmacSha256AeadNonce::make();
334        let truncated = Aes256CbcHmacSha256AeadCiphertext {
335            encrypted_bytes: vec![0u8; MAC_SIZE - 1],
336        };
337        let result = Aes256CbcHmacSha256Aead::decrypt(&key, &nonce, &truncated, b"");
338        assert!(result.is_err());
339    }
340
341    #[test]
342    fn test_length_prefixing_prevents_data_ad_boundary_confusion() {
343        // Without length-prefixing, HMAC(iv || data || ad) for ("ab", "cd") would collide with
344        // ("a", "bcd") since the concatenation is identical. Length-prefixing must prevent this.
345        let mac_key = [0u8; MAC_KEY_SIZE];
346        let iv = [0u8; NONCE_SIZE];
347        let mac1 = calculate_mac_with_aad(&iv, b"ab", b"cd", &mac_key);
348        let mac2 = calculate_mac_with_aad(&iv, b"a", b"bcd", &mac_key);
349        assert_ne!(mac1, mac2);
350    }
351}