Skip to main content

bitwarden_crypto/hazmat/symmetric_encryption/
xaes_256_gcm.rs

1//! # XAES-256-GCM operations
2//!
3//! Contains low-level XAES-256-GCM operations used by the rest of the crate. XAES-256-GCM uses a
4//! 256-bit key and a 192-bit nonce. According to the current C2SP specification, random nonces
5//! support approximately 2^80 messages under one key at collision risk 2^-32.
6//!
7//! XAES-256-GCM is constructed from the SP 800-108r1 counter-mode KDF with CMAC-AES256 and
8//! AES-256-GCM. It is neither nonce-misuse-resistant nor key-committing.
9//!
10//! In most cases, use higher-level APIs instead of this raw-key primitive when available.
11
12use aes::cipher::common::Generate;
13use coset::{CoseEncrypt, CoseEncrypt0};
14use typenum::Unsigned;
15use xaes_256_gcm::{
16    Xaes256Gcm as Xaes256GcmAlg,
17    aead::{AeadCore, AeadInOut, KeyInit, Nonce},
18};
19
20use super::Aead;
21use crate::CryptoError;
22
23pub(crate) const KEY_SIZE: usize = 32;
24pub(crate) const NONCE_SIZE: usize = <Xaes256GcmAlg as AeadCore>::NonceSize::USIZE;
25
26/// XAES-256-GCM authenticated encryption with associated data.
27///
28/// See the [module documentation](self) for the security caveats that apply to this cipher.
29pub(crate) struct XAes256Gcm;
30
31impl Aead for XAes256Gcm {
32    type Key = [u8; KEY_SIZE];
33    type Ciphertext = XAes256GcmCiphertext;
34    type Nonce = XAes256GcmNonce;
35
36    fn encrypt(
37        key: &Self::Key,
38        nonce: &Self::Nonce,
39        plaintext: &[u8],
40        associated_data: &[u8],
41    ) -> Self::Ciphertext {
42        let mut buffer = plaintext.to_vec();
43        Xaes256GcmAlg::new(key.into())
44            .encrypt_in_place(&nonce.0, associated_data, &mut buffer)
45            .expect("encryption failed");
46
47        XAes256GcmCiphertext {
48            encrypted_bytes: buffer,
49        }
50    }
51
52    fn decrypt(
53        key: &Self::Key,
54        nonce: &Self::Nonce,
55        ciphertext: &Self::Ciphertext,
56        associated_data: &[u8],
57    ) -> Result<Vec<u8>, CryptoError> {
58        let mut buffer = ciphertext.encrypted_bytes().to_vec();
59        Xaes256GcmAlg::new(key.into())
60            .decrypt_in_place(&nonce.0, associated_data, &mut buffer)
61            .map_err(|_| CryptoError::KeyDecrypt)?;
62        Ok(buffer)
63    }
64}
65
66/// A 192-bit XAES-256-GCM nonce.
67pub(crate) struct XAes256GcmNonce(Nonce<Xaes256GcmAlg>);
68
69impl XAes256GcmNonce {
70    /// Generates a fresh, cryptographically random nonce.
71    pub(crate) fn make() -> Self {
72        let mut rng = bitwarden_random::rng();
73        Self(Nonce::<Xaes256GcmAlg>::generate_from_rng(&mut rng))
74    }
75
76    /// Returns the raw nonce bytes.
77    pub(crate) fn as_bytes(&self) -> &[u8] {
78        self.0.as_slice()
79    }
80
81    /// Parses the nonce from a COSE message's unprotected `iv` header bytes.
82    fn from_cose_iv(iv: &[u8]) -> Result<Self, CryptoError> {
83        let nonce: [u8; NONCE_SIZE] = iv.try_into().map_err(|_| CryptoError::InvalidNonceLength)?;
84        Ok(Self(nonce.into()))
85    }
86}
87
88/// Parses the nonce from the unprotected `iv` header of a [`CoseEncrypt`] message.
89impl TryFrom<&CoseEncrypt> for XAes256GcmNonce {
90    type Error = CryptoError;
91
92    fn try_from(cose_encrypt: &CoseEncrypt) -> Result<Self, Self::Error> {
93        Self::from_cose_iv(cose_encrypt.unprotected.iv.as_slice())
94    }
95}
96
97/// Parses the nonce from the unprotected `iv` header of a [`CoseEncrypt0`] message.
98impl TryFrom<&CoseEncrypt0> for XAes256GcmNonce {
99    type Error = CryptoError;
100
101    fn try_from(cose_encrypt0: &CoseEncrypt0) -> Result<Self, Self::Error> {
102        Self::from_cose_iv(cose_encrypt0.unprotected.iv.as_slice())
103    }
104}
105
106pub(crate) struct XAes256GcmCiphertext {
107    encrypted_bytes: Vec<u8>,
108}
109
110impl XAes256GcmCiphertext {
111    pub(crate) fn encrypted_bytes(&self) -> &[u8] {
112        &self.encrypted_bytes
113    }
114}
115
116/// Wraps already-encrypted bytes (e.g. read from a COSE message) so they can be passed to
117/// [`XAes256Gcm::decrypt`](Aead::decrypt).
118impl From<Vec<u8>> for XAes256GcmCiphertext {
119    fn from(encrypted_bytes: Vec<u8>) -> Self {
120        Self { encrypted_bytes }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use coset::{CoseEncrypt0Builder, CoseEncryptBuilder, HeaderBuilder};
127
128    use super::*;
129
130    const NONCE: &[u8; NONCE_SIZE] = b"ABCDEFGHIJKLMNOPQRSTUVWX";
131    const PLAINTEXT: &[u8] = b"XAES-256-GCM";
132
133    fn nonce(bytes: &[u8]) -> XAes256GcmNonce {
134        XAes256GcmNonce::from_cose_iv(bytes).unwrap()
135    }
136
137    #[test]
138    fn test_c2sp_vectors() {
139        // https://c2sp.org/XAES-256-GCM: the two vectors exercise both CMAC subkey branches.
140        let vectors = [
141            (
142                [1u8; KEY_SIZE],
143                b"".as_slice(),
144                "ce546ef63c9cc60765923609b33a9a1974e96e52daf2fcf7075e2271",
145            ),
146            (
147                [3u8; KEY_SIZE],
148                b"c2sp.org/XAES-256-GCM".as_slice(),
149                "986ec1832593df5443a179437fd083bf3fdb41abd740a21f71eb769d",
150            ),
151        ];
152
153        for (key, associated_data, expected_ciphertext) in vectors {
154            let encrypted = XAes256Gcm::encrypt(&key, &nonce(NONCE), PLAINTEXT, associated_data);
155            assert_eq!(
156                encrypted.encrypted_bytes(),
157                hex::decode(expected_ciphertext).unwrap()
158            );
159        }
160    }
161
162    #[test]
163    fn test_make_nonce_has_correct_length() {
164        assert_eq!(XAes256GcmNonce::make().as_bytes().len(), 24);
165    }
166
167    #[test]
168    fn test_encrypt_decrypt_roundtrip() {
169        let key = [7u8; KEY_SIZE];
170        let nonce = XAes256GcmNonce::make();
171        let associated_data = b"authenticated data";
172        let encrypted = XAes256Gcm::encrypt(&key, &nonce, PLAINTEXT, associated_data);
173
174        let decrypted = XAes256Gcm::decrypt(&key, &nonce, &encrypted, associated_data).unwrap();
175        assert_eq!(decrypted, PLAINTEXT);
176    }
177
178    #[test]
179    fn test_authentication_failures() {
180        let key = [7u8; KEY_SIZE];
181        let original_nonce = nonce(NONCE);
182        let associated_data = b"authenticated data";
183        let encrypted = XAes256Gcm::encrypt(&key, &original_nonce, PLAINTEXT, associated_data);
184
185        assert!(matches!(
186            XAes256Gcm::decrypt(
187                &[8u8; KEY_SIZE],
188                &original_nonce,
189                &encrypted,
190                associated_data,
191            ),
192            Err(CryptoError::KeyDecrypt)
193        ));
194        assert!(matches!(
195            XAes256Gcm::decrypt(
196                &key,
197                &original_nonce,
198                &encrypted,
199                b"modified authenticated data",
200            ),
201            Err(CryptoError::KeyDecrypt)
202        ));
203
204        let mut modified_ciphertext = encrypted.encrypted_bytes().to_vec();
205        modified_ciphertext[0] ^= 1;
206        assert!(matches!(
207            XAes256Gcm::decrypt(
208                &key,
209                &original_nonce,
210                &XAes256GcmCiphertext::from(modified_ciphertext),
211                associated_data,
212            ),
213            Err(CryptoError::KeyDecrypt)
214        ));
215
216        let mut wrong_nonce = *NONCE;
217        wrong_nonce[0] ^= 1;
218        assert!(matches!(
219            XAes256Gcm::decrypt(&key, &nonce(&wrong_nonce), &encrypted, associated_data),
220            Err(CryptoError::KeyDecrypt)
221        ));
222    }
223
224    #[test]
225    fn test_rejects_malformed_cose_ivs() {
226        let malformed_iv = vec![0; NONCE_SIZE - 1];
227        let cose_encrypt = CoseEncryptBuilder::new()
228            .unprotected(HeaderBuilder::new().iv(malformed_iv.clone()).build())
229            .build();
230        let cose_encrypt0 = CoseEncrypt0Builder::new()
231            .unprotected(HeaderBuilder::new().iv(malformed_iv).build())
232            .build();
233
234        assert!(matches!(
235            XAes256GcmNonce::try_from(&cose_encrypt),
236            Err(CryptoError::InvalidNonceLength)
237        ));
238        assert!(matches!(
239            XAes256GcmNonce::try_from(&cose_encrypt0),
240            Err(CryptoError::InvalidNonceLength)
241        ));
242    }
243}