Skip to main content

dryoc/classic/
crypto_aead_xchacha20poly1305_ietf.rs

1//! # XChaCha20-Poly1305-IETF authenticated encryption
2//!
3//! Implements libsodium's `crypto_aead_xchacha20poly1305_ietf_*` functions.
4//! This construction authenticates optional additional data, appends the
5//! authentication tag in combined mode, and uses 192-bit public nonces.
6//!
7//! ## Compatibility note
8//!
9//! This module follows libsodium's XChaCha20-Poly1305-IETF API and message
10//! size limit. The `_ietf` suffix refers to the RFC 8439 AEAD layout and
11//! Poly1305 input format; libsodium's XChaCha implementation uses an
12//! extended-counter XChaCha20 stream so it can support larger individual
13//! messages than plain ChaCha20-Poly1305-IETF.
14//!
15//! ## Classic API example
16//!
17//! ```
18//! use dryoc::classic::crypto_aead_xchacha20poly1305_ietf::*;
19//! use dryoc::constants::CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES;
20//! use dryoc::types::*;
21//!
22//! let key = crypto_aead_xchacha20poly1305_ietf_keygen();
23//! let nonce = Nonce::generate();
24//! let message = b"hello";
25//! let aad = b"metadata";
26//!
27//! let mut ciphertext = vec![0u8; message.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
28//! crypto_aead_xchacha20poly1305_ietf_encrypt(&mut ciphertext, message, Some(aad), &nonce, &key)
29//!     .expect("encrypt failed");
30//!
31//! let mut decrypted = vec![0u8; message.len()];
32//! crypto_aead_xchacha20poly1305_ietf_decrypt(
33//!     &mut decrypted,
34//!     &ciphertext,
35//!     Some(aad),
36//!     &nonce,
37//!     &key,
38//! )
39//! .expect("decrypt failed");
40//!
41//! assert_eq!(message, decrypted.as_slice());
42//! ```
43
44use chacha20::ChaCha20Legacy;
45use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
46use subtle::ConstantTimeEq;
47use zeroize::Zeroize;
48
49use crate::classic::crypto_core::{HChaCha20Key, crypto_core_hchacha20};
50use crate::constants::{
51    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
52    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_MESSAGEBYTES_MAX,
53    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES, CRYPTO_CORE_HCHACHA20_INPUTBYTES,
54};
55use crate::error::Error;
56use crate::poly1305::{Key as Poly1305Key, Poly1305};
57use crate::rng::copy_randombytes;
58use crate::types::*;
59use crate::utils::pad16;
60
61/// Authentication tag for XChaCha20-Poly1305-IETF AEAD.
62pub type Mac = [u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
63/// Public nonce for XChaCha20-Poly1305-IETF AEAD.
64pub type Nonce = [u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES];
65/// Secret key for XChaCha20-Poly1305-IETF AEAD.
66pub type Key = [u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES];
67
68const PAD0: [u8; 16] = [0u8; 16];
69
70/// In-place variant of [`crypto_aead_xchacha20poly1305_ietf_keygen`].
71pub fn crypto_aead_xchacha20poly1305_ietf_keygen_inplace(key: &mut Key) {
72    copy_randombytes(key)
73}
74
75/// Generates a random key using [`copy_randombytes`].
76pub fn crypto_aead_xchacha20poly1305_ietf_keygen() -> Key {
77    Key::generate()
78}
79
80fn validate_message_len(message_len: usize) -> Result<(), Error> {
81    if message_len > CRYPTO_AEAD_XCHACHA20POLY1305_IETF_MESSAGEBYTES_MAX {
82        Err(length_error!(
83            crate::ErrorContext::Message,
84            message_len,
85            max CRYPTO_AEAD_XCHACHA20POLY1305_IETF_MESSAGEBYTES_MAX
86        ))
87    } else {
88        Ok(())
89    }
90}
91
92fn validate_output_len(
93    output_len: usize,
94    expected_len: usize,
95    context: crate::ErrorContext,
96) -> Result<(), Error> {
97    if output_len != expected_len {
98        Err(length_error!(context, output_len, exact expected_len))
99    } else {
100        Ok(())
101    }
102}
103
104fn message_len_from_combined_len(
105    combined_len: usize,
106    context: crate::ErrorContext,
107) -> Result<usize, Error> {
108    if combined_len < CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES {
109        Err(length_error!(context, combined_len, min CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES))
110    } else {
111        let message_len = combined_len - CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES;
112        validate_message_len(message_len)?;
113        Ok(message_len)
114    }
115}
116
117fn chacha20_xietf_ext(nonce: &Nonce, key: &Key) -> ChaCha20Legacy {
118    let mut subkey = HChaCha20Key::default();
119    crypto_core_hchacha20(
120        &mut subkey,
121        ByteArray::as_array(&nonce[..CRYPTO_CORE_HCHACHA20_INPUTBYTES]),
122        key,
123        None,
124    );
125
126    // libsodium's `chacha20_ietf_ext` starts with IETF layout but allows the
127    // 32-bit block counter to overflow into the leading zero nonce word. With
128    // XChaCha's `0 || nonce_tail` derived nonce, that is equivalent to the
129    // original 64-bit-counter ChaCha20 layout with `nonce_tail`.
130    let mut legacy_nonce =
131        [0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES - CRYPTO_CORE_HCHACHA20_INPUTBYTES];
132    legacy_nonce.copy_from_slice(&nonce[CRYPTO_CORE_HCHACHA20_INPUTBYTES..]);
133
134    let mut chacha_key = subkey.into();
135    let chacha_nonce = legacy_nonce.into();
136    let cipher = ChaCha20Legacy::new(&chacha_key, &chacha_nonce);
137
138    subkey.zeroize();
139    chacha_key.zeroize();
140
141    cipher
142}
143
144fn poly1305_key(cipher: &mut ChaCha20Legacy) -> Poly1305Key {
145    let mut mac_key = Poly1305Key::new();
146    cipher.apply_keystream(&mut mac_key);
147    mac_key
148}
149
150fn compute_mac(mac: &mut Mac, mac_key: &mut Poly1305Key, ciphertext: &[u8], ad: &[u8]) {
151    let mut state = Poly1305::new(mac_key);
152    mac_key.zeroize();
153
154    state.update(ad);
155    state.update(&PAD0[..pad16(ad.len())]);
156    state.update(ciphertext);
157    state.update(&PAD0[..pad16(ciphertext.len())]);
158    state.update(&(ad.len() as u64).to_le_bytes());
159    state.update(&(ciphertext.len() as u64).to_le_bytes());
160    state.finalize(mac);
161}
162
163fn compute_mac_to_array(mac_key: &mut Poly1305Key, ciphertext: &[u8], ad: &[u8]) -> Mac {
164    let mut mac = Mac::default();
165    compute_mac(&mut mac, mac_key, ciphertext, ad);
166    mac
167}
168
169fn verify_mac(mac: &Mac, computed_mac: &Mac) -> Result<(), Error> {
170    if mac.ct_eq(computed_mac).unwrap_u8() == 1 {
171        Ok(())
172    } else {
173        Err(Error::AuthenticationFailed)
174    }
175}
176
177/// Detached version of [`crypto_aead_xchacha20poly1305_ietf_encrypt`].
178///
179/// Compatible with libsodium's
180/// `crypto_aead_xchacha20poly1305_ietf_encrypt_detached`.
181///
182/// # Errors
183///
184/// Returns an error if `message` exceeds the maximum supported length or
185/// `ciphertext.len()` does not equal `message.len()`.
186pub fn crypto_aead_xchacha20poly1305_ietf_encrypt_detached(
187    ciphertext: &mut [u8],
188    mac: &mut Mac,
189    message: &[u8],
190    associated_data: Option<&[u8]>,
191    nonce: &Nonce,
192    key: &Key,
193) -> Result<(), Error> {
194    validate_message_len(message.len())?;
195    validate_output_len(
196        ciphertext.len(),
197        message.len(),
198        crate::ErrorContext::Ciphertext,
199    )?;
200
201    let associated_data = associated_data.unwrap_or(&[]);
202    let mut cipher = chacha20_xietf_ext(nonce, key);
203    let mut mac_key = poly1305_key(&mut cipher);
204
205    ciphertext.copy_from_slice(message);
206    cipher.seek(64);
207    cipher.apply_keystream(ciphertext);
208
209    compute_mac(mac, &mut mac_key, ciphertext, associated_data);
210    Ok(())
211}
212
213/// In-place detached variant of
214/// [`crypto_aead_xchacha20poly1305_ietf_encrypt_detached`].
215///
216/// # Errors
217///
218/// Returns an error if `data` exceeds the maximum supported message length.
219pub fn crypto_aead_xchacha20poly1305_ietf_encrypt_detached_inplace(
220    data: &mut [u8],
221    mac: &mut Mac,
222    associated_data: Option<&[u8]>,
223    nonce: &Nonce,
224    key: &Key,
225) -> Result<(), Error> {
226    validate_message_len(data.len())?;
227
228    let associated_data = associated_data.unwrap_or(&[]);
229    let mut cipher = chacha20_xietf_ext(nonce, key);
230    let mut mac_key = poly1305_key(&mut cipher);
231
232    cipher.seek(64);
233    cipher.apply_keystream(data);
234
235    compute_mac(mac, &mut mac_key, data, associated_data);
236    Ok(())
237}
238
239/// Detached version of [`crypto_aead_xchacha20poly1305_ietf_decrypt`].
240///
241/// Compatible with libsodium's
242/// `crypto_aead_xchacha20poly1305_ietf_decrypt_detached`.
243///
244/// # Errors
245///
246/// Returns an error if `ciphertext` is too long, `message.len()` does not equal
247/// `ciphertext.len()`, or authentication fails.
248pub fn crypto_aead_xchacha20poly1305_ietf_decrypt_detached(
249    message: &mut [u8],
250    ciphertext: &[u8],
251    mac: &Mac,
252    associated_data: Option<&[u8]>,
253    nonce: &Nonce,
254    key: &Key,
255) -> Result<(), Error> {
256    validate_message_len(ciphertext.len())?;
257    validate_output_len(
258        message.len(),
259        ciphertext.len(),
260        crate::ErrorContext::Message,
261    )?;
262
263    let associated_data = associated_data.unwrap_or(&[]);
264    let mut cipher = chacha20_xietf_ext(nonce, key);
265    let mut mac_key = poly1305_key(&mut cipher);
266    let computed_mac = compute_mac_to_array(&mut mac_key, ciphertext, associated_data);
267
268    verify_mac(mac, &computed_mac)?;
269    message.copy_from_slice(ciphertext);
270    cipher.seek(64);
271    cipher.apply_keystream(message);
272    Ok(())
273}
274
275/// In-place detached variant of
276/// [`crypto_aead_xchacha20poly1305_ietf_decrypt_detached`].
277///
278/// # Errors
279///
280/// Returns an error if `data` exceeds the maximum supported message length or
281/// authentication fails.
282pub fn crypto_aead_xchacha20poly1305_ietf_decrypt_detached_inplace(
283    data: &mut [u8],
284    mac: &Mac,
285    associated_data: Option<&[u8]>,
286    nonce: &Nonce,
287    key: &Key,
288) -> Result<(), Error> {
289    validate_message_len(data.len())?;
290
291    let associated_data = associated_data.unwrap_or(&[]);
292    let mut cipher = chacha20_xietf_ext(nonce, key);
293    let mut mac_key = poly1305_key(&mut cipher);
294    let computed_mac = compute_mac_to_array(&mut mac_key, data, associated_data);
295
296    verify_mac(mac, &computed_mac)?;
297    cipher.seek(64);
298    cipher.apply_keystream(data);
299    Ok(())
300}
301
302/// Encrypts `message` with `nonce`, `key`, and optional associated data.
303///
304/// Compatible with libsodium's `crypto_aead_xchacha20poly1305_ietf_encrypt`.
305///
306/// # Errors
307///
308/// Returns an error if `message` exceeds the maximum supported length or
309/// `ciphertext` is not exactly one authentication tag longer than `message`.
310pub fn crypto_aead_xchacha20poly1305_ietf_encrypt(
311    ciphertext: &mut [u8],
312    message: &[u8],
313    associated_data: Option<&[u8]>,
314    nonce: &Nonce,
315    key: &Key,
316) -> Result<(), Error> {
317    validate_message_len(message.len())?;
318    validate_output_len(
319        ciphertext.len(),
320        message.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES,
321        crate::ErrorContext::Ciphertext,
322    )?;
323
324    let (ciphertext, mac) = ciphertext.split_at_mut(message.len());
325    let mac = MutByteArray::as_mut_array(mac);
326    crypto_aead_xchacha20poly1305_ietf_encrypt_detached(
327        ciphertext,
328        mac,
329        message,
330        associated_data,
331        nonce,
332        key,
333    )
334}
335
336/// Decrypts `ciphertext` with `nonce`, `key`, and optional associated data.
337///
338/// Compatible with libsodium's `crypto_aead_xchacha20poly1305_ietf_decrypt`.
339///
340/// # Errors
341///
342/// Returns an error if `ciphertext` is shorter than an authentication tag,
343/// `message` has the wrong length, or authentication fails.
344pub fn crypto_aead_xchacha20poly1305_ietf_decrypt(
345    message: &mut [u8],
346    ciphertext: &[u8],
347    associated_data: Option<&[u8]>,
348    nonce: &Nonce,
349    key: &Key,
350) -> Result<(), Error> {
351    let message_len =
352        message_len_from_combined_len(ciphertext.len(), crate::ErrorContext::Ciphertext)?;
353    validate_output_len(message.len(), message_len, crate::ErrorContext::Message)?;
354
355    let (ciphertext, mac) = ciphertext.split_at(message_len);
356    let mac = ByteArray::as_array(mac);
357    crypto_aead_xchacha20poly1305_ietf_decrypt_detached(
358        message,
359        ciphertext,
360        mac,
361        associated_data,
362        nonce,
363        key,
364    )
365}
366
367/// Encrypts `data` in place and appends the authentication tag.
368///
369/// The last [`CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES`] bytes are reserved
370/// for the tag and are ignored as plaintext input.
371///
372/// # Errors
373///
374/// Returns an error if `data` is shorter than an authentication tag or its
375/// plaintext portion exceeds the maximum supported message length.
376pub fn crypto_aead_xchacha20poly1305_ietf_encrypt_inplace(
377    data: &mut [u8],
378    associated_data: Option<&[u8]>,
379    nonce: &Nonce,
380    key: &Key,
381) -> Result<(), Error> {
382    let message_len = message_len_from_combined_len(data.len(), crate::ErrorContext::Data)?;
383    let (data, mac) = data.split_at_mut(message_len);
384    let mac = MutByteArray::as_mut_array(mac);
385    crypto_aead_xchacha20poly1305_ietf_encrypt_detached_inplace(
386        data,
387        mac,
388        associated_data,
389        nonce,
390        key,
391    )
392}
393
394/// Decrypts `data` in place after verifying the appended authentication tag.
395///
396/// After success, the first `data.len() -
397/// CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES` bytes contain the plaintext.
398///
399/// # Errors
400///
401/// Returns an error if `data` is shorter than an authentication tag or
402/// authentication fails.
403pub fn crypto_aead_xchacha20poly1305_ietf_decrypt_inplace(
404    data: &mut [u8],
405    associated_data: Option<&[u8]>,
406    nonce: &Nonce,
407    key: &Key,
408) -> Result<(), Error> {
409    let message_len = message_len_from_combined_len(data.len(), crate::ErrorContext::Data)?;
410    let (data, mac) = data.split_at_mut(message_len);
411    let mac = ByteArray::as_array(mac);
412    crypto_aead_xchacha20poly1305_ietf_decrypt_detached_inplace(
413        data,
414        mac,
415        associated_data,
416        nonce,
417        key,
418    )
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    const MESSAGE: &[u8] =
426        b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
427    const AD: &[u8] = &[
428        0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
429    ];
430    const KEY: Key = [
431        0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e,
432        0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d,
433        0x9e, 0x9f,
434    ];
435    const NONCE: Nonce = [
436        0xf2, 0x8a, 0x50, 0xa7, 0x8a, 0x7e, 0x23, 0xc9, 0xcb, 0xa6, 0x78, 0x34, 0x66, 0xf8, 0x03,
437        0x59, 0x0f, 0x04, 0xe9, 0x22, 0x31, 0xa3, 0x2d, 0x5d,
438    ];
439    const EXPECTED: &[u8] = &[
440        0x20, 0xf1, 0xae, 0x75, 0xe1, 0xe5, 0xe0, 0x00, 0x40, 0x29, 0x4f, 0x0f, 0xb1, 0x0e, 0xbb,
441        0x08, 0x10, 0xc5, 0x93, 0xc7, 0xdb, 0xa4, 0xec, 0x10, 0x4c, 0x1e, 0x5e, 0xf9, 0x50, 0x7f,
442        0xae, 0xef, 0x58, 0xfc, 0x28, 0x98, 0xbb, 0xd0, 0xe4, 0x7b, 0x2f, 0x53, 0x31, 0xfb, 0xc3,
443        0x67, 0xd3, 0xc2, 0x78, 0x4e, 0x36, 0x48, 0xce, 0x1e, 0xaa, 0x77, 0x87, 0xad, 0x18, 0x6d,
444        0xb2, 0x68, 0x5e, 0xe8, 0x9a, 0xe4, 0xd3, 0x44, 0x1f, 0x6e, 0xa0, 0xb2, 0x22, 0x4c, 0xd5,
445        0xa1, 0x34, 0x16, 0x1b, 0x55, 0x4d, 0x8b, 0x48, 0x35, 0x0b, 0x4a, 0xd4, 0x01, 0x15, 0xdb,
446        0x81, 0xea, 0x82, 0x09, 0x68, 0xe9, 0x43, 0x89, 0x2f, 0x2b, 0x80, 0x51, 0xcb, 0x5f, 0x7a,
447        0x86, 0x66, 0xe7, 0xe7, 0xef, 0x7f, 0x84, 0xc0, 0xa2, 0xf8, 0x0a, 0x12, 0xd0, 0x66, 0x80,
448        0xc8, 0xee, 0xbb, 0xd9, 0x30, 0x04, 0x10, 0x9d, 0xe8, 0x42,
449    ];
450
451    #[test]
452    fn test_known_answer() {
453        let mut ciphertext = vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
454        crypto_aead_xchacha20poly1305_ietf_encrypt(
455            &mut ciphertext,
456            MESSAGE,
457            Some(AD),
458            &NONCE,
459            &KEY,
460        )
461        .expect("encrypt");
462        assert_eq!(ciphertext, EXPECTED);
463
464        let mut decrypted = vec![0u8; MESSAGE.len()];
465        crypto_aead_xchacha20poly1305_ietf_decrypt(
466            &mut decrypted,
467            &ciphertext,
468            Some(AD),
469            &NONCE,
470            &KEY,
471        )
472        .expect("decrypt");
473        assert_eq!(decrypted, MESSAGE);
474    }
475
476    #[test]
477    fn test_detached_matches_combined() {
478        let mut combined = vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
479        crypto_aead_xchacha20poly1305_ietf_encrypt(&mut combined, MESSAGE, Some(AD), &NONCE, &KEY)
480            .expect("encrypt");
481
482        let mut detached = vec![0u8; MESSAGE.len()];
483        let mut mac = Mac::default();
484        crypto_aead_xchacha20poly1305_ietf_encrypt_detached(
485            &mut detached,
486            &mut mac,
487            MESSAGE,
488            Some(AD),
489            &NONCE,
490            &KEY,
491        )
492        .expect("detached encrypt");
493
494        assert_eq!(detached, combined[..MESSAGE.len()]);
495        assert_eq!(mac.as_slice(), &combined[MESSAGE.len()..]);
496    }
497
498    #[test]
499    fn test_failures_do_not_mutate_output() {
500        let mut ciphertext = vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
501        crypto_aead_xchacha20poly1305_ietf_encrypt(
502            &mut ciphertext,
503            MESSAGE,
504            Some(AD),
505            &NONCE,
506            &KEY,
507        )
508        .expect("encrypt");
509        ciphertext[0] ^= 1;
510
511        let mut decrypted = vec![0xa5; MESSAGE.len()];
512        let original = decrypted.clone();
513        crypto_aead_xchacha20poly1305_ietf_decrypt(
514            &mut decrypted,
515            &ciphertext,
516            Some(AD),
517            &NONCE,
518            &KEY,
519        )
520        .expect_err("expected auth failure");
521        assert_eq!(decrypted, original);
522    }
523
524    #[test]
525    fn test_empty_message_and_no_aad() {
526        let mut ciphertext = vec![0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
527        crypto_aead_xchacha20poly1305_ietf_encrypt(&mut ciphertext, &[], None, &NONCE, &KEY)
528            .expect("encrypt");
529
530        let mut decrypted = vec![];
531        crypto_aead_xchacha20poly1305_ietf_decrypt(&mut decrypted, &ciphertext, None, &NONCE, &KEY)
532            .expect("decrypt");
533        assert!(decrypted.is_empty());
534    }
535
536    #[test]
537    fn test_wrong_aad_fails() {
538        let mut ciphertext = vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
539        crypto_aead_xchacha20poly1305_ietf_encrypt(
540            &mut ciphertext,
541            MESSAGE,
542            Some(AD),
543            &NONCE,
544            &KEY,
545        )
546        .expect("encrypt");
547
548        let mut decrypted = vec![0u8; MESSAGE.len()];
549        crypto_aead_xchacha20poly1305_ietf_decrypt(
550            &mut decrypted,
551            &ciphertext,
552            Some(b"wrong aad"),
553            &NONCE,
554            &KEY,
555        )
556        .expect_err("expected auth failure");
557    }
558
559    #[test]
560    fn test_wrong_key_and_nonce_fail() {
561        let mut ciphertext = vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
562        crypto_aead_xchacha20poly1305_ietf_encrypt(
563            &mut ciphertext,
564            MESSAGE,
565            Some(AD),
566            &NONCE,
567            &KEY,
568        )
569        .expect("encrypt");
570
571        let mut wrong_key = KEY;
572        wrong_key[0] ^= 1;
573        let mut decrypted = vec![0u8; MESSAGE.len()];
574        crypto_aead_xchacha20poly1305_ietf_decrypt(
575            &mut decrypted,
576            &ciphertext,
577            Some(AD),
578            &NONCE,
579            &wrong_key,
580        )
581        .expect_err("expected wrong key auth failure");
582
583        let mut wrong_nonce = NONCE;
584        wrong_nonce[0] ^= 1;
585        crypto_aead_xchacha20poly1305_ietf_decrypt(
586            &mut decrypted,
587            &ciphertext,
588            Some(AD),
589            &wrong_nonce,
590            &KEY,
591        )
592        .expect_err("expected wrong nonce auth failure");
593    }
594
595    #[test]
596    fn test_wrong_mac_and_short_ciphertext_fail() {
597        let mut ciphertext = vec![0u8; MESSAGE.len()];
598        let mut mac = Mac::default();
599        crypto_aead_xchacha20poly1305_ietf_encrypt_detached(
600            &mut ciphertext,
601            &mut mac,
602            MESSAGE,
603            Some(AD),
604            &NONCE,
605            &KEY,
606        )
607        .expect("detached encrypt");
608
609        mac[0] ^= 1;
610        let mut decrypted = vec![0u8; MESSAGE.len()];
611        crypto_aead_xchacha20poly1305_ietf_decrypt_detached(
612            &mut decrypted,
613            &ciphertext,
614            &mac,
615            Some(AD),
616            &NONCE,
617            &KEY,
618        )
619        .expect_err("expected wrong mac auth failure");
620
621        let mut short_decrypted = vec![];
622        crypto_aead_xchacha20poly1305_ietf_decrypt(
623            &mut short_decrypted,
624            &[0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES - 1],
625            Some(AD),
626            &NONCE,
627            &KEY,
628        )
629        .expect_err("expected short ciphertext failure");
630    }
631
632    #[test]
633    fn test_inplace_roundtrip() {
634        let mut data = MESSAGE.to_vec();
635        data.resize(MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES, 0);
636        crypto_aead_xchacha20poly1305_ietf_encrypt_inplace(&mut data, Some(AD), &NONCE, &KEY)
637            .expect("inplace encrypt");
638        assert_eq!(data, EXPECTED);
639
640        crypto_aead_xchacha20poly1305_ietf_decrypt_inplace(&mut data, Some(AD), &NONCE, &KEY)
641            .expect("inplace decrypt");
642        assert_eq!(&data[..MESSAGE.len()], MESSAGE);
643    }
644
645    #[test]
646    fn test_xietf_ext_stream_crosses_ietf_counter_boundary() {
647        let mut cipher = chacha20_xietf_ext(&NONCE, &KEY);
648        cipher.seek(64u64 * u64::from(u32::MAX));
649
650        let mut stream = [0u8; 128];
651        cipher.apply_keystream(&mut stream);
652
653        assert_ne!(&stream[..64], &[0u8; 64]);
654        assert_ne!(&stream[64..], &[0u8; 64]);
655        assert_ne!(&stream[..64], &stream[64..]);
656    }
657
658    #[test]
659    fn test_inplace_failures_do_not_mutate_data() {
660        let mut data = MESSAGE.to_vec();
661        data.resize(MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES, 0);
662        crypto_aead_xchacha20poly1305_ietf_encrypt_inplace(&mut data, Some(AD), &NONCE, &KEY)
663            .expect("inplace encrypt");
664        data[MESSAGE.len()] ^= 1;
665        let original = data.clone();
666
667        crypto_aead_xchacha20poly1305_ietf_decrypt_inplace(&mut data, Some(AD), &NONCE, &KEY)
668            .expect_err("expected auth failure");
669        assert_eq!(data, original);
670
671        let mut detached = MESSAGE.to_vec();
672        let mut mac = Mac::default();
673        crypto_aead_xchacha20poly1305_ietf_encrypt_detached_inplace(
674            &mut detached,
675            &mut mac,
676            Some(AD),
677            &NONCE,
678            &KEY,
679        )
680        .expect("detached inplace encrypt");
681        mac[0] ^= 1;
682        let original = detached.clone();
683
684        crypto_aead_xchacha20poly1305_ietf_decrypt_detached_inplace(
685            &mut detached,
686            &mac,
687            Some(AD),
688            &NONCE,
689            &KEY,
690        )
691        .expect_err("expected detached auth failure");
692        assert_eq!(detached, original);
693    }
694
695    #[cfg(dryoc_native_tests)]
696    mod native_tests {
697        use super::*;
698
699        #[test]
700        fn test_sodiumoxide_interop() {
701            use sodiumoxide::crypto::aead::xchacha20poly1305_ietf::{
702                Key as SOKey, Nonce as SONonce, open, seal,
703            };
704
705            let so_key = SOKey::from_slice(&KEY).expect("key");
706            let so_nonce = SONonce::from_slice(&NONCE).expect("nonce");
707
708            let mut ciphertext =
709                vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
710            crypto_aead_xchacha20poly1305_ietf_encrypt(
711                &mut ciphertext,
712                MESSAGE,
713                Some(AD),
714                &NONCE,
715                &KEY,
716            )
717            .expect("encrypt");
718            let so_plaintext =
719                open(&ciphertext, Some(AD), &so_nonce, &so_key).expect("sodiumoxide open");
720            assert_eq!(so_plaintext, MESSAGE);
721
722            let so_ciphertext = seal(MESSAGE, Some(AD), &so_nonce, &so_key);
723            let mut plaintext = vec![0u8; MESSAGE.len()];
724            crypto_aead_xchacha20poly1305_ietf_decrypt(
725                &mut plaintext,
726                &so_ciphertext,
727                Some(AD),
728                &NONCE,
729                &KEY,
730            )
731            .expect("decrypt");
732            assert_eq!(plaintext, MESSAGE);
733        }
734
735        #[test]
736        fn test_counter_boundary_matches_libsodium_xchacha_stream() {
737            use libsodium_sys::crypto_stream_xchacha20_xor_ic;
738
739            let initial_counter = u64::from(u32::MAX);
740            let input = [0u8; 128];
741            let mut expected = [0u8; 128];
742            // SAFETY: All pointers are derived from initialized fixed-size
743            // buffers with lengths matching the arguments passed to
744            // libsodium. The key and nonce are exact-size test vectors.
745            unsafe {
746                assert_eq!(
747                    crypto_stream_xchacha20_xor_ic(
748                        expected.as_mut_ptr(),
749                        input.as_ptr(),
750                        input.len() as u64,
751                        NONCE.as_ptr(),
752                        initial_counter,
753                        KEY.as_ptr(),
754                    ),
755                    0
756                );
757            }
758
759            let mut actual = [0u8; 128];
760            let mut cipher = chacha20_xietf_ext(&NONCE, &KEY);
761            cipher.seek(64 * initial_counter);
762            cipher.apply_keystream(&mut actual);
763
764            assert_eq!(actual, expected);
765        }
766    }
767}