Skip to main content

dryoc/
dryocaead.rs

1//! # Authenticated encryption with additional data
2//!
3//! [`DryocAead`] provides libsodium-compatible XChaCha20-Poly1305-IETF
4//! authenticated encryption. The [`chacha20poly1305_ietf`] module provides the
5//! RFC 8439 variant with shorter, 96-bit nonces. Both encrypt a message and can
6//! authenticate unencrypted metadata, called _additional data_. If the
7//! ciphertext or additional data changes, decryption fails.
8//!
9//! Use [`DryocAead`] when your application manages nonces and needs libsodium's
10//! `ciphertext || tag` wire format. Use [`DryocAeadEnvelope`] to have dryoc
11//! generate a random XChaCha20 nonce and store it as
12//! `nonce || ciphertext || tag`.
13//!
14//! Nonces are public, but a nonce must never repeat with the same key.
15//! [`DryocAeadEnvelope`] generates and stores a nonce for each message. Callers
16//! using [`DryocAead`] must enforce nonce uniqueness themselves.
17//!
18//! If the `serde` feature is enabled,
19//! [`serde::Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) and
20//! [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) are implemented
21//! for [`AeadBox`] and [`AeadEnvelope`].
22//! If the `wincode_0_6` feature is enabled,
23//! [`wincode::SchemaRead`](https://docs.rs/wincode/0.6/wincode/trait.SchemaRead.html) and
24//! [`wincode::SchemaWrite`](https://docs.rs/wincode/0.6/wincode/trait.SchemaWrite.html) are
25//! implemented for [`VecBox`] and [`VecEnvelope`].
26//!
27//! ## Rustaceous API example
28//!
29//! ```
30//! # #[cfg(feature = "alloc")]
31//! # {
32//! use dryoc::dryocaead::*;
33//! use dryoc::types::*;
34//!
35//! let key = Key::generate();
36//! let nonce = Nonce::generate();
37//! let message = b"Arbitrary data to encrypt";
38//! let aad = b"metadata";
39//!
40//! let dryocaead =
41//!     DryocAead::encrypt_to_vecbox(message, Some(aad), &nonce, &key).expect("encrypt failed");
42//! let bytes = dryocaead.to_vec();
43//! let dryocaead = VecBox::from_bytes(&bytes).expect("from bytes");
44//! let decrypted = dryocaead
45//!     .decrypt_to_vec(Some(aad), &nonce, &key)
46//!     .expect("decrypt failed");
47//!
48//! assert_eq!(message, decrypted.as_slice());
49//! # }
50//! ```
51//!
52//! ## Generated nonce envelope example
53//!
54//! ```
55//! # #[cfg(feature = "alloc")]
56//! # {
57//! use dryoc::dryocaead::*;
58//! use dryoc::types::*;
59//!
60//! let key = Key::generate();
61//! let message = b"Arbitrary data to encrypt";
62//! let aad = b"metadata";
63//!
64//! let envelope =
65//!     DryocAeadEnvelope::seal_to_vecbox(message, Some(aad), &key).expect("seal failed");
66//! let bytes = envelope.to_vec();
67//! let envelope = VecEnvelope::from_bytes(&bytes).expect("from bytes");
68//! let decrypted = envelope.open_to_vec(Some(aad), &key).expect("open failed");
69//!
70//! assert_eq!(message, decrypted.as_slice());
71//! # }
72//! ```
73
74#[cfg(feature = "alloc")]
75use alloc::vec::Vec;
76use core::marker::PhantomData;
77
78#[cfg(feature = "serde")]
79use serde::{Deserialize, Serialize};
80use zeroize::Zeroize;
81
82use crate::constants::{
83    CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES,
84    CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX,
85    CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES,
86    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
87    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_MESSAGEBYTES_MAX,
88    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES,
89};
90use crate::error::{Error, ErrorContext};
91use crate::types::*;
92use crate::utils::{ct_eq_bytes, split_suffix};
93
94mod sealed {
95    /// The construction's nonce and tag sizes, private to dryoc. `AeadBox`
96    /// and `AeadEnvelope` accept any `Bytes` nonce and tag, so serialization
97    /// uses these to write the same fixed-size prefix the construction reads.
98    pub trait Sealed {
99        const NPUBBYTES: usize;
100        const ABYTES: usize;
101    }
102}
103
104/// Marker trait for AEAD algorithms supported by dryoc.
105///
106/// This trait is sealed so applications cannot plug in custom cryptographic
107/// algorithms while still allowing dryoc to add future AEAD constructions
108/// without changing the container types.
109pub trait AeadAlgorithm:
110    sealed::Sealed + Clone + Copy + core::fmt::Debug + Default + Eq + PartialEq
111{
112}
113
114/// XChaCha20-Poly1305-IETF AEAD algorithm marker.
115#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
116pub struct XChaCha20Poly1305Ietf;
117
118impl sealed::Sealed for XChaCha20Poly1305Ietf {
119    const ABYTES: usize = CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES;
120    const NPUBBYTES: usize = CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES;
121}
122impl AeadAlgorithm for XChaCha20Poly1305Ietf {}
123
124/// ChaCha20-Poly1305-IETF AEAD algorithm marker.
125#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
126pub struct ChaCha20Poly1305Ietf;
127
128impl sealed::Sealed for ChaCha20Poly1305Ietf {
129    const ABYTES: usize = CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
130    const NPUBBYTES: usize = CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES;
131}
132impl AeadAlgorithm for ChaCha20Poly1305Ietf {}
133
134pub use xchacha20poly1305_ietf::*;
135
136/// XChaCha20-Poly1305-IETF Rustaceous AEAD API, the default algorithm.
137///
138/// Everything in this module is re-exported from
139/// [`dryocaead`](crate::dryocaead), so `dryoc::dryocaead::Key` and
140/// `dryoc::dryocaead::xchacha20poly1305_ietf::Key` name the same type.
141pub mod xchacha20poly1305_ietf {
142    #[cfg(feature = "alloc")]
143    use alloc::vec::Vec;
144
145    pub use super::{AeadAlgorithm, AeadBox, AeadEnvelope, XChaCha20Poly1305Ietf};
146    use crate::constants::{
147        CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
148        CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES,
149    };
150    use crate::types::*;
151
152    /// Stack-allocated secret key for XChaCha20-Poly1305-IETF AEAD.
153    pub type Key = StackByteArray<CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES>;
154    /// Stack-allocated public nonce for XChaCha20-Poly1305-IETF AEAD.
155    pub type Nonce = StackByteArray<CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES>;
156    /// Stack-allocated authentication tag for XChaCha20-Poly1305-IETF AEAD.
157    pub type Mac = StackByteArray<CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES>;
158
159    /// XChaCha20-Poly1305-IETF AEAD box.
160    pub type DryocAead<Mac, Data> = AeadBox<XChaCha20Poly1305Ietf, Mac, Data>;
161    /// XChaCha20-Poly1305-IETF AEAD envelope with stored nonce.
162    pub type DryocAeadEnvelope<Nonce, Mac, Data> =
163        AeadEnvelope<XChaCha20Poly1305Ietf, Nonce, Mac, Data>;
164    /// [`Vec`]-based XChaCha20-Poly1305-IETF AEAD box.
165    #[cfg(feature = "alloc")]
166    pub type VecBox = DryocAead<Mac, Vec<u8>>;
167    /// [`Vec`]-based XChaCha20-Poly1305-IETF AEAD envelope.
168    #[cfg(feature = "alloc")]
169    pub type VecEnvelope = DryocAeadEnvelope<Nonce, Mac, Vec<u8>>;
170
171    #[cfg(any(
172        all(feature = "protected", any(unix, windows)),
173        all(doc, not(doctest), feature = "std")
174    ))]
175    #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
176    pub mod protected {
177        //! # Protected memory type aliases for [`AeadBox`] and [`AeadEnvelope`]
178        //!
179        //! This mod provides protected-memory type aliases for the
180        //! XChaCha20-Poly1305-IETF Rustaceous AEAD API.
181        use super::*;
182        pub use crate::protected::*;
183
184        /// Heap-allocated, page-aligned secret key for XChaCha20-Poly1305-IETF.
185        pub type Key = HeapByteArray<CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES>;
186        /// Heap-allocated, page-aligned public nonce for
187        /// XChaCha20-Poly1305-IETF.
188        pub type Nonce = HeapByteArray<CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES>;
189        /// Heap-allocated, page-aligned authentication tag for
190        /// XChaCha20-Poly1305-IETF.
191        pub type Mac = HeapByteArray<CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES>;
192
193        /// Locked AEAD box, provided as a type alias for convenience.
194        pub type LockedBox = AeadBox<XChaCha20Poly1305Ietf, Locked<Mac>, LockedBytes>;
195        /// Locked AEAD envelope with stored nonce, provided as a type alias for
196        /// convenience.
197        pub type LockedEnvelope =
198            AeadEnvelope<XChaCha20Poly1305Ietf, Locked<Nonce>, Locked<Mac>, LockedBytes>;
199    }
200}
201
202/// ChaCha20-Poly1305-IETF Rustaceous AEAD API.
203///
204/// A nonce must never repeat with the same key. RFC 8439 requires callers to
205/// manage these 96-bit nonces uniquely, typically with a counter, rather than
206/// generate them randomly. Accordingly, this variant does not provide the
207/// generated-nonce [`AeadEnvelope::seal`] convenience available to XChaCha20.
208/// Use [`AeadBox::encrypt`] with an explicitly managed nonce; an
209/// [`AeadEnvelope`] can store that nonce via [`AeadEnvelope::from_parts`].
210///
211/// ## Rustaceous API example
212///
213/// ```
214/// # #[cfg(feature = "alloc")]
215/// # {
216/// use dryoc::dryocaead::chacha20poly1305_ietf::*;
217/// use dryoc::types::*;
218///
219/// let key = Key::generate();
220/// // This 96-bit nonce must be unique for every message encrypted with `key`.
221/// let nonce = Nonce::from([0u8; 12]);
222/// let message = b"Better three hours too soon than a minute too late.";
223/// let aad = b"metadata";
224///
225/// let dryocaead =
226///     VecBox::encrypt_to_vecbox(message, Some(aad), &nonce, &key).expect("encrypt failed");
227/// let bytes = dryocaead.to_vec();
228/// let dryocaead = VecBox::from_bytes(&bytes).expect("from bytes");
229/// let decrypted = dryocaead
230///     .decrypt_to_vec(Some(aad), &nonce, &key)
231///     .expect("decrypt failed");
232///
233/// assert_eq!(message, decrypted.as_slice());
234/// # }
235/// ```
236pub mod chacha20poly1305_ietf {
237    #[cfg(feature = "alloc")]
238    use alloc::vec::Vec;
239
240    pub use super::{AeadAlgorithm, AeadBox, AeadEnvelope, ChaCha20Poly1305Ietf};
241    use crate::constants::{
242        CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES, CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES,
243        CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES,
244    };
245    use crate::types::*;
246
247    /// Stack-allocated secret key.
248    pub type Key = StackByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES>;
249    /// Stack-allocated public nonce.
250    pub type Nonce = StackByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES>;
251    /// Stack-allocated authentication tag.
252    pub type Mac = StackByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES>;
253    /// ChaCha20-Poly1305-IETF AEAD box.
254    pub type DryocAead<Mac, Data> = AeadBox<ChaCha20Poly1305Ietf, Mac, Data>;
255    /// ChaCha20-Poly1305-IETF AEAD envelope with stored nonce.
256    pub type DryocAeadEnvelope<Nonce, Mac, Data> =
257        AeadEnvelope<ChaCha20Poly1305Ietf, Nonce, Mac, Data>;
258    /// [`Vec`]-based ChaCha20-Poly1305-IETF AEAD box.
259    #[cfg(feature = "alloc")]
260    pub type VecBox = DryocAead<Mac, Vec<u8>>;
261    /// [`Vec`]-based ChaCha20-Poly1305-IETF AEAD envelope.
262    #[cfg(feature = "alloc")]
263    pub type VecEnvelope = DryocAeadEnvelope<Nonce, Mac, Vec<u8>>;
264
265    #[cfg(any(
266        all(feature = "protected", any(unix, windows)),
267        all(doc, not(doctest), feature = "std")
268    ))]
269    #[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
270    pub mod protected {
271        //! Protected-memory aliases for ChaCha20-Poly1305-IETF.
272        use super::*;
273        pub use crate::protected::*;
274
275        /// Heap-allocated, page-aligned secret key.
276        pub type Key = HeapByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES>;
277        /// Heap-allocated, page-aligned public nonce.
278        pub type Nonce = HeapByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES>;
279        /// Heap-allocated, page-aligned authentication tag.
280        pub type Mac = HeapByteArray<CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES>;
281        /// Locked AEAD box.
282        pub type LockedBox = AeadBox<ChaCha20Poly1305Ietf, Locked<Mac>, LockedBytes>;
283        /// Locked AEAD envelope with stored nonce.
284        pub type LockedEnvelope =
285            AeadEnvelope<ChaCha20Poly1305Ietf, Locked<Nonce>, Locked<Mac>, LockedBytes>;
286    }
287}
288
289#[derive(Clone, Debug)]
290#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
291/// Authenticated encrypted data for a concrete AEAD algorithm.
292///
293/// The byte representation for the supported algorithms is `ciphertext || tag`.
294pub struct AeadBox<Algorithm: AeadAlgorithm, Mac, Data> {
295    #[cfg_attr(feature = "serde", serde(skip))]
296    algorithm: PhantomData<Algorithm>,
297    tag: Mac,
298    data: Data,
299}
300
301#[derive(Clone, Debug)]
302#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
303/// Authenticated encrypted data with its nonce stored alongside it.
304///
305/// The byte representation for the supported algorithms is
306/// `nonce || ciphertext || tag`.
307pub struct AeadEnvelope<Algorithm: AeadAlgorithm, Nonce, Mac, Data> {
308    #[cfg_attr(feature = "serde", serde(skip))]
309    algorithm: PhantomData<Algorithm>,
310    nonce: Nonce,
311    tag: Mac,
312    data: Data,
313}
314
315/// Generates the wincode schema implementations for one algorithm's `VecBox`
316/// and `VecEnvelope`: `ciphertext || tag` for the box and
317/// `nonce || ciphertext || tag` for the envelope.
318#[cfg(feature = "wincode_0_6")]
319macro_rules! impl_wincode_aead {
320    ($box:ty, $envelope:ty, $abytes:expr, $npubbytes:expr) => {
321        impl_wincode_schema!($box {
322            data: Vec<u8> = (src => &src.data, data => data),
323            tag: [u8; $abytes] = (src => src.tag.as_array(), tag => tag.into()),
324        } extra { algorithm: PhantomData });
325
326        impl_wincode_schema!($envelope {
327            nonce: [u8; $npubbytes] = (src => src.nonce.as_array(), nonce => nonce.into()),
328            data: Vec<u8> = (src => &src.data, data => data),
329            tag: [u8; $abytes] = (src => src.tag.as_array(), tag => tag.into()),
330        } extra { algorithm: PhantomData });
331    };
332}
333
334#[cfg(feature = "wincode_0_6")]
335impl_wincode_aead!(
336    VecBox,
337    VecEnvelope,
338    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES,
339    CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
340);
341#[cfg(feature = "wincode_0_6")]
342impl_wincode_aead!(
343    chacha20poly1305_ietf::VecBox,
344    chacha20poly1305_ietf::VecEnvelope,
345    CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES,
346    CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES
347);
348
349/// Generates the algorithm-specific [`AeadBox`] and [`AeadEnvelope`] methods
350/// for one AEAD construction: `encrypt`, `decrypt`, `from_bytes`, `open`, and
351/// the `VecBox`/`VecEnvelope` convenience wrappers, all dispatching to the
352/// Classic implementation in `crate::classic::$module`.
353macro_rules! impl_aead_algorithm {
354    (
355        algorithm:
356        $algorithm:ident,module:
357        $module:ident,encrypt_detached:
358        $encrypt_detached:ident,decrypt_detached:
359        $decrypt_detached:ident,keybytes:
360        $keybytes:expr,npubbytes:
361        $npubbytes:expr,abytes:
362        $abytes:expr,messagebytes_max:
363        $messagebytes_max:expr
364    ) => {
365        impl<Mac: NewByteArray<$abytes> + Zeroize, Data: NewBytes + ResizableBytes + Zeroize>
366            AeadBox<$algorithm, Mac, Data>
367        {
368            /// Encrypts a message using `key`, `nonce`, and optional associated data.
369            ///
370            /// # Errors
371            ///
372            /// Returns an error if the message exceeds the construction's maximum
373            /// length or the output storage does not resize to the message length.
374            pub fn encrypt<
375                Message: Bytes + ?Sized,
376                Nonce: ByteArray<$npubbytes>,
377                SecretKey: ByteArray<$keybytes>,
378            >(
379                message: &Message,
380                associated_data: Option<&[u8]>,
381                nonce: &Nonce,
382                key: &SecretKey,
383            ) -> Result<Self, Error> {
384                use crate::classic::$module::$encrypt_detached;
385
386                // Reject oversized input before allocating output for it.
387                validate_length!(max $messagebytes_max, message.len(), ErrorContext::Message);
388                let mut new = Self {
389                    algorithm: PhantomData,
390                    tag: Mac::new_byte_array(),
391                    data: Data::new_bytes(),
392                };
393                new.data.resize(message.len(), 0);
394
395                $encrypt_detached(
396                    new.data.as_mut_slice(),
397                    new.tag.as_mut_array(),
398                    message.as_slice(),
399                    associated_data,
400                    nonce.as_array(),
401                    key.as_array(),
402                )?;
403
404                Ok(new)
405            }
406        }
407
408        impl<
409            'a,
410            Mac: ByteArray<$abytes> + core::convert::TryFrom<&'a [u8]> + Zeroize,
411            Data: Bytes + From<&'a [u8]> + Zeroize,
412        > AeadBox<$algorithm, Mac, Data>
413        {
414            /// Initializes an [`AeadBox`] from `ciphertext || tag`.
415            ///
416            /// # Errors
417            ///
418            /// Returns an error if `bytes` is shorter than one authentication tag or
419            /// the tag cannot be converted to `Mac`.
420            pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
421                let (data, tag) = split_suffix(bytes, $abytes, ErrorContext::AeadCiphertext)?;
422                Ok(Self {
423                    algorithm: PhantomData,
424                    tag: Mac::try_from(tag)
425                        .map_err(|_| Error::invalid_encoding(ErrorContext::AuthenticationTag))?,
426                    data: Data::from(data),
427                })
428            }
429        }
430
431        impl<Mac: ByteArray<$abytes>, Data: Bytes> AeadBox<$algorithm, Mac, Data> {
432            /// Decrypts this box using `key`, `nonce`, and optional associated data.
433            ///
434            /// # Errors
435            ///
436            /// Returns an error if the ciphertext exceeds the construction's maximum
437            /// length, the output storage has the wrong length, or authentication
438            /// fails. Authentication fails when the key, nonce, associated data,
439            /// ciphertext, or tag does not match the value used during encryption.
440            pub fn decrypt<
441                Output: ResizableBytes + NewBytes,
442                Nonce: ByteArray<$npubbytes>,
443                SecretKey: ByteArray<$keybytes>,
444            >(
445                &self,
446                associated_data: Option<&[u8]>,
447                nonce: &Nonce,
448                key: &SecretKey,
449            ) -> Result<Output, Error> {
450                use crate::classic::$module::$decrypt_detached;
451
452                // Reject oversized input before allocating output for it.
453                validate_length!(max $messagebytes_max, self.data.as_slice().len(), ErrorContext::Message);
454                let mut message = Output::new_bytes();
455                message.resize(self.data.as_slice().len(), 0);
456
457                $decrypt_detached(
458                    message.as_mut_slice(),
459                    self.data.as_slice(),
460                    self.tag.as_array(),
461                    associated_data,
462                    nonce.as_array(),
463                    key.as_array(),
464                )?;
465
466                Ok(message)
467            }
468        }
469
470        impl<
471            'a,
472            Nonce: ByteArray<$npubbytes> + core::convert::TryFrom<&'a [u8]> + Zeroize,
473            Mac: ByteArray<$abytes> + core::convert::TryFrom<&'a [u8]> + Zeroize,
474            Data: Bytes + From<&'a [u8]> + Zeroize,
475        > AeadEnvelope<$algorithm, Nonce, Mac, Data>
476        {
477            /// Initializes an [`AeadEnvelope`] from `nonce || ciphertext || tag`.
478            ///
479            /// # Errors
480            ///
481            /// Returns an error if `bytes` is shorter than one nonce plus one
482            /// authentication tag, or if either field cannot be converted to its
483            /// target type.
484            pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
485                validate_length!(min $npubbytes + $abytes, bytes.len(), ErrorContext::AeadEnvelope);
486                let (nonce, rest) = bytes.split_at($npubbytes);
487                let (data, tag) = rest.split_at(rest.len() - $abytes);
488                Ok(Self {
489                    algorithm: PhantomData,
490                    nonce: Nonce::try_from(nonce)
491                        .map_err(|_| Error::invalid_encoding(ErrorContext::Nonce))?,
492                    tag: Mac::try_from(tag)
493                        .map_err(|_| Error::invalid_encoding(ErrorContext::AuthenticationTag))?,
494                    data: Data::from(data),
495                })
496            }
497        }
498
499        impl<Nonce: ByteArray<$npubbytes>, Mac: ByteArray<$abytes>, Data: Bytes>
500            AeadEnvelope<$algorithm, Nonce, Mac, Data>
501        {
502            /// Decrypts this envelope using `key` and optional associated data.
503            ///
504            /// # Errors
505            ///
506            /// Returns an error if the ciphertext exceeds the construction's maximum
507            /// length, the output storage has the wrong length, or authentication
508            /// fails. Authentication fails when the key, associated data, stored
509            /// nonce, ciphertext, or tag does not match the value used during
510            /// encryption.
511            pub fn open<Output: ResizableBytes + NewBytes, SecretKey: ByteArray<$keybytes>>(
512                &self,
513                associated_data: Option<&[u8]>,
514                key: &SecretKey,
515            ) -> Result<Output, Error> {
516                use crate::classic::$module::$decrypt_detached;
517
518                // Reject oversized input before allocating output for it.
519                validate_length!(max $messagebytes_max, self.data.as_slice().len(), ErrorContext::Message);
520                let mut message = Output::new_bytes();
521                message.resize(self.data.as_slice().len(), 0);
522
523                $decrypt_detached(
524                    message.as_mut_slice(),
525                    self.data.as_slice(),
526                    self.tag.as_array(),
527                    associated_data,
528                    self.nonce.as_array(),
529                    key.as_array(),
530                )?;
531
532                Ok(message)
533            }
534        }
535
536        #[cfg(feature = "alloc")]
537        impl AeadBox<$algorithm, StackByteArray<$abytes>, Vec<u8>> {
538            /// Encrypts a message and returns a [`VecBox`].
539            ///
540            /// # Errors
541            ///
542            /// Returns an error if the message exceeds the construction's maximum
543            /// length.
544            pub fn encrypt_to_vecbox<
545                Message: Bytes + ?Sized,
546                Nonce: ByteArray<$npubbytes>,
547                SecretKey: ByteArray<$keybytes>,
548            >(
549                message: &Message,
550                associated_data: Option<&[u8]>,
551                nonce: &Nonce,
552                key: &SecretKey,
553            ) -> Result<Self, Error> {
554                Self::encrypt(message, associated_data, nonce, key)
555            }
556
557            /// Decrypts this box and returns the plaintext as a [`Vec`].
558            ///
559            /// # Errors
560            ///
561            /// Returns an error if the ciphertext exceeds the construction's maximum
562            /// length or authentication fails because the key, nonce, associated data,
563            /// ciphertext, or tag does not match.
564            pub fn decrypt_to_vec<Nonce: ByteArray<$npubbytes>, SecretKey: ByteArray<$keybytes>>(
565                &self,
566                associated_data: Option<&[u8]>,
567                nonce: &Nonce,
568                key: &SecretKey,
569            ) -> Result<Vec<u8>, Error> {
570                self.decrypt(associated_data, nonce, key)
571            }
572
573            /// Consumes this box and returns it as `ciphertext || tag`.
574            #[must_use]
575            pub fn into_vec(mut self) -> Vec<u8> {
576                self.data.resize(self.data.len() + $abytes, 0);
577                let tag_offset = self.data.len() - $abytes;
578                self.data[tag_offset..].copy_from_slice(self.tag.as_slice());
579                self.data
580            }
581        }
582
583        #[cfg(feature = "alloc")]
584        impl
585            AeadEnvelope<$algorithm, StackByteArray<$npubbytes>, StackByteArray<$abytes>, Vec<u8>>
586        {
587            /// Decrypts this envelope and returns the plaintext as a [`Vec`].
588            ///
589            /// # Errors
590            ///
591            /// Returns an error if the ciphertext exceeds the construction's maximum
592            /// length or authentication fails because the key, associated data, stored
593            /// nonce, ciphertext, or tag does not match.
594            pub fn open_to_vec<SecretKey: ByteArray<$keybytes>>(
595                &self,
596                associated_data: Option<&[u8]>,
597                key: &SecretKey,
598            ) -> Result<Vec<u8>, Error> {
599                self.open(associated_data, key)
600            }
601
602            /// Consumes this envelope and returns it as `nonce || ciphertext || tag`.
603            #[must_use]
604            pub fn into_vec(self) -> Vec<u8> {
605                let mut output = self.nonce.to_vec();
606                output.extend_from_slice(self.data.as_slice());
607                output.extend_from_slice(self.tag.as_slice());
608                output
609            }
610        }
611    };
612}
613
614/// Generates the random-nonce `seal` family for one AEAD construction whose
615/// nonce is large enough to choose at random (XChaCha20-Poly1305-IETF).
616macro_rules! impl_aead_envelope_seal {
617    (
618        algorithm:
619        $algorithm:ident,keybytes:
620        $keybytes:expr,npubbytes:
621        $npubbytes:expr,abytes:
622        $abytes:expr
623    ) => {
624        impl<
625            Nonce: NewByteArray<$npubbytes> + Zeroize,
626            Mac: NewByteArray<$abytes> + Zeroize,
627            Data: NewBytes + ResizableBytes + Zeroize,
628        > AeadEnvelope<$algorithm, Nonce, Mac, Data>
629        {
630            /// Encrypts a message with a generated nonce and stores that nonce with the
631            /// ciphertext and tag.
632            ///
633            /// # Errors
634            ///
635            /// Returns an error if the message exceeds the construction's maximum
636            /// length or the output storage does not resize to the message length.
637            ///
638            /// # Panics
639            ///
640            /// Panics if the operating system's random number generator fails.
641            pub fn seal<Message: Bytes + ?Sized, SecretKey: ByteArray<$keybytes>>(
642                message: &Message,
643                associated_data: Option<&[u8]>,
644                key: &SecretKey,
645            ) -> Result<Self, Error> {
646                let nonce = Nonce::generate();
647                let aead_box = AeadBox::<$algorithm, Mac, Data>::encrypt(
648                    message,
649                    associated_data,
650                    &nonce,
651                    key,
652                )?;
653                let (tag, data) = aead_box.into_parts();
654
655                Ok(Self {
656                    algorithm: PhantomData,
657                    nonce,
658                    tag,
659                    data,
660                })
661            }
662        }
663
664        #[cfg(feature = "alloc")]
665        impl
666            AeadEnvelope<$algorithm, StackByteArray<$npubbytes>, StackByteArray<$abytes>, Vec<u8>>
667        {
668            /// Encrypts a message with a generated nonce and returns a [`VecEnvelope`].
669            ///
670            /// # Errors
671            ///
672            /// Returns an error if the message exceeds the construction's maximum
673            /// length.
674            ///
675            /// # Panics
676            ///
677            /// Panics if the operating system's random number generator fails.
678            pub fn seal_to_vecbox<Message: Bytes + ?Sized, SecretKey: ByteArray<$keybytes>>(
679                message: &Message,
680                associated_data: Option<&[u8]>,
681                key: &SecretKey,
682            ) -> Result<Self, Error> {
683                Self::seal(message, associated_data, key)
684            }
685        }
686    };
687}
688
689impl_aead_algorithm! {
690    algorithm: XChaCha20Poly1305Ietf,
691    module: crypto_aead_xchacha20poly1305_ietf,
692    encrypt_detached: crypto_aead_xchacha20poly1305_ietf_encrypt_detached,
693    decrypt_detached: crypto_aead_xchacha20poly1305_ietf_decrypt_detached,
694    keybytes: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
695    npubbytes: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES,
696    abytes: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES,
697    messagebytes_max: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_MESSAGEBYTES_MAX
698}
699
700impl_aead_envelope_seal! {
701    algorithm: XChaCha20Poly1305Ietf,
702    keybytes: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
703    npubbytes: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES,
704    abytes: CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
705}
706
707impl_aead_algorithm! {
708    algorithm: ChaCha20Poly1305Ietf,
709    module: crypto_aead_chacha20poly1305_ietf,
710    encrypt_detached: crypto_aead_chacha20poly1305_ietf_encrypt_detached,
711    decrypt_detached: crypto_aead_chacha20poly1305_ietf_decrypt_detached,
712    keybytes: CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES,
713    npubbytes: CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES,
714    abytes: CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES,
715    messagebytes_max: CRYPTO_AEAD_CHACHA20POLY1305_IETF_MESSAGEBYTES_MAX
716}
717
718impl<Algorithm: AeadAlgorithm, Mac: Zeroize, Data: Zeroize> Zeroize
719    for AeadBox<Algorithm, Mac, Data>
720{
721    fn zeroize(&mut self) {
722        self.tag.zeroize();
723        self.data.zeroize();
724    }
725}
726
727impl<Algorithm: AeadAlgorithm, Nonce: Zeroize, Mac: Zeroize, Data: Zeroize> Zeroize
728    for AeadEnvelope<Algorithm, Nonce, Mac, Data>
729{
730    fn zeroize(&mut self) {
731        self.nonce.zeroize();
732        self.tag.zeroize();
733        self.data.zeroize();
734    }
735}
736
737impl<Algorithm: AeadAlgorithm, Mac, Data> AeadBox<Algorithm, Mac, Data> {
738    /// Returns a new AEAD box from `tag` and ciphertext `data`.
739    #[must_use]
740    pub fn from_parts(tag: Mac, data: Data) -> Self {
741        Self {
742            algorithm: PhantomData,
743            tag,
744            data,
745        }
746    }
747
748    /// Returns the authentication tag.
749    pub fn tag(&self) -> &Mac {
750        &self.tag
751    }
752
753    /// Returns the ciphertext.
754    pub fn data(&self) -> &Data {
755        &self.data
756    }
757
758    /// Moves the tag and ciphertext out of this instance.
759    #[must_use]
760    pub fn into_parts(self) -> (Mac, Data) {
761        (self.tag, self.data)
762    }
763}
764
765impl<Algorithm: AeadAlgorithm, Mac: Bytes, Data: Bytes> AeadBox<Algorithm, Mac, Data> {
766    /// Copies `self` into a new [`Vec`].
767    #[cfg(feature = "alloc")]
768    #[must_use]
769    pub fn to_vec(&self) -> Vec<u8> {
770        self.to_bytes()
771    }
772
773    /// Copies `self` into the target as `ciphertext || tag`, where the tag is
774    /// the first tag-size bytes of `Mac`.
775    ///
776    /// # Panics
777    ///
778    /// Panics if the tag is shorter than the algorithm's tag size.
779    #[must_use]
780    pub fn to_bytes<Output: NewBytes + ResizableBytes>(&self) -> Output {
781        concat_bytes(
782            self.data.as_slice(),
783            &self.tag.as_slice()[..Algorithm::ABYTES],
784        )
785    }
786}
787
788impl<Algorithm: AeadAlgorithm, Nonce, Mac, Data> AeadEnvelope<Algorithm, Nonce, Mac, Data> {
789    /// Returns a new AEAD envelope from `nonce`, `tag`, and ciphertext `data`.
790    #[must_use]
791    pub fn from_parts(nonce: Nonce, tag: Mac, data: Data) -> Self {
792        Self {
793            algorithm: PhantomData,
794            nonce,
795            tag,
796            data,
797        }
798    }
799
800    /// Returns the stored nonce.
801    pub fn nonce(&self) -> &Nonce {
802        &self.nonce
803    }
804
805    /// Returns the authentication tag.
806    pub fn tag(&self) -> &Mac {
807        &self.tag
808    }
809
810    /// Returns the ciphertext.
811    pub fn data(&self) -> &Data {
812        &self.data
813    }
814
815    /// Moves the nonce, tag, and ciphertext out of this instance.
816    #[must_use]
817    pub fn into_parts(self) -> (Nonce, Mac, Data) {
818        (self.nonce, self.tag, self.data)
819    }
820}
821
822impl<Algorithm: AeadAlgorithm, Nonce: Bytes, Mac: Bytes, Data: Bytes>
823    AeadEnvelope<Algorithm, Nonce, Mac, Data>
824{
825    /// Copies `self` into a new [`Vec`].
826    #[cfg(feature = "alloc")]
827    #[must_use]
828    pub fn to_vec(&self) -> Vec<u8> {
829        self.to_bytes()
830    }
831
832    /// Copies `self` into the target as `nonce || ciphertext || tag`, where
833    /// the nonce and tag are the first nonce-size and tag-size bytes of
834    /// `Nonce` and `Mac`.
835    ///
836    /// # Panics
837    ///
838    /// Panics if the nonce or tag is shorter than the algorithm's size.
839    #[must_use]
840    pub fn to_bytes<Output: NewBytes + ResizableBytes>(&self) -> Output {
841        let nonce = &self.nonce.as_slice()[..Algorithm::NPUBBYTES];
842        let tag = &self.tag.as_slice()[..Algorithm::ABYTES];
843        let mut data = Output::new_bytes();
844        data.resize(nonce.len() + self.data.len() + tag.len(), 0);
845        let s = data.as_mut_slice();
846        s[..nonce.len()].copy_from_slice(nonce);
847        s[nonce.len()..nonce.len() + self.data.len()].copy_from_slice(self.data.as_slice());
848        s[nonce.len() + self.data.len()..].copy_from_slice(tag);
849        data
850    }
851}
852
853impl<Algorithm: AeadAlgorithm, Mac: Bytes, Data: Bytes> PartialEq
854    for AeadBox<Algorithm, Mac, Data>
855{
856    fn eq(&self, other: &Self) -> bool {
857        ct_eq_bytes(self.tag.as_slice(), other.tag.as_slice())
858            && ct_eq_bytes(self.data.as_slice(), other.data.as_slice())
859    }
860}
861
862impl<Algorithm: AeadAlgorithm, Nonce: Bytes, Mac: Bytes, Data: Bytes> PartialEq
863    for AeadEnvelope<Algorithm, Nonce, Mac, Data>
864{
865    fn eq(&self, other: &Self) -> bool {
866        ct_eq_bytes(self.nonce.as_slice(), other.nonce.as_slice())
867            && ct_eq_bytes(self.tag.as_slice(), other.tag.as_slice())
868            && ct_eq_bytes(self.data.as_slice(), other.data.as_slice())
869    }
870}
871
872#[cfg(all(test, feature = "alloc"))]
873mod tests {
874    use super::*;
875
876    #[test]
877    fn test_explicit_box_layout() {
878        let key = Key::generate();
879        let nonce = Nonce::generate();
880        let message = b"hello";
881        let aad = b"metadata";
882
883        let aead = VecBox::encrypt_to_vecbox(message, Some(aad), &nonce, &key).expect("encrypt");
884        let bytes = aead.to_vec();
885        assert_eq!(
886            bytes.len(),
887            message.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
888        );
889
890        let parsed = VecBox::from_bytes(&bytes).expect("from bytes");
891        let decrypted = parsed
892            .decrypt_to_vec(Some(aad), &nonce, &key)
893            .expect("decrypt");
894        assert_eq!(decrypted, message);
895    }
896
897    #[test]
898    fn test_explicit_box_failures() {
899        let key = Key::generate();
900        let nonce = Nonce::generate();
901        let message = b"hello";
902        let aad = b"metadata";
903
904        let aead = VecBox::encrypt_to_vecbox(message, Some(aad), &nonce, &key).expect("encrypt");
905
906        aead.decrypt_to_vec(Some(b"wrong aad"), &nonce, &key)
907            .expect_err("wrong aad should fail");
908
909        let mut wrong_key = key.clone();
910        wrong_key.as_mut_slice()[0] ^= 1;
911        aead.decrypt_to_vec(Some(aad), &nonce, &wrong_key)
912            .expect_err("wrong key should fail");
913
914        let mut wrong_nonce = nonce.clone();
915        wrong_nonce.as_mut_slice()[0] ^= 1;
916        aead.decrypt_to_vec(Some(aad), &wrong_nonce, &key)
917            .expect_err("wrong nonce should fail");
918
919        let mut modified_ciphertext = aead.clone();
920        modified_ciphertext.data.as_mut_slice()[0] ^= 1;
921        modified_ciphertext
922            .decrypt_to_vec(Some(aad), &nonce, &key)
923            .expect_err("modified ciphertext should fail");
924
925        let mut modified_tag = aead.clone();
926        modified_tag.tag.as_mut_slice()[0] ^= 1;
927        modified_tag
928            .decrypt_to_vec(Some(aad), &nonce, &key)
929            .expect_err("modified tag should fail");
930    }
931
932    #[test]
933    fn test_explicit_box_empty_message_and_no_aad() {
934        let key = Key::generate();
935        let nonce = Nonce::generate();
936
937        let aead = VecBox::encrypt_to_vecbox(&[], None, &nonce, &key).expect("encrypt");
938        assert_eq!(
939            aead.to_vec().len(),
940            CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
941        );
942
943        let decrypted = aead
944            .decrypt_to_vec(None, &nonce, &key)
945            .expect("decrypt empty");
946        assert!(decrypted.is_empty());
947    }
948
949    #[test]
950    fn test_envelope_layout() {
951        let key = Key::generate();
952        let message = b"hello";
953        let aad = b"metadata";
954
955        let envelope = VecEnvelope::seal_to_vecbox(message, Some(aad), &key).expect("seal");
956        let bytes = envelope.to_vec();
957        assert_eq!(
958            bytes.len(),
959            CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
960                + message.len()
961                + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
962        );
963        assert_eq!(
964            &bytes[..CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES],
965            envelope.nonce().as_slice()
966        );
967
968        let parsed = VecEnvelope::from_bytes(&bytes).expect("from bytes");
969        let decrypted = parsed.open_to_vec(Some(aad), &key).expect("open");
970        assert_eq!(decrypted, message);
971    }
972
973    #[test]
974    fn test_envelope_failures() {
975        let key = Key::generate();
976        let message = b"hello";
977        let aad = b"metadata";
978
979        let envelope = VecEnvelope::seal_to_vecbox(message, Some(aad), &key).expect("seal");
980
981        envelope
982            .open_to_vec(Some(b"wrong aad"), &key)
983            .expect_err("wrong aad should fail");
984
985        let mut wrong_key = key.clone();
986        wrong_key.as_mut_slice()[0] ^= 1;
987        envelope
988            .open_to_vec(Some(aad), &wrong_key)
989            .expect_err("wrong key should fail");
990
991        let mut modified_nonce = envelope.clone();
992        modified_nonce.nonce.as_mut_slice()[0] ^= 1;
993        modified_nonce
994            .open_to_vec(Some(aad), &key)
995            .expect_err("modified nonce should fail");
996
997        let mut modified_ciphertext = envelope.clone();
998        modified_ciphertext.data.as_mut_slice()[0] ^= 1;
999        modified_ciphertext
1000            .open_to_vec(Some(aad), &key)
1001            .expect_err("modified ciphertext should fail");
1002
1003        let mut modified_tag = envelope.clone();
1004        modified_tag.tag.as_mut_slice()[0] ^= 1;
1005        modified_tag
1006            .open_to_vec(Some(aad), &key)
1007            .expect_err("modified tag should fail");
1008    }
1009
1010    #[test]
1011    fn test_envelope_empty_message_and_no_aad() {
1012        let key = Key::generate();
1013
1014        let envelope = VecEnvelope::seal_to_vecbox(&[], None, &key).expect("seal");
1015        assert_eq!(
1016            envelope.to_vec().len(),
1017            CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1018                + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
1019        );
1020
1021        let decrypted = envelope.open_to_vec(None, &key).expect("open empty");
1022        assert!(decrypted.is_empty());
1023    }
1024
1025    #[test]
1026    fn test_from_bytes_boundaries() {
1027        assert!(VecBox::from_bytes(&[]).is_err());
1028
1029        let empty_box_bytes = [0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
1030        let empty_box = VecBox::from_bytes(&empty_box_bytes).expect("empty box parses");
1031        assert!(empty_box.data().is_empty());
1032        assert_eq!(empty_box.tag().as_slice(), empty_box_bytes.as_slice());
1033
1034        let short_envelope_bytes = [0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1035            + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES
1036            - 1];
1037        const ENVELOPE_MIN: usize = CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1038            + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES;
1039        // Truncated envelopes report the whole input against the whole minimum,
1040        // whether the truncation lands inside the nonce or inside the tag.
1041        for short in [&short_envelope_bytes[..], &short_envelope_bytes[..1]] {
1042            assert!(matches!(
1043                VecEnvelope::from_bytes(short),
1044                Err(Error::InvalidLength {
1045                    context: crate::ErrorContext::AeadEnvelope,
1046                    actual,
1047                    constraint: crate::error::LengthConstraint::AtLeast(ENVELOPE_MIN),
1048                }) if actual == short.len()
1049            ));
1050        }
1051
1052        let empty_envelope_bytes = [0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1053            + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
1054        let empty_envelope =
1055            VecEnvelope::from_bytes(&empty_envelope_bytes).expect("empty envelope parses");
1056        assert!(empty_envelope.data().is_empty());
1057        assert_eq!(
1058            empty_envelope.nonce().as_slice(),
1059            &empty_envelope_bytes[..CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES]
1060        );
1061        assert_eq!(
1062            empty_envelope.tag().as_slice(),
1063            &empty_envelope_bytes[CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES..]
1064        );
1065    }
1066
1067    /// Known-answer vectors shared by the ChaCha20-Poly1305-IETF (RFC 8439
1068    /// section 2.8.2) and XChaCha20-Poly1305-IETF tests. The XChaCha case
1069    /// reuses the RFC 8439 key, associated data and message with a 24-byte
1070    /// nonce; its expected bytes are libsodium's output for those inputs, the
1071    /// same vector `classic::crypto_aead_xchacha20poly1305_ietf` checks against
1072    /// libsodium at runtime in its native tests.
1073    mod kat {
1074        use super::*;
1075        use crate::classic::crypto_aead_chacha20poly1305_ietf::{
1076            crypto_aead_chacha20poly1305_ietf_decrypt, crypto_aead_chacha20poly1305_ietf_encrypt,
1077        };
1078        use crate::classic::crypto_aead_xchacha20poly1305_ietf::{
1079            crypto_aead_xchacha20poly1305_ietf_decrypt, crypto_aead_xchacha20poly1305_ietf_encrypt,
1080        };
1081        use crate::dryocaead::chacha20poly1305_ietf as chacha;
1082
1083        const MESSAGE: &[u8] = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.";
1084        const AD: &[u8] = &[
1085            0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
1086        ];
1087        const KEY: [u8; 32] = [
1088            0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
1089            0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
1090            0x9c, 0x9d, 0x9e, 0x9f,
1091        ];
1092        const CHACHA_NONCE: [u8; CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES] = [
1093            0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
1094        ];
1095        const CHACHA_EXPECTED: &str = concat!(
1096            "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69",
1097            "da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad67594",
1098            "5585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd0600691",
1099        );
1100        const XCHACHA_NONCE: [u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES] = [
1101            0xf2, 0x8a, 0x50, 0xa7, 0x8a, 0x7e, 0x23, 0xc9, 0xcb, 0xa6, 0x78, 0x34, 0x66, 0xf8,
1102            0x03, 0x59, 0x0f, 0x04, 0xe9, 0x22, 0x31, 0xa3, 0x2d, 0x5d,
1103        ];
1104        const XCHACHA_EXPECTED: &str = concat!(
1105            "20f1ae75e1e5e00040294f0fb10ebb0810c593c7dba4ec104c1e5ef9507faeef58fc2898bbd0e47b2f5331fb",
1106            "c367d3c2784e3648ce1eaa7787ad186db2685ee89ae4d3441f6ea0b2224cd5a134161b554d8b48350b4ad401",
1107            "15db81ea820968e943892f2b8051cb5f7a8666e7e7ef7f84c0a2f80a12d06680c8eebbd93004109de842",
1108        );
1109
1110        fn chacha_expected() -> Vec<u8> {
1111            hex::decode(CHACHA_EXPECTED).expect("hex")
1112        }
1113
1114        fn xchacha_expected() -> Vec<u8> {
1115            hex::decode(XCHACHA_EXPECTED).expect("hex")
1116        }
1117
1118        #[test]
1119        fn chacha_box_and_envelope_match_rfc_8439_and_classic() {
1120            let key = chacha::Key::from(KEY);
1121            let nonce = chacha::Nonce::from(CHACHA_NONCE);
1122            let expected = chacha_expected();
1123
1124            let aead = chacha::VecBox::encrypt_to_vecbox(MESSAGE, Some(AD), &nonce, &key)
1125                .expect("encrypt");
1126            assert_eq!(aead.to_vec(), expected);
1127            assert_eq!(aead.clone().into_vec(), expected);
1128            assert_eq!(aead.data(), &expected[..MESSAGE.len()]);
1129            assert_eq!(aead.tag().as_slice(), &expected[MESSAGE.len()..]);
1130
1131            // Rustaceous bytes decrypt with the Classic API and vice versa.
1132            let mut classic_decrypted = vec![0u8; MESSAGE.len()];
1133            crypto_aead_chacha20poly1305_ietf_decrypt(
1134                &mut classic_decrypted,
1135                &aead.to_vec(),
1136                Some(AD),
1137                &CHACHA_NONCE,
1138                &KEY,
1139            )
1140            .expect("classic decrypt");
1141            assert_eq!(classic_decrypted, MESSAGE);
1142
1143            let mut classic_ciphertext =
1144                vec![0u8; MESSAGE.len() + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES];
1145            crypto_aead_chacha20poly1305_ietf_encrypt(
1146                &mut classic_ciphertext,
1147                MESSAGE,
1148                Some(AD),
1149                &CHACHA_NONCE,
1150                &KEY,
1151            )
1152            .expect("classic encrypt");
1153            let parsed = chacha::VecBox::from_bytes(&classic_ciphertext).expect("parse");
1154            assert_eq!(parsed, aead);
1155            assert_eq!(
1156                parsed
1157                    .decrypt_to_vec(Some(AD), &nonce, &key)
1158                    .expect("decrypt"),
1159                MESSAGE
1160            );
1161
1162            let (tag, data) = parsed.into_parts();
1163            let envelope = chacha::VecEnvelope::from_parts(nonce.clone(), tag, data);
1164            let mut envelope_bytes = CHACHA_NONCE.to_vec();
1165            envelope_bytes.extend_from_slice(&expected);
1166            assert_eq!(envelope.to_vec(), envelope_bytes);
1167            assert_eq!(envelope.clone().into_vec(), envelope_bytes);
1168            let envelope = chacha::VecEnvelope::from_bytes(&envelope_bytes).expect("parse");
1169            assert_eq!(envelope.nonce(), &nonce);
1170            assert_eq!(envelope.open_to_vec(Some(AD), &key).expect("open"), MESSAGE);
1171        }
1172
1173        #[test]
1174        fn xchacha_box_and_envelope_match_libsodium_vector_and_classic() {
1175            let key = Key::from(KEY);
1176            let nonce = Nonce::from(XCHACHA_NONCE);
1177            let expected = xchacha_expected();
1178
1179            let aead = VecBox::encrypt_to_vecbox(MESSAGE, Some(AD), &nonce, &key).expect("encrypt");
1180            assert_eq!(aead.to_vec(), expected);
1181            assert_eq!(aead.clone().into_vec(), expected);
1182
1183            let mut classic_decrypted = vec![0u8; MESSAGE.len()];
1184            crypto_aead_xchacha20poly1305_ietf_decrypt(
1185                &mut classic_decrypted,
1186                &aead.to_vec(),
1187                Some(AD),
1188                &XCHACHA_NONCE,
1189                &KEY,
1190            )
1191            .expect("classic decrypt");
1192            assert_eq!(classic_decrypted, MESSAGE);
1193
1194            let mut classic_ciphertext =
1195                vec![0u8; MESSAGE.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
1196            crypto_aead_xchacha20poly1305_ietf_encrypt(
1197                &mut classic_ciphertext,
1198                MESSAGE,
1199                Some(AD),
1200                &XCHACHA_NONCE,
1201                &KEY,
1202            )
1203            .expect("classic encrypt");
1204            let parsed = VecBox::from_bytes(&classic_ciphertext).expect("parse");
1205            assert_eq!(parsed, aead);
1206            assert_eq!(
1207                parsed
1208                    .decrypt_to_vec(Some(AD), &nonce, &key)
1209                    .expect("decrypt"),
1210                MESSAGE
1211            );
1212
1213            let mut envelope_bytes = XCHACHA_NONCE.to_vec();
1214            envelope_bytes.extend_from_slice(&expected);
1215            let envelope = VecEnvelope::from_bytes(&envelope_bytes).expect("parse");
1216            assert_eq!(envelope.to_vec(), envelope_bytes);
1217            assert_eq!(envelope.open_to_vec(Some(AD), &key).expect("open"), MESSAGE);
1218            let (parsed_nonce, tag, data) = envelope.into_parts();
1219            assert_eq!(parsed_nonce, nonce);
1220            assert_eq!(
1221                VecEnvelope::from_parts(parsed_nonce, tag, data).into_vec(),
1222                envelope_bytes
1223            );
1224        }
1225
1226        #[test]
1227        fn chacha_tampering_and_wrong_inputs_are_rejected() {
1228            let key = chacha::Key::from(KEY);
1229            let nonce = chacha::Nonce::from(CHACHA_NONCE);
1230            let expected = chacha_expected();
1231            let aead = chacha::VecBox::from_bytes(&expected).expect("parse");
1232
1233            assert!(matches!(
1234                aead.decrypt_to_vec(None, &nonce, &key),
1235                Err(Error::AuthenticationFailed)
1236            ));
1237            assert!(matches!(
1238                aead.decrypt_to_vec(Some(&AD[..AD.len() - 1]), &nonce, &key),
1239                Err(Error::AuthenticationFailed)
1240            ));
1241
1242            let mut wrong_key = key.clone();
1243            wrong_key[31] ^= 1;
1244            assert!(matches!(
1245                aead.decrypt_to_vec(Some(AD), &nonce, &wrong_key),
1246                Err(Error::AuthenticationFailed)
1247            ));
1248
1249            let mut wrong_nonce = nonce.clone();
1250            wrong_nonce[0] ^= 1;
1251            assert!(matches!(
1252                aead.decrypt_to_vec(Some(AD), &wrong_nonce, &key),
1253                Err(Error::AuthenticationFailed)
1254            ));
1255
1256            let tag_start = expected.len() - CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
1257            for index in [0, tag_start - 1, tag_start, expected.len() - 1] {
1258                let mut tampered = expected.clone();
1259                tampered[index] ^= 0x80;
1260                let tampered = chacha::VecBox::from_bytes(&tampered).expect("parse");
1261                assert!(matches!(
1262                    tampered.decrypt_to_vec(Some(AD), &nonce, &key),
1263                    Err(Error::AuthenticationFailed)
1264                ));
1265
1266                let mut envelope_bytes = CHACHA_NONCE.to_vec();
1267                envelope_bytes.extend_from_slice(tampered.to_vec().as_slice());
1268                let envelope = chacha::VecEnvelope::from_bytes(&envelope_bytes).expect("parse");
1269                assert!(matches!(
1270                    envelope.open_to_vec(Some(AD), &key),
1271                    Err(Error::AuthenticationFailed)
1272                ));
1273            }
1274
1275            // A ChaCha20 box must not open under XChaCha20 with a zero-extended
1276            // nonce.
1277            let mut xnonce = [0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES];
1278            xnonce[..CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES].copy_from_slice(&CHACHA_NONCE);
1279            let as_xchacha = VecBox::from_bytes(&expected).expect("parse");
1280            assert!(
1281                as_xchacha
1282                    .decrypt_to_vec(Some(AD), &Nonce::from(xnonce), &Key::from(KEY))
1283                    .is_err()
1284            );
1285
1286            assert_eq!(
1287                aead.decrypt_to_vec(Some(AD), &nonce, &key)
1288                    .expect("decrypt"),
1289                MESSAGE
1290            );
1291        }
1292
1293        #[test]
1294        fn chacha_from_bytes_boundaries() {
1295            const BOX_MIN: usize = CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
1296            const ENVELOPE_MIN: usize = CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES
1297                + CRYPTO_AEAD_CHACHA20POLY1305_IETF_ABYTES;
1298
1299            for len in [0, 1, BOX_MIN - 1] {
1300                assert!(matches!(
1301                    chacha::VecBox::from_bytes(&vec![0u8; len]),
1302                    Err(Error::InvalidLength {
1303                        context: ErrorContext::AeadCiphertext,
1304                        actual,
1305                        constraint: crate::error::LengthConstraint::AtLeast(BOX_MIN),
1306                    }) if actual == len
1307                ));
1308            }
1309            let empty_box = chacha::VecBox::from_bytes(&[0x5au8; BOX_MIN]).expect("empty box");
1310            assert!(empty_box.data().is_empty());
1311            assert_eq!(empty_box.tag().as_slice(), &[0x5au8; BOX_MIN]);
1312
1313            for len in [
1314                0,
1315                1,
1316                CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES,
1317                ENVELOPE_MIN - 1,
1318            ] {
1319                assert!(matches!(
1320                    chacha::VecEnvelope::from_bytes(&vec![0u8; len]),
1321                    Err(Error::InvalidLength {
1322                        context: ErrorContext::AeadEnvelope,
1323                        actual,
1324                        constraint: crate::error::LengthConstraint::AtLeast(ENVELOPE_MIN),
1325                    }) if actual == len
1326                ));
1327            }
1328            let mut envelope_bytes = CHACHA_NONCE.to_vec();
1329            envelope_bytes.extend_from_slice(&[0x5au8; BOX_MIN]);
1330            let empty_envelope =
1331                chacha::VecEnvelope::from_bytes(&envelope_bytes).expect("empty envelope");
1332            assert!(empty_envelope.data().is_empty());
1333            assert_eq!(empty_envelope.nonce().as_slice(), &CHACHA_NONCE);
1334            assert_eq!(empty_envelope.tag().as_slice(), &[0x5au8; BOX_MIN]);
1335
1336            // An XChaCha envelope is one nonce longer; the ChaCha parser sees
1337            // the extra 12 bytes as ciphertext rather than
1338            // rejecting them.
1339            let xchacha_min = [0u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1340                + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
1341            assert_eq!(
1342                chacha::VecEnvelope::from_bytes(&xchacha_min)
1343                    .expect("parses")
1344                    .data()
1345                    .len(),
1346                CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1347                    - CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES
1348            );
1349            assert!(VecEnvelope::from_bytes(&envelope_bytes).is_err());
1350        }
1351
1352        #[cfg(feature = "serde")]
1353        #[test]
1354        fn chacha_and_xchacha_json_round_trips_decrypt_with_classic() {
1355            let expected = chacha_expected();
1356            let aead = chacha::VecBox::from_bytes(&expected).expect("parse");
1357            let json = serde_json::to_string(&aead).expect("serialize box");
1358            let decoded: chacha::VecBox = serde_json::from_str(&json).expect("deserialize box");
1359            assert_eq!(decoded, aead);
1360            let mut decrypted = vec![0u8; MESSAGE.len()];
1361            crypto_aead_chacha20poly1305_ietf_decrypt(
1362                &mut decrypted,
1363                &decoded.to_vec(),
1364                Some(AD),
1365                &CHACHA_NONCE,
1366                &KEY,
1367            )
1368            .expect("classic decrypt");
1369            assert_eq!(decrypted, MESSAGE);
1370
1371            let mut envelope_bytes = CHACHA_NONCE.to_vec();
1372            envelope_bytes.extend_from_slice(&expected);
1373            let envelope = chacha::VecEnvelope::from_bytes(&envelope_bytes).expect("parse");
1374            let json = serde_json::to_string(&envelope).expect("serialize envelope");
1375            let decoded: chacha::VecEnvelope =
1376                serde_json::from_str(&json).expect("deserialize envelope");
1377            assert_eq!(decoded, envelope);
1378            assert_eq!(decoded.to_vec(), envelope_bytes);
1379            assert_eq!(
1380                decoded
1381                    .open_to_vec(Some(AD), &chacha::Key::from(KEY))
1382                    .expect("open"),
1383                MESSAGE
1384            );
1385
1386            let xexpected = xchacha_expected();
1387            let xaead = VecBox::from_bytes(&xexpected).expect("parse");
1388            let decoded: VecBox =
1389                serde_json::from_str(&serde_json::to_string(&xaead).expect("ser")).expect("de");
1390            assert_eq!(decoded.to_vec(), xexpected);
1391            let mut xenvelope_bytes = XCHACHA_NONCE.to_vec();
1392            xenvelope_bytes.extend_from_slice(&xexpected);
1393            let xenvelope = VecEnvelope::from_bytes(&xenvelope_bytes).expect("parse");
1394            let decoded: VecEnvelope =
1395                serde_json::from_str(&serde_json::to_string(&xenvelope).expect("ser")).expect("de");
1396            assert_eq!(decoded.to_vec(), xenvelope_bytes);
1397            let mut decrypted = vec![0u8; MESSAGE.len()];
1398            crypto_aead_xchacha20poly1305_ietf_decrypt(
1399                &mut decrypted,
1400                &decoded.to_vec()[CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES..],
1401                Some(AD),
1402                &XCHACHA_NONCE,
1403                &KEY,
1404            )
1405            .expect("classic decrypt");
1406            assert_eq!(decrypted, MESSAGE);
1407        }
1408
1409        #[cfg(feature = "wincode_0_6")]
1410        #[test]
1411        fn chacha_wincode_round_trips_decrypt_with_classic() {
1412            let expected = chacha_expected();
1413            let aead = chacha::VecBox::from_bytes(&expected).expect("parse");
1414            let encoded = wincode::serialize(&aead).expect("serialize box");
1415            let decoded: chacha::VecBox = wincode::deserialize(&encoded).expect("deserialize box");
1416            assert_eq!(decoded, aead);
1417            let mut decrypted = vec![0u8; MESSAGE.len()];
1418            crypto_aead_chacha20poly1305_ietf_decrypt(
1419                &mut decrypted,
1420                &decoded.to_vec(),
1421                Some(AD),
1422                &CHACHA_NONCE,
1423                &KEY,
1424            )
1425            .expect("classic decrypt");
1426            assert_eq!(decrypted, MESSAGE);
1427
1428            let mut envelope_bytes = CHACHA_NONCE.to_vec();
1429            envelope_bytes.extend_from_slice(&expected);
1430            let envelope = chacha::VecEnvelope::from_bytes(&envelope_bytes).expect("parse");
1431            let encoded = wincode::serialize(&envelope).expect("serialize envelope");
1432            let decoded: chacha::VecEnvelope =
1433                wincode::deserialize(&encoded).expect("deserialize envelope");
1434            assert_eq!(decoded, envelope);
1435            assert_eq!(decoded.to_vec(), envelope_bytes);
1436            assert_eq!(
1437                decoded
1438                    .open_to_vec(Some(AD), &chacha::Key::from(KEY))
1439                    .expect("open"),
1440                MESSAGE
1441            );
1442
1443            // Truncated encodings are rejected rather than misparsed.
1444            for encoded in [wincode::serialize(&aead).expect("ser"), encoded] {
1445                assert!(
1446                    wincode::deserialize::<chacha::VecBox>(&encoded[..encoded.len() - 1]).is_err()
1447                );
1448                assert!(
1449                    wincode::deserialize::<chacha::VecEnvelope>(&encoded[..encoded.len() - 1])
1450                        .is_err()
1451                );
1452            }
1453        }
1454    }
1455
1456    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1457    mod property_tests {
1458        use proptest::prelude::*;
1459
1460        use super::*;
1461        use crate::classic::crypto_aead_xchacha20poly1305_ietf::{
1462            Mac as ClassicMac, crypto_aead_xchacha20poly1305_ietf_decrypt,
1463            crypto_aead_xchacha20poly1305_ietf_decrypt_detached,
1464            crypto_aead_xchacha20poly1305_ietf_decrypt_inplace,
1465            crypto_aead_xchacha20poly1305_ietf_encrypt,
1466            crypto_aead_xchacha20poly1305_ietf_encrypt_detached,
1467            crypto_aead_xchacha20poly1305_ietf_encrypt_inplace,
1468        };
1469
1470        fn length_strategy(max: usize) -> impl Strategy<Value = usize> {
1471            prop_oneof![
1472                Just(0usize),
1473                Just(1),
1474                Just(15),
1475                Just(16),
1476                Just(17),
1477                Just(63),
1478                Just(64),
1479                Just(65),
1480                Just(max.saturating_sub(1)),
1481                Just(max),
1482                0usize..=max,
1483            ]
1484        }
1485
1486        fn bytes_strategy(max: usize) -> impl Strategy<Value = Vec<u8>> {
1487            length_strategy(max).prop_flat_map(|len| prop::collection::vec(any::<u8>(), len))
1488        }
1489
1490        fn aad_strategy() -> impl Strategy<Value = Option<Vec<u8>>> {
1491            prop::option::of(bytes_strategy(256))
1492        }
1493
1494        proptest! {
1495            #![proptest_config(crate::utils::test_util::proptest_config(96))]
1496
1497            #[test]
1498            fn proptest_classic_modes_and_rustaceous_layouts_agree(
1499                key in any::<[u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES]>(),
1500                nonce in any::<[u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES]>(),
1501                aad in aad_strategy(),
1502                message in bytes_strategy(512),
1503            ) {
1504                let aad = aad.as_deref();
1505
1506                let mut combined =
1507                    vec![0u8; message.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
1508                crypto_aead_xchacha20poly1305_ietf_encrypt(
1509                    &mut combined,
1510                    &message,
1511                    aad,
1512                    &nonce,
1513                    &key,
1514                )
1515                .expect("classic combined encrypt");
1516
1517                let mut decrypted = vec![0u8; message.len()];
1518                crypto_aead_xchacha20poly1305_ietf_decrypt(
1519                    &mut decrypted,
1520                    &combined,
1521                    aad,
1522                    &nonce,
1523                    &key,
1524                )
1525                .expect("classic combined decrypt");
1526                prop_assert_eq!(decrypted.as_slice(), message.as_slice());
1527
1528                let mut detached = vec![0u8; message.len()];
1529                let mut mac = ClassicMac::default();
1530                crypto_aead_xchacha20poly1305_ietf_encrypt_detached(
1531                    &mut detached,
1532                    &mut mac,
1533                    &message,
1534                    aad,
1535                    &nonce,
1536                    &key,
1537                )
1538                .expect("classic detached encrypt");
1539                prop_assert_eq!(&detached, &combined[..message.len()]);
1540                prop_assert_eq!(mac.as_slice(), &combined[message.len()..]);
1541
1542                let mut detached_decrypted = vec![0u8; message.len()];
1543                crypto_aead_xchacha20poly1305_ietf_decrypt_detached(
1544                    &mut detached_decrypted,
1545                    &detached,
1546                    &mac,
1547                    aad,
1548                    &nonce,
1549                    &key,
1550                )
1551                .expect("classic detached decrypt");
1552                prop_assert_eq!(detached_decrypted.as_slice(), message.as_slice());
1553
1554                let mut inplace = message.clone();
1555                inplace.resize(message.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES, 0);
1556                crypto_aead_xchacha20poly1305_ietf_encrypt_inplace(
1557                    &mut inplace,
1558                    aad,
1559                    &nonce,
1560                    &key,
1561                )
1562                .expect("classic inplace encrypt");
1563                prop_assert_eq!(&inplace, &combined);
1564
1565                crypto_aead_xchacha20poly1305_ietf_decrypt_inplace(
1566                    &mut inplace,
1567                    aad,
1568                    &nonce,
1569                    &key,
1570                )
1571                .expect("classic inplace decrypt");
1572                prop_assert_eq!(&inplace[..message.len()], message.as_slice());
1573
1574                let rust_key = Key::from(key);
1575                let rust_nonce = Nonce::from(nonce);
1576                let aead = VecBox::encrypt_to_vecbox(&message, aad, &rust_nonce, &rust_key)
1577                    .expect("rustaceous encrypt");
1578                let aead_bytes = aead.to_vec();
1579                prop_assert_eq!(aead_bytes.as_slice(), combined.as_slice());
1580                let aead_decrypted = aead
1581                    .decrypt_to_vec(aad, &rust_nonce, &rust_key)
1582                    .expect("rustaceous decrypt");
1583                prop_assert_eq!(aead_decrypted.as_slice(), message.as_slice());
1584
1585                let mut envelope_bytes = rust_nonce.to_vec();
1586                envelope_bytes.extend_from_slice(&combined);
1587                let envelope = VecEnvelope::from_bytes(&envelope_bytes).expect("envelope parses");
1588                prop_assert_eq!(envelope.to_vec(), envelope_bytes);
1589                let envelope_decrypted = envelope
1590                    .open_to_vec(aad, &rust_key)
1591                    .expect("rustaceous envelope open");
1592                prop_assert_eq!(envelope_decrypted.as_slice(), message.as_slice());
1593            }
1594
1595            #[test]
1596            fn proptest_tampering_is_rejected_without_mutating_outputs(
1597                key in any::<[u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES]>(),
1598                nonce in any::<[u8; CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES]>(),
1599                aad in aad_strategy(),
1600                message in bytes_strategy(512),
1601                tamper_index in any::<usize>(),
1602            ) {
1603                let aad = aad.as_deref();
1604                let rust_key = Key::from(key);
1605                let rust_nonce = Nonce::from(nonce);
1606                let mut combined =
1607                    vec![0u8; message.len() + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES];
1608                crypto_aead_xchacha20poly1305_ietf_encrypt(
1609                    &mut combined,
1610                    &message,
1611                    aad,
1612                    &nonce,
1613                    &key,
1614                )
1615                .expect("classic combined encrypt");
1616
1617                let mut tampered = combined;
1618                let tamper_index = tamper_index % tampered.len();
1619                tampered[tamper_index] ^= 1;
1620
1621                let mut output = vec![0xa5; message.len()];
1622                let original_output = output.clone();
1623                prop_assert!(
1624                    crypto_aead_xchacha20poly1305_ietf_decrypt(
1625                        &mut output,
1626                        &tampered,
1627                        aad,
1628                        &nonce,
1629                        &key,
1630                    )
1631                    .is_err()
1632                );
1633                prop_assert_eq!(output, original_output);
1634
1635                let parsed_box = VecBox::from_bytes(&tampered).expect("tampered box parses");
1636                prop_assert!(
1637                    parsed_box
1638                        .decrypt_to_vec(aad, &rust_nonce, &rust_key)
1639                        .is_err()
1640                );
1641
1642                let mut tampered_envelope = rust_nonce.to_vec();
1643                tampered_envelope.extend_from_slice(&tampered);
1644                let parsed_envelope =
1645                    VecEnvelope::from_bytes(&tampered_envelope).expect("tampered envelope parses");
1646                prop_assert!(parsed_envelope.open_to_vec(aad, &rust_key).is_err());
1647            }
1648
1649            #[test]
1650            fn proptest_from_bytes_round_trips_or_rejects_by_length(
1651                raw in bytes_strategy(768),
1652            ) {
1653                match VecBox::from_bytes(&raw) {
1654                    Ok(parsed) => {
1655                        prop_assert!(raw.len() >= CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES);
1656                        let bytes = parsed.to_vec();
1657                        prop_assert_eq!(bytes.as_slice(), raw.as_slice());
1658                    }
1659                    Err(_) => {
1660                        prop_assert!(raw.len() < CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES);
1661                    }
1662                }
1663
1664                let envelope_min_len = CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES
1665                    + CRYPTO_AEAD_XCHACHA20POLY1305_IETF_ABYTES;
1666                match VecEnvelope::from_bytes(&raw) {
1667                    Ok(parsed) => {
1668                        prop_assert!(raw.len() >= envelope_min_len);
1669                        let bytes = parsed.to_vec();
1670                        prop_assert_eq!(bytes.as_slice(), raw.as_slice());
1671                    }
1672                    Err(_) => {
1673                        prop_assert!(raw.len() < envelope_min_len);
1674                    }
1675                }
1676            }
1677        }
1678    }
1679}