Skip to main content

aws_lc_rs/aead/
unbound_key.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0 OR ISC
3
4use super::aead_ctx::AeadCtx;
5use super::{
6    Algorithm, Nonce, Tag, AES_128_GCM, AES_128_GCM_SIV, AES_192_GCM, AES_256_GCM, AES_256_GCM_SIV,
7    CHACHA20_POLY1305, MAX_KEY_LEN, MAX_TAG_LEN, NONCE_LEN,
8};
9use crate::aws_lc::{
10    EVP_AEAD_CTX_open, EVP_AEAD_CTX_open_gather, EVP_AEAD_CTX_seal, EVP_AEAD_CTX_seal_scatter,
11};
12use crate::error::Unspecified;
13use crate::fips::indicator_check;
14use crate::hkdf;
15use crate::iv::FixedLength;
16use core::fmt::Debug;
17use core::mem::MaybeUninit;
18use core::ops::RangeFrom;
19use core::ptr::null;
20
21/// The maximum length of a nonce returned by our AEAD API.
22const MAX_NONCE_LEN: usize = NONCE_LEN;
23
24/// The maximum required tag buffer needed if using AWS-LC generated nonce construction
25const MAX_TAG_NONCE_BUFFER_LEN: usize = MAX_TAG_LEN + MAX_NONCE_LEN;
26
27/// An AEAD key without a designated role or nonce sequence.
28pub struct UnboundKey {
29    ctx: AeadCtx,
30    algorithm: &'static Algorithm,
31}
32
33#[allow(clippy::missing_fields_in_debug)]
34impl Debug for UnboundKey {
35    fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
36        f.debug_struct("UnboundKey")
37            .field("algorithm", &self.algorithm)
38            .finish()
39    }
40}
41
42impl UnboundKey {
43    /// Constructs an `UnboundKey`.
44    /// # Errors
45    /// `error::Unspecified` if `key_bytes.len() != algorithm.key_len()`.
46    pub fn new(algorithm: &'static Algorithm, key_bytes: &[u8]) -> Result<Self, Unspecified> {
47        Ok(Self {
48            ctx: (algorithm.init)(key_bytes, algorithm.tag_len())?,
49            algorithm,
50        })
51    }
52
53    #[inline]
54    pub(crate) fn open_within<'in_out>(
55        &self,
56        nonce: Nonce,
57        aad: &[u8],
58        in_out: &'in_out mut [u8],
59        ciphertext_and_tag: RangeFrom<usize>,
60    ) -> Result<&'in_out mut [u8], Unspecified> {
61        let in_prefix_len = ciphertext_and_tag.start;
62        let ciphertext_and_tag_len = in_out.len().checked_sub(in_prefix_len).ok_or(Unspecified)?;
63        let ciphertext_len = ciphertext_and_tag_len
64            .checked_sub(self.algorithm().tag_len())
65            .ok_or(Unspecified)?;
66        self.check_per_nonce_max_bytes(ciphertext_len)?;
67
68        match self.ctx {
69            AeadCtx::AES_128_GCM_RANDNONCE(_) | AeadCtx::AES_256_GCM_RANDNONCE(_) => {
70                self.open_combined_randnonce(nonce, aad, &mut in_out[in_prefix_len..])
71            }
72            _ => self.open_combined(nonce, aad.as_ref(), &mut in_out[in_prefix_len..]),
73        }?;
74
75        // shift the plaintext to the left
76        in_out.copy_within(in_prefix_len..in_prefix_len + ciphertext_len, 0);
77
78        // `ciphertext_len` is also the plaintext length.
79        Ok(&mut in_out[..ciphertext_len])
80    }
81
82    #[inline]
83    pub(crate) fn open_separate_gather(
84        &self,
85        nonce: &Nonce,
86        aad: &[u8],
87        in_ciphertext: &[u8],
88        in_tag: &[u8],
89        out_plaintext: &mut [u8],
90    ) -> Result<(), Unspecified> {
91        self.open_separate_gather_impl(
92            nonce,
93            aad,
94            in_ciphertext.as_ptr(),
95            in_ciphertext.len(),
96            in_tag,
97            out_plaintext.as_mut_ptr(),
98            out_plaintext.len(),
99        )
100    }
101
102    #[inline]
103    pub(crate) fn open_in_place_separate_tag(
104        &self,
105        nonce: &Nonce,
106        aad: &[u8],
107        in_tag: &[u8],
108        in_out: &mut [u8],
109    ) -> Result<(), Unspecified> {
110        let ptr = in_out.as_mut_ptr();
111        let len = in_out.len();
112        self.open_separate_gather_impl(nonce, aad, ptr.cast_const(), len, in_tag, ptr, len)
113    }
114
115    /// Common FFI path for `EVP_AEAD_CTX_open_gather`-based opening.
116    ///
117    /// `in_ciphertext` / `out_plaintext` may alias (exactly, i.e. same base
118    /// pointer and length). `EVP_AEAD_CTX_open_gather` explicitly permits
119    /// `out == in`, which is how `open_in_place_separate_tag` works.
120    ///
121    /// Callers must ensure:
122    /// * `in_ciphertext` is valid for reads of `in_ciphertext_len` bytes.
123    /// * `out_plaintext` is valid for writes of `out_plaintext_len` bytes.
124    /// * If the two pointers alias, they must alias exactly (same base, same length).
125    #[inline]
126    #[allow(clippy::too_many_arguments)]
127    fn open_separate_gather_impl(
128        &self,
129        nonce: &Nonce,
130        aad: &[u8],
131        in_ciphertext: *const u8,
132        in_ciphertext_len: usize,
133        in_tag: &[u8],
134        out_plaintext: *mut u8,
135        out_plaintext_len: usize,
136    ) -> Result<(), Unspecified> {
137        self.check_per_nonce_max_bytes(in_ciphertext_len)?;
138
139        // ensure that the lengths match
140        if in_ciphertext_len != out_plaintext_len {
141            return Err(Unspecified);
142        }
143
144        unsafe {
145            let aead_ctx = self.ctx.as_ref();
146            let nonce = nonce.as_ref();
147
148            if 1 != EVP_AEAD_CTX_open_gather(
149                aead_ctx.as_const_ptr(),
150                out_plaintext,
151                nonce.as_ptr(),
152                nonce.len(),
153                in_ciphertext,
154                in_ciphertext_len,
155                in_tag.as_ptr(),
156                in_tag.len(),
157                aad.as_ptr(),
158                aad.len(),
159            ) {
160                return Err(Unspecified);
161            }
162            Ok(())
163        }
164    }
165
166    #[inline]
167    pub(crate) fn seal_in_place_append_tag<'a, InOut>(
168        &self,
169        nonce: Option<Nonce>,
170        aad: &[u8],
171        in_out: &'a mut InOut,
172    ) -> Result<Nonce, Unspecified>
173    where
174        InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>,
175    {
176        self.check_per_nonce_max_bytes(in_out.as_mut().len())?;
177        match nonce {
178            Some(nonce) => self.seal_combined(nonce, aad, in_out),
179            None => self.seal_combined_randnonce(aad, in_out),
180        }
181    }
182
183    #[inline]
184    pub(crate) fn seal_in_place_separate_tag(
185        &self,
186        nonce: Option<Nonce>,
187        aad: &[u8],
188        in_out: &mut [u8],
189    ) -> Result<(Nonce, Tag), Unspecified> {
190        self.check_per_nonce_max_bytes(in_out.len())?;
191        match nonce {
192            Some(nonce) => self.seal_separate(nonce, aad, in_out),
193            None => self.seal_separate_randnonce(aad, in_out),
194        }
195    }
196
197    #[inline]
198    #[allow(clippy::needless_pass_by_value)]
199    pub(crate) fn seal_out_of_place_scatter(
200        &self,
201        nonce: Nonce,
202        aad: &[u8],
203        in_plaintext: &[u8],
204        out_ciphertext: &mut [u8],
205        extra_in: &[u8],
206        extra_out_and_tag: &mut [u8],
207    ) -> Result<(), Unspecified> {
208        self.check_per_nonce_max_bytes(in_plaintext.len() + extra_in.len())?;
209        if out_ciphertext.len() != in_plaintext.len()
210            || extra_out_and_tag.len() != extra_in.len() + self.algorithm().tag_len()
211        {
212            return Err(Unspecified);
213        }
214
215        let nonce = nonce.as_ref();
216        // Set to a value the AEAD never reports on success, so the assertion below
217        // catches a missing write as well as a short one.
218        let mut out_tag_len = 0;
219
220        if 1 != unsafe {
221            EVP_AEAD_CTX_seal_scatter(
222                self.ctx.as_ref().as_const_ptr(),
223                out_ciphertext.as_mut_ptr(),
224                extra_out_and_tag.as_mut_ptr(),
225                &mut out_tag_len,
226                extra_out_and_tag.len(),
227                nonce.as_ptr(),
228                nonce.len(),
229                in_plaintext.as_ptr(),
230                in_plaintext.len(),
231                extra_in.as_ptr(),
232                extra_in.len(),
233                aad.as_ptr(),
234                aad.len(),
235            )
236        } {
237            return Err(Unspecified);
238        }
239        debug_assert_eq!(out_tag_len, extra_out_and_tag.len());
240        Ok(())
241    }
242
243    #[inline]
244    #[allow(clippy::needless_pass_by_value)]
245    pub(crate) fn seal_in_place_separate_scatter(
246        &self,
247        nonce: Nonce,
248        aad: &[u8],
249        in_out: &mut [u8],
250        extra_in: &[u8],
251        extra_out_and_tag: &mut [u8],
252    ) -> Result<(), Unspecified> {
253        self.check_per_nonce_max_bytes(in_out.len())?;
254        // ensure that the extra lengths match
255        {
256            let actual = extra_in.len() + self.algorithm().tag_len();
257            let expected = extra_out_and_tag.len();
258
259            if actual != expected {
260                return Err(Unspecified);
261            }
262        }
263
264        let nonce = nonce.as_ref();
265        let mut out_tag_len = extra_out_and_tag.len();
266
267        if 1 != unsafe {
268            EVP_AEAD_CTX_seal_scatter(
269                self.ctx.as_ref().as_const_ptr(),
270                in_out.as_mut_ptr(),
271                extra_out_and_tag.as_mut_ptr(),
272                &mut out_tag_len,
273                extra_out_and_tag.len(),
274                nonce.as_ptr(),
275                nonce.len(),
276                in_out.as_ptr(),
277                in_out.len(),
278                extra_in.as_ptr(),
279                extra_in.len(),
280                aad.as_ptr(),
281                aad.len(),
282            )
283        } {
284            return Err(Unspecified);
285        }
286        Ok(())
287    }
288
289    /// The key's AEAD algorithm.
290    #[inline]
291    #[must_use]
292    pub fn algorithm(&self) -> &'static Algorithm {
293        self.algorithm
294    }
295
296    #[inline]
297    pub(crate) fn check_per_nonce_max_bytes(&self, in_out_len: usize) -> Result<(), Unspecified> {
298        if in_out_len as u64 > self.algorithm().max_input_len {
299            return Err(Unspecified);
300        }
301        Ok(())
302    }
303
304    #[inline]
305    #[allow(clippy::needless_pass_by_value)]
306    fn open_combined(
307        &self,
308        nonce: Nonce,
309        aad: &[u8],
310        in_out: &mut [u8],
311    ) -> Result<(), Unspecified> {
312        let nonce = nonce.as_ref();
313
314        debug_assert_eq!(nonce.len(), self.algorithm().nonce_len());
315
316        let plaintext_len = in_out.len() - self.algorithm().tag_len();
317
318        let mut out_len = MaybeUninit::<usize>::uninit();
319        if 1 != indicator_check!(unsafe {
320            EVP_AEAD_CTX_open(
321                self.ctx.as_ref().as_const_ptr(),
322                in_out.as_mut_ptr(),
323                out_len.as_mut_ptr(),
324                plaintext_len,
325                nonce.as_ptr(),
326                nonce.len(),
327                in_out.as_ptr(),
328                plaintext_len + self.algorithm().tag_len(),
329                aad.as_ptr(),
330                aad.len(),
331            )
332        }) {
333            return Err(Unspecified);
334        }
335
336        Ok(())
337    }
338
339    #[inline]
340    #[allow(clippy::needless_pass_by_value)]
341    fn open_combined_randnonce(
342        &self,
343        nonce: Nonce,
344        aad: &[u8],
345        in_out: &mut [u8],
346    ) -> Result<(), Unspecified> {
347        let nonce = nonce.as_ref();
348
349        let alg_nonce_len = self.algorithm().nonce_len();
350        let alg_tag_len = self.algorithm().tag_len();
351
352        debug_assert_eq!(nonce.len(), alg_nonce_len);
353        debug_assert!(alg_tag_len + alg_nonce_len <= MAX_TAG_NONCE_BUFFER_LEN);
354
355        let plaintext_len = in_out.len() - alg_tag_len;
356
357        let mut tag_buffer = [0u8; MAX_TAG_NONCE_BUFFER_LEN];
358
359        tag_buffer[..alg_tag_len]
360            .copy_from_slice(&in_out[plaintext_len..plaintext_len + alg_tag_len]);
361        tag_buffer[alg_tag_len..alg_tag_len + alg_nonce_len].copy_from_slice(nonce);
362
363        let tag_slice = &tag_buffer[0..alg_tag_len + alg_nonce_len];
364
365        if 1 != indicator_check!(unsafe {
366            EVP_AEAD_CTX_open_gather(
367                self.ctx.as_ref().as_const_ptr(),
368                in_out.as_mut_ptr(),
369                null(),
370                0,
371                in_out.as_ptr(),
372                plaintext_len,
373                tag_slice.as_ptr(),
374                tag_slice.len(),
375                aad.as_ptr(),
376                aad.len(),
377            )
378        }) {
379            return Err(Unspecified);
380        }
381
382        Ok(())
383    }
384
385    #[inline]
386    fn seal_combined<InOut>(
387        &self,
388        nonce: Nonce,
389        aad: &[u8],
390        in_out: &mut InOut,
391    ) -> Result<Nonce, Unspecified>
392    where
393        InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>,
394    {
395        let plaintext_len = in_out.as_mut().len();
396
397        let alg_tag_len = self.algorithm().tag_len();
398
399        debug_assert!(alg_tag_len <= MAX_TAG_LEN);
400
401        let tag_buffer = [0u8; MAX_TAG_LEN];
402
403        in_out.extend(tag_buffer[..alg_tag_len].iter());
404
405        // Safe `Extend` implementations are not required to grow the buffer,
406        // so derive the FFI capacity from the actual post-extend slice.
407        let mut_in_out = in_out.as_mut();
408        let out_capacity = mut_in_out.len();
409        let expected_len = plaintext_len.checked_add(alg_tag_len).ok_or(Unspecified)?;
410        // Only under-growth is unsound; an exact match also fails closed on over-growth.
411        if out_capacity != expected_len {
412            return Err(Unspecified);
413        }
414
415        let mut out_len = MaybeUninit::<usize>::uninit();
416
417        {
418            let nonce = nonce.as_ref();
419
420            debug_assert_eq!(nonce.len(), self.algorithm().nonce_len());
421
422            if 1 != indicator_check!(unsafe {
423                EVP_AEAD_CTX_seal(
424                    self.ctx.as_ref().as_const_ptr(),
425                    mut_in_out.as_mut_ptr(),
426                    out_len.as_mut_ptr(),
427                    out_capacity,
428                    nonce.as_ptr(),
429                    nonce.len(),
430                    mut_in_out.as_ptr(),
431                    plaintext_len,
432                    aad.as_ptr(),
433                    aad.len(),
434                )
435            }) {
436                return Err(Unspecified);
437            }
438        }
439
440        Ok(nonce)
441    }
442
443    #[inline]
444    fn seal_combined_randnonce<InOut>(
445        &self,
446        aad: &[u8],
447        in_out: &mut InOut,
448    ) -> Result<Nonce, Unspecified>
449    where
450        InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>,
451    {
452        let mut tag_buffer = [0u8; MAX_TAG_NONCE_BUFFER_LEN];
453
454        let mut out_tag_len = MaybeUninit::<usize>::uninit();
455        let plaintext_len;
456
457        {
458            // Derive both the FFI pointer and length from the same slice. `AsMut`
459            // implementations are not required to return the same view across calls.
460            let mut_in_out = in_out.as_mut();
461            plaintext_len = mut_in_out.len();
462
463            if 1 != indicator_check!(unsafe {
464                EVP_AEAD_CTX_seal_scatter(
465                    self.ctx.as_ref().as_const_ptr(),
466                    mut_in_out.as_mut_ptr(),
467                    tag_buffer.as_mut_ptr(),
468                    out_tag_len.as_mut_ptr(),
469                    tag_buffer.len(),
470                    null(),
471                    0,
472                    mut_in_out.as_ptr(),
473                    plaintext_len,
474                    null(),
475                    0,
476                    aad.as_ptr(),
477                    aad.len(),
478                )
479            }) {
480                return Err(Unspecified);
481            }
482        }
483
484        let tag_len = self.algorithm().tag_len();
485        let nonce_len = self.algorithm().nonce_len();
486
487        let nonce = Nonce(FixedLength::<NONCE_LEN>::try_from(
488            &tag_buffer[tag_len..tag_len + nonce_len],
489        )?);
490
491        in_out.extend(&tag_buffer[..tag_len]);
492
493        let expected_len = plaintext_len.checked_add(tag_len).ok_or(Unspecified)?;
494        if in_out.as_mut().len() != expected_len {
495            return Err(Unspecified);
496        }
497
498        Ok(nonce)
499    }
500
501    #[inline]
502    fn seal_separate(
503        &self,
504        nonce: Nonce,
505        aad: &[u8],
506        in_out: &mut [u8],
507    ) -> Result<(Nonce, Tag), Unspecified> {
508        let mut tag = [0u8; MAX_TAG_LEN];
509        let mut out_tag_len = MaybeUninit::<usize>::uninit();
510        {
511            let nonce = nonce.as_ref();
512
513            debug_assert_eq!(nonce.len(), self.algorithm().nonce_len());
514
515            if 1 != indicator_check!(unsafe {
516                EVP_AEAD_CTX_seal_scatter(
517                    self.ctx.as_ref().as_const_ptr(),
518                    in_out.as_mut_ptr(),
519                    tag.as_mut_ptr(),
520                    out_tag_len.as_mut_ptr(),
521                    tag.len(),
522                    nonce.as_ptr(),
523                    nonce.len(),
524                    in_out.as_ptr(),
525                    in_out.len(),
526                    null(),
527                    0usize,
528                    aad.as_ptr(),
529                    aad.len(),
530                )
531            }) {
532                return Err(Unspecified);
533            }
534        }
535        Ok((nonce, Tag(tag, unsafe { out_tag_len.assume_init() })))
536    }
537
538    #[inline]
539    fn seal_separate_randnonce(
540        &self,
541        aad: &[u8],
542        in_out: &mut [u8],
543    ) -> Result<(Nonce, Tag), Unspecified> {
544        let mut tag_buffer = [0u8; MAX_TAG_NONCE_BUFFER_LEN];
545
546        debug_assert!(
547            self.algorithm().tag_len() + self.algorithm().nonce_len() <= tag_buffer.len()
548        );
549
550        let mut out_tag_len = MaybeUninit::<usize>::uninit();
551
552        if 1 != indicator_check!(unsafe {
553            EVP_AEAD_CTX_seal_scatter(
554                self.ctx.as_ref().as_const_ptr(),
555                in_out.as_mut_ptr(),
556                tag_buffer.as_mut_ptr(),
557                out_tag_len.as_mut_ptr(),
558                tag_buffer.len(),
559                null(),
560                0,
561                in_out.as_ptr(),
562                in_out.len(),
563                null(),
564                0usize,
565                aad.as_ptr(),
566                aad.len(),
567            )
568        }) {
569            return Err(Unspecified);
570        }
571
572        let tag_len = self.algorithm().tag_len();
573        let nonce_len = self.algorithm().nonce_len();
574
575        let nonce = Nonce(FixedLength::<NONCE_LEN>::try_from(
576            &tag_buffer[tag_len..tag_len + nonce_len],
577        )?);
578
579        let mut tag = [0u8; MAX_TAG_LEN];
580        tag.copy_from_slice(&tag_buffer[..tag_len]);
581
582        Ok((nonce, Tag(tag, tag_len)))
583    }
584}
585
586impl From<AeadCtx> for UnboundKey {
587    fn from(value: AeadCtx) -> Self {
588        let algorithm = match value {
589            AeadCtx::AES_128_GCM(_)
590            | AeadCtx::AES_128_GCM_TLS12(_)
591            | AeadCtx::AES_128_GCM_TLS13(_)
592            | AeadCtx::AES_128_GCM_RANDNONCE(_) => &AES_128_GCM,
593            AeadCtx::AES_192_GCM(_) => &AES_192_GCM,
594            AeadCtx::AES_128_GCM_SIV(_) => &AES_128_GCM_SIV,
595            AeadCtx::AES_256_GCM(_)
596            | AeadCtx::AES_256_GCM_RANDNONCE(_)
597            | AeadCtx::AES_256_GCM_TLS12(_)
598            | AeadCtx::AES_256_GCM_TLS13(_) => &AES_256_GCM,
599            AeadCtx::AES_256_GCM_SIV(_) => &AES_256_GCM_SIV,
600            AeadCtx::CHACHA20_POLY1305(_) => &CHACHA20_POLY1305,
601        };
602        Self {
603            ctx: value,
604            algorithm,
605        }
606    }
607}
608
609impl From<hkdf::Okm<'_, &'static Algorithm>> for UnboundKey {
610    fn from(okm: hkdf::Okm<&'static Algorithm>) -> Self {
611        let mut key_bytes = [0; MAX_KEY_LEN];
612        let key_bytes = &mut key_bytes[..okm.len().key_len];
613        let algorithm = *okm.len();
614        okm.fill(key_bytes).unwrap();
615        Self::new(algorithm, key_bytes).unwrap()
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    struct NormalBuffer(Vec<u8>);
624
625    impl AsMut<[u8]> for NormalBuffer {
626        fn as_mut(&mut self) -> &mut [u8] {
627            self.0.as_mut_slice()
628        }
629    }
630
631    impl<'a> Extend<&'a u8> for NormalBuffer {
632        fn extend<T: IntoIterator<Item = &'a u8>>(&mut self, iter: T) {
633            self.0.extend(iter);
634        }
635    }
636
637    struct NoGrowBuffer(Vec<u8>);
638
639    impl AsMut<[u8]> for NoGrowBuffer {
640        fn as_mut(&mut self) -> &mut [u8] {
641            self.0.as_mut_slice()
642        }
643    }
644
645    impl<'a> Extend<&'a u8> for NoGrowBuffer {
646        fn extend<T: IntoIterator<Item = &'a u8>>(&mut self, _iter: T) {}
647    }
648
649    struct ShortExtendBuffer(Vec<u8>);
650
651    impl AsMut<[u8]> for ShortExtendBuffer {
652        fn as_mut(&mut self) -> &mut [u8] {
653            self.0.as_mut_slice()
654        }
655    }
656
657    impl<'a> Extend<&'a u8> for ShortExtendBuffer {
658        fn extend<T: IntoIterator<Item = &'a u8>>(&mut self, iter: T) {
659            self.0.extend(iter.into_iter().take(1));
660        }
661    }
662
663    struct ShrinkingBuffer(Vec<u8>);
664
665    impl AsMut<[u8]> for ShrinkingBuffer {
666        fn as_mut(&mut self) -> &mut [u8] {
667            self.0.as_mut_slice()
668        }
669    }
670
671    impl<'a> Extend<&'a u8> for ShrinkingBuffer {
672        fn extend<T: IntoIterator<Item = &'a u8>>(&mut self, _iter: T) {
673            let new_len = self.0.len().saturating_sub(1);
674            self.0.truncate(new_len);
675        }
676    }
677
678    fn test_key() -> UnboundKey {
679        UnboundKey::new(&AES_128_GCM, &[0x42u8; 16]).unwrap()
680    }
681
682    fn test_randnonce_key() -> UnboundKey {
683        UnboundKey::from(
684            AeadCtx::aes_128_gcm_randnonce(
685                &[0x42u8; 16],
686                AES_128_GCM.tag_len(),
687                AES_128_GCM.nonce_len(),
688            )
689            .unwrap(),
690        )
691    }
692
693    fn test_nonce() -> Nonce {
694        Nonce::try_assume_unique_for_key(&[0x24u8; NONCE_LEN]).unwrap()
695    }
696
697    #[test]
698    fn seal_combined_normal_extend_succeeds_and_roundtrips() {
699        let key = test_key();
700        let plaintext = b"seal_combined soundness regression test".to_vec();
701        let mut in_out = NormalBuffer(plaintext.clone());
702
703        let nonce = key
704            .seal_combined(test_nonce(), &[], &mut in_out)
705            .expect("a normal, Vec-like Extend impl must succeed");
706
707        assert_eq!(in_out.0.len(), plaintext.len() + key.algorithm().tag_len());
708
709        let opened: &[u8] = key
710            .open_within(nonce, &[], &mut in_out.0, 0..)
711            .expect("the sealed output must open back to the original plaintext");
712        assert_eq!(opened, plaintext.as_slice());
713    }
714
715    #[test]
716    fn seal_combined_rejects_no_grow_extend() {
717        let key = test_key();
718        let plaintext = b"some plaintext".to_vec();
719        let original_len = plaintext.len();
720        let mut in_out = NoGrowBuffer(plaintext);
721
722        let result = key.seal_combined(test_nonce(), &[], &mut in_out);
723
724        assert!(
725            result.is_err(),
726            "a no-op Extend impl must not be trusted to have appended the tag"
727        );
728        assert_eq!(in_out.0.len(), original_len);
729    }
730
731    #[test]
732    fn seal_combined_rejects_short_extend() {
733        let key = test_key();
734        let mut in_out = ShortExtendBuffer(b"some plaintext".to_vec());
735
736        let result = key.seal_combined(test_nonce(), &[], &mut in_out);
737
738        assert!(
739            result.is_err(),
740            "an Extend impl that appends fewer bytes than the tag length must be rejected"
741        );
742    }
743
744    #[test]
745    fn seal_combined_rejects_shrinking_extend() {
746        let key = test_key();
747        let mut in_out = ShrinkingBuffer(b"some plaintext".to_vec());
748
749        let result = key.seal_combined(test_nonce(), &[], &mut in_out);
750
751        assert!(
752            result.is_err(),
753            "an Extend impl that shrinks the collection must be rejected"
754        );
755    }
756
757    #[test]
758    fn seal_combined_randnonce_normal_extend_succeeds_and_roundtrips() {
759        let key = test_randnonce_key();
760        let plaintext = b"seal_combined_randnonce soundness regression test".to_vec();
761        let mut in_out = NormalBuffer(plaintext.clone());
762
763        let nonce = key
764            .seal_combined_randnonce(&[], &mut in_out)
765            .expect("a normal, Vec-like Extend impl must succeed");
766
767        assert_eq!(in_out.0.len(), plaintext.len() + key.algorithm().tag_len());
768
769        let opened = key
770            .open_within(nonce, &[], &mut in_out.0, 0..)
771            .expect("the sealed output must open back to the original plaintext");
772        assert_eq!(opened, plaintext.as_slice());
773    }
774
775    #[test]
776    fn seal_combined_randnonce_rejects_no_grow_extend() {
777        let key = test_randnonce_key();
778        let plaintext = b"some plaintext".to_vec();
779        let original_len = plaintext.len();
780        let mut in_out = NoGrowBuffer(plaintext);
781
782        let result = key.seal_combined_randnonce(&[], &mut in_out);
783
784        assert!(
785            result.is_err(),
786            "a no-op Extend impl must not be trusted to have appended the tag"
787        );
788        assert_eq!(in_out.0.len(), original_len);
789    }
790
791    #[test]
792    fn seal_combined_randnonce_rejects_short_extend() {
793        let key = test_randnonce_key();
794        let mut in_out = ShortExtendBuffer(b"some plaintext".to_vec());
795
796        let result = key.seal_combined_randnonce(&[], &mut in_out);
797
798        assert!(
799            result.is_err(),
800            "an Extend impl that appends fewer bytes than the tag length must be rejected"
801        );
802    }
803
804    #[test]
805    fn seal_combined_randnonce_rejects_shrinking_extend() {
806        let key = test_randnonce_key();
807        let mut in_out = ShrinkingBuffer(b"some plaintext".to_vec());
808
809        let result = key.seal_combined_randnonce(&[], &mut in_out);
810
811        assert!(
812            result.is_err(),
813            "an Extend impl that shrinks the collection must be rejected"
814        );
815    }
816}