Skip to main content

crypto/chacha/
chacha20_poly1305.rs

1#[cfg(feature = "zeroize")]
2use zeroize::Zeroize;
3
4use super::{CHACHA20_IETF_MAX_LEN, ChaCha, hchacha20};
5use crate::{Aead, AeadError, Hash, StreamCipher, bytes::Bytes, poly1305::Poly1305};
6
7/// ChaCha20-Poly1305 AEAD as specified in RFC 8439.
8///
9/// # Parameters
10///
11/// - Key: 256 bits (32 bytes)
12/// - Nonce: 96 bits (12 bytes)
13/// - Tag: 128 bits (16 bytes)
14///
15/// # Panics
16///
17/// [`encrypt_in_place`](Aead::encrypt_in_place) panics if `in_out` exceeds
18/// [`CHACHA20_IETF_MAX_LEN`] bytes (~256 GiB), the maximum allowed by RFC 8439's 32-bit block
19/// counter. [`decrypt_in_place`](Aead::decrypt_in_place) returns
20/// [`AeadError::InvalidCiphertext`] in that case instead.
21#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
22pub struct ChaCha20Poly1305 {
23    key: [u8; 32],
24}
25
26impl ChaCha20Poly1305 {
27    /// Creates a new AEAD instance from a 32-byte key.
28    pub fn new(key: &[u8; 32]) -> ChaCha20Poly1305 {
29        return ChaCha20Poly1305 {
30            key: *key,
31        };
32    }
33
34    /// Generates the one-time Poly1305 key using ChaCha20 with counter=0.
35    #[inline]
36    fn poly1305_key_gen(&self, nonce: &[u8; 12]) -> ([u8; 32], ChaCha<20, true>) {
37        let mut cipher = ChaCha::<20, true>::new(&self.key, nonce);
38        cipher.set_counter(0);
39        let mut block = [0u8; 64];
40        cipher.xor_keystream(&mut block);
41        let mut key = [0u8; 32];
42        key.copy_from_slice(&block[..32]);
43        #[cfg(feature = "zeroize")]
44        block.zeroize();
45        return (key, cipher);
46    }
47}
48
49impl Aead for ChaCha20Poly1305 {
50    const KEY_SIZE: usize = 32;
51    const TAG_SIZE: usize = 16;
52    const NONCE_SIZE: usize = 12;
53
54    fn encrypt_in_place(&self, in_out: &mut [u8], nonce: &[u8], aad: &[u8]) -> Hash {
55        assert!(
56            in_out.len() as u64 <= CHACHA20_IETF_MAX_LEN,
57            "ChaCha20-Poly1305 plaintext exceeds maximum allowed length (RFC 8439 limit)"
58        );
59
60        let nonce: &[u8; 12] = nonce.try_into().expect("nonce must be 12 bytes");
61        #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))]
62        let (mut poly1305_key, mut cipher) = self.poly1305_key_gen(nonce);
63
64        cipher.set_counter(1);
65        cipher.xor_keystream(in_out);
66
67        let mut mac = Poly1305::new(&poly1305_key);
68        #[cfg(feature = "zeroize")]
69        poly1305_key.zeroize();
70        update_poly1305_padded(&mut mac, aad);
71        update_poly1305_padded(&mut mac, in_out);
72        mac.update(&(aad.len() as u64).to_le_bytes());
73        mac.update(&(in_out.len() as u64).to_le_bytes());
74        let tag_bytes = mac.finalize();
75
76        let mut tag = Hash(Bytes::<64>::with_length(16));
77        tag.as_mut().copy_from_slice(&tag_bytes);
78        return tag;
79    }
80
81    fn decrypt_in_place(&self, in_out: &mut [u8], nonce: &[u8], aad: &[u8], tag: &[u8]) -> Result<(), AeadError> {
82        if in_out.len() as u64 > CHACHA20_IETF_MAX_LEN {
83            return Err(AeadError::InvalidCiphertext);
84        }
85        if tag.len() != Self::TAG_SIZE {
86            return Err(AeadError::InvalidCiphertext);
87        }
88        let nonce: &[u8; 12] = nonce.try_into().map_err(|_| AeadError::InvalidNonce)?;
89        #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))]
90        let (mut poly1305_key, mut cipher) = self.poly1305_key_gen(nonce);
91
92        let mut mac = Poly1305::new(&poly1305_key);
93        #[cfg(feature = "zeroize")]
94        poly1305_key.zeroize();
95        update_poly1305_padded(&mut mac, aad);
96        update_poly1305_padded(&mut mac, in_out);
97        mac.update(&(aad.len() as u64).to_le_bytes());
98        mac.update(&(in_out.len() as u64).to_le_bytes());
99        let computed = mac.finalize();
100
101        if !constant_time_eq::constant_time_eq(&computed, tag) {
102            return Err(AeadError::InvalidCiphertext);
103        }
104
105        cipher.set_counter(1);
106        cipher.xor_keystream(in_out);
107
108        return Ok(());
109    }
110}
111
112/// XChaCha20-Poly1305 AEAD (draft-irtf-cfrg-xchacha-03).
113///
114/// Extends ChaCha20-Poly1305 with a 24-byte (192-bit) nonce.
115/// Internally uses HChaCha20 to derive a subkey from the first 16 nonce bytes.
116///
117/// # Parameters
118///
119/// - Key: 256 bits (32 bytes)
120/// - Nonce: 192 bits (24 bytes)
121/// - Tag: 128 bits (16 bytes)
122///
123/// # Panics
124///
125/// [`encrypt_in_place`](Aead::encrypt_in_place) panics if `in_out` exceeds
126/// [`CHACHA20_IETF_MAX_LEN`] bytes (~256 GiB), the maximum allowed by the inner ChaCha20 IETF
127/// 32-bit block counter. [`decrypt_in_place`](Aead::decrypt_in_place) returns
128/// [`AeadError::InvalidCiphertext`] in that case instead.
129#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
130pub struct XChaCha20Poly1305 {
131    key: [u8; 32],
132}
133
134impl XChaCha20Poly1305 {
135    pub fn new(key: &[u8; 32]) -> XChaCha20Poly1305 {
136        return XChaCha20Poly1305 {
137            key: *key,
138        };
139    }
140
141    #[inline]
142    fn derive_subkey(&self, nonce: &[u8; 24]) -> ([u8; 32], [u8; 12]) {
143        let subkey = hchacha20(&self.key, nonce[..16].try_into().unwrap());
144        let mut ietf_nonce = [0u8; 12];
145        ietf_nonce[4..12].copy_from_slice(&nonce[16..24]);
146        return (subkey, ietf_nonce);
147    }
148}
149
150impl Aead for XChaCha20Poly1305 {
151    const KEY_SIZE: usize = 32;
152    const TAG_SIZE: usize = 16;
153    const NONCE_SIZE: usize = 24;
154
155    fn encrypt_in_place(&self, in_out: &mut [u8], nonce: &[u8], aad: &[u8]) -> Hash {
156        assert!(
157            in_out.len() as u64 <= CHACHA20_IETF_MAX_LEN,
158            "XChaCha20-Poly1305 plaintext exceeds maximum allowed length (RFC 8439 limit)"
159        );
160
161        let nonce: &[u8; 24] = nonce.try_into().expect("nonce must be 24 bytes");
162        #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))]
163        let (mut subkey, ietf_nonce) = self.derive_subkey(nonce);
164
165        let mut keygen = ChaCha::<20, true>::new(&subkey, &ietf_nonce);
166        keygen.set_counter(0);
167        let mut block = [0u8; 64];
168        keygen.xor_keystream(&mut block);
169        let mut otk = [0u8; 32];
170        otk.copy_from_slice(&block[..32]);
171        #[cfg(feature = "zeroize")]
172        block.zeroize();
173
174        let mut cipher = ChaCha::<20, true>::new(&subkey, &ietf_nonce);
175        cipher.set_counter(1);
176        cipher.xor_keystream(in_out);
177        #[cfg(feature = "zeroize")]
178        subkey.zeroize();
179
180        let mut mac = Poly1305::new(&otk);
181        #[cfg(feature = "zeroize")]
182        otk.zeroize();
183        update_poly1305_padded(&mut mac, aad);
184        update_poly1305_padded(&mut mac, in_out);
185        mac.update(&(aad.len() as u64).to_le_bytes());
186        mac.update(&(in_out.len() as u64).to_le_bytes());
187        let tag_bytes = mac.finalize();
188
189        let mut tag = Hash(Bytes::<64>::with_length(16));
190        tag.as_mut().copy_from_slice(&tag_bytes);
191        return tag;
192    }
193
194    fn decrypt_in_place(&self, in_out: &mut [u8], nonce: &[u8], aad: &[u8], tag: &[u8]) -> Result<(), AeadError> {
195        if in_out.len() as u64 > CHACHA20_IETF_MAX_LEN {
196            return Err(AeadError::InvalidCiphertext);
197        }
198        if tag.len() != Self::TAG_SIZE {
199            return Err(AeadError::InvalidCiphertext);
200        }
201        let nonce: &[u8; 24] = nonce.try_into().map_err(|_| AeadError::InvalidNonce)?;
202        #[cfg_attr(not(feature = "zeroize"), allow(unused_mut))]
203        let (mut subkey, ietf_nonce) = self.derive_subkey(nonce);
204
205        let mut keygen = ChaCha::<20, true>::new(&subkey, &ietf_nonce);
206        keygen.set_counter(0);
207        let mut block = [0u8; 64];
208        keygen.xor_keystream(&mut block);
209        let mut otk = [0u8; 32];
210        otk.copy_from_slice(&block[..32]);
211        #[cfg(feature = "zeroize")]
212        block.zeroize();
213
214        let mut cipher = ChaCha::<20, true>::new(&subkey, &ietf_nonce);
215        cipher.set_counter(1);
216        #[cfg(feature = "zeroize")]
217        subkey.zeroize();
218
219        let mut mac = Poly1305::new(&otk);
220        update_poly1305_padded(&mut mac, aad);
221        update_poly1305_padded(&mut mac, in_out);
222        mac.update(&(aad.len() as u64).to_le_bytes());
223        mac.update(&(in_out.len() as u64).to_le_bytes());
224        let computed = mac.finalize();
225        #[cfg(feature = "zeroize")]
226        otk.zeroize();
227
228        if !constant_time_eq::constant_time_eq(&computed, tag) {
229            in_out.fill(0);
230            return Err(AeadError::InvalidCiphertext);
231        }
232
233        cipher.xor_keystream(in_out);
234
235        return Ok(());
236    }
237}
238
239#[inline]
240pub(crate) fn update_poly1305_padded(mac: &mut Poly1305, data: &[u8]) {
241    mac.update(data);
242    let rem = data.len() % 16;
243    if rem != 0 {
244        let pad = [0u8; 15];
245        mac.update(&pad[..16 - rem]);
246    }
247}
248
249#[cfg(test)]
250mod test {
251    use super::ChaCha20Poly1305;
252    use crate::Aead;
253
254    /// RFC 8439 Appendix A.4: Poly1305 Key Generation Using ChaCha20.
255    #[test]
256    fn poly1305_key_gen_vectors() {
257        struct KeyGenTest {
258            key_hex: &'static str,
259            nonce_hex: &'static str,
260            expected_otk_hex: &'static str,
261        }
262
263        let tests = [
264            KeyGenTest {
265                key_hex: "0000000000000000000000000000000000000000000000000000000000000000",
266                nonce_hex: "000000000000000000000000",
267                expected_otk_hex: "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7",
268            },
269            KeyGenTest {
270                key_hex: "0000000000000000000000000000000000000000000000000000000000000001",
271                nonce_hex: "000000000000000000000002",
272                expected_otk_hex: "ecfa254f845f647473d3cb140da9e87606cb33066c447b87bc2666dde3fbb739",
273            },
274            KeyGenTest {
275                key_hex: "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
276                nonce_hex: "000000000000000000000002",
277                expected_otk_hex: "965e3bc6f9ec7ed9560808f4d229f94b137ff275ca9b3fcbdd59deaad23310ae",
278            },
279        ];
280
281        for (i, test) in tests.iter().enumerate() {
282            let key: [u8; 32] = hex::decode(test.key_hex).unwrap().try_into().unwrap();
283            let nonce: [u8; 12] = hex::decode(test.nonce_hex).unwrap().try_into().unwrap();
284
285            let ae = ChaCha20Poly1305::new(&key);
286            let (otk, _) = ae.poly1305_key_gen(&nonce);
287            let expected_otk = hex::decode(test.expected_otk_hex).unwrap();
288
289            assert_eq!(otk.as_slice(), expected_otk.as_slice(), "key gen test [{i}] failed");
290        }
291    }
292
293    /// RFC 8439 Appendix A.5: ChaCha20-Poly1305 AEAD Decryption.
294    #[test]
295    fn aead_decrypt_test() {
296        let key: [u8; 32] = hex::decode("1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0")
297            .unwrap()
298            .try_into()
299            .unwrap();
300        let nonce: [u8; 12] = hex::decode("000000000102030405060708").unwrap().try_into().unwrap();
301        let aad = hex::decode("f33388860000000000004e91").unwrap();
302
303        let ciphertext = hex::decode(concat!(
304            "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2",
305            "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf",
306            "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855",
307            "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4",
308            "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e",
309            "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a",
310            "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10",
311            "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29",
312            "a6ad5cb4022b02709b",
313        ))
314        .unwrap();
315
316        let tag = hex::decode("eead9d67890cbb22392336fea1851f38").unwrap();
317
318        let ae = ChaCha20Poly1305::new(&key);
319
320        let mut decrypted = ciphertext.clone();
321        ae.decrypt_in_place(&mut decrypted, &nonce, &aad, &tag).unwrap();
322
323        let expected_plaintext = hex::decode(concat!(
324            "496e7465726e65742d4472616674732061726520647261667420646f63756d65",
325            "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d",
326            "6f6e74687320616e64206d617920626520757064617465642c207265706c6163",
327            "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65",
328            "6e747320617420616e792074696d652e20497420697320696e617070726f7072",
329            "6961746520746f2075736520496e7465726e65742d4472616674732061732072",
330            "65666572656e6365206d6174657269616c206f7220746f206369746520746865",
331            "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67",
332            "726573732e2fe2809d",
333        ))
334        .unwrap();
335
336        assert_eq!(decrypted, expected_plaintext);
337    }
338
339    /// Round-trip: encrypt then decrypt.
340    #[test]
341    fn aead_roundtrip() {
342        let key: [u8; 32] = [0x55; 32];
343        let nonce: [u8; 12] = [0xaa; 12];
344        let aad = b"associated data";
345        let plaintext = b"hello, world!";
346
347        let ae = ChaCha20Poly1305::new(&key);
348
349        let mut ciphertext = plaintext.to_vec();
350        let tag = ae.encrypt_in_place(&mut ciphertext, &nonce, aad);
351
352        let mut decrypted = ciphertext.clone();
353        ae.decrypt_in_place(&mut decrypted, &nonce, aad, tag.as_ref()).unwrap();
354        assert_eq!(decrypted, plaintext);
355    }
356
357    /// Tampered tag should fail.
358    #[test]
359    fn aead_tampered_tag_fails() {
360        let key: [u8; 32] = [0x55; 32];
361        let nonce: [u8; 12] = [0xaa; 12];
362
363        let ae = ChaCha20Poly1305::new(&key);
364
365        let mut ciphertext = b"secret".to_vec();
366        let mut tag = ae.encrypt_in_place(&mut ciphertext, &nonce, b"");
367        tag.as_mut()[0] ^= 1;
368
369        let mut decrypted = ciphertext.clone();
370        let result = ae.decrypt_in_place(&mut decrypted, &nonce, b"", tag.as_ref());
371        assert!(result.is_err());
372    }
373
374    /// Tampered ciphertext should fail.
375    #[test]
376    fn aead_tampered_ciphertext_fails() {
377        let key: [u8; 32] = [0x55; 32];
378        let nonce: [u8; 12] = [0xaa; 12];
379
380        let ae = ChaCha20Poly1305::new(&key);
381
382        let mut ciphertext = b"secret".to_vec();
383        let tag = ae.encrypt_in_place(&mut ciphertext, &nonce, b"");
384
385        ciphertext[3] ^= 1;
386
387        let mut decrypted = ciphertext.clone();
388        let result = ae.decrypt_in_place(&mut decrypted, &nonce, b"", tag.as_ref());
389        assert!(result.is_err());
390    }
391
392    /// Wrong nonce should fail decryption.
393    #[test]
394    fn aead_wrong_nonce_fails() {
395        let key: [u8; 32] = [0x55; 32];
396
397        let ae = ChaCha20Poly1305::new(&key);
398
399        let mut ciphertext = b"secret".to_vec();
400        let tag = ae.encrypt_in_place(&mut ciphertext, &[0u8; 12], b"");
401
402        let mut decrypted = ciphertext.clone();
403        let result = ae.decrypt_in_place(&mut decrypted, &[1u8; 12], b"", tag.as_ref());
404        assert!(result.is_err());
405    }
406
407    // --- Wycheproof test vectors ---
408
409    #[test]
410    fn wycheproof_chacha20_poly1305_vectors() {
411        let data: serde_json::Value = serde_json::from_str(include_str!(
412            "../../testdata/wycheproof/testvectors_v1/chacha20_poly1305_test.json"
413        ))
414        .unwrap();
415        let mut valid_tested = 0u64;
416        let mut invalid_tested = 0u64;
417        for group in data["testGroups"].as_array().unwrap() {
418            if group["keySize"].as_u64() != Some(256) {
419                continue;
420            }
421            if group["ivSize"].as_u64() != Some(96) {
422                continue;
423            }
424            if group["tagSize"].as_u64() != Some(128) {
425                continue;
426            }
427            for test in group["tests"].as_array().unwrap() {
428                let key_hex = test["key"].as_str().unwrap();
429                let iv_hex = test["iv"].as_str().unwrap();
430                let msg_hex = test["msg"].as_str().unwrap();
431                let aad_hex = test["aad"].as_str().unwrap();
432                let ct_hex = test["ct"].as_str().unwrap();
433                let tag_hex = test["tag"].as_str().unwrap();
434                let result = test["result"].as_str().unwrap();
435
436                let key: [u8; 32] = hex::decode(key_hex).unwrap().try_into().unwrap();
437                let nonce: [u8; 12] = hex::decode(iv_hex).unwrap().try_into().unwrap();
438                let expected_ct = hex::decode(ct_hex).unwrap();
439                let expected_tag: [u8; 16] = hex::decode(tag_hex).unwrap().try_into().unwrap();
440                let pt = hex::decode(msg_hex).unwrap();
441                let aad = hex::decode(aad_hex).unwrap();
442
443                let cipher = ChaCha20Poly1305::new(&key);
444
445                if result == "valid" {
446                    let mut buf = pt.clone();
447                    let tag = cipher.encrypt_in_place(&mut buf, &nonce, &aad);
448                    assert_eq!(
449                        buf, expected_ct,
450                        "wycheproof ChaCha20-Poly1305 tcId={} ct mismatch",
451                        test["tcId"]
452                    );
453                    assert_eq!(
454                        tag.as_ref(),
455                        &expected_tag[..],
456                        "wycheproof ChaCha20-Poly1305 tcId={} tag mismatch",
457                        test["tcId"]
458                    );
459
460                    let mut buf2 = expected_ct.clone();
461                    cipher
462                        .decrypt_in_place(&mut buf2, &nonce, &aad, &expected_tag)
463                        .expect("wycheproof ChaCha20-Poly1305 decrypt failed");
464                    assert_eq!(buf2, pt, "wycheproof ChaCha20-Poly1305 tcId={} pt mismatch", test["tcId"]);
465                    valid_tested += 1;
466                } else {
467                    let mut buf = expected_ct.clone();
468                    let res = cipher.decrypt_in_place(&mut buf, &nonce, &aad, &expected_tag);
469                    assert!(
470                        res.is_err(),
471                        "wycheproof ChaCha20-Poly1305 tcId={} expected invalid but passed",
472                        test["tcId"]
473                    );
474                    invalid_tested += 1;
475                }
476            }
477        }
478        assert!(valid_tested > 0, "no valid ChaCha20-Poly1305 wycheproof tests were run");
479        assert!(invalid_tested > 0, "no invalid ChaCha20-Poly1305 wycheproof tests were run");
480    }
481}
482
483#[cfg(test)]
484mod xchacha20poly1305_tests {
485    use super::XChaCha20Poly1305;
486    use crate::{Aead, StreamCipher};
487
488    /// draft-irtf-cfrg-xchacha-03, Appendix A.3.1: AEAD_XCHACHA20_POLY1305
489    #[test]
490    fn aead_xchacha20poly1305_test_vector() {
491        let key: [u8; 32] = hex::decode("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
492            .unwrap()
493            .try_into()
494            .unwrap();
495        let nonce: [u8; 24] = hex::decode("404142434445464748494a4b4c4d4e4f5051525354555657")
496            .unwrap()
497            .try_into()
498            .unwrap();
499        let aad = hex::decode("50515253c0c1c2c3c4c5c6c7").unwrap();
500
501        let plaintext = hex::decode(concat!(
502            "4c616469657320616e642047656e746c",
503            "656d656e206f662074686520636c6173",
504            "73206f66202739393a20496620492063",
505            "6f756c64206f6666657220796f75206f",
506            "6e6c79206f6e652074697020666f7220",
507            "746865206675747572652c2073756e73",
508            "637265656e20776f756c642062652069",
509            "742e",
510        ))
511        .unwrap();
512
513        // Verify Poly1305 key derivation
514        let ae = XChaCha20Poly1305::new(&key);
515        let (subkey, _ietf_nonce) = ae.derive_subkey(&nonce);
516        // The draft gives the Poly1305 key (first 32 bytes of ChaCha20 block 0)
517        let mut keygen = crate::chacha::ChaCha::<20, true>::new(&subkey, &_ietf_nonce);
518        keygen.set_counter(0);
519        let mut block = [0u8; 64];
520        keygen.xor_keystream(&mut block);
521        let expected_otk = hex::decode("7b191f80f361f099094f6f4b8fb97df847cc6873a8f2b190dd73807183f907d5").unwrap();
522        assert_eq!(&block[..32], expected_otk.as_slice(), "Poly1305 key derivation failed");
523
524        // Encrypt
525        let mut ciphertext = plaintext.clone();
526        let tag = ae.encrypt_in_place(&mut ciphertext, &nonce, &aad);
527
528        let expected_ciphertext = hex::decode(concat!(
529            "bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb",
530            "731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452",
531            "2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9",
532            "21f9664c97637da9768812f615c68b13b52e",
533        ))
534        .unwrap();
535        assert_eq!(ciphertext, expected_ciphertext, "ciphertext mismatch");
536
537        let expected_tag = hex::decode("c0875924c1c7987947deafd8780acf49").unwrap();
538        assert_eq!(tag.as_ref(), expected_tag.as_slice(), "tag mismatch");
539
540        // Decrypt
541        let mut decrypted = ciphertext.clone();
542        ae.decrypt_in_place(&mut decrypted, &nonce, &aad, tag.as_ref()).unwrap();
543        assert_eq!(decrypted, plaintext);
544    }
545
546    #[test]
547    fn aead_xchacha20poly1305_roundtrip() {
548        let key: [u8; 32] = [0x55; 32];
549        let nonce: [u8; 24] = [0xaa; 24];
550        let aad = b"associated data";
551        let plaintext = b"hello, world!";
552
553        let ae = XChaCha20Poly1305::new(&key);
554
555        let mut ciphertext = plaintext.to_vec();
556        let tag = ae.encrypt_in_place(&mut ciphertext, &nonce, aad);
557
558        let mut decrypted = ciphertext.clone();
559        ae.decrypt_in_place(&mut decrypted, &nonce, aad, tag.as_ref()).unwrap();
560        assert_eq!(decrypted, plaintext);
561    }
562
563    #[test]
564    fn aead_xchacha20poly1305_tampered_tag_fails() {
565        let key: [u8; 32] = [0x55; 32];
566        let nonce: [u8; 24] = [0xaa; 24];
567
568        let ae = XChaCha20Poly1305::new(&key);
569
570        let mut ciphertext = b"secret".to_vec();
571        let mut tag = ae.encrypt_in_place(&mut ciphertext, &nonce, b"");
572        tag.as_mut()[0] ^= 1;
573
574        let mut decrypted = ciphertext.clone();
575        let result = ae.decrypt_in_place(&mut decrypted, &nonce, b"", tag.as_ref());
576        assert!(result.is_err());
577        assert!(decrypted.iter().all(|b| *b == 0));
578    }
579
580    #[test]
581    fn aead_xchacha20poly1305_wrong_nonce_fails() {
582        let key: [u8; 32] = [0x55; 32];
583
584        let ae = XChaCha20Poly1305::new(&key);
585
586        let mut ciphertext = b"secret".to_vec();
587        let tag = ae.encrypt_in_place(&mut ciphertext, &[0u8; 24], b"");
588
589        let mut decrypted = ciphertext.clone();
590        let result = ae.decrypt_in_place(&mut decrypted, &[1u8; 24], b"", tag.as_ref());
591        assert!(result.is_err());
592    }
593
594    // --- Wycheproof test vectors ---
595
596    #[test]
597    fn wycheproof_xchacha20_poly1305_vectors() {
598        let data: serde_json::Value = serde_json::from_str(include_str!(
599            "../../testdata/wycheproof/testvectors_v1/xchacha20_poly1305_test.json"
600        ))
601        .unwrap();
602        let mut valid_tested = 0u64;
603        let mut invalid_tested = 0u64;
604        for group in data["testGroups"].as_array().unwrap() {
605            if group["keySize"].as_u64() != Some(256) {
606                continue;
607            }
608            if group["ivSize"].as_u64() != Some(192) {
609                continue;
610            }
611            if group["tagSize"].as_u64() != Some(128) {
612                continue;
613            }
614            for test in group["tests"].as_array().unwrap() {
615                let key_hex = test["key"].as_str().unwrap();
616                let iv_hex = test["iv"].as_str().unwrap();
617                let msg_hex = test["msg"].as_str().unwrap();
618                let aad_hex = test["aad"].as_str().unwrap();
619                let ct_hex = test["ct"].as_str().unwrap();
620                let tag_hex = test["tag"].as_str().unwrap();
621                let result = test["result"].as_str().unwrap();
622
623                let key: [u8; 32] = hex::decode(key_hex).unwrap().try_into().unwrap();
624                let nonce: [u8; 24] = hex::decode(iv_hex).unwrap().try_into().unwrap();
625                let expected_ct = hex::decode(ct_hex).unwrap();
626                let expected_tag: [u8; 16] = hex::decode(tag_hex).unwrap().try_into().unwrap();
627                let pt = hex::decode(msg_hex).unwrap();
628                let aad = hex::decode(aad_hex).unwrap();
629
630                let cipher = XChaCha20Poly1305::new(&key);
631
632                if result == "valid" {
633                    let mut buf = pt.clone();
634                    let tag = cipher.encrypt_in_place(&mut buf, &nonce, &aad);
635                    assert_eq!(
636                        buf, expected_ct,
637                        "wycheproof XChaCha20-Poly1305 tcId={} ct mismatch",
638                        test["tcId"]
639                    );
640                    assert_eq!(
641                        tag.as_ref(),
642                        &expected_tag[..],
643                        "wycheproof XChaCha20-Poly1305 tcId={} tag mismatch",
644                        test["tcId"]
645                    );
646
647                    let mut buf2 = expected_ct.clone();
648                    cipher
649                        .decrypt_in_place(&mut buf2, &nonce, &aad, &expected_tag)
650                        .expect("wycheproof XChaCha20-Poly1305 decrypt failed");
651                    assert_eq!(buf2, pt, "wycheproof XChaCha20-Poly1305 tcId={} pt mismatch", test["tcId"]);
652                    valid_tested += 1;
653                } else {
654                    let mut buf = expected_ct.clone();
655                    let res = cipher.decrypt_in_place(&mut buf, &nonce, &aad, &expected_tag);
656                    assert!(
657                        res.is_err(),
658                        "wycheproof XChaCha20-Poly1305 tcId={} expected invalid but passed",
659                        test["tcId"]
660                    );
661                    invalid_tested += 1;
662                }
663            }
664        }
665        assert!(valid_tested > 0, "no valid XChaCha20-Poly1305 wycheproof tests were run");
666        assert!(invalid_tested > 0, "no invalid XChaCha20-Poly1305 wycheproof tests were run");
667    }
668}