bitwarden_crypto/
xchacha20.rs

1//! # XChaCha20Poly1305 operations
2//!
3//! Contains low level XChaCha20Poly1305 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//! XChaCha20Poly1305 encrypts data, and authenticates both the cipher text and associated
10//! data. This does not provide key-commitment, and assumes there can only be one key.
11//!
12//! If multiple keys are possible, a key-committing cipher such as
13//! XChaCha20Poly1305Blake3CTX should be used: `https://github.com/bitwarden/sdk-internal/pull/41` to prevent invisible-salamander style attacks.
14//! `https://eprint.iacr.org/2019/016.pdf`
15//! `https://soatok.blog/2024/09/10/invisible-salamanders-are-not-what-you-think/`
16
17use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, XChaCha20Poly1305};
18use generic_array::GenericArray;
19use rand::{CryptoRng, RngCore};
20use typenum::Unsigned;
21
22use crate::CryptoError;
23
24pub(crate) const NONCE_SIZE: usize = <XChaCha20Poly1305 as AeadCore>::NonceSize::USIZE;
25pub(crate) const KEY_SIZE: usize = 32;
26
27pub(crate) struct XChaCha20Poly1305Ciphertext {
28    nonce: GenericArray<u8, <XChaCha20Poly1305 as AeadCore>::NonceSize>,
29    encrypted_bytes: Vec<u8>,
30}
31
32impl XChaCha20Poly1305Ciphertext {
33    pub(crate) fn nonce(&self) -> [u8; NONCE_SIZE] {
34        self.nonce.into()
35    }
36
37    pub(crate) fn encrypted_bytes(&self) -> &[u8] {
38        &self.encrypted_bytes
39    }
40}
41
42pub(crate) fn encrypt_xchacha20_poly1305(
43    key: &[u8; KEY_SIZE],
44    plaintext_secret_data: &[u8],
45    associated_data: &[u8],
46) -> XChaCha20Poly1305Ciphertext {
47    let rng = rand::thread_rng();
48    encrypt_xchacha20_poly1305_internal(rng, key, plaintext_secret_data, associated_data)
49}
50
51fn encrypt_xchacha20_poly1305_internal(
52    rng: impl RngCore + CryptoRng,
53    key: &[u8; KEY_SIZE],
54    plaintext_secret_data: &[u8],
55    associated_data: &[u8],
56) -> XChaCha20Poly1305Ciphertext {
57    let nonce = &XChaCha20Poly1305::generate_nonce(rng);
58    // This buffer contains the plaintext, that will be encrypted in-place
59    let mut buffer = plaintext_secret_data.to_vec();
60    XChaCha20Poly1305::new(GenericArray::from_slice(key))
61        .encrypt_in_place(nonce, associated_data, &mut buffer)
62        .expect("encryption failed");
63
64    XChaCha20Poly1305Ciphertext {
65        nonce: *nonce,
66        encrypted_bytes: buffer,
67    }
68}
69
70pub(crate) fn decrypt_xchacha20_poly1305(
71    nonce: &[u8; NONCE_SIZE],
72    key: &[u8; KEY_SIZE],
73    ciphertext: &[u8],
74    associated_data: &[u8],
75) -> Result<Vec<u8>, CryptoError> {
76    let mut buffer = ciphertext.to_vec();
77    XChaCha20Poly1305::new(GenericArray::from_slice(key))
78        .decrypt_in_place(
79            GenericArray::from_slice(nonce),
80            associated_data,
81            &mut buffer,
82        )
83        .map_err(|_| CryptoError::KeyDecrypt)?;
84    Ok(buffer)
85}
86
87mod tests {
88    #[cfg(test)]
89    use crate::xchacha20::*;
90
91    #[test]
92    fn test_encrypt_decrypt_xchacha20() {
93        let key = [0u8; KEY_SIZE];
94        let plaintext_secret_data = b"My secret data";
95        let authenticated_data = b"My authenticated data";
96        let encrypted = encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
97        let decrypted = decrypt_xchacha20_poly1305(
98            &encrypted.nonce.into(),
99            &key,
100            &encrypted.encrypted_bytes,
101            authenticated_data,
102        )
103        .unwrap();
104        assert_eq!(plaintext_secret_data, decrypted.as_slice());
105    }
106
107    #[test]
108    fn test_fails_when_ciphertext_changed() {
109        let key = [0u8; KEY_SIZE];
110        let plaintext_secret_data = b"My secret data";
111        let authenticated_data = b"My authenticated data";
112
113        let mut encrypted =
114            encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
115        encrypted.encrypted_bytes[0] = encrypted.encrypted_bytes[0].wrapping_add(1);
116        let result = decrypt_xchacha20_poly1305(
117            &encrypted.nonce.into(),
118            &key,
119            &encrypted.encrypted_bytes,
120            authenticated_data,
121        );
122        assert!(result.is_err());
123    }
124
125    #[test]
126    fn test_fails_when_associated_data_changed() {
127        let key = [0u8; KEY_SIZE];
128        let plaintext_secret_data = b"My secret data";
129        let mut authenticated_data = b"My authenticated data".to_vec();
130
131        let encrypted =
132            encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data.as_slice());
133        authenticated_data[0] = authenticated_data[0].wrapping_add(1);
134        let result = decrypt_xchacha20_poly1305(
135            &encrypted.nonce.into(),
136            &key,
137            &encrypted.encrypted_bytes,
138            authenticated_data.as_slice(),
139        );
140        assert!(result.is_err());
141    }
142
143    #[test]
144    fn test_fails_when_nonce_changed() {
145        let key = [0u8; KEY_SIZE];
146        let plaintext_secret_data = b"My secret data";
147        let authenticated_data = b"My authenticated data";
148
149        let mut encrypted =
150            encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
151        encrypted.nonce[0] = encrypted.nonce[0].wrapping_add(1);
152        let result = decrypt_xchacha20_poly1305(
153            &encrypted.nonce.into(),
154            &key,
155            &encrypted.encrypted_bytes,
156            authenticated_data,
157        );
158        assert!(result.is_err());
159    }
160}