Skip to main content

dryoc/classic/
crypto_aead_chacha20poly1305_ietf.rs

1//! # ChaCha20-Poly1305-IETF authenticated encryption
2//!
3//! Implements libsodium's `crypto_aead_chacha20poly1305_ietf_*` functions.
4//! This construction authenticates optional additional data, appends the
5//! authentication tag in combined mode, and uses 96-bit public nonces as
6//! specified by RFC 8439. This is not the legacy 64-bit-nonce construction.
7//!
8//! ## Behavior on failure
9//!
10//! Every decrypt function checks the buffer lengths and verifies the tag
11//! before it writes anything, so any error, a length error or
12//! [`Error::AuthenticationFailed`](crate::Error::AuthenticationFailed), leaves
13//! the output (or, in place, `data`) exactly as it found it. The tag case is
14//! the one deliberate departure from libsodium, whose
15//! `crypto_aead_chacha20poly1305_ietf_decrypt*` zero the output buffer on a
16//! failed tag check, destroying the ciphertext when decrypting in place.
17//!
18//! ## Classic API example
19//!
20//! ```
21//! use dryoc::classic::crypto_aead_chacha20poly1305_ietf::*;
22//! use dryoc::constants::{
23//!     CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES,
24//! };
25//! use dryoc::types::*;
26//!
27//! let key = crypto_aead_chacha20poly1305_ietf_keygen();
28//! // This 96-bit nonce must be unique for every message encrypted with `key`.
29//! let nonce = [0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES];
30//! let message =
31//!     b"Our doubts are traitors, and make us lose the good we oft might win, by fearing to attempt.";
32//! let aad = b"metadata";
33//!
34//! let mut ciphertext = vec![0u8; message.len() + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
35//! crypto_aead_chacha20poly1305_ietf_encrypt(&mut ciphertext, message, Some(aad), &nonce, &key)
36//!     .expect("encrypt failed");
37//!
38//! let mut decrypted = vec![0u8; message.len()];
39//! crypto_aead_chacha20poly1305_ietf_decrypt(&mut decrypted, &ciphertext, Some(aad), &nonce, &key)
40//!     .expect("decrypt failed");
41//!
42//! assert_eq!(message, decrypted.as_slice());
43//! ```
44
45use crate::chacha20::ChaCha20;
46use crate::classic::crypto_aead_chacha20poly1305_impl::impl_chacha20poly1305_aead;
47use crate::constants::{
48    CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES,
49    CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX,
50    CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES,
51};
52use crate::types::*;
53
54/// Authentication tag for ChaCha20-Poly1305-IETF AEAD.
55pub type Mac = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
56/// Public nonce for ChaCha20-Poly1305-IETF AEAD.
57pub type Nonce = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES];
58/// Secret key for ChaCha20-Poly1305-IETF AEAD.
59pub type Key = [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES];
60
61impl_chacha20poly1305_aead! {
62    abytes: CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES,
63    // Bounds the message to fewer than `u32::MAX` blocks, so the IETF
64    // stream's 32-bit counter never wraps.
65    messagebytes_max: CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX,
66    stream: |nonce: &Nonce, key: &Key| ChaCha20::ietf(key, nonce, 0),
67    key: Key,
68    nonce: Nonce,
69    mac: Mac,
70
71    /// In-place variant of [`crypto_aead_chacha20poly1305_ietf_keygen`].
72    keygen_inplace: crypto_aead_chacha20poly1305_ietf_keygen_inplace,
73
74    /// Generates a random key using [`copy_randombytes`](crate::rng::copy_randombytes).
75    keygen: crypto_aead_chacha20poly1305_ietf_keygen,
76
77    /// Detached version of [`crypto_aead_chacha20poly1305_ietf_encrypt`].
78    ///
79    /// Compatible with libsodium's
80    /// `crypto_aead_chacha20poly1305_ietf_encrypt_detached`.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if `message` exceeds the maximum supported length or
85    /// `ciphertext.len()` does not equal `message.len()`.
86    encrypt_detached: crypto_aead_chacha20poly1305_ietf_encrypt_detached,
87
88    /// In-place detached variant of
89    /// [`crypto_aead_chacha20poly1305_ietf_encrypt_detached`].
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if `data` exceeds the maximum supported message length.
94    encrypt_detached_inplace: crypto_aead_chacha20poly1305_ietf_encrypt_detached_inplace,
95
96    /// Detached version of [`crypto_aead_chacha20poly1305_ietf_decrypt`].
97    ///
98    /// Compatible with libsodium's
99    /// `crypto_aead_chacha20poly1305_ietf_decrypt_detached`, except that a
100    /// failed tag check leaves `message` untouched (see [Behavior on
101    /// failure](self#behavior-on-failure)).
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if `ciphertext` is too long, `message.len()` does not equal
106    /// `ciphertext.len()`, or authentication fails.
107    decrypt_detached: crypto_aead_chacha20poly1305_ietf_decrypt_detached,
108
109    /// In-place detached variant of
110    /// [`crypto_aead_chacha20poly1305_ietf_decrypt_detached`]. On a failed tag
111    /// check `data` is left unchanged, so the ciphertext survives (libsodium
112    /// zeroes it; see [Behavior on failure](self#behavior-on-failure)).
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if `data` exceeds the maximum supported message length or
117    /// authentication fails.
118    decrypt_detached_inplace: crypto_aead_chacha20poly1305_ietf_decrypt_detached_inplace,
119
120    /// Encrypts `message` with `nonce`, `key`, and optional associated data.
121    ///
122    /// Compatible with libsodium's `crypto_aead_chacha20poly1305_ietf_encrypt`.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if `message` exceeds the maximum supported length or
127    /// `ciphertext` is not exactly one authentication tag longer than `message`.
128    encrypt: crypto_aead_chacha20poly1305_ietf_encrypt,
129
130    /// Decrypts `ciphertext` with `nonce`, `key`, and optional associated data.
131    ///
132    /// Compatible with libsodium's `crypto_aead_chacha20poly1305_ietf_decrypt`,
133    /// except that a failed tag check leaves `message` untouched (see
134    /// [Behavior on failure](self#behavior-on-failure)).
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if `ciphertext` is shorter than an authentication tag,
139    /// `message` has the wrong length, or authentication fails.
140    decrypt: crypto_aead_chacha20poly1305_ietf_decrypt,
141
142    /// Encrypts `data` in place and appends the authentication tag.
143    ///
144    /// The last [`CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES`] bytes are reserved
145    /// for the tag and are ignored as plaintext input.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if `data` is shorter than an authentication tag or its
150    /// plaintext portion exceeds the maximum supported message length.
151    encrypt_inplace: crypto_aead_chacha20poly1305_ietf_encrypt_inplace,
152
153    /// Decrypts `data` in place after verifying the appended authentication tag.
154    ///
155    /// After success, the first `data.len() -
156    /// CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES` bytes contain the plaintext.
157    /// On a failed tag check `data` is left unchanged, so the ciphertext
158    /// survives (libsodium zeroes it; see [Behavior on
159    /// failure](self#behavior-on-failure)).
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if `data` is shorter than an authentication tag or
164    /// authentication fails.
165    decrypt_inplace: crypto_aead_chacha20poly1305_ietf_decrypt_inplace,
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    #[cfg(dryoc_native_tests)]
172    use crate::classic::crypto_aead_chacha20poly1305_impl::test_util::check_matches_libsodium;
173    use crate::classic::crypto_aead_chacha20poly1305_impl::test_util::{
174        Aead, check_failures_leave_outputs_untouched,
175    };
176    use crate::error::{Error, LengthConstraint};
177    use crate::test_prelude::*;
178
179    #[test]
180    fn test_message_len_bound_is_ietf_max() {
181        const MAX: usize = CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX;
182        const ABYTES: usize = CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
183
184        assert!(matches!(
185            message_len_from_combined_len(MAX + ABYTES, crate::ErrorContext::Ciphertext),
186            Ok(len) if len == MAX
187        ));
188        if let Some(combined_len) = MAX.checked_add(ABYTES + 1) {
189            assert!(matches!(
190                message_len_from_combined_len(combined_len, crate::ErrorContext::Ciphertext),
191                Err(Error::InvalidLength {
192                    context: crate::ErrorContext::Message,
193                    actual,
194                    constraint: LengthConstraint::AtMost(max),
195                }) if actual == MAX + 1 && max == MAX
196            ));
197        }
198    }
199
200    const MESSAGE: &[u8] =
201        b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
202    const AD: &[u8] = &[
203        0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
204    ];
205    const KEY: Key = [
206        0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e,
207        0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d,
208        0x9e, 0x9f,
209    ];
210    const NONCE: Nonce = [
211        0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
212    ];
213
214    fn expected() -> Vec<u8> {
215        hex::decode(concat!(
216            "d31a8d34648e60db7b86afbc53ef7ec2",
217            "a4aded51296e08fea9e2b5a736ee62d6",
218            "3dbea45e8ca9671282fafb69da92728b",
219            "1a71de0a9e060b2905d6a5b67ecd3b36",
220            "92ddbd7f2d778b8c9803aee328091b58",
221            "fab324e4fad675945585808b4831d7bc",
222            "3ff4def08e4b7a9de576d26586cec64b",
223            "61161ae10b594f09e26a7e902ecbd0600691"
224        ))
225        .expect("valid test vector")
226    }
227
228    #[test]
229    fn test_rfc_8439_known_answer() {
230        let mut ciphertext = vec![0u8; MESSAGE.len() + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
231        crypto_aead_chacha20poly1305_ietf_encrypt(&mut ciphertext, MESSAGE, Some(AD), &NONCE, &KEY)
232            .expect("encrypt");
233        assert_eq!(ciphertext, expected());
234
235        let mut decrypted = vec![0u8; MESSAGE.len()];
236        crypto_aead_chacha20poly1305_ietf_decrypt(
237            &mut decrypted,
238            &ciphertext,
239            Some(AD),
240            &NONCE,
241            &KEY,
242        )
243        .expect("decrypt");
244        assert_eq!(decrypted, MESSAGE);
245    }
246
247    #[test]
248    fn test_detached_and_inplace_match_combined() {
249        let expected = expected();
250        let mut detached = MESSAGE.to_vec();
251        let mut mac = Mac::default();
252        crypto_aead_chacha20poly1305_ietf_encrypt_detached_inplace(
253            &mut detached,
254            &mut mac,
255            Some(AD),
256            &NONCE,
257            &KEY,
258        )
259        .expect("detached encrypt");
260        assert_eq!(detached, expected[..MESSAGE.len()]);
261        assert_eq!(mac, expected[MESSAGE.len()..]);
262
263        crypto_aead_chacha20poly1305_ietf_decrypt_detached_inplace(
264            &mut detached,
265            &mac,
266            Some(AD),
267            &NONCE,
268            &KEY,
269        )
270        .expect("detached decrypt");
271        assert_eq!(detached, MESSAGE);
272
273        let mut combined = MESSAGE.to_vec();
274        combined.resize(MESSAGE.len() + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, 0);
275        crypto_aead_chacha20poly1305_ietf_encrypt_inplace(&mut combined, Some(AD), &NONCE, &KEY)
276            .expect("in-place encrypt");
277        assert_eq!(combined, expected);
278    }
279
280    #[test]
281    fn test_empty_message_and_length_errors() {
282        let mut ciphertext = [0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
283        crypto_aead_chacha20poly1305_ietf_encrypt(&mut ciphertext, &[], None, &NONCE, &KEY)
284            .expect("empty encrypt");
285        crypto_aead_chacha20poly1305_ietf_decrypt(&mut [], &ciphertext, None, &NONCE, &KEY)
286            .expect("empty decrypt");
287
288        assert!(
289            crypto_aead_chacha20poly1305_ietf_decrypt(
290                &mut [],
291                &[0u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES - 1],
292                None,
293                &NONCE,
294                &KEY,
295            )
296            .is_err()
297        );
298        assert!(
299            crypto_aead_chacha20poly1305_ietf_encrypt(&mut [0u8; 1], &[], None, &NONCE, &KEY,)
300                .is_err()
301        );
302    }
303
304    /// The RFC 8439 state for `nonce`, as the scalar block function's input:
305    /// constants, key, then the counter word and the three nonce words. The
306    /// block function takes the block position as a 64-bit value over words
307    /// 12 and 13, so it overrides the first nonce word.
308    fn rfc8439_state(key: &Key, nonce: &Nonce) -> [u32; 16] {
309        let mut state = [0u32; 16];
310        state[..4].copy_from_slice(&crate::utils::SIGMA);
311        for (word, bytes) in state[4..12].iter_mut().zip(key.as_chunks::<4>().0) {
312            *word = u32::from_le_bytes(*bytes);
313        }
314        for (word, bytes) in state[13..].iter_mut().zip(nonce.as_chunks::<4>().0) {
315            *word = u32::from_le_bytes(*bytes);
316        }
317        state
318    }
319
320    /// One stream across the 32-bit counter boundary: 128 bytes from block
321    /// `u32::MAX` are block `u32::MAX` followed by block 0 of the nonce whose
322    /// first word is one higher (the carry libsodium's reference code makes;
323    /// with an all-`ff` nonce the packed 64-bit position wraps to zero).
324    #[test]
325    fn test_stream_crosses_ietf_counter_boundary() {
326        for nonce in [NONCE, [0xffu8; 12]] {
327            let state = rfc8439_state(&KEY, &nonce);
328            let nonce_word = u32::from_le_bytes(nonce[..4].try_into().unwrap());
329            let start = u64::from(u32::MAX) | (u64::from(nonce_word) << 32);
330            let next = u64::from(nonce_word.wrapping_add(1)) << 32;
331            assert_eq!(next, start.wrapping_add(1));
332
333            let mut expected = [0u8; 128];
334            let (first, second) = expected.split_at_mut(64);
335            crate::chacha20::scalar_block(&state, start, first.try_into().unwrap());
336            crate::chacha20::scalar_block(&state, next, second.try_into().unwrap());
337            assert_ne!(first, second);
338
339            let mut stream = [0u8; 128];
340            ChaCha20::ietf(&KEY, &nonce, u32::MAX).apply_keystream(&mut stream);
341            assert_eq!(stream, expected, "nonce {nonce:02x?}");
342        }
343    }
344
345    fn aead() -> Aead<Nonce> {
346        Aead {
347            encrypt_detached: crypto_aead_chacha20poly1305_ietf_encrypt_detached,
348            encrypt_detached_inplace: crypto_aead_chacha20poly1305_ietf_encrypt_detached_inplace,
349            decrypt_detached: crypto_aead_chacha20poly1305_ietf_decrypt_detached,
350            decrypt_detached_inplace: crypto_aead_chacha20poly1305_ietf_decrypt_detached_inplace,
351            encrypt: crypto_aead_chacha20poly1305_ietf_encrypt,
352            decrypt: crypto_aead_chacha20poly1305_ietf_decrypt,
353            encrypt_inplace: crypto_aead_chacha20poly1305_ietf_encrypt_inplace,
354            decrypt_inplace: crypto_aead_chacha20poly1305_ietf_decrypt_inplace,
355        }
356    }
357
358    #[test]
359    fn test_failures_leave_outputs_untouched() {
360        check_failures_leave_outputs_untouched(&aead(), &KEY, &NONCE);
361    }
362
363    /// libsodium's `crypto_stream_chacha20_ietf_xor_ic` accepts block
364    /// `u32::MAX` alone (one more block trips its misuse check), so the
365    /// final block is compared directly and the carry beyond it through the
366    /// legacy function, whose 64-bit counter occupies the same two words.
367    #[cfg(dryoc_native_tests)]
368    #[test]
369    fn test_final_counter_blocks_match_libsodium() {
370        use libc::c_ulonglong;
371        use libsodium_sys::{crypto_stream_chacha20_ietf_xor_ic, crypto_stream_chacha20_xor_ic};
372
373        crate::native_test_util::init();
374
375        for nonce in [NONCE, [0xffu8; 12]] {
376            let mut stream = [0u8; 128];
377            ChaCha20::ietf(&KEY, &nonce, u32::MAX).apply_keystream(&mut stream);
378
379            let mut expected = [0u8; 128];
380            let start = u64::from(u32::MAX)
381                | (u64::from(u32::from_le_bytes(nonce[..4].try_into().unwrap())) << 32);
382            // SAFETY: every buffer is valid for the length passed beside it;
383            // libsodium permits `c == m`.
384            let rc = unsafe {
385                crypto_stream_chacha20_xor_ic(
386                    expected.as_mut_ptr(),
387                    expected.as_ptr(),
388                    expected.len() as c_ulonglong,
389                    nonce[4..].as_ptr(),
390                    start,
391                    KEY.as_ptr(),
392                )
393            };
394            assert_eq!(rc, 0);
395            assert_eq!(stream, expected, "nonce {nonce:02x?}");
396
397            let mut final_block = [0u8; 64];
398            // SAFETY: as above.
399            let rc = unsafe {
400                crypto_stream_chacha20_ietf_xor_ic(
401                    final_block.as_mut_ptr(),
402                    final_block.as_ptr(),
403                    final_block.len() as c_ulonglong,
404                    nonce.as_ptr(),
405                    u32::MAX,
406                    KEY.as_ptr(),
407                )
408            };
409            assert_eq!(rc, 0);
410            assert_eq!(
411                stream[..64],
412                final_block,
413                "nonce {nonce:02x?}, ietf final block"
414            );
415        }
416    }
417
418    #[cfg(dryoc_native_tests)]
419    #[test]
420    fn test_matches_libsodium_detached_and_combined() {
421        crate::native_test_util::init();
422        check_matches_libsodium(
423            &aead(),
424            libsodium_sys::crypto_aead_chacha20poly1305_ietf_encrypt_detached,
425            &KEY,
426            &NONCE,
427        );
428    }
429
430    #[cfg(dryoc_native_tests)]
431    #[test]
432    fn test_libsodium_constants() {
433        use libsodium_sys::{
434            crypto_aead_chacha20poly1305_ietf_abytes, crypto_aead_chacha20poly1305_ietf_keybytes,
435            crypto_aead_chacha20poly1305_ietf_messagebytes_max,
436            crypto_aead_chacha20poly1305_ietf_npubbytes,
437            crypto_aead_chacha20poly1305_ietf_nsecbytes,
438        };
439
440        use crate::constants::CRYPTO_AEAD_CHACHA20POLY1305_IETF_NSECBYTES;
441
442        crate::native_test_util::init();
443
444        // SAFETY: These parameter-free libsodium functions only return
445        // compile-time constants.
446        unsafe {
447            assert_eq!(
448                crypto_aead_chacha20poly1305_ietf_keybytes(),
449                CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES
450            );
451            assert_eq!(
452                crypto_aead_chacha20poly1305_ietf_nsecbytes(),
453                CRYPTO_AEAD_CHACHA20POLY1305_IETF_NSECBYTES
454            );
455            assert_eq!(
456                crypto_aead_chacha20poly1305_ietf_npubbytes(),
457                CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES
458            );
459            assert_eq!(
460                crypto_aead_chacha20poly1305_ietf_abytes(),
461                CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES
462            );
463            assert_eq!(
464                crypto_aead_chacha20poly1305_ietf_messagebytes_max(),
465                CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX
466            );
467        }
468    }
469
470    #[cfg(dryoc_native_tests)]
471    #[test]
472    fn test_libsodium_interop() {
473        use crate::native_test_util::{
474            crypto_aead_chacha20poly1305_ietf_decrypt as open,
475            crypto_aead_chacha20poly1305_ietf_encrypt as seal,
476        };
477
478        let ciphertext = expected();
479        assert_eq!(
480            open(&ciphertext, Some(AD), &NONCE, &KEY).expect("libsodium open"),
481            MESSAGE
482        );
483
484        let sodium_ciphertext = seal(MESSAGE, Some(AD), &NONCE, &KEY);
485        let mut plaintext = vec![0u8; MESSAGE.len()];
486        crypto_aead_chacha20poly1305_ietf_decrypt(
487            &mut plaintext,
488            &sodium_ciphertext,
489            Some(AD),
490            &NONCE,
491            &KEY,
492        )
493        .expect("dryoc decrypt");
494        assert_eq!(plaintext, MESSAGE);
495    }
496}