Skip to main content

bitwarden_crypto/hazmat/symmetric_encryption/
xchacha20.rs

1//! # XChaCha20-Poly1305 operations
2//!
3//! Contains low level XChaCha20-Poly1305 operations used by the rest of the crate.
4//!
5//! In most cases you should use the [EncString][crate::EncString] with
6//! [KeyEncryptable][crate::KeyEncryptable] & [KeyDecryptable][crate::KeyDecryptable] instead.
7//!
8//! Note:
9//! XChaCha20-Poly1305 encrypts data, and authenticates both the cipher text and associated data.
10//! This does not provide key-commitment, and assumes there can only be one key. It has a large
11//! (192-bit) nonce, so a fresh random nonce can be safely generated per message without a
12//! nonce-reuse concern.
13//!
14//! If multiple keys are possible, a key-committing cipher such as XChaCha20Poly1305Blake3CTX should
15//! be used (`https://github.com/bitwarden/sdk-internal/pull/41`) to prevent invisible-salamander
16//! style attacks.
17//! `https://eprint.iacr.org/2019/016.pdf`
18//! `https://soatok.blog/2024/09/10/invisible-salamanders-are-not-what-you-think/`
19
20use aes::cipher::common::Generate;
21use chacha20poly1305::{
22    AeadCore, AeadInOut, KeyInit, XChaCha20Poly1305 as XChaCha20Poly1305Alg, aead::Nonce,
23};
24use coset::{CoseEncrypt, CoseEncrypt0};
25use typenum::Unsigned;
26
27use super::Aead;
28use crate::CryptoError;
29
30pub(crate) const NONCE_SIZE: usize = <XChaCha20Poly1305Alg as AeadCore>::NonceSize::USIZE;
31pub(crate) const KEY_SIZE: usize = 32;
32
33/// XChaCha20-Poly1305 authenticated encryption with associated data.
34///
35/// See the [module documentation](self) for the security caveats (no key-commitment) that apply to
36/// this cipher.
37pub(crate) struct XChaCha20Poly1305;
38
39impl Aead for XChaCha20Poly1305 {
40    type Key = [u8; KEY_SIZE];
41    type Ciphertext = XChaCha20Poly1305Ciphertext;
42    type Nonce = XChaCha20Poly1305Nonce;
43
44    fn encrypt(
45        key: &Self::Key,
46        nonce: &Self::Nonce,
47        plaintext: &[u8],
48        associated_data: &[u8],
49    ) -> Self::Ciphertext {
50        // This buffer contains the plaintext, that will be encrypted in-place
51        let mut buffer = plaintext.to_vec();
52        XChaCha20Poly1305Alg::new(key.into())
53            .encrypt_in_place(&nonce.0, associated_data, &mut buffer)
54            .expect("encryption failed");
55
56        XChaCha20Poly1305Ciphertext {
57            encrypted_bytes: buffer,
58        }
59    }
60
61    fn decrypt(
62        key: &Self::Key,
63        nonce: &Self::Nonce,
64        ciphertext: &Self::Ciphertext,
65        associated_data: &[u8],
66    ) -> Result<Vec<u8>, CryptoError> {
67        let mut buffer = ciphertext.encrypted_bytes().to_vec();
68        XChaCha20Poly1305Alg::new(key.into())
69            .decrypt_in_place(&nonce.0, associated_data, &mut buffer)
70            .map_err(|_| CryptoError::KeyDecrypt)?;
71        Ok(buffer)
72    }
73}
74
75/// A 192-bit XChaCha20-Poly1305 nonce.
76///
77/// XChaCha20-Poly1305 has a large (192-bit) nonce, so a fresh, cryptographically random nonce can
78/// be generated for every message with [`XChaCha20Poly1305Nonce::make`] without a practical risk of
79/// nonce reuse.
80pub(crate) struct XChaCha20Poly1305Nonce(Nonce<XChaCha20Poly1305Alg>);
81
82impl XChaCha20Poly1305Nonce {
83    /// Generates a fresh, cryptographically random nonce.
84    pub(crate) fn make() -> Self {
85        let mut rng = rand::rng();
86        XChaCha20Poly1305Nonce(Nonce::<XChaCha20Poly1305Alg>::generate_from_rng(&mut rng))
87    }
88
89    /// Returns the raw nonce bytes.
90    pub(crate) fn as_bytes(&self) -> &[u8] {
91        self.0.as_slice()
92    }
93
94    /// Parses the nonce from a COSE message's unprotected `iv` header bytes.
95    fn from_cose_iv(iv: &[u8]) -> Result<Self, CryptoError> {
96        let nonce: [u8; NONCE_SIZE] = iv.try_into().map_err(|_| CryptoError::InvalidNonceLength)?;
97        Ok(XChaCha20Poly1305Nonce(nonce.into()))
98    }
99}
100
101/// Parses the nonce from the unprotected `iv` header of a [`CoseEncrypt`] message.
102impl TryFrom<&CoseEncrypt> for XChaCha20Poly1305Nonce {
103    type Error = CryptoError;
104
105    fn try_from(cose_encrypt: &CoseEncrypt) -> Result<Self, Self::Error> {
106        Self::from_cose_iv(cose_encrypt.unprotected.iv.as_slice())
107    }
108}
109
110/// Parses the nonce from the unprotected `iv` header of a [`CoseEncrypt0`] message.
111impl TryFrom<&CoseEncrypt0> for XChaCha20Poly1305Nonce {
112    type Error = CryptoError;
113
114    fn try_from(cose_encrypt0: &CoseEncrypt0) -> Result<Self, Self::Error> {
115        Self::from_cose_iv(cose_encrypt0.unprotected.iv.as_slice())
116    }
117}
118
119pub(crate) struct XChaCha20Poly1305Ciphertext {
120    encrypted_bytes: Vec<u8>,
121}
122
123impl XChaCha20Poly1305Ciphertext {
124    pub(crate) fn encrypted_bytes(&self) -> &[u8] {
125        &self.encrypted_bytes
126    }
127}
128
129/// Wraps already-encrypted bytes (e.g. read from a COSE message) so they can be passed to
130/// [`XChaCha20Poly1305::decrypt`](Aead::decrypt).
131impl From<Vec<u8>> for XChaCha20Poly1305Ciphertext {
132    fn from(encrypted_bytes: Vec<u8>) -> Self {
133        XChaCha20Poly1305Ciphertext { encrypted_bytes }
134    }
135}
136
137mod tests {
138    #[cfg(test)]
139    use super::*;
140
141    #[test]
142    fn test_encrypt_decrypt_xchacha20() {
143        let key = [0u8; KEY_SIZE];
144        let nonce = XChaCha20Poly1305Nonce::make();
145        let plaintext_secret_data = b"My secret data";
146        let authenticated_data = b"My authenticated data";
147        let encrypted =
148            XChaCha20Poly1305::encrypt(&key, &nonce, plaintext_secret_data, authenticated_data);
149        let decrypted =
150            XChaCha20Poly1305::decrypt(&key, &nonce, &encrypted, authenticated_data).unwrap();
151        assert_eq!(plaintext_secret_data, decrypted.as_slice());
152    }
153
154    #[test]
155    fn test_make_nonce_has_correct_length() {
156        let nonce = XChaCha20Poly1305Nonce::make();
157        assert_eq!(nonce.as_bytes().len(), NONCE_SIZE);
158    }
159
160    #[test]
161    fn test_fails_when_ciphertext_changed() {
162        let key = [0u8; KEY_SIZE];
163        let nonce = XChaCha20Poly1305Nonce::make();
164        let plaintext_secret_data = b"My secret data";
165        let authenticated_data = b"My authenticated data";
166
167        let mut encrypted =
168            XChaCha20Poly1305::encrypt(&key, &nonce, plaintext_secret_data, authenticated_data);
169        encrypted.encrypted_bytes[0] = encrypted.encrypted_bytes[0].wrapping_add(1);
170        let result = XChaCha20Poly1305::decrypt(&key, &nonce, &encrypted, authenticated_data);
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_fails_when_associated_data_changed() {
176        let key = [0u8; KEY_SIZE];
177        let nonce = XChaCha20Poly1305Nonce::make();
178        let plaintext_secret_data = b"My secret data";
179        let mut authenticated_data = b"My authenticated data".to_vec();
180
181        let encrypted = XChaCha20Poly1305::encrypt(
182            &key,
183            &nonce,
184            plaintext_secret_data,
185            authenticated_data.as_slice(),
186        );
187        authenticated_data[0] = authenticated_data[0].wrapping_add(1);
188        let result =
189            XChaCha20Poly1305::decrypt(&key, &nonce, &encrypted, authenticated_data.as_slice());
190        assert!(result.is_err());
191    }
192
193    #[test]
194    fn test_fails_when_nonce_changed() {
195        let key = [0u8; KEY_SIZE];
196        let nonce = XChaCha20Poly1305Nonce::make();
197        let plaintext_secret_data = b"My secret data";
198        let authenticated_data = b"My authenticated data";
199
200        let encrypted =
201            XChaCha20Poly1305::encrypt(&key, &nonce, plaintext_secret_data, authenticated_data);
202        // Decrypting with a different (freshly generated) nonce must fail.
203        let other_nonce = XChaCha20Poly1305Nonce::make();
204        let result = XChaCha20Poly1305::decrypt(&key, &other_nonce, &encrypted, authenticated_data);
205        assert!(result.is_err());
206    }
207}