Skip to main content

crypto/chacha/
chacha20_blake3.rs

1#[cfg(feature = "zeroize")]
2use zeroize::{Zeroize, ZeroizeOnDrop};
3
4use crate::{Aead, AeadError, Hash, Hasher, StreamCipher, Xof, blake3::Blake3, chacha::ChaCha20Djb};
5
6/// ChaCha20-BLAKE3 AEAD (encrypt-then-MAC).
7///
8/// The master key and nonce are fed through a BLAKE3-based KDF to derive
9/// a ChaCha20 encryption key, a ChaCha20 nonce, and an authentication key.
10/// The plaintext is encrypted with ChaCha20 and then MACed with
11/// BLAKE3(keyed, aad || len(aad) || ciphertext || len(ciphertext)).
12///
13/// # Parameters
14///
15/// - Key: 256 bits (32 bytes)
16/// - Nonce: 256 bits (32 bytes)
17/// - Tag: 256 bits (32 bytes)
18///
19/// # Panics
20///
21/// [`encrypt_in_place`](Aead::encrypt_in_place) and
22/// [`decrypt_in_place`](Aead::decrypt_in_place) **panic** if the nonce is
23/// not exactly 32 bytes.
24///
25/// # Example
26///
27/// ```
28/// use crypto::{Aead, chacha::ChaCha20Blake3};
29///
30/// let key = [0xab; 32];
31/// let nonce = [0xcd; 32];
32/// let aad = b"associated data";
33/// let plaintext = b"hello world";
34///
35/// let cipher = ChaCha20Blake3::new(&key);
36///
37/// let mut buf = plaintext.to_vec();
38/// let tag = cipher.encrypt_in_place(&mut buf, &nonce, aad);
39///
40/// // buf now holds the ciphertext; tag is the 32-byte authentication tag.
41///
42/// cipher.decrypt_in_place(&mut buf, &nonce, aad, tag.as_ref())
43///     .expect("decryption failed");
44/// assert_eq!(&buf, plaintext);
45/// ```
46#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
47pub struct ChaCha20Blake3 {
48    key: [u8; 32],
49}
50
51impl ChaCha20Blake3 {
52    pub fn new(key: &[u8; 32]) -> Self {
53        return ChaCha20Blake3 {
54            key: *key,
55        };
56    }
57}
58
59impl Aead for ChaCha20Blake3 {
60    const KEY_SIZE: usize = 32;
61    const TAG_SIZE: usize = 32;
62    const NONCE_SIZE: usize = 32;
63
64    fn encrypt_in_place(&self, in_out: &mut [u8], nonce: &[u8], aad: &[u8]) -> Hash {
65        assert!(nonce.len() == 32, "nonce must be 32 bytes");
66
67        // kdf_out = BLAKE3.keyed(key, nonce)
68        let mut kdf_out = [0u8; 72];
69        let mut blake3_kdf = Blake3::new_keyed(&self.key);
70        blake3_kdf.update(nonce);
71        blake3_kdf.finalize_xof().squeeze(&mut kdf_out);
72
73        // chacha20_key = kdf_out[0..32]
74        // authentication_key = kdf_out[32..64]
75        // chacha20_nonce = kdf_out[64..72]
76        let chacha20_key: &[u8; 32] = &kdf_out[..32].try_into().unwrap();
77        let authentication_key: &[u8; 32] = &kdf_out[32..64].try_into().unwrap();
78        let chacha20_nonce: &[u8; 8] = &kdf_out[64..].try_into().unwrap();
79
80        ChaCha20Djb::new(chacha20_key, chacha20_nonce).xor_keystream(in_out);
81
82        // mac = BLAKE3.keyed(authentication_key, aad || aad.len_uint64_little_endian() || ciphertext || ciphertext.len_uint64_little_endian())
83        let mut mac_hasher = Blake3::new_keyed(authentication_key);
84        mac_hasher.update(aad);
85        mac_hasher.update(&(aad.len() as u64).to_le_bytes());
86        mac_hasher.update(in_out);
87        mac_hasher.update(&(in_out.len() as u64).to_le_bytes());
88        let tag = mac_hasher.sum();
89
90        #[cfg(feature = "zeroize")]
91        kdf_out.zeroize();
92
93        return tag;
94    }
95
96    fn decrypt_in_place(&self, in_out: &mut [u8], nonce: &[u8], aad: &[u8], tag: &[u8]) -> Result<(), AeadError> {
97        if nonce.len() != 32 {
98            return Err(AeadError::InvalidNonce);
99        }
100
101        // kdf_out = BLAKE3.keyed(key, nonce)
102        let mut kdf_out = [0u8; 72];
103        let mut blake3_kdf = Blake3::new_keyed(&self.key);
104        blake3_kdf.update(nonce);
105        blake3_kdf.finalize_xof().squeeze(&mut kdf_out);
106
107        // chacha20_key = kdf_out[0..32]
108        // authentication_key = kdf_out[32..64]
109        // chacha20_nonce = kdf_out[64..72]
110        let chacha20_key: &[u8; 32] = kdf_out[..32].try_into().unwrap();
111        let authentication_key: &[u8; 32] = kdf_out[32..64].try_into().unwrap();
112        let chacha20_nonce: &[u8; 8] = kdf_out[64..].try_into().unwrap();
113
114        let mut mac_hasher = Blake3::new_keyed(authentication_key);
115        mac_hasher.update(aad);
116        mac_hasher.update(&(aad.len() as u64).to_le_bytes());
117        mac_hasher.update(in_out);
118        mac_hasher.update(&(in_out.len() as u64).to_le_bytes());
119        let mac = mac_hasher.sum();
120
121        if !constant_time_eq::constant_time_eq(mac.as_ref(), tag) {
122            return Err(AeadError::InvalidCiphertext);
123        }
124
125        ChaCha20Djb::new(chacha20_key, chacha20_nonce).xor_keystream(in_out);
126
127        #[cfg(feature = "zeroize")]
128        kdf_out.zeroize();
129
130        return Ok(());
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use super::ChaCha20Blake3;
137    use crate::Aead;
138
139    struct Test {
140        plaintext: &'static str,
141        key: &'static str,
142        nonce: &'static str,
143        aad: &'static str,
144        ct: &'static str,
145    }
146
147    #[test]
148    fn aead_chacha20blake3_test_vectors() {
149        let tests = [
150            Test {
151                plaintext: "",
152                key: "0000000000000000000000000000000000000000000000000000000000000000",
153                nonce: "0000000000000000000000000000000000000000000000000000000000000000",
154                aad: "",
155                ct: "e074bcc1f324f0139dea37f8465aa7edf565f968aeae9bfa348c9a9c1c702ad2",
156            },
157            Test {
158                plaintext: "4368614368613230",
159                key: "0100000000000000000000000000000000000000000000000000000000000010",
160                nonce: "1000000000000000000000000000000000000000000000000000000000000001",
161                aad: "424c414b4533",
162                ct: "af4d5f3ac75f3753a764e5af1d3396f9f6f5b5ea94889665372f39a9ae7aa55aa3d77b69680bfe45",
163            },
164            Test {
165                plaintext: "b8f60975cd7057a003ac84df00d514624fe40cb7855c50dd6594f59b3a2580e5",
166                key: "3eb02a239a2a66de159b9bb5486ccc10a6f63ddf5862ef076650513372353622",
167                nonce: "719d34360dcf03dc7af6a4d1d9fd311b035cbc148241f1419f166537a5552aec",
168                aad: "c8d69ca92da6c5fd22f1805179fcd36cb7a9d45848fa346ba7118c2f34d23a48",
169                ct: "42c948ee385574606ce91ed09f4ef744c69b7101ef682aee8acdd14fd827499eefe6f43193680e3685f3fe0a702a0c19ceb8d7b539a02edec99783fdb9816eb5",
170            },
171        ];
172
173        for (i, test) in tests.iter().enumerate() {
174            let key: [u8; 32] = hex::decode(test.key).unwrap().try_into().unwrap();
175            let nonce: [u8; 32] = hex::decode(test.nonce).unwrap().try_into().unwrap();
176            let pt = hex::decode(test.plaintext).unwrap();
177            let aad = hex::decode(test.aad).unwrap();
178            let ct_tag = hex::decode(test.ct).unwrap();
179
180            let expected_ct = &ct_tag[..ct_tag.len() - 32];
181            let expected_tag = &ct_tag[ct_tag.len() - 32..];
182
183            let cipher = ChaCha20Blake3::new(&key);
184
185            let mut buf = pt.clone();
186            let tag = cipher.encrypt_in_place(&mut buf, &nonce, &aad);
187            assert_eq!(buf, expected_ct, "test {i}: ciphertext mismatch");
188            assert_eq!(tag.as_ref(), expected_tag, "test {i}: tag mismatch");
189
190            let mut buf2 = expected_ct.to_vec();
191            cipher.decrypt_in_place(&mut buf2, &nonce, &aad, expected_tag).unwrap();
192            assert_eq!(buf2, pt, "test {i}: plaintext mismatch");
193        }
194    }
195}