Skip to main content

dryoc/
dryocstream.rs

1//! # Encrypted streams
2//!
3//! [`DryocStream`] provides libsodium-compatible authenticated encryption for
4//! an ordered sequence of messages, also known as a _secret stream_. It uses a
5//! shared secret key. Each message is encrypted and authenticated separately,
6//! while shared stream state links the messages and requires ordered
7//! processing. A tag can mark ordinary messages, rekeying points, or the
8//! expected end of the stream.
9//!
10//! Use [`DryocStream`] to encrypt messages written in order to a file or
11//! network connection. A modified, reordered, or duplicated message is
12//! rejected when it is pulled; a removed message is detected when the next
13//! message is pulled. Removing the end of a stream cannot be detected
14//! automatically: the sender must use [`Tag::Final`], and the application must
15//! reject a stream that ends without that tag.
16//!
17//! The shared key can be generated directly or derived with
18//! [`Kdf`](crate::kdf), [`Session`](crate::kx), or a password-hashing function
19//! such as [`crypto_pwhash`](crate::classic::crypto_pwhash).
20//!
21//! [`DryocStream::init_push`] generates a public header for each stream. Send
22//! the header to the receiving side before the ciphertexts. Never reuse the
23//! same key and header for another stream.
24//!
25//! # Rustaceous API example
26//!
27//! ```
28//! # #[cfg(feature = "alloc")]
29//! # {
30//! use dryoc::dryocstream::*;
31//! use dryoc::types::*;
32//! let message1 = b"Arbitrary data to encrypt";
33//! let message2 = b"split into";
34//! let message3 = b"three messages";
35//!
36//! // Generate a random secret key for this stream
37//! let key = Key::generate();
38//!
39//! // Initialize the push side, type annotations required on return type
40//! let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
41//!
42//! // Encrypt a series of messages
43//! let c1 = push_stream
44//!     .push_to_vec(message1, None, Tag::Message)
45//!     .expect("Encrypt failed");
46//! let c2 = push_stream
47//!     .push_to_vec(message2, None, Tag::Message)
48//!     .expect("Encrypt failed");
49//! let c3 = push_stream
50//!     .push_to_vec(message3, None, Tag::Final)
51//!     .expect("Encrypt failed");
52//!
53//! // Initialize the pull side using header generated by the push side
54//! let mut pull_stream = DryocStream::init_pull(&key, &header);
55//!
56//! // Decrypt the encrypted messages, type annotations required
57//! let (m1, tag1) = pull_stream.pull_to_vec(&c1, None).expect("Decrypt failed");
58//! let (m2, tag2) = pull_stream.pull_to_vec(&c2, None).expect("Decrypt failed");
59//! let (m3, tag3) = pull_stream.pull_to_vec(&c3, None).expect("Decrypt failed");
60//!
61//! assert_eq!(message1, m1.as_slice());
62//! assert_eq!(message2, m2.as_slice());
63//! assert_eq!(message3, m3.as_slice());
64//!
65//! assert_eq!(tag1, Tag::Message);
66//! assert_eq!(tag2, Tag::Message);
67//! assert_eq!(tag3, Tag::Final);
68//! # }
69//! ```
70//!
71//! ## Additional resources
72//!
73//! * See the [libsodium documentation](https://doc.libsodium.org/secret-key_cryptography/secretstream)
74//!   for more about secret streams
75//! * For public-key encryption, see [`DryocBox`](crate::dryocbox)
76//! * For individual messages encrypted with a shared key, see
77//!   [`DryocSecretBox`](crate::dryocsecretbox)
78//! * See the [`protected`] module for an example that stores keys in protected
79//!   memory
80
81#[cfg(feature = "alloc")]
82use alloc::vec::Vec;
83
84use zeroize::Zeroize;
85
86use crate::classic::crypto_secretstream_xchacha20poly1305::{
87    State, ciphertext_len_from_message_len, crypto_secretstream_xchacha20poly1305_init_pull,
88    crypto_secretstream_xchacha20poly1305_init_push, crypto_secretstream_xchacha20poly1305_pull,
89    crypto_secretstream_xchacha20poly1305_push, crypto_secretstream_xchacha20poly1305_rekey,
90    message_len_from_ciphertext_len,
91};
92use crate::constants::{
93    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES,
94    CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES,
95};
96use crate::error::Error;
97use crate::types::*;
98
99mod tag;
100pub use tag::Tag;
101
102mod sealed {
103    pub trait Sealed {}
104}
105
106/// Stream mode marker trait: [`Push`] or [`Pull`].
107///
108/// This trait is sealed and cannot be implemented outside dryoc.
109pub trait Mode: sealed::Sealed {}
110
111/// Indicates a push stream
112pub struct Push;
113/// Indicates a pull stream
114pub struct Pull;
115
116impl sealed::Sealed for Push {}
117impl sealed::Sealed for Pull {}
118impl Mode for Push {}
119impl Mode for Pull {}
120
121/// Stack-allocated secret for authenticated secret streams.
122pub type Key = StackByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES>;
123/// Stack-allocated header data for authenticated secret streams.
124pub type Header = StackByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES>;
125
126#[cfg(any(
127    all(feature = "protected", any(unix, windows)),
128    all(doc, not(doctest), feature = "std")
129))]
130#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
131pub mod protected {
132    //! # Protected memory type aliases for [`DryocStream`]
133    //!
134    //! Type aliases for using [`DryocStream`] with protected memory.
135    //!
136    //! ## Example
137    //! ```
138    //! use dryoc::dryocstream::protected::*;
139    //! use dryoc::dryocstream::{DryocStream, Tag};
140    //!
141    //! // Load some message into locked readonly memory.
142    //! let message1 = HeapBytes::from_slice_into_readonly_locked(b"Arbitrary data to encrypt")
143    //!     .expect("from slice failed");
144    //! let message2 =
145    //!     HeapBytes::from_slice_into_readonly_locked(b"split into").expect("from slice failed");
146    //! let message3 =
147    //!     HeapBytes::from_slice_into_readonly_locked(b"three messages").expect("from slice failed");
148    //!
149    //! // Generate a random key into locked readonly memory.
150    //! let key = Key::generate_readonly_locked().expect("key failed");
151    //!
152    //! // Initialize the push stream, place the header into locked memory
153    //! let (mut push_stream, header): (_, Locked<Header>) = DryocStream::init_push(&key);
154    //!
155    //! // Encrypt the set of messages, placing everything into locked memory.
156    //! let c1: LockedBytes = push_stream
157    //!     .push(&message1, None, Tag::Message)
158    //!     .expect("Encrypt failed");
159    //! let c2: LockedBytes = push_stream
160    //!     .push(&message2, None, Tag::Message)
161    //!     .expect("Encrypt failed");
162    //! let c3: LockedBytes = push_stream
163    //!     .push(&message3, None, Tag::Final)
164    //!     .expect("Encrypt failed");
165    //!
166    //! // Initialize the pull stream
167    //! let mut pull_stream = DryocStream::init_pull(&key, &header);
168    //!
169    //! // Decrypt the set of messages, putting everything into locked memory
170    //! let (m1, tag1): (LockedBytes, Tag) = pull_stream.pull(&c1, None).expect("Decrypt failed");
171    //! let (m2, tag2): (LockedBytes, Tag) = pull_stream.pull(&c2, None).expect("Decrypt failed");
172    //! let (m3, tag3): (LockedBytes, Tag) = pull_stream.pull(&c3, None).expect("Decrypt failed");
173    //!
174    //! assert_eq!(message1.as_slice(), m1.as_slice());
175    //! assert_eq!(message2.as_slice(), m2.as_slice());
176    //! assert_eq!(message3.as_slice(), m3.as_slice());
177    //!
178    //! assert_eq!(tag1, Tag::Message);
179    //! assert_eq!(tag2, Tag::Message);
180    //! assert_eq!(tag3, Tag::Final);
181    //! ```
182    use super::*;
183    pub use crate::protected::*;
184
185    /// Heap-allocated, page-aligned secret key for authenticated secret
186    /// streams, for use with protected memory.
187    pub type Key = HeapByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES>;
188    /// Heap-allocated, page-aligned header for authenticated secret
189    /// streams, for use with protected memory.
190    pub type Header = HeapByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES>;
191}
192
193/// Secret-key authenticated encrypted streams
194#[derive(PartialEq, Eq, Clone, Zeroize)]
195pub struct DryocStream<M> {
196    state: State,
197    phantom: core::marker::PhantomData<M>,
198}
199
200impl<M> Drop for DryocStream<M> {
201    fn drop(&mut self) {
202        self.state.zeroize()
203    }
204}
205
206impl<M> DryocStream<M> {
207    /// Rekeys the stream immediately. The sender and receiver must rekey at the
208    /// same position.
209    ///
210    /// Manual rekeying is unnecessary when the sender uses [`Tag::Rekey`] or
211    /// [`Tag::Final`], because those tags rekey after the message. The stream
212    /// also rekeys if its internal counter wraps. See the
213    /// [libsodium documentation](https://doc.libsodium.org/secret-key_cryptography/secretstream#rekeying)
214    /// for details.
215    pub fn rekey(&mut self) {
216        crypto_secretstream_xchacha20poly1305_rekey(&mut self.state)
217    }
218}
219
220impl DryocStream<Push> {
221    /// Returns a new push stream, initialized from `key`.
222    #[must_use]
223    pub fn init_push<
224        Header: NewByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES>,
225        Key: ByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES>,
226    >(
227        key: &Key,
228    ) -> (Self, Header) {
229        let mut state = State::new();
230        let mut header = Header::new_byte_array();
231        crypto_secretstream_xchacha20poly1305_init_push(
232            &mut state,
233            header.as_mut_array(),
234            key.as_array(),
235        );
236        (
237            Self {
238                state,
239                phantom: core::marker::PhantomData,
240            },
241            header,
242        )
243    }
244
245    /// Encrypts `message` for this stream with `associated_data` and `tag`,
246    /// returning the ciphertext.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the message exceeds the stream's maximum message
251    /// length, or the output storage does not resize to exactly the required
252    /// ciphertext length.
253    pub fn push<Output: NewBytes + ResizableBytes, Input: Bytes + ?Sized>(
254        &mut self,
255        message: &Input,
256        associated_data: Option<&[u8]>,
257        tag: Tag,
258    ) -> Result<Output, Error> {
259        let mut ciphertext = Output::new_bytes();
260        ciphertext.resize(
261            ciphertext_len_from_message_len(message.as_slice().len())?,
262            0,
263        );
264        crypto_secretstream_xchacha20poly1305_push(
265            &mut self.state,
266            ciphertext.as_mut_slice(),
267            message.as_slice(),
268            associated_data,
269            tag.bits(),
270        )?;
271        Ok(ciphertext)
272    }
273
274    /// Encrypts `message` for this stream with `associated_data` and `tag`,
275    /// returning the ciphertext.
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if the message exceeds the stream's maximum message
280    /// length.
281    #[cfg(feature = "alloc")]
282    pub fn push_to_vec<Input: Bytes + ?Sized>(
283        &mut self,
284        message: &Input,
285        associated_data: Option<&[u8]>,
286        tag: Tag,
287    ) -> Result<Vec<u8>, Error> {
288        self.push(message, associated_data, tag)
289    }
290}
291
292impl DryocStream<Pull> {
293    /// Returns a new pull stream, initialized from `key` and `header`.
294    #[must_use]
295    pub fn init_pull<
296        Key: ByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES>,
297        Header: ByteArray<CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES>,
298    >(
299        key: &Key,
300        header: &Header,
301    ) -> Self {
302        let mut state = State::new();
303        crypto_secretstream_xchacha20poly1305_init_pull(
304            &mut state,
305            header.as_array(),
306            key.as_array(),
307        );
308        Self {
309            state,
310            phantom: core::marker::PhantomData,
311        }
312    }
313
314    /// Decrypts `ciphertext` for this stream with `associated_data`, returning
315    /// the decrypted message and tag.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if the ciphertext is too short or too long, the output
320    /// storage cannot hold the plaintext, or authentication fails.
321    /// Authentication fails for a wrong key or header, mismatched associated
322    /// data, modified ciphertext, or messages processed out of order.
323    /// An authenticated tag byte that is not one of the four [`Tag`] values is
324    /// also rejected without advancing the stream.
325    pub fn pull<Output: NewBytes + ResizableBytes, Input: Bytes + ?Sized>(
326        &mut self,
327        ciphertext: &Input,
328        associated_data: Option<&[u8]>,
329    ) -> Result<(Output, Tag), Error> {
330        let message_len = message_len_from_ciphertext_len(ciphertext.as_slice().len())?;
331
332        let mut message = Output::new_bytes();
333        message.resize(message_len, 0);
334        let mut tag = 0u8;
335        let mut next_state = self.state.clone();
336        crypto_secretstream_xchacha20poly1305_pull(
337            &mut next_state,
338            message.as_mut_slice(),
339            &mut tag,
340            ciphertext.as_slice(),
341            associated_data,
342        )?;
343
344        let tag = match Tag::try_from(tag) {
345            Ok(tag) => tag,
346            Err(error) => {
347                message.as_mut_slice().zeroize();
348                return Err(error);
349            }
350        };
351        self.state = next_state;
352
353        Ok((message, tag))
354    }
355
356    /// Decrypts `ciphertext` for this stream with `associated_data`, returning
357    /// the decrypted message and tag into a [`Vec`].
358    ///
359    /// # Errors
360    ///
361    /// Returns an error if the ciphertext is too short or too long, or
362    /// authentication fails because the key, header, associated data, stream
363    /// position, or ciphertext does not match. An authenticated tag byte that
364    /// is not one of the four [`Tag`] values is also rejected without
365    /// advancing the stream.
366    #[cfg(feature = "alloc")]
367    pub fn pull_to_vec<Input: Bytes + ?Sized>(
368        &mut self,
369        ciphertext: &Input,
370        associated_data: Option<&[u8]>,
371    ) -> Result<(Vec<u8>, Tag), Error> {
372        self.pull(ciphertext, associated_data)
373    }
374}
375
376#[cfg(all(test, feature = "alloc"))]
377mod validation_tests {
378    use super::*;
379    use crate::constants::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES;
380
381    /// Key and header of the [`TAG_KAT`] stream.
382    pub(super) const TAG_KAT_KEY: [u8; 32] = *b"dryoc secretstream tag kat key!!";
383    pub(super) const TAG_KAT_HEADER: [u8; 24] = *b"fixed secretstream hdr!!";
384    /// `(message, associated data, tag, hex ciphertext)`.
385    pub(super) type TagKat = (&'static [u8], Option<&'static [u8]>, Tag, &'static str);
386    /// One stream carrying every tag, as [`TagKat`]s, pushed in order by
387    /// libsodium 1.0.22 from [`TAG_KAT_KEY`] and [`TAG_KAT_HEADER`]. The
388    /// message after `Rekey` checks that the tag rekeyed the stream.
389    pub(super) const TAG_KAT: [TagKat; 5] = [
390        (
391            b"message",
392            None,
393            Tag::Message,
394            "a59b40f34c0389665e80425058a5d5700805c1050cf44c1e",
395        ),
396        (
397            b"push",
398            Some(b"tag kat"),
399            Tag::Push,
400            "11f5df40c058fcf62f4caec9785bb132550a9ea0f7",
401        ),
402        (
403            b"rekey",
404            None,
405            Tag::Rekey,
406            "682581e6eb47fee588aa927a183e85660ed0f186de83",
407        ),
408        (
409            b"after rekey",
410            Some(b"tag kat"),
411            Tag::Message,
412            "54547a88d24cdb2d009376b265e49973dde328ac249762bfbc9cd83d",
413        ),
414        (
415            b"final",
416            None,
417            Tag::Final,
418            "84a19391176cc87a0e58212ec9cfe35d1d348223e901",
419        ),
420    ];
421
422    #[test]
423    fn every_tag_matches_libsodium_known_answers() {
424        // `init_push` always draws a random header, so build the push side
425        // for the fixed header directly: after initialization a push state
426        // equals the pull state for the same key and header.
427        let mut state = State::new();
428        crypto_secretstream_xchacha20poly1305_init_pull(&mut state, &TAG_KAT_HEADER, &TAG_KAT_KEY);
429        let mut push_stream = DryocStream::<Push> {
430            state,
431            phantom: core::marker::PhantomData,
432        };
433        let mut pull_stream =
434            DryocStream::init_pull(&Key::from(TAG_KAT_KEY), &Header::from(TAG_KAT_HEADER));
435
436        for (message, associated_data, tag, expected) in TAG_KAT {
437            let expected = hex::decode(expected).expect("hex");
438            let ciphertext = push_stream
439                .push_to_vec(message, associated_data, tag)
440                .expect("push failed");
441            assert_eq!(ciphertext, expected, "push {tag:?}");
442
443            let (decrypted, pulled_tag) = pull_stream
444                .pull_to_vec(&expected, associated_data)
445                .expect("pull failed");
446            assert_eq!((decrypted.as_slice(), pulled_tag), (message, tag));
447        }
448    }
449
450    #[test]
451    fn rustaceous_pull_rejects_every_unknown_tag_without_advancing_state() {
452        let key = Key::generate();
453        let mut push_state = State::new();
454        let mut raw_header =
455            [0u8; crate::constants::CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES];
456        crypto_secretstream_xchacha20poly1305_init_push(
457            &mut push_state,
458            &mut raw_header,
459            key.as_array(),
460        );
461        let header = Header::try_from(raw_header.as_slice()).expect("header conversion failed");
462        let mut pull_stream = DryocStream::init_pull(&key, &header);
463        let original_pull_state = pull_stream.state.clone();
464        let message = b"authenticated unknown tag";
465
466        // Classic push authenticates any tag byte; the Rustaceous pull must
467        // reject every byte other than the four `Tag` values.
468        for bits in 4..=u8::MAX {
469            let mut invalid_ciphertext =
470                vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
471            crypto_secretstream_xchacha20poly1305_push(
472                &mut push_state.clone(),
473                &mut invalid_ciphertext,
474                message,
475                None,
476                bits,
477            )
478            .expect("classic push failed");
479
480            let error = pull_stream
481                .pull_to_vec(&invalid_ciphertext, None)
482                .expect_err("unknown tag must be rejected");
483            assert!(
484                matches!(
485                    error,
486                    Error::InvalidValue {
487                        context: crate::ErrorContext::Tag,
488                        actual,
489                        ..
490                    } if actual == u64::from(bits)
491                ),
492                "tag byte {bits:#04x}: {error:?}"
493            );
494            assert!(pull_stream.state == original_pull_state);
495        }
496
497        let mut valid_ciphertext =
498            vec![0u8; message.len() + CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES];
499        crypto_secretstream_xchacha20poly1305_push(
500            &mut push_state,
501            &mut valid_ciphertext,
502            message,
503            None,
504            Tag::Message.bits(),
505        )
506        .expect("classic push failed");
507        let (decrypted, tag) = pull_stream
508            .pull_to_vec(&valid_ciphertext, None)
509            .expect("state must remain usable after rejection");
510        assert_eq!(decrypted, message);
511        assert_eq!(tag, Tag::Message);
512    }
513
514    #[test]
515    fn rustaceous_pull_rejects_short_ciphertext_without_advancing_state() {
516        let key = Key::generate();
517        let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
518        let c1 = push_stream
519            .push_to_vec(b"first", None, Tag::Message)
520            .expect("push failed");
521
522        let mut pull_stream = DryocStream::init_pull(&key, &header);
523        let original_state = pull_stream.state.clone();
524        for len in [0, 1, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES - 1] {
525            let short: &[u8] = &c1[..len];
526            assert!(matches!(
527                pull_stream.pull_to_vec(&short, None),
528                Err(Error::InvalidLength {
529                    context: crate::ErrorContext::Ciphertext,
530                    actual,
531                    ..
532                }) if actual == len
533            ));
534            assert!(pull_stream.state == original_state);
535        }
536
537        let (message, tag) = pull_stream.pull_to_vec(&c1, None).expect("pull failed");
538        assert_eq!(message, b"first");
539        assert_eq!(tag, Tag::Message);
540    }
541
542    #[test]
543    fn rustaceous_pull_out_of_order_fails_and_state_stays_usable() {
544        let key = Key::generate();
545        let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
546        let aad: &[u8] = b"stream aad";
547        let c1 = push_stream
548            .push_to_vec(b"first", Some(aad), Tag::Message)
549            .expect("push failed");
550        let c2 = push_stream
551            .push_to_vec(b"second", Some(aad), Tag::Push)
552            .expect("push failed");
553        let c3 = push_stream
554            .push_to_vec(b"third", Some(aad), Tag::Final)
555            .expect("push failed");
556
557        let mut pull_stream = DryocStream::init_pull(&key, &header);
558        let initial_state = pull_stream.state.clone();
559
560        // Skipping ahead is rejected and does not consume the position.
561        assert!(matches!(
562            pull_stream.pull_to_vec(&c2, Some(aad)),
563            Err(Error::AuthenticationFailed)
564        ));
565        assert!(matches!(
566            pull_stream.pull_to_vec(&c3, Some(aad)),
567            Err(Error::AuthenticationFailed)
568        ));
569        // Mismatched associated data is rejected the same way.
570        assert!(matches!(
571            pull_stream.pull_to_vec(&c1, None),
572            Err(Error::AuthenticationFailed)
573        ));
574        assert!(pull_stream.state == initial_state);
575
576        let (m1, t1) = pull_stream.pull_to_vec(&c1, Some(aad)).expect("pull c1");
577        assert_eq!((m1.as_slice(), t1), (&b"first"[..], Tag::Message));
578
579        // Replaying the consumed message and skipping c2 both fail.
580        let after_c1 = pull_stream.state.clone();
581        assert!(pull_stream.pull_to_vec(&c1, Some(aad)).is_err());
582        assert!(pull_stream.pull_to_vec(&c3, Some(aad)).is_err());
583        assert!(pull_stream.state == after_c1);
584
585        let (m2, t2) = pull_stream.pull_to_vec(&c2, Some(aad)).expect("pull c2");
586        assert_eq!((m2.as_slice(), t2), (&b"second"[..], Tag::Push));
587        let (m3, t3) = pull_stream.pull_to_vec(&c3, Some(aad)).expect("pull c3");
588        assert_eq!((m3.as_slice(), t3), (&b"third"[..], Tag::Final));
589    }
590
591    #[test]
592    fn manual_rekey_must_be_mirrored_by_the_pull_side() {
593        let key = Key::generate();
594        let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
595        let c1 = push_stream
596            .push_to_vec(b"before rekey", None, Tag::Message)
597            .expect("push failed");
598        push_stream.rekey();
599        let c2 = push_stream
600            .push_to_vec(b"after rekey", None, Tag::Message)
601            .expect("push failed");
602        let c3 = push_stream
603            .push_to_vec(b"final", None, Tag::Final)
604            .expect("push failed");
605
606        let mut pull_stream = DryocStream::init_pull(&key, &header);
607        let (m1, _) = pull_stream.pull_to_vec(&c1, None).expect("pull c1");
608        assert_eq!(m1, b"before rekey");
609
610        // Without the matching rekey the stream is desynchronized...
611        let before_rekey = pull_stream.state.clone();
612        assert!(matches!(
613            pull_stream.pull_to_vec(&c2, None),
614            Err(Error::AuthenticationFailed)
615        ));
616        assert!(pull_stream.state == before_rekey);
617
618        // ...and the same rekey resynchronizes it.
619        pull_stream.rekey();
620        let (m2, t2) = pull_stream.pull_to_vec(&c2, None).expect("pull c2");
621        assert_eq!((m2.as_slice(), t2), (&b"after rekey"[..], Tag::Message));
622        let (m3, t3) = pull_stream.pull_to_vec(&c3, None).expect("pull c3");
623        assert_eq!((m3.as_slice(), t3), (&b"final"[..], Tag::Final));
624    }
625}
626
627#[cfg(all(test, dryoc_native_tests, feature = "alloc"))]
628mod tests {
629    use super::*;
630    use crate::native_test_util::{
631        SECRETSTREAM_TAG_FINAL, SECRETSTREAM_TAG_MESSAGE, SECRETSTREAM_TAG_PUSH,
632        SECRETSTREAM_TAG_REKEY, SecretStream as SOStream,
633    };
634
635    /// The known answers in [`validation_tests::TAG_KAT`] are libsodium's,
636    /// and every [`Tag`] byte is libsodium's constant.
637    #[test]
638    fn tag_known_answers_are_libsodiums() {
639        use super::validation_tests::{TAG_KAT, TAG_KAT_HEADER, TAG_KAT_KEY};
640
641        for (tag, so_tag) in [
642            (Tag::Message, SECRETSTREAM_TAG_MESSAGE),
643            (Tag::Push, SECRETSTREAM_TAG_PUSH),
644            (Tag::Rekey, SECRETSTREAM_TAG_REKEY),
645            (Tag::Final, SECRETSTREAM_TAG_FINAL),
646        ] {
647            assert_eq!(tag.bits(), so_tag, "{tag:?}");
648        }
649
650        // libsodium's push state for a fixed header is its pull state.
651        let mut so_push = SOStream::init_pull(&TAG_KAT_HEADER, &TAG_KAT_KEY).expect("init pull");
652        for (message, associated_data, tag, expected) in TAG_KAT {
653            assert_eq!(
654                so_push
655                    .push(message, associated_data, tag.bits())
656                    .expect("push"),
657                hex::decode(expected).expect("hex"),
658                "{tag:?}"
659            );
660        }
661    }
662
663    /// Shared stream key; each stream still derives a random header.
664    fn fixed_key() -> Key {
665        Key::from(*b"dryoc secretstream rekey key 32b")
666    }
667
668    #[test]
669    fn rekey_on_push_side_is_mirrored_by_libsodium_pull() {
670        let key = fixed_key();
671        let aad: &[u8] = b"associated";
672        let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
673        let c1 = push_stream
674            .push_to_vec(b"before rekey", Some(aad), Tag::Message)
675            .expect("push failed");
676        push_stream.rekey();
677        let c2 = push_stream
678            .push_to_vec(b"after rekey", Some(aad), Tag::Push)
679            .expect("push failed");
680        let c3 = push_stream
681            .push_to_vec(b"final", Some(aad), Tag::Final)
682            .expect("push failed");
683
684        let so_header = header.as_slice();
685        let so_key = key.as_slice();
686
687        // Without the rekey, sodium cannot continue past c1.
688        let mut unsynced = SOStream::init_pull(so_header, so_key).expect("init pull");
689        assert_eq!(
690            unsynced.pull(&c1, Some(aad)).expect("pull c1"),
691            (b"before rekey".to_vec(), SECRETSTREAM_TAG_MESSAGE)
692        );
693        assert!(unsynced.pull(&c2, Some(aad)).is_err());
694
695        let mut so_pull = SOStream::init_pull(so_header, so_key).expect("init pull");
696        assert_eq!(
697            so_pull.pull(&c1, Some(aad)).expect("pull c1"),
698            (b"before rekey".to_vec(), SECRETSTREAM_TAG_MESSAGE)
699        );
700        so_pull.rekey().expect("rekey");
701        assert_eq!(
702            so_pull.pull(&c2, Some(aad)).expect("pull c2"),
703            (b"after rekey".to_vec(), SECRETSTREAM_TAG_PUSH)
704        );
705        assert_eq!(
706            so_pull.pull(&c3, Some(aad)).expect("pull c3"),
707            (b"final".to_vec(), SECRETSTREAM_TAG_FINAL)
708        );
709        assert!(so_pull.is_finalized());
710    }
711
712    #[test]
713    fn rekey_on_libsodium_push_side_is_mirrored_by_rustaceous_pull() {
714        let key = fixed_key();
715        let (mut so_push, so_header) = SOStream::init_push(key.as_slice());
716        let c1 = so_push
717            .push(b"before rekey", None, SECRETSTREAM_TAG_MESSAGE)
718            .expect("push failed");
719        so_push.rekey().expect("rekey");
720        let c2 = so_push
721            .push(b"after rekey", None, SECRETSTREAM_TAG_REKEY)
722            .expect("push failed");
723        let c3 = so_push
724            .push(b"after tag rekey", None, SECRETSTREAM_TAG_MESSAGE)
725            .expect("push failed");
726        let c4 = so_push
727            .push(b"final", None, SECRETSTREAM_TAG_FINAL)
728            .expect("push failed");
729
730        let header = Header::try_from(so_header.as_slice()).expect("header");
731        let mut pull_stream = DryocStream::init_pull(&key, &header);
732        let (m1, t1) = pull_stream.pull_to_vec(&c1, None).expect("pull c1");
733        assert_eq!((m1.as_slice(), t1), (&b"before rekey"[..], Tag::Message));
734
735        let before_rekey = pull_stream.state.clone();
736        assert!(matches!(
737            pull_stream.pull_to_vec(&c2, None),
738            Err(Error::AuthenticationFailed)
739        ));
740        assert!(pull_stream.state == before_rekey);
741
742        pull_stream.rekey();
743        let (m2, t2) = pull_stream.pull_to_vec(&c2, None).expect("pull c2");
744        assert_eq!((m2.as_slice(), t2), (&b"after rekey"[..], Tag::Rekey));
745        // The authenticated REKEY tag rekeys automatically; no manual call.
746        let (m3, t3) = pull_stream.pull_to_vec(&c3, None).expect("pull c3");
747        assert_eq!((m3.as_slice(), t3), (&b"after tag rekey"[..], Tag::Message));
748        let (m4, t4) = pull_stream.pull_to_vec(&c4, None).expect("pull c4");
749        assert_eq!((m4.as_slice(), t4), (&b"final"[..], Tag::Final));
750    }
751
752    #[test]
753    fn libsodium_messages_pulled_out_of_order_fail_and_state_stays_usable() {
754        let key = fixed_key();
755        let (mut so_push, so_header) = SOStream::init_push(key.as_slice());
756        let c1 = so_push
757            .push(b"one", None, SECRETSTREAM_TAG_MESSAGE)
758            .expect("push");
759        let c2 = so_push
760            .push(b"two", None, SECRETSTREAM_TAG_MESSAGE)
761            .expect("push");
762        let c3 = so_push
763            .push(b"three", None, SECRETSTREAM_TAG_FINAL)
764            .expect("push");
765
766        let header = Header::try_from(so_header.as_slice()).expect("header");
767        let mut pull_stream = DryocStream::init_pull(&key, &header);
768
769        assert!(matches!(
770            pull_stream.pull_to_vec(&c2, None),
771            Err(Error::AuthenticationFailed)
772        ));
773        let (m1, _) = pull_stream.pull_to_vec(&c1, None).expect("pull c1");
774        assert_eq!(m1, b"one");
775        assert!(pull_stream.pull_to_vec(&c3, None).is_err());
776        let (m2, _) = pull_stream.pull_to_vec(&c2, None).expect("pull c2");
777        assert_eq!(m2, b"two");
778        let (m3, t3) = pull_stream.pull_to_vec(&c3, None).expect("pull c3");
779        assert_eq!((m3.as_slice(), t3), (&b"three"[..], Tag::Final));
780    }
781
782    #[test]
783    fn test_stream_push() {
784        let message1 = b"Arbitrary data to encrypt";
785        let message2 = b"split into";
786        let message3 = b"three messages";
787
788        // Generate a random secret key for this stream
789        let key = Key::generate();
790
791        // Initialize the push side, type annotations required on return type
792        let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
793        // Encrypt a series of messages
794        let c1: Vec<u8> = push_stream
795            .push(message1, None, Tag::Message)
796            .expect("Encrypt failed");
797        let c2: Vec<u8> = push_stream
798            .push(message2, None, Tag::Message)
799            .expect("Encrypt failed");
800        let c3: Vec<u8> = push_stream
801            .push(message3, None, Tag::Final)
802            .expect("Encrypt failed");
803
804        // Initialize the pull side using header generated by the push side
805        let mut so_stream_pull =
806            SOStream::init_pull(header.as_slice(), key.as_slice()).expect("pull init failed");
807
808        let (m1, tag1) = so_stream_pull.pull(&c1, None).expect("decrypt failed");
809        let (m2, tag2) = so_stream_pull.pull(&c2, None).expect("decrypt failed");
810        let (m3, tag3) = so_stream_pull.pull(&c3, None).expect("decrypt failed");
811
812        assert_eq!(message1, m1.as_slice());
813        assert_eq!(message2, m2.as_slice());
814        assert_eq!(message3, m3.as_slice());
815
816        assert_eq!(tag1, SECRETSTREAM_TAG_MESSAGE);
817        assert_eq!(tag2, SECRETSTREAM_TAG_MESSAGE);
818        assert_eq!(tag3, SECRETSTREAM_TAG_FINAL);
819    }
820
821    #[test]
822    fn test_stream_pull() {
823        use core::convert::TryFrom;
824
825        let message1 = b"Arbitrary data to encrypt";
826        let message2 = b"split into";
827        let message3 = b"three messages";
828
829        // Generate a random secret key for this stream
830        let key = Key::generate();
831
832        // Initialize the push side, type annotations required on return type
833        let (mut so_push_stream, so_header) = SOStream::init_push(key.as_slice());
834        // Encrypt a series of messages
835        let c1: Vec<u8> = so_push_stream
836            .push(message1, None, SECRETSTREAM_TAG_MESSAGE)
837            .expect("Encrypt failed");
838        let c2: Vec<u8> = so_push_stream
839            .push(message2, None, SECRETSTREAM_TAG_MESSAGE)
840            .expect("Encrypt failed");
841        let c3: Vec<u8> = so_push_stream
842            .push(message3, None, SECRETSTREAM_TAG_FINAL)
843            .expect("Encrypt failed");
844
845        // Initialize the pull side using header generated by the push side
846        let mut pull_stream = DryocStream::init_pull(
847            &key,
848            &Header::try_from(so_header.as_slice()).expect("header"),
849        );
850
851        // Decrypt the encrypted messages, type annotations required
852        let (m1, tag1): (Vec<u8>, Tag) = pull_stream.pull(&c1, None).expect("Decrypt failed");
853        let (m2, tag2): (Vec<u8>, Tag) = pull_stream.pull(&c2, None).expect("Decrypt failed");
854        let (m3, tag3): (Vec<u8>, Tag) = pull_stream.pull(&c3, None).expect("Decrypt failed");
855
856        assert_eq!(message1, m1.as_slice());
857        assert_eq!(message2, m2.as_slice());
858        assert_eq!(message3, m3.as_slice());
859
860        assert_eq!(tag1, Tag::Message);
861        assert_eq!(tag2, Tag::Message);
862        assert_eq!(tag3, Tag::Final);
863    }
864
865    #[cfg(all(feature = "protected", any(unix, windows)))]
866    #[test]
867    fn test_protected_memory() {
868        use crate::protected::*;
869
870        let message1 = b"Arbitrary data to encrypt";
871        let message2 = b"split into";
872        let message3 = b"three messages";
873
874        // Generate a random secret key for this stream
875        let key = protected::Key::generate_locked().expect("generate locked");
876
877        // Initialize the push side, type annotations required on return type
878        let (mut push_stream, header): (_, Header) = DryocStream::init_push(&key);
879
880        // Set secret key memory to no-access, but it must be unlocked first
881        let key = key
882            .munlock()
883            .expect("munlock")
884            .mprotect_noaccess()
885            .expect("mprotect");
886
887        // Encrypt a series of messages
888        let c1: Locked<HeapBytes> = push_stream
889            .push(message1, None, Tag::Message)
890            .expect("Encrypt failed");
891        let c2: Vec<u8> = push_stream
892            .push(message2, None, Tag::Message)
893            .expect("Encrypt failed");
894        let c3: Vec<u8> = push_stream
895            .push(message3, None, Tag::Final)
896            .expect("Encrypt failed");
897
898        // allow access again
899        let key = key.mprotect_readonly().expect("mprotect");
900
901        // Initialize the pull side using header generated by the push side
902        let mut pull_stream = DryocStream::init_pull(&key, &header);
903
904        // Set secret key memory to no-access
905        let _key = key.mprotect_noaccess().expect("mprotect");
906
907        // Decrypt the encrypted messages, type annotations required
908        let (m1, tag1): (Locked<HeapBytes>, Tag) =
909            pull_stream.pull(&c1, None).expect("Decrypt failed");
910        let (m2, tag2): (Locked<HeapBytes>, Tag) =
911            pull_stream.pull(&c2, None).expect("Decrypt failed");
912        let (m3, tag3): (Locked<HeapBytes>, Tag) =
913            pull_stream.pull(&c3, None).expect("Decrypt failed");
914
915        assert_eq!(message1, m1.as_slice());
916        assert_eq!(message2, m2.as_slice());
917        assert_eq!(message3, m3.as_slice());
918
919        assert_eq!(tag1, Tag::Message);
920        assert_eq!(tag2, Tag::Message);
921        assert_eq!(tag3, Tag::Final);
922    }
923}