Skip to main content

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 aes::cipher::common::Generate;
18use chacha20poly1305::{AeadCore, AeadInOut, KeyInit, XChaCha20Poly1305, aead::Nonce};
19use typenum::Unsigned;
20
21use crate::CryptoError;
22
23pub(crate) const NONCE_SIZE: usize = <XChaCha20Poly1305 as AeadCore>::NonceSize::USIZE;
24pub(crate) const KEY_SIZE: usize = 32;
25
26pub(crate) struct XChaCha20Poly1305Ciphertext {
27    nonce: Nonce<XChaCha20Poly1305>,
28    encrypted_bytes: Vec<u8>,
29}
30
31impl XChaCha20Poly1305Ciphertext {
32    pub(crate) fn nonce(&self) -> [u8; NONCE_SIZE] {
33        self.nonce.into()
34    }
35
36    pub(crate) fn encrypted_bytes(&self) -> &[u8] {
37        &self.encrypted_bytes
38    }
39}
40
41pub(crate) fn encrypt_xchacha20_poly1305(
42    key: &[u8; KEY_SIZE],
43    plaintext_secret_data: &[u8],
44    associated_data: &[u8],
45) -> XChaCha20Poly1305Ciphertext {
46    let rng = rand::rng();
47    encrypt_xchacha20_poly1305_internal(rng, key, plaintext_secret_data, associated_data)
48}
49
50fn encrypt_xchacha20_poly1305_internal(
51    mut rng: impl rand::CryptoRng,
52    key: &[u8; KEY_SIZE],
53    plaintext_secret_data: &[u8],
54    associated_data: &[u8],
55) -> XChaCha20Poly1305Ciphertext {
56    let nonce = Nonce::<XChaCha20Poly1305>::generate_from_rng(&mut rng);
57    // This buffer contains the plaintext, that will be encrypted in-place
58    let mut buffer = plaintext_secret_data.to_vec();
59    XChaCha20Poly1305::new(key.into())
60        .encrypt_in_place(&nonce, associated_data, &mut buffer)
61        .expect("encryption failed");
62
63    XChaCha20Poly1305Ciphertext {
64        nonce,
65        encrypted_bytes: buffer,
66    }
67}
68
69pub(crate) fn decrypt_xchacha20_poly1305(
70    nonce: &[u8; NONCE_SIZE],
71    key: &[u8; KEY_SIZE],
72    ciphertext: &[u8],
73    associated_data: &[u8],
74) -> Result<Vec<u8>, CryptoError> {
75    let mut buffer = ciphertext.to_vec();
76    XChaCha20Poly1305::new(key.into())
77        .decrypt_in_place(nonce.into(), associated_data, &mut buffer)
78        .map_err(|_| CryptoError::KeyDecrypt)?;
79    Ok(buffer)
80}
81
82mod tests {
83    #[cfg(test)]
84    use crate::xchacha20::*;
85
86    #[test]
87    fn test_encrypt_decrypt_xchacha20() {
88        let key = [0u8; KEY_SIZE];
89        let plaintext_secret_data = b"My secret data";
90        let authenticated_data = b"My authenticated data";
91        let encrypted = encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
92        let decrypted = decrypt_xchacha20_poly1305(
93            &encrypted.nonce.into(),
94            &key,
95            &encrypted.encrypted_bytes,
96            authenticated_data,
97        )
98        .unwrap();
99        assert_eq!(plaintext_secret_data, decrypted.as_slice());
100    }
101
102    #[test]
103    fn test_fails_when_ciphertext_changed() {
104        let key = [0u8; KEY_SIZE];
105        let plaintext_secret_data = b"My secret data";
106        let authenticated_data = b"My authenticated data";
107
108        let mut encrypted =
109            encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
110        encrypted.encrypted_bytes[0] = encrypted.encrypted_bytes[0].wrapping_add(1);
111        let result = decrypt_xchacha20_poly1305(
112            &encrypted.nonce.into(),
113            &key,
114            &encrypted.encrypted_bytes,
115            authenticated_data,
116        );
117        assert!(result.is_err());
118    }
119
120    #[test]
121    fn test_fails_when_associated_data_changed() {
122        let key = [0u8; KEY_SIZE];
123        let plaintext_secret_data = b"My secret data";
124        let mut authenticated_data = b"My authenticated data".to_vec();
125
126        let encrypted =
127            encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data.as_slice());
128        authenticated_data[0] = authenticated_data[0].wrapping_add(1);
129        let result = decrypt_xchacha20_poly1305(
130            &encrypted.nonce.into(),
131            &key,
132            &encrypted.encrypted_bytes,
133            authenticated_data.as_slice(),
134        );
135        assert!(result.is_err());
136    }
137
138    #[test]
139    fn test_fails_when_nonce_changed() {
140        let key = [0u8; KEY_SIZE];
141        let plaintext_secret_data = b"My secret data";
142        let authenticated_data = b"My authenticated data";
143
144        let mut encrypted =
145            encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
146        encrypted.nonce[0] = encrypted.nonce[0].wrapping_add(1);
147        let result = decrypt_xchacha20_poly1305(
148            &encrypted.nonce.into(),
149            &key,
150            &encrypted.encrypted_bytes,
151            authenticated_data,
152        );
153        assert!(result.is_err());
154    }
155}