Skip to main content

dryoc/classic/
crypto_secretstream_xchacha20poly1305.rs

1//! # Secret streams
2//!
3//! Implements libsodium's `crypto_secretstream_xchacha20poly1305_*` functions
4//! for encrypting an ordered sequence of messages with a shared secret key.
5//! See the [libsodium documentation](https://doc.libsodium.org/secret-key_cryptography/secretstream)
6//! for details.
7//!
8//! # Classic API example
9//!
10//! ```
11//! use dryoc::classic::crypto_secretstream_xchacha20poly1305::*;
12//! use dryoc::constants::{
13//!     CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES,
14//!     CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL,
15//!     CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE,
16//! };
17//! let message1 = b"Arbitrary data to encrypt";
18//! let message2 = b"split into";
19//! let message3 = b"three messages";
20//!
21//! // Generate a key
22//! let mut key = Key::default();
23//! crypto_secretstream_xchacha20poly1305_keygen(&mut key);
24//!
25//! // Create stream push state
26//! let mut state = State::new();
27//! let mut header = Header::default();
28//! crypto_secretstream_xchacha20poly1305_init_push(&mut state, &mut header, &key);
29//!
30//! let (mut c1, mut c2, mut c3) = (
31//!     vec![0u8; message1.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES],
32//!     vec![0u8; message2.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES],
33//!     vec![0u8; message3.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES],
34//! );
35//! // Encrypt a series of messages
36//! crypto_secretstream_xchacha20poly1305_push(
37//!     &mut state,
38//!     &mut c1,
39//!     message1,
40//!     None,
41//!     CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE,
42//! )
43//! .expect("Encrypt failed");
44//! crypto_secretstream_xchacha20poly1305_push(
45//!     &mut state,
46//!     &mut c2,
47//!     message2,
48//!     None,
49//!     CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE,
50//! )
51//! .expect("Encrypt failed");
52//! crypto_secretstream_xchacha20poly1305_push(
53//!     &mut state,
54//!     &mut c3,
55//!     message3,
56//!     None,
57//!     CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL,
58//! )
59//! .expect("Encrypt failed");
60//!
61//! // Create stream pull state, using the same key as above with a new state.
62//! let mut state = State::new();
63//! crypto_secretstream_xchacha20poly1305_init_pull(&mut state, &header, &key);
64//!
65//! let (mut m1, mut m2, mut m3) = (
66//!     vec![0u8; message1.len()],
67//!     vec![0u8; message2.len()],
68//!     vec![0u8; message3.len()],
69//! );
70//! let (mut tag1, mut tag2, mut tag3) = (0u8, 0u8, 0u8);
71//!
72//! // Decrypt the stream of messages
73//! crypto_secretstream_xchacha20poly1305_pull(&mut state, &mut m1, &mut tag1, &c1, None)
74//!     .expect("Decrypt failed");
75//! crypto_secretstream_xchacha20poly1305_pull(&mut state, &mut m2, &mut tag2, &c2, None)
76//!     .expect("Decrypt failed");
77//! crypto_secretstream_xchacha20poly1305_pull(&mut state, &mut m3, &mut tag3, &c3, None)
78//!     .expect("Decrypt failed");
79//!
80//! assert_eq!(message1, m1.as_slice());
81//! assert_eq!(message2, m2.as_slice());
82//! assert_eq!(message3, m3.as_slice());
83//!
84//! assert_eq!(tag1, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE);
85//! assert_eq!(tag2, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE);
86//! assert_eq!(tag3, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL);
87//! ```
88
89use subtle::ConstantTimeEq;
90use zeroize::{Zeroize, ZeroizeOnDrop};
91
92use crate::chacha20::ChaCha20;
93use crate::classic::crypto_core::crypto_core_hchacha20;
94use crate::constants::{
95    CRYPTO_CORE_HCHACHA20_INPUTBYTES, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES,
96    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES,
97    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES,
98    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES,
99    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES,
100    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX,
101    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY, CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES,
102    CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES,
103};
104use crate::error::*;
105use crate::poly1305::Poly1305;
106use crate::rng::copy_randombytes;
107use crate::types::*;
108use crate::utils::{WideZeroizing, increment_bytes, pad16, verify_ct, xor_buf, zeroize_bytes};
109
110/// A secret for authenticated secret streams.
111pub type Key = [u8; CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES];
112/// A nonce for authenticated secret streams.
113pub type Nonce = [u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES];
114/// Container for stream header data
115pub type Header = [u8; CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES];
116
117/// Stream state data
118///
119/// Equality compares the key and nonce in constant time.
120#[derive(Clone, Default)]
121pub struct State {
122    k: Key,
123    nonce: Nonce,
124}
125
126/// Wipes sixteen bytes per store, where a derived impl stores one byte at a
127/// time; states are cloned and dropped around every message.
128impl Zeroize for State {
129    fn zeroize(&mut self) {
130        zeroize_bytes(&mut self.k);
131        zeroize_bytes(&mut self.nonce);
132    }
133}
134
135impl Drop for State {
136    fn drop(&mut self) {
137        self.zeroize();
138    }
139}
140
141impl ZeroizeOnDrop for State {}
142
143impl PartialEq for State {
144    fn eq(&self, other: &Self) -> bool {
145        (self.k.ct_eq(&other.k) & self.nonce.ct_eq(&other.nonce)).into()
146    }
147}
148
149impl Eq for State {}
150
151impl State {
152    /// Returns a new stream state with an empty key and nonce.
153    #[must_use]
154    pub fn new() -> Self {
155        Self::default()
156    }
157}
158
159/// Generates a random stream key using [crate::rng::copy_randombytes].
160pub fn crypto_secretstream_xchacha20poly1305_keygen(key: &mut Key) {
161    copy_randombytes(key);
162}
163
164fn state_counter(nonce: &mut Nonce) -> &mut [u8] {
165    &mut nonce[..CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES]
166}
167
168fn state_inonce(nonce: &mut Nonce) -> &mut [u8] {
169    &mut nonce[CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES
170        ..CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES
171            + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES]
172}
173
174/// The ciphertext length for a `message_len`-byte message, which must not
175/// exceed [`CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX`].
176pub(crate) fn ciphertext_len_from_message_len(message_len: usize) -> Result<usize, Error> {
177    validate_length!(
178        max CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX,
179        message_len,
180        crate::ErrorContext::Message
181    );
182    Ok(message_len + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES)
183}
184
185/// The message length carried by a `ciphertext_len`-byte ciphertext, which
186/// must hold the overhead and at most
187/// [`CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX`] message bytes.
188pub(crate) fn message_len_from_ciphertext_len(ciphertext_len: usize) -> Result<usize, Error> {
189    validate_length!(
190        min CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES,
191        ciphertext_len,
192        crate::ErrorContext::Ciphertext
193    );
194    validate_length!(
195        max CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX
196            + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES,
197        ciphertext_len,
198        crate::ErrorContext::Ciphertext
199    );
200    Ok(ciphertext_len - CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES)
201}
202
203fn secretstream_length_block(associated_data_len: usize, message_len: usize) -> [u8; 16] {
204    let mut lengths = [0u8; 16];
205    lengths[..8].copy_from_slice(&(associated_data_len as u64).to_le_bytes());
206    lengths[8..].copy_from_slice(&(64u64 + message_len as u64).to_le_bytes());
207    lengths
208}
209
210/// Runs keystream blocks 0 and 1 for the current `state`: block 0 keys the
211/// Poly1305 MAC (and is zeroized here), block 1 is XORed into `block`, whose
212/// first byte the caller has set to the tag byte to encrypt or decrypt. The
213/// returned MAC has absorbed `associated_data` and its padding. The cipher is
214/// left positioned at block 2, where the message starts. Nothing in `state` is
215/// touched.
216#[inline]
217fn secretstream_init_mac(
218    state: &State,
219    block: &mut [u8; 64],
220    associated_data: &[u8],
221) -> (ChaCha20, Poly1305) {
222    let mut cipher = ChaCha20::ietf(&state.k, &state.nonce, 0);
223
224    // Blocks 0 and 1 come out of one keystream run.
225    let mut block0 = WideZeroizing::new([0u8; 64]);
226    cipher.apply_keystream_with_head(&mut block0, block);
227    let mut mac_key = crate::poly1305::Key::default();
228    mac_key.copy_from_slice(&block0[..mac_key.len()]);
229    let mut mac = Poly1305::new(&mac_key);
230    mac_key.zeroize();
231    drop(block0);
232
233    mac.update(associated_data);
234    mac.update(&[0u8; 16][..pad16(associated_data.len())]);
235
236    (cipher, mac)
237}
238
239/// Advances `state` past an authenticated message: XORs `mac` into the
240/// implicit nonce, increments the counter, and rekeys when `tag` requests it
241/// or the counter wrapped to zero.
242#[inline]
243fn secretstream_advance(state: &mut State, mac: &[u8], tag: u8) {
244    let inonce = state_inonce(&mut state.nonce);
245    xor_buf(inonce, mac);
246
247    let counter = state_counter(&mut state.nonce);
248    increment_bytes(counter);
249
250    if tag & CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY
251        == CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY
252        || state_counter(&mut state.nonce)
253            .ct_eq(&[0u8; CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES])
254            .unwrap_u8()
255            == 1
256    {
257        crypto_secretstream_xchacha20poly1305_rekey(state);
258    }
259}
260
261fn _crypto_secretstream_xchacha20poly1305_counter_reset(state: &mut State) {
262    let counter = state_counter(&mut state.nonce);
263    counter.fill(0);
264    counter[0] = 1;
265}
266
267/// Initializes a push stream for streaming encryption.
268///
269/// Initializes a push stream into `state` using `key` and returns a stream
270/// header. The stream header can be used to initialize a pull stream using the
271/// same key (i.e., using [crypto_secretstream_xchacha20poly1305_init_pull]).
272///
273/// Compatible with libsodium's
274/// `crypto_secretstream_xchacha20poly1305_init_push`.
275pub fn crypto_secretstream_xchacha20poly1305_init_push(
276    state: &mut State,
277    header: &mut Header,
278    key: &Key,
279) {
280    copy_randombytes(header);
281    secretstream_init(state, header, key);
282}
283
284/// Initializes a pull stream for streaming decryption.
285///
286/// Initializes `state` using `key` and a `header` returned by
287/// [`crypto_secretstream_xchacha20poly1305_init_push`].
288///
289/// Compatible with libsodium's
290/// `crypto_secretstream_xchacha20poly1305_init_pull`.
291pub fn crypto_secretstream_xchacha20poly1305_init_pull(
292    state: &mut State,
293    header: &Header,
294    key: &Key,
295) {
296    secretstream_init(state, header, key);
297}
298
299fn secretstream_init(state: &mut State, header: &Header, key: &Key) {
300    crypto_core_hchacha20(
301        &mut state.k,
302        header.first_chunk::<16>().expect("16-byte prefix"),
303        key,
304        None,
305    );
306    _crypto_secretstream_xchacha20poly1305_counter_reset(state);
307
308    let inonce = state_inonce(&mut state.nonce);
309    inonce.copy_from_slice(
310        &header[CRYPTO_CORE_HCHACHA20_INPUTBYTES
311            ..(CRYPTO_CORE_HCHACHA20_INPUTBYTES
312                + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES)],
313    );
314}
315
316/// Manually rekeys a stream.
317///
318/// Compatible with libsodium's
319/// `crypto_secretstream_xchacha20poly1305_rekey`.
320pub fn crypto_secretstream_xchacha20poly1305_rekey(state: &mut State) {
321    let mut new_state = WideZeroizing::new(
322        [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES
323            + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES],
324    );
325
326    new_state[..CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES].copy_from_slice(&state.k);
327    new_state[CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES..]
328        .copy_from_slice(state_inonce(&mut state.nonce));
329
330    ChaCha20::ietf(&state.k, &state.nonce, 0).apply_keystream(&mut *new_state);
331
332    state
333        .k
334        .copy_from_slice(&new_state[0..CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES]);
335    state_inonce(&mut state.nonce)
336        .copy_from_slice(&new_state[CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES..]);
337
338    _crypto_secretstream_xchacha20poly1305_counter_reset(state);
339}
340
341/// Encrypts `message` from the stream for `state`, with `tag` and optional
342/// `associated_data`, placing the result into `ciphertext`.
343///
344/// Compatible with libsodium's `crypto_secretstream_xchacha20poly1305_push`.
345///
346/// NOTE: The libsodium version of this function contains an alignment bug which
347/// was left in place, and is reflected in this implementation for compatibility
348/// purposes. Refer to [commit
349/// 290197ba3ee72245fdab5e971c8de43a82b19874](https://github.com/jedisct1/libsodium/commit/290197ba3ee72245fdab5e971c8de43a82b19874#diff-dbd9b6026ac3fd057df0ddf00e4d671af16e5df99b4cc7d08b73b61f193d10f5)
350///
351/// # Errors
352///
353/// Returns an error if `message` exceeds the maximum supported length or
354/// `ciphertext` is not exactly one authentication tag longer than `message`.
355pub fn crypto_secretstream_xchacha20poly1305_push(
356    state: &mut State,
357    ciphertext: &mut [u8],
358    message: &[u8],
359    associated_data: Option<&[u8]>,
360    tag: u8,
361) -> Result<(), Error> {
362    let expected_ciphertext_len = ciphertext_len_from_message_len(message.len())?;
363    validate_length!(
364        exact expected_ciphertext_len,
365        ciphertext.len(),
366        crate::ErrorContext::Ciphertext
367    );
368
369    let associated_data = associated_data.unwrap_or(&[]);
370    let _pad0 = [0u8; 16];
371
372    // Block 0 keys the MAC, block 1 carries the tag, the message starts at
373    // block 2; each call below consumes whole blocks.
374    let mut block = WideZeroizing::new([0u8; 64]);
375    block[0] = tag;
376    let (mut cipher, mut mac) = secretstream_init_mac(state, &mut block, associated_data);
377    mac.update(&*block);
378
379    let mlen = message.len();
380    ciphertext[0] = block[0];
381    cipher.apply_keystream_and_mac(Some(message), &mut ciphertext[1..(1 + mlen)], &mut mac);
382
383    let size_data = secretstream_length_block(associated_data.len(), mlen);
384
385    // this is to workaround an unfortunate padding bug in libsodium, there's a
386    // note in commit 290197ba3ee72245fdab5e971c8de43a82b19874. There's no
387    // safety issue, so we can just pretend it's not a bug.
388    let buffer_mac_pad = ((0x10 - block.len() as i64 + mlen as i64) & 0xf) as usize;
389    mac.update(&_pad0[0..buffer_mac_pad]);
390    mac.update(&size_data);
391
392    mac.finalize(&mut ciphertext[1 + mlen..]);
393
394    secretstream_advance(state, &ciphertext[1 + mlen..], tag);
395
396    Ok(())
397}
398
399/// Decrypts `ciphertext` from the stream for `state` with optional
400/// `additional_data`, placing the result into `message` (which must be manually
401/// resized) and `tag`. Returns the length of the message.
402///
403/// Due to a quirk in libsodium's implementation, you need to manually resize
404/// `message` to the message length after decrypting when using this function.
405///
406/// Compatible with libsodium's `crypto_secretstream_xchacha20poly1305_pull`.
407///
408/// NOTE: The libsodium version of this function contains an alignment bug which
409/// was left in place, and is reflected in this implementation for compatibility
410/// purposes. Refer to [commit
411/// 290197ba3ee72245fdab5e971c8de43a82b19874](https://github.com/jedisct1/libsodium/commit/290197ba3ee72245fdab5e971c8de43a82b19874#diff-dbd9b6026ac3fd057df0ddf00e4d671af16e5df99b4cc7d08b73b61f193d10f5)
412///
413/// # Errors
414///
415/// Returns an error if `ciphertext` is too short or too long, `message` lacks
416/// space for the plaintext, or authentication fails.
417pub fn crypto_secretstream_xchacha20poly1305_pull(
418    state: &mut State,
419    message: &mut [u8],
420    tag: &mut u8,
421    ciphertext: &[u8],
422    associated_data: Option<&[u8]>,
423) -> Result<usize, Error> {
424    let _pad0 = [0u8; 16];
425
426    let mlen = message_len_from_ciphertext_len(ciphertext.len())?;
427
428    validate_length!(min mlen, message.len(), crate::ErrorContext::Message);
429
430    let associated_data = associated_data.unwrap_or(&[]);
431
432    // Block 0 keys the MAC, block 1 carries the tag, the message starts at
433    // block 2; each call below consumes whole blocks.
434    let mut block = WideZeroizing::new([0u8; 64]);
435    block[0] = ciphertext[0];
436    let (mut cipher, mut mac) = secretstream_init_mac(state, &mut block, associated_data);
437
438    let decrypted_tag = block[0];
439    block[0] = ciphertext[0];
440
441    mac.update(&*block);
442
443    // this is to workaround an unfortunate padding bug in libsodium, there's a
444    // note in commit 290197ba3ee72245fdab5e971c8de43a82b19874. There's no
445    // safety issue, so we can just pretend it's not a bug.
446    let buffer_mac_pad = ((0x10 - block.len() as i64 + mlen as i64) & 0xf) as usize;
447    mac.update(&ciphertext[1..1 + mlen]);
448    mac.update(&_pad0[..buffer_mac_pad]);
449
450    let size_data = secretstream_length_block(associated_data.len(), mlen);
451    mac.update(&size_data);
452    let computed = WideZeroizing::new(mac.finalize_to_array());
453    // `finalize_to_array` leaves the wipe to the state's drop, which comes
454    // only at the end of this function: wipe it before decrypting.
455    mac.zeroize();
456
457    verify_ct(&ciphertext[1 + mlen..], computed.as_slice())?;
458
459    cipher.apply_keystream_b2b(&ciphertext[1..1 + mlen], &mut message[..mlen]);
460    *tag = decrypted_tag;
461
462    secretstream_advance(state, &*computed, decrypted_tag);
463
464    Ok(mlen)
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::dryocstream::Tag;
471    #[cfg(dryoc_native_tests)]
472    use crate::test_prelude::*;
473
474    /// The length checks shared by Classic and Rustaceous push and pull
475    /// accept exactly `MESSAGEBYTES_MAX` message bytes (libsodium's
476    /// `64 * (2^32 - 2)`, capped so the ciphertext length fits a `usize`) and
477    /// reject one more, checked on lengths since no test can allocate such a
478    /// buffer.
479    #[test]
480    fn length_checks_accept_exactly_messagebytes_max() {
481        const MAX: usize = CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX;
482        const ABYTES: usize = CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES;
483
484        #[cfg(target_pointer_width = "64")]
485        assert_eq!(MAX, 64 * ((1 << 32) - 2));
486        #[cfg(not(target_pointer_width = "64"))]
487        assert_eq!(MAX, usize::MAX - ABYTES);
488
489        assert!(matches!(ciphertext_len_from_message_len(0), Ok(ABYTES)));
490        assert!(matches!(
491            ciphertext_len_from_message_len(MAX),
492            Ok(len) if len == MAX + ABYTES
493        ));
494        assert!(matches!(
495            ciphertext_len_from_message_len(MAX + 1),
496            Err(Error::InvalidLength {
497                context: crate::ErrorContext::Message,
498                actual,
499                constraint: LengthConstraint::AtMost(max),
500            }) if actual == MAX + 1 && max == MAX
501        ));
502
503        assert!(matches!(
504            message_len_from_ciphertext_len(ABYTES - 1),
505            Err(Error::InvalidLength {
506                context: crate::ErrorContext::Ciphertext,
507                actual,
508                constraint: LengthConstraint::AtLeast(ABYTES),
509            }) if actual == ABYTES - 1
510        ));
511        assert!(matches!(message_len_from_ciphertext_len(ABYTES), Ok(0)));
512        assert!(matches!(
513            message_len_from_ciphertext_len(MAX + ABYTES),
514            Ok(len) if len == MAX
515        ));
516        // On 32-bit targets `MAX + ABYTES` is `usize::MAX`, so no longer
517        // ciphertext length exists.
518        if let Some(too_long) = (MAX + ABYTES).checked_add(1) {
519            assert!(matches!(
520                message_len_from_ciphertext_len(too_long),
521                Err(Error::InvalidLength {
522                    context: crate::ErrorContext::Ciphertext,
523                    actual,
524                    constraint: LengthConstraint::AtMost(max),
525                }) if actual == too_long && max == MAX + ABYTES
526            ));
527        }
528    }
529
530    /// Push and pull must reject wrong buffer lengths with the right error
531    /// and leave the sentinel output, the tag and the (cloned) state exactly
532    /// as they were, and the state must remain usable afterwards. Neither
533    /// libsodium nor this implementation validates the tag byte itself.
534    #[test]
535    fn push_and_pull_reject_invalid_buffer_lengths_without_mutation() {
536        let key = Key::default();
537        let mut state = State::new();
538        let mut header = Header::default();
539        crypto_secretstream_xchacha20poly1305_init_push(&mut state, &mut header, &key);
540        let original_state = state.clone();
541        let message = b"message";
542
543        for len in [
544            0,
545            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES,
546            message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES - 1,
547            message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES + 1,
548        ] {
549            let mut ciphertext = vec![0xa5u8; len];
550            let error = crypto_secretstream_xchacha20poly1305_push(
551                &mut state,
552                &mut ciphertext,
553                message,
554                None,
555                Tag::Message.bits(),
556            )
557            .expect_err("ciphertext must be exactly the message plus overhead");
558            assert!(
559                matches!(
560                    error,
561                    Error::InvalidLength {
562                        context: crate::ErrorContext::Ciphertext,
563                        ..
564                    }
565                ),
566                "push into {len} bytes"
567            );
568            assert_eq!(ciphertext, vec![0xa5u8; len], "push into {len} bytes");
569            assert!(state == original_state, "push into {len} bytes");
570        }
571
572        let mut ciphertext =
573            vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
574        crypto_secretstream_xchacha20poly1305_push(
575            &mut state,
576            &mut ciphertext,
577            message,
578            None,
579            Tag::Message.bits(),
580        )
581        .expect("state must remain usable");
582
583        let mut state = State::new();
584        crypto_secretstream_xchacha20poly1305_init_pull(&mut state, &header, &key);
585        let original_state = state.clone();
586        let mut tag = 0x5a;
587
588        let short_ciphertext = [0u8; CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES - 1];
589        let mut output = [0xa5u8; 8];
590        let error = crypto_secretstream_xchacha20poly1305_pull(
591            &mut state,
592            &mut output,
593            &mut tag,
594            &short_ciphertext,
595            None,
596        )
597        .expect_err("ciphertext must include secretstream overhead");
598        assert!(matches!(
599            error,
600            Error::InvalidLength {
601                context: crate::ErrorContext::Ciphertext,
602                ..
603            }
604        ));
605        assert_eq!((output, tag), ([0xa5u8; 8], 0x5a));
606        assert!(state == original_state);
607
608        let mut output = vec![0xa5u8; message.len() - 1];
609        let error = crypto_secretstream_xchacha20poly1305_pull(
610            &mut state,
611            &mut output,
612            &mut tag,
613            &ciphertext,
614            None,
615        )
616        .expect_err("the message buffer must hold the plaintext");
617        assert!(matches!(
618            error,
619            Error::InvalidLength {
620                context: crate::ErrorContext::Message,
621                ..
622            }
623        ));
624        assert_eq!(output, vec![0xa5u8; message.len() - 1]);
625        assert_eq!(tag, 0x5a);
626        assert!(state == original_state);
627
628        let mut output = vec![0u8; message.len()];
629        crypto_secretstream_xchacha20poly1305_pull(
630            &mut state,
631            &mut output,
632            &mut tag,
633            &ciphertext,
634            None,
635        )
636        .expect("state must remain usable");
637        assert_eq!(
638            (output.as_slice(), tag),
639            (&message[..], Tag::Message.bits())
640        );
641    }
642
643    #[test]
644    fn pull_authenticates_before_mutating_outputs_or_state() {
645        let key = Key::default();
646        let mut push_state = State::new();
647        let mut header = Header::default();
648        crypto_secretstream_xchacha20poly1305_init_push(&mut push_state, &mut header, &key);
649
650        let plaintext = b"do not publish unauthenticated plaintext";
651        let mut ciphertext =
652            vec![0u8; plaintext.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
653        crypto_secretstream_xchacha20poly1305_push(
654            &mut push_state,
655            &mut ciphertext,
656            plaintext,
657            Some(b"associated data"),
658            Tag::Final.bits(),
659        )
660        .expect("push failed");
661
662        let mut pull_state = State::new();
663        crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &header, &key);
664        let original_state = pull_state.clone();
665        let mut tampered = ciphertext.clone();
666        *tampered.last_mut().expect("authentication tag") ^= 1;
667        let mut output = vec![0xa5; plaintext.len()];
668        let original_output = output.clone();
669        let mut tag = 0x5a;
670
671        assert!(matches!(
672            crypto_secretstream_xchacha20poly1305_pull(
673                &mut pull_state,
674                &mut output,
675                &mut tag,
676                &tampered,
677                Some(b"associated data"),
678            ),
679            Err(Error::AuthenticationFailed)
680        ));
681        assert_eq!(output, original_output);
682        assert_eq!(tag, 0x5a);
683        assert!(pull_state == original_state);
684
685        crypto_secretstream_xchacha20poly1305_pull(
686            &mut pull_state,
687            &mut output,
688            &mut tag,
689            &ciphertext,
690            Some(b"associated data"),
691        )
692        .expect("state must remain usable after authentication failure");
693        assert_eq!(output, plaintext);
694        assert_eq!(tag, Tag::Final.bits());
695    }
696
697    #[test]
698    fn length_block_uses_fixed_width_little_endian_values() {
699        let lengths = secretstream_length_block(0x0102_0304, 0x0506_0708);
700
701        assert_eq!(&lengths[..8], &0x0102_0304u64.to_le_bytes());
702        assert_eq!(&lengths[8..], &(64u64 + 0x0506_0708).to_le_bytes());
703    }
704
705    #[test]
706    fn test_sizes() {
707        use crate::constants::*;
708
709        const _: () = assert!(
710            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES
711                == CRYPTO_CORE_HCHACHA20_INPUTBYTES
712                    + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES
713        );
714
715        const _: () = assert!(
716            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES
717                == CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
718        );
719
720        const _: () = assert!(
721            CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES
722                == CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES
723                    + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES
724        );
725
726        const _: () = assert!(
727            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX
728                <= CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX
729        );
730
731        const _: () = assert!(
732            CRYPTO_ONETIMEAUTH_POLY1305_BYTES >= CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_INONCEBYTES
733        );
734
735        #[cfg(target_pointer_width = "32")]
736        {
737            assert_eq!(
738                CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX,
739                usize::MAX - CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES
740            );
741            assert_eq!(
742                CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX,
743                usize::MAX - CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
744            );
745            assert_eq!(
746                CRYPTO_SECRETBOX_MESSAGEBYTES_MAX,
747                usize::MAX - CRYPTO_SECRETBOX_MACBYTES
748            );
749        }
750    }
751
752    #[test]
753    fn test_secretstream_large_aad() {
754        let mut key = Key::default();
755        crypto_secretstream_xchacha20poly1305_keygen(&mut key);
756
757        let mut push_state = State::new();
758        let mut push_header = Header::default();
759        crypto_secretstream_xchacha20poly1305_init_push(&mut push_state, &mut push_header, &key);
760
761        let message = b"hello world";
762        let large_aad = vec![0x42u8; 328]; // 328 bytes of 0x42
763
764        let mut ciphertext =
765            vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
766        crypto_secretstream_xchacha20poly1305_push(
767            &mut push_state,
768            &mut ciphertext,
769            message,
770            Some(&large_aad),
771            Tag::Message.bits(),
772        )
773        .expect("push failed");
774
775        let mut pull_state = State::new();
776        crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &push_header, &key);
777
778        let mut decrypted = vec![0u8; message.len()];
779        let mut tag = 0u8;
780
781        crypto_secretstream_xchacha20poly1305_pull(
782            &mut pull_state,
783            &mut decrypted,
784            &mut tag,
785            &ciphertext,
786            Some(&large_aad),
787        )
788        .expect("pull failed");
789
790        assert_eq!(message.as_slice(), decrypted.as_slice());
791        assert_eq!(tag, Tag::Message.bits());
792
793        // Test with wrong AAD should fail
794        let mut wrong_aad = large_aad.clone();
795        wrong_aad[100] = 0x43; // Change one byte
796
797        let mut wrong_aad_pull_state = State::new();
798        crypto_secretstream_xchacha20poly1305_init_pull(
799            &mut wrong_aad_pull_state,
800            &push_header,
801            &key,
802        );
803
804        let mut decrypted = vec![0u8; message.len()];
805        let mut tag = 0u8;
806
807        assert!(
808            crypto_secretstream_xchacha20poly1305_pull(
809                &mut wrong_aad_pull_state,
810                &mut decrypted,
811                &mut tag,
812                &ciphertext,
813                Some(&wrong_aad),
814            )
815            .is_err()
816        );
817    }
818
819    #[test]
820    fn test_secretstream_small_aad() {
821        let mut key = Key::default();
822        crypto_secretstream_xchacha20poly1305_keygen(&mut key);
823
824        let mut push_state = State::new();
825        let mut push_header = Header::default();
826        crypto_secretstream_xchacha20poly1305_init_push(&mut push_state, &mut push_header, &key);
827
828        let message = b"hello world";
829        let small_aad = b"abc"; // 3 bytes of AAD
830
831        let mut ciphertext =
832            vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
833        crypto_secretstream_xchacha20poly1305_push(
834            &mut push_state,
835            &mut ciphertext,
836            message,
837            Some(small_aad),
838            Tag::Message.bits(),
839        )
840        .expect("push failed");
841
842        let mut pull_state = State::new();
843        crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &push_header, &key);
844
845        let mut decrypted = vec![0u8; message.len()];
846        let mut tag = 0u8;
847
848        crypto_secretstream_xchacha20poly1305_pull(
849            &mut pull_state,
850            &mut decrypted,
851            &mut tag,
852            &ciphertext,
853            Some(small_aad),
854        )
855        .expect("pull failed");
856
857        assert_eq!(message.as_slice(), decrypted.as_slice());
858        assert_eq!(tag, Tag::Message.bits());
859
860        // Test with wrong AAD should fail
861        let wrong_aad = b"xyz"; // Different 3 byte AAD
862
863        let mut wrong_aad_pull_state = State::new();
864        crypto_secretstream_xchacha20poly1305_init_pull(
865            &mut wrong_aad_pull_state,
866            &push_header,
867            &key,
868        );
869
870        let mut decrypted = vec![0u8; message.len()];
871        let mut tag = 0u8;
872
873        assert!(
874            crypto_secretstream_xchacha20poly1305_pull(
875                &mut wrong_aad_pull_state,
876                &mut decrypted,
877                &mut tag,
878                &ciphertext,
879                Some(wrong_aad),
880            )
881            .is_err()
882        );
883    }
884
885    #[cfg(dryoc_native_tests)]
886    mod native_tests {
887        use super::*;
888        use crate::constants::{
889            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL,
890            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE,
891            CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_PUSH,
892        };
893
894        /// libsodium's state mirroring `state`.
895        fn so_state(state: &State) -> libsodium_sys::crypto_secretstream_xchacha20poly1305_state {
896            libsodium_sys::crypto_secretstream_xchacha20poly1305_state {
897                k: state.k,
898                nonce: state.nonce,
899                _pad: [0u8; 8],
900            }
901        }
902
903        fn assert_same_state(
904            so: &libsodium_sys::crypto_secretstream_xchacha20poly1305_state,
905            state: &State,
906            ctx: &str,
907        ) {
908            assert_eq!((so.k, so.nonce), (state.k, state.nonce), "{ctx}: state");
909        }
910
911        /// libsodium's push, returning the ciphertext.
912        fn so_push(
913            so: &mut libsodium_sys::crypto_secretstream_xchacha20poly1305_state,
914            message: &[u8],
915            ad: &[u8],
916            tag: u8,
917        ) -> Vec<u8> {
918            crate::native_test_util::init();
919            let mut ciphertext =
920                vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
921            let mut clen: libc::c_ulonglong = 0;
922            // SAFETY: every pointer comes from a live buffer of the length
923            // passed beside it; `ciphertext` has room for the message and
924            // the overhead.
925            let rc = unsafe {
926                libsodium_sys::crypto_secretstream_xchacha20poly1305_push(
927                    so,
928                    ciphertext.as_mut_ptr(),
929                    &mut clen,
930                    message.as_ptr(),
931                    message.len() as libc::c_ulonglong,
932                    ad.as_ptr(),
933                    ad.len() as libc::c_ulonglong,
934                    tag,
935                )
936            };
937            assert_eq!((rc, clen as usize), (0, ciphertext.len()));
938            ciphertext
939        }
940
941        /// libsodium's pull, returning the message and tag.
942        fn so_pull(
943            so: &mut libsodium_sys::crypto_secretstream_xchacha20poly1305_state,
944            ciphertext: &[u8],
945            ad: &[u8],
946        ) -> (Vec<u8>, u8) {
947            crate::native_test_util::init();
948            let mut message =
949                vec![0u8; ciphertext.len() - CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
950            let mut mlen: libc::c_ulonglong = 0;
951            let mut tag: libc::c_uchar = 0;
952            // SAFETY: as in `so_push`; `message` has room for the plaintext.
953            let rc = unsafe {
954                libsodium_sys::crypto_secretstream_xchacha20poly1305_pull(
955                    so,
956                    message.as_mut_ptr(),
957                    &mut mlen,
958                    &mut tag,
959                    ciphertext.as_ptr(),
960                    ciphertext.len() as libc::c_ulonglong,
961                    ad.as_ptr(),
962                    ad.len() as libc::c_ulonglong,
963                )
964            };
965            assert_eq!((rc, mlen as usize), (0, message.len()));
966            (message, tag)
967        }
968
969        /// Pushes `message` with both implementations from equal states and
970        /// checks the ciphertexts and the advanced states agree, then pulls
971        /// it with both from equal pull states and checks the message, tag
972        /// and advanced states agree.
973        #[allow(clippy::too_many_arguments)]
974        fn check_push_pull_match_libsodium(
975            push_state: &mut State,
976            so_push_state: &mut libsodium_sys::crypto_secretstream_xchacha20poly1305_state,
977            pull_state: &mut State,
978            so_pull_state: &mut libsodium_sys::crypto_secretstream_xchacha20poly1305_state,
979            message: &[u8],
980            ad: &[u8],
981            tag: u8,
982            ctx: &str,
983        ) {
984            let expected = so_push(so_push_state, message, ad, tag);
985            let mut ciphertext =
986                vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
987            crypto_secretstream_xchacha20poly1305_push(
988                push_state,
989                &mut ciphertext,
990                message,
991                Some(ad),
992                tag,
993            )
994            .expect("push");
995            assert_eq!(ciphertext, expected, "{ctx}: ciphertext");
996            assert_same_state(so_push_state, push_state, &format!("{ctx}: push"));
997
998            let (expected_message, expected_tag) = so_pull(so_pull_state, &ciphertext, ad);
999            let mut output = vec![0u8; message.len()];
1000            let mut pulled_tag = 0xa5;
1001            let mlen = crypto_secretstream_xchacha20poly1305_pull(
1002                pull_state,
1003                &mut output,
1004                &mut pulled_tag,
1005                &ciphertext,
1006                Some(ad),
1007            )
1008            .expect("pull");
1009            assert_eq!(mlen, message.len(), "{ctx}: pulled length");
1010            assert_eq!(
1011                (&output, pulled_tag),
1012                (&expected_message, expected_tag),
1013                "{ctx}: pull"
1014            );
1015            assert_eq!(
1016                (&output, pulled_tag),
1017                (&message.to_vec(), tag),
1018                "{ctx}: round trip"
1019            );
1020            assert_same_state(so_pull_state, pull_state, &format!("{ctx}: pull"));
1021        }
1022
1023        /// Message lengths around the ChaCha20 block (the message starts at
1024        /// block 2, after the MAC key and tag blocks) and around the vector
1025        /// kernels' 8-, 9- and 16-block chunks.
1026        fn boundary_lens() -> Vec<usize> {
1027            let mut lens = vec![0, 1, 63, 64, 65, 127, 128, 129];
1028            for chunk in [8 * 64, 9 * 64, 16 * 64] {
1029                lens.extend([
1030                    chunk - 129,
1031                    chunk - 128,
1032                    chunk - 127,
1033                    chunk - 1,
1034                    chunk,
1035                    chunk + 1,
1036                ]);
1037            }
1038            lens.sort_unstable();
1039            lens.dedup();
1040            lens
1041        }
1042
1043        /// One stream carrying every tag (MESSAGE, PUSH, REKEY, FINAL, and an
1044        /// arbitrary byte, which both implementations pass through and rekey
1045        /// on because its REKEY bit is set) over every message length in
1046        /// [`boundary_lens`] with empty, partial-block and whole-block
1047        /// associated data, against libsodium at every step.
1048        #[test]
1049        fn test_every_tag_and_length_matches_libsodium() {
1050            let mut rng = crate::utils::test_util::XorShift64::new(0x7f4a_7c15_9e37_79b9);
1051            let key: Key = rng.next_bytes32();
1052            let mut push_state = State::new();
1053            let mut header = Header::default();
1054            crypto_secretstream_xchacha20poly1305_init_push(&mut push_state, &mut header, &key);
1055            let mut pull_state = State::new();
1056            crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &header, &key);
1057            let mut so_push_state = so_state(&push_state);
1058            let mut so_pull_state = so_state(&pull_state);
1059
1060            let tags = [
1061                CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE,
1062                CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_PUSH,
1063                CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY,
1064                CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL,
1065                0x42,
1066            ];
1067            let ads: [&[u8]; 3] = [b"", b"aad", &[0x5a; 16]];
1068            for len in boundary_lens() {
1069                let message: Vec<u8> = (0..len.div_ceil(8))
1070                    .flat_map(|_| rng.next_u64().to_le_bytes())
1071                    .take(len)
1072                    .collect();
1073                for tag in tags {
1074                    for ad in ads {
1075                        check_push_pull_match_libsodium(
1076                            &mut push_state,
1077                            &mut so_push_state,
1078                            &mut pull_state,
1079                            &mut so_pull_state,
1080                            &message,
1081                            ad,
1082                            tag,
1083                            &format!("len {len}, tag {tag:#04x}, ad {}", ad.len()),
1084                        );
1085                    }
1086                }
1087            }
1088        }
1089
1090        /// With the counter set to its last values, the pushes that carry it
1091        /// through `ff ff ff ff` to zero must rekey automatically exactly as
1092        /// libsodium does (a MESSAGE tag, so only the wrap triggers it), and
1093        /// the stream must continue in step afterwards, on both sides.
1094        #[test]
1095        fn test_counter_wrap_rekeys_like_libsodium() {
1096            let mut rng = crate::utils::test_util::XorShift64::new(0x2545_f491_4f6c_dd1d);
1097            let key: Key = rng.next_bytes32();
1098            for start in [
1099                [0xfd, 0xff, 0xff, 0xff],
1100                [0xfe, 0xff, 0xff, 0xff],
1101                [0xff; 4],
1102            ] {
1103                let mut push_state = State::new();
1104                let mut header = Header::default();
1105                crypto_secretstream_xchacha20poly1305_init_push(&mut push_state, &mut header, &key);
1106                let mut pull_state = State::new();
1107                crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &header, &key);
1108                push_state.nonce[..CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES]
1109                    .copy_from_slice(&start);
1110                pull_state.nonce[..CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES]
1111                    .copy_from_slice(&start);
1112                let mut so_push_state = so_state(&push_state);
1113                let mut so_pull_state = so_state(&pull_state);
1114
1115                let before_wrap = push_state.clone();
1116                for step in 0..5 {
1117                    let message: Vec<u8> = (0..37 * step).map(|i| i as u8).collect();
1118                    check_push_pull_match_libsodium(
1119                        &mut push_state,
1120                        &mut so_push_state,
1121                        &mut pull_state,
1122                        &mut so_pull_state,
1123                        &message,
1124                        b"counter",
1125                        CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE,
1126                        &format!("start {start:02x?}, step {step}"),
1127                    );
1128                    let counter = u32::from_le_bytes(
1129                        push_state.nonce[..CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_COUNTERBYTES]
1130                            .try_into()
1131                            .unwrap(),
1132                    );
1133                    let wrapped = u32::from_le_bytes(start).checked_add(step + 1).is_none();
1134                    if wrapped {
1135                        // Rekeyed: fresh key, counter reset to 1.
1136                        assert_ne!(
1137                            push_state.k, before_wrap.k,
1138                            "start {start:02x?}, step {step}"
1139                        );
1140                        assert_eq!(
1141                            counter,
1142                            (step + 1) - (u32::MAX - u32::from_le_bytes(start)),
1143                            "start {start:02x?}, step {step}"
1144                        );
1145                    } else {
1146                        assert_eq!(
1147                            push_state.k, before_wrap.k,
1148                            "start {start:02x?}, step {step}"
1149                        );
1150                        assert_eq!(
1151                            counter,
1152                            u32::from_le_bytes(start) + step + 1,
1153                            "start {start:02x?}, step {step}"
1154                        );
1155                    }
1156                }
1157            }
1158        }
1159
1160        #[test]
1161        fn test_secretstream_basic_push() {
1162            use base64::Engine as _;
1163            use base64::engine::general_purpose;
1164            use libsodium_sys::{
1165                crypto_secretstream_xchacha20poly1305_init_pull as so_crypto_secretstream_xchacha20poly1305_init_pull,
1166                crypto_secretstream_xchacha20poly1305_pull as so_crypto_secretstream_xchacha20poly1305_pull,
1167                crypto_secretstream_xchacha20poly1305_push as so_crypto_secretstream_xchacha20poly1305_push,
1168                crypto_secretstream_xchacha20poly1305_state,
1169            };
1170
1171            use crate::constants::CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES;
1172            use crate::dryocstream::Tag;
1173
1174            crate::native_test_util::init();
1175
1176            let mut key = Key::default();
1177            crypto_secretstream_xchacha20poly1305_keygen(&mut key);
1178
1179            let mut push_state = State::new();
1180            let mut push_header = Header::default();
1181            crypto_secretstream_xchacha20poly1305_init_push(
1182                &mut push_state,
1183                &mut push_header,
1184                &key,
1185            );
1186            let push_state_init = push_state.clone();
1187
1188            let message = b"hello";
1189            let mut output =
1190                vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1191            let aad = b"";
1192            let tag = Tag::Message.bits();
1193            crypto_secretstream_xchacha20poly1305_push(
1194                &mut push_state,
1195                &mut output,
1196                message,
1197                Some(aad),
1198                tag,
1199            )
1200            .expect("push failed");
1201
1202            let mut so_output = output.clone();
1203            unsafe {
1204                use libc::{c_uchar, c_ulonglong};
1205                let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1206                    k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1207                    nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1208                    _pad: [0u8; 8],
1209                };
1210                so_state.k.copy_from_slice(&push_state_init.k);
1211                so_state.nonce.copy_from_slice(&push_state_init.nonce);
1212                let mut clen_p: c_ulonglong = 0;
1213                let ret = so_crypto_secretstream_xchacha20poly1305_push(
1214                    &mut so_state,
1215                    so_output.as_mut_ptr(),
1216                    &mut clen_p,
1217                    message.as_ptr(),
1218                    message.len() as u64,
1219                    aad.as_ptr(),
1220                    aad.len() as u64,
1221                    0,
1222                );
1223                assert_eq!(ret, 0);
1224                so_output.resize(clen_p as usize, 0);
1225                assert_eq!(
1226                    general_purpose::STANDARD.encode(&so_output),
1227                    general_purpose::STANDARD.encode(&output)
1228                );
1229                assert_eq!(
1230                    general_purpose::STANDARD.encode(so_state.k),
1231                    general_purpose::STANDARD.encode(push_state.k)
1232                );
1233                assert_eq!(
1234                    general_purpose::STANDARD.encode(so_state.nonce),
1235                    general_purpose::STANDARD.encode(push_state.nonce)
1236                );
1237
1238                let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1239                    k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1240                    nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1241                    _pad: [0u8; 8],
1242                };
1243                let mut mlen_p: c_ulonglong = 0;
1244                let mut tag_p: c_uchar = 0;
1245                let ret = so_crypto_secretstream_xchacha20poly1305_init_pull(
1246                    &mut so_state,
1247                    push_header.as_ptr(),
1248                    key.as_ptr(),
1249                );
1250                assert_eq!(ret, 0);
1251                assert_eq!(
1252                    general_purpose::STANDARD.encode(so_state.k),
1253                    general_purpose::STANDARD.encode(push_state_init.k)
1254                );
1255                assert_eq!(
1256                    general_purpose::STANDARD.encode(so_state.nonce),
1257                    general_purpose::STANDARD.encode(push_state_init.nonce)
1258                );
1259                assert!(so_output.len() >= CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES);
1260                let ret = so_crypto_secretstream_xchacha20poly1305_pull(
1261                    &mut so_state,
1262                    so_output.as_mut_ptr(),
1263                    &mut mlen_p,
1264                    &mut tag_p,
1265                    output.as_ptr(),
1266                    output.len() as u64,
1267                    aad.as_ptr(),
1268                    aad.len() as u64,
1269                );
1270                assert_eq!(ret, 0);
1271                so_output.resize(mlen_p as usize, 0);
1272            }
1273            assert_eq!(
1274                general_purpose::STANDARD.encode(message),
1275                general_purpose::STANDARD.encode(&so_output)
1276            );
1277
1278            let mut pull_state = State::default();
1279            crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &push_header, &key);
1280
1281            assert_eq!(
1282                general_purpose::STANDARD.encode(pull_state.k),
1283                general_purpose::STANDARD.encode(push_state_init.k)
1284            );
1285            assert_eq!(
1286                general_purpose::STANDARD.encode(pull_state.nonce),
1287                general_purpose::STANDARD.encode(push_state_init.nonce)
1288            );
1289
1290            let mut pull_result_message =
1291                vec![0u8; output.len() - CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1292            let mut pull_result_tag = 0u8;
1293            crypto_secretstream_xchacha20poly1305_pull(
1294                &mut pull_state,
1295                &mut pull_result_message,
1296                &mut pull_result_tag,
1297                &output,
1298                Some(&[]),
1299            )
1300            .expect("pull failed");
1301
1302            assert_eq!(Tag::Message, Tag::try_from(pull_result_tag).expect("tag"));
1303            assert_eq!(
1304                general_purpose::STANDARD.encode(&pull_result_message),
1305                general_purpose::STANDARD.encode(message)
1306            );
1307        }
1308
1309        #[test]
1310        fn test_rekey() {
1311            use base64::Engine as _;
1312            use base64::engine::general_purpose;
1313            use libsodium_sys::{
1314                crypto_secretstream_xchacha20poly1305_rekey as so_crypto_secretstream_xchacha20poly1305_rekey,
1315                crypto_secretstream_xchacha20poly1305_state,
1316            };
1317
1318            use crate::constants::CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES;
1319
1320            crate::native_test_util::init();
1321
1322            let mut key = Key::default();
1323            crypto_secretstream_xchacha20poly1305_keygen(&mut key);
1324
1325            let mut push_state = State::default();
1326            let mut push_header: Header = Header::default();
1327            crypto_secretstream_xchacha20poly1305_init_push(
1328                &mut push_state,
1329                &mut push_header,
1330                &key,
1331            );
1332            let push_state_init = push_state.clone();
1333
1334            crypto_secretstream_xchacha20poly1305_rekey(&mut push_state);
1335
1336            let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1337                k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1338                nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1339                _pad: [0u8; 8],
1340            };
1341            so_state.k.copy_from_slice(&push_state_init.k);
1342            so_state.nonce.copy_from_slice(&push_state_init.nonce);
1343            unsafe {
1344                so_crypto_secretstream_xchacha20poly1305_rekey(&mut so_state);
1345            }
1346            assert_eq!(
1347                general_purpose::STANDARD.encode(so_state.k),
1348                general_purpose::STANDARD.encode(push_state.k)
1349            );
1350            assert_eq!(
1351                general_purpose::STANDARD.encode(so_state.nonce),
1352                general_purpose::STANDARD.encode(push_state.nonce)
1353            );
1354        }
1355
1356        #[test]
1357        fn test_secretstream_lots_of_messages_push() {
1358            use base64::Engine as _;
1359            use base64::engine::general_purpose;
1360            use libc::{c_uchar, c_ulonglong};
1361            use libsodium_sys::{
1362                crypto_secretstream_xchacha20poly1305_init_pull as so_crypto_secretstream_xchacha20poly1305_init_pull,
1363                crypto_secretstream_xchacha20poly1305_pull as so_crypto_secretstream_xchacha20poly1305_pull,
1364                crypto_secretstream_xchacha20poly1305_state,
1365            };
1366
1367            use crate::constants::CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES;
1368            use crate::dryocstream::Tag;
1369
1370            crate::native_test_util::init();
1371
1372            let mut key = Key::default();
1373            crypto_secretstream_xchacha20poly1305_keygen(&mut key);
1374
1375            let mut push_state = State::new();
1376            let mut push_header = Header::default();
1377            crypto_secretstream_xchacha20poly1305_init_push(
1378                &mut push_state,
1379                &mut push_header,
1380                &key,
1381            );
1382            let push_state_init = push_state.clone();
1383
1384            let mut pull_state = State::default();
1385            crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &push_header, &key);
1386
1387            assert_eq!(
1388                general_purpose::STANDARD.encode(pull_state.k),
1389                general_purpose::STANDARD.encode(push_state_init.k)
1390            );
1391            assert_eq!(
1392                general_purpose::STANDARD.encode(pull_state.nonce),
1393                general_purpose::STANDARD.encode(push_state_init.nonce)
1394            );
1395
1396            let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1397                k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1398                nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1399                _pad: [0u8; 8],
1400            };
1401            so_state.k.copy_from_slice(&push_state_init.k);
1402            so_state.nonce.copy_from_slice(&push_state_init.nonce);
1403
1404            let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1405                k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1406                nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1407                _pad: [0u8; 8],
1408            };
1409            let mut mlen_p: c_ulonglong = 0;
1410            let mut tag_p: c_uchar = 0;
1411            unsafe {
1412                let ret = so_crypto_secretstream_xchacha20poly1305_init_pull(
1413                    &mut so_state,
1414                    push_header.as_ptr(),
1415                    key.as_ptr(),
1416                );
1417                assert_eq!(ret, 0);
1418            }
1419            assert_eq!(
1420                general_purpose::STANDARD.encode(so_state.k),
1421                general_purpose::STANDARD.encode(push_state_init.k)
1422            );
1423            assert_eq!(
1424                general_purpose::STANDARD.encode(so_state.nonce),
1425                general_purpose::STANDARD.encode(push_state_init.nonce)
1426            );
1427
1428            for i in 0..100 {
1429                let message = format!("hello {}", i);
1430                let aad = format!("aad {}", i);
1431                let tag = if i % 7 == 0 { Tag::Rekey } else { Tag::Message };
1432
1433                let mut output =
1434                    vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1435                crypto_secretstream_xchacha20poly1305_push(
1436                    &mut push_state,
1437                    &mut output,
1438                    message.as_bytes(),
1439                    Some(aad.as_bytes()),
1440                    tag.bits(),
1441                )
1442                .expect("push failed");
1443
1444                let mut so_output = output.clone();
1445                unsafe {
1446                    assert!(so_output.len() >= CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES);
1447                    let ret = so_crypto_secretstream_xchacha20poly1305_pull(
1448                        &mut so_state,
1449                        so_output.as_mut_ptr(),
1450                        &mut mlen_p,
1451                        &mut tag_p,
1452                        output.as_ptr(),
1453                        output.len() as u64,
1454                        aad.as_ptr(),
1455                        aad.len() as u64,
1456                    );
1457                    assert_eq!(ret, 0);
1458                    so_output.resize(mlen_p as usize, 0);
1459                }
1460                assert_eq!(
1461                    general_purpose::STANDARD.encode(&message),
1462                    general_purpose::STANDARD.encode(&so_output)
1463                );
1464
1465                let mut pull_result_message =
1466                    vec![0u8; output.len() - CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1467                let mut pull_result_tag = 0u8;
1468                crypto_secretstream_xchacha20poly1305_pull(
1469                    &mut pull_state,
1470                    &mut pull_result_message,
1471                    &mut pull_result_tag,
1472                    &output,
1473                    Some(aad.as_bytes()),
1474                )
1475                .expect("pull failed");
1476
1477                assert_eq!(tag, Tag::try_from(pull_result_tag).expect("tag"));
1478                assert_eq!(
1479                    general_purpose::STANDARD.encode(&pull_result_message),
1480                    general_purpose::STANDARD.encode(&message)
1481                );
1482            }
1483        }
1484
1485        #[test]
1486        fn test_secretstream_basic_pull() {
1487            use base64::Engine as _;
1488            use base64::engine::general_purpose;
1489            use libc::c_ulonglong;
1490            use libsodium_sys::{
1491                crypto_secretstream_xchacha20poly1305_init_push as so_crypto_secretstream_xchacha20poly1305_init_push,
1492                crypto_secretstream_xchacha20poly1305_push as so_crypto_secretstream_xchacha20poly1305_push,
1493                crypto_secretstream_xchacha20poly1305_state,
1494            };
1495
1496            use crate::constants::CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES;
1497
1498            crate::native_test_util::init();
1499
1500            let mut key = Key::default();
1501            crypto_secretstream_xchacha20poly1305_keygen(&mut key);
1502
1503            let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1504                k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1505                nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1506                _pad: [0u8; 8],
1507            };
1508            let mut so_header = Header::default();
1509            unsafe {
1510                so_crypto_secretstream_xchacha20poly1305_init_push(
1511                    &mut so_state,
1512                    so_header.as_mut_ptr(),
1513                    key.as_ptr(),
1514                );
1515            }
1516
1517            let mut pull_state = State::new();
1518            crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &so_header, &key);
1519
1520            let message = b"hello";
1521            let aad = b"aad";
1522            let mut so_output =
1523                vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1524            let mut clen_p: c_ulonglong = 0;
1525
1526            unsafe {
1527                let ret = so_crypto_secretstream_xchacha20poly1305_push(
1528                    &mut so_state,
1529                    so_output.as_mut_ptr(),
1530                    &mut clen_p,
1531                    message.as_ptr(),
1532                    message.len() as u64,
1533                    aad.as_ptr(),
1534                    aad.len() as u64,
1535                    0,
1536                );
1537                assert_eq!(ret, 0);
1538                so_output.resize(clen_p as usize, 0);
1539            }
1540
1541            let mut output = vec![0u8; so_output.len()];
1542            let mut tag = 0u8;
1543            let mlen = crypto_secretstream_xchacha20poly1305_pull(
1544                &mut pull_state,
1545                &mut output,
1546                &mut tag,
1547                &so_output,
1548                Some(aad),
1549            )
1550            .expect("decrypt failed");
1551            output.resize(mlen, 0);
1552
1553            assert_eq!(
1554                general_purpose::STANDARD.encode(&output),
1555                general_purpose::STANDARD.encode(message)
1556            );
1557            assert_eq!(tag, 0);
1558        }
1559
1560        #[test]
1561        fn test_secretstream_lots_of_messages_pull() {
1562            use base64::Engine as _;
1563            use base64::engine::general_purpose;
1564            use libc::c_ulonglong;
1565            use libsodium_sys::{
1566                crypto_secretstream_xchacha20poly1305_init_push as so_crypto_secretstream_xchacha20poly1305_init_push,
1567                crypto_secretstream_xchacha20poly1305_push as so_crypto_secretstream_xchacha20poly1305_push,
1568                crypto_secretstream_xchacha20poly1305_state,
1569            };
1570
1571            use crate::constants::CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES;
1572            use crate::dryocstream::Tag;
1573
1574            crate::native_test_util::init();
1575
1576            let mut key = Key::default();
1577            crypto_secretstream_xchacha20poly1305_keygen(&mut key);
1578
1579            let mut so_state = crypto_secretstream_xchacha20poly1305_state {
1580                k: [0u8; CRYPTO_STREAM_CHACHA20_IETF_KEYBYTES],
1581                nonce: [0u8; CRYPTO_STREAM_CHACHA20_IETF_NONCEBYTES],
1582                _pad: [0u8; 8],
1583            };
1584            let mut so_header = Header::default();
1585            unsafe {
1586                so_crypto_secretstream_xchacha20poly1305_init_push(
1587                    &mut so_state,
1588                    so_header.as_mut_ptr(),
1589                    key.as_ptr(),
1590                );
1591            }
1592
1593            let mut pull_state = State::new();
1594            crypto_secretstream_xchacha20poly1305_init_pull(&mut pull_state, &so_header, &key);
1595
1596            for i in 0..100 {
1597                let message = format!("hello {}", i);
1598                let aad = format!("aad {}", i);
1599                let mut so_output =
1600                    vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1601                let mut clen_p: c_ulonglong = 0;
1602
1603                let tag = if i % 7 == 0 { Tag::Rekey } else { Tag::Message };
1604
1605                unsafe {
1606                    let ret = so_crypto_secretstream_xchacha20poly1305_push(
1607                        &mut so_state,
1608                        so_output.as_mut_ptr(),
1609                        &mut clen_p,
1610                        message.as_ptr(),
1611                        message.len() as u64,
1612                        aad.as_ptr(),
1613                        aad.len() as u64,
1614                        tag.bits(),
1615                    );
1616                    assert_eq!(ret, 0);
1617                    so_output.resize(clen_p as usize, 0);
1618                }
1619
1620                let mut output =
1621                    vec![0u8; so_output.len() - CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
1622                let mut outtag = 0u8;
1623                crypto_secretstream_xchacha20poly1305_pull(
1624                    &mut pull_state,
1625                    &mut output,
1626                    &mut outtag,
1627                    &so_output,
1628                    Some(aad.as_bytes()),
1629                )
1630                .expect("decrypt failed");
1631
1632                assert_eq!(
1633                    general_purpose::STANDARD.encode(so_state.k),
1634                    general_purpose::STANDARD.encode(pull_state.k)
1635                );
1636                assert_eq!(
1637                    general_purpose::STANDARD.encode(so_state.nonce),
1638                    general_purpose::STANDARD.encode(pull_state.nonce)
1639                );
1640
1641                assert_eq!(
1642                    general_purpose::STANDARD.encode(&output),
1643                    general_purpose::STANDARD.encode(&message)
1644                );
1645                assert_eq!(outtag, tag.bits());
1646            }
1647        }
1648    }
1649}