Skip to main content

oxicrypto_aead/
stream.rs

1//! STREAM chunked AEAD construction (Hoang-Reyhanitabar-Rogaway-Vizár 2015).
2//!
3//! STREAM wraps a nonce-based AEAD to provide streaming authenticated
4//! encryption with per-chunk authentication.  Each chunk gets a unique nonce
5//! derived from a nonce prefix and a 32-bit counter; the final chunk is
6//! distinguished by a 1-byte flag.
7//!
8//! # Nonce layout (12-byte AES-GCM)
9//!
10//! ```text
11//! ┌────────────── 7 bytes ──────────────┬── 4 bytes ──┬─ 1 byte ─┐
12//! │            nonce prefix             │   counter   │   flag   │
13//! └─────────────────────────────────────┴─────────────┴──────────┘
14//! ```
15//!
16//! flag = 0x00 for non-final chunks, 0x01 for the final chunk.
17//!
18//! # Nonce layout (24-byte XChaCha20-Poly1305)
19//!
20//! ```text
21//! ┌──────────────── 19 bytes ───────────────┬── 4 bytes ──┬─ 1 byte ─┐
22//! │              nonce prefix               │   counter   │   flag   │
23//! └─────────────────────────────────────────┴─────────────┴──────────┘
24//! ```
25//!
26//! # Trait contract
27//!
28//! The `init` method's `nonce` parameter is the **nonce prefix** (not the
29//! full per-chunk nonce).  Its required length is `NONCE_FULL - 5` bytes.
30//!
31//! Each `encrypt_update` call encrypts **one buffered chunk** (not the
32//! supplied chunk) — the supplied chunk is stored for the next call.
33//! This "look-ahead by one chunk" is necessary so `encrypt_finalize` can
34//! correctly tag the last chunk with flag=0x01.
35
36use aead::{AeadInPlace, KeyInit};
37use aes_gcm::Aes256Gcm as AesGcm256;
38use chacha20poly1305::XChaCha20Poly1305;
39use oxicrypto_core::{CryptoError, StreamingAead};
40use subtle::ConstantTimeEq as _;
41
42/// Operating mode of a streaming AEAD instance.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum StreamMode {
45    /// `init` has been called; ready to encrypt.
46    Encrypting,
47    /// `init` has been called; ready to decrypt.
48    Decrypting,
49    /// `encrypt_finalize` or `decrypt_finalize` has been called; must call `reset`.
50    Finished,
51}
52
53// ── Generic STREAM helpers ──────────────────────────────────────────────────
54
55/// Build the per-chunk nonce from a prefix, counter, and final flag.
56///
57/// `prefix` has length `NONCE_FULL - 5`.
58fn build_nonce<const NONCE_FULL: usize>(
59    prefix: &[u8],
60    counter: u32,
61    is_final: bool,
62) -> [u8; NONCE_FULL] {
63    let mut nonce = [0u8; NONCE_FULL];
64    let prefix_len = NONCE_FULL - 5;
65    nonce[..prefix_len].copy_from_slice(prefix);
66    let counter_bytes = counter.to_be_bytes();
67    nonce[prefix_len..prefix_len + 4].copy_from_slice(&counter_bytes);
68    nonce[NONCE_FULL - 1] = if is_final { 0x01 } else { 0x00 };
69    nonce
70}
71
72/// Seal one STREAM chunk using the provided AEAD cipher type.
73///
74/// The output `ct_out` must have room for `pt.len() + tag_len` bytes.
75/// Returns the number of bytes written.
76fn stream_seal_chunk<C, const NONCE_FULL: usize>(
77    cipher: &C,
78    nonce: &[u8; NONCE_FULL],
79    aad: &[u8],
80    pt: &[u8],
81    ct_out: &mut [u8],
82) -> Result<usize, CryptoError>
83where
84    C: AeadInPlace,
85{
86    let tag_len = <<C as aead::AeadCore>::TagSize as aead::generic_array::typenum::Unsigned>::USIZE;
87    let required = pt.len().checked_add(tag_len).ok_or(CryptoError::BadInput)?;
88    if ct_out.len() < required {
89        return Err(CryptoError::BufferTooSmall);
90    }
91    ct_out[..pt.len()].copy_from_slice(pt);
92    let nonce_ga = aead::generic_array::GenericArray::from_slice(nonce.as_ref());
93    let tag = cipher
94        .encrypt_in_place_detached(nonce_ga, aad, &mut ct_out[..pt.len()])
95        .map_err(|_| CryptoError::Internal("STREAM encrypt chunk failed"))?;
96    ct_out[pt.len()..required].copy_from_slice(&tag);
97    Ok(required)
98}
99
100/// Open one STREAM chunk; returns plaintext length on success.
101fn stream_open_chunk<C, const NONCE_FULL: usize>(
102    cipher: &C,
103    nonce: &[u8; NONCE_FULL],
104    aad: &[u8],
105    ct_and_tag: &[u8],
106    pt_out: &mut [u8],
107) -> Result<usize, CryptoError>
108where
109    C: AeadInPlace,
110{
111    let tag_len = <<C as aead::AeadCore>::TagSize as aead::generic_array::typenum::Unsigned>::USIZE;
112    if ct_and_tag.len() < tag_len {
113        return Err(CryptoError::BadInput);
114    }
115    let pt_len = ct_and_tag.len() - tag_len;
116    if pt_out.len() < pt_len {
117        return Err(CryptoError::BufferTooSmall);
118    }
119    pt_out[..pt_len].copy_from_slice(&ct_and_tag[..pt_len]);
120    let nonce_ga = aead::generic_array::GenericArray::from_slice(nonce.as_ref());
121    let tag_bytes = &ct_and_tag[pt_len..];
122    let tag = aead::Tag::<C>::clone_from_slice(tag_bytes);
123    cipher
124        .decrypt_in_place_detached(nonce_ga, aad, &mut pt_out[..pt_len], &tag)
125        .map_err(|_| CryptoError::InvalidTag)?;
126    Ok(pt_len)
127}
128
129// ── AES-256-GCM STREAM ────────────────────────────────────────────────────────
130
131/// STREAM chunked AEAD using AES-256-GCM.
132///
133/// Nonce layout: 7-byte prefix ‖ 4-byte counter (big-endian) ‖ 1-byte flag.
134///
135/// Provide a 7-byte prefix to `init`.  Each `encrypt_update` or
136/// `decrypt_update` call processes exactly one buffered chunk.
137pub struct Aes256GcmStream {
138    /// The underlying AES-256-GCM cipher, present after `init`.
139    cipher: Option<AesGcm256>,
140    /// 7-byte nonce prefix (nonce[0..7]).
141    nonce_prefix: [u8; 7],
142    /// Per-chunk counter; incremented after each chunk is processed.
143    counter: u32,
144    /// AAD to be applied to every chunk.
145    aad: alloc::vec::Vec<u8>,
146    /// Buffered chunk (one chunk look-ahead for encryption).
147    pending: alloc::vec::Vec<u8>,
148    /// Operating mode.
149    mode: StreamMode,
150}
151
152impl core::fmt::Debug for Aes256GcmStream {
153    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
154        f.debug_struct("Aes256GcmStream")
155            .field("mode", &self.mode)
156            .field("counter", &self.counter)
157            .field("pending_len", &self.pending.len())
158            .finish()
159    }
160}
161
162extern crate alloc;
163use alloc::vec::Vec;
164
165impl Aes256GcmStream {
166    /// Full 12-byte nonce for the current chunk.
167    fn current_nonce(&self, is_final: bool) -> [u8; 12] {
168        build_nonce(&self.nonce_prefix, self.counter, is_final)
169    }
170
171    /// Advance the counter, returning an error if it would overflow.
172    fn advance_counter(&mut self) -> Result<(), CryptoError> {
173        self.counter = self
174            .counter
175            .checked_add(1)
176            .ok_or(CryptoError::Internal("STREAM counter overflow"))?;
177        Ok(())
178    }
179}
180
181impl StreamingAead for Aes256GcmStream {
182    /// Initialise the stream.
183    ///
184    /// `nonce` must be exactly 7 bytes (the nonce prefix).
185    fn init(key: &[u8], nonce: &[u8], aad: &[u8]) -> Result<Self, CryptoError> {
186        if key.len() != 32 {
187            return Err(CryptoError::InvalidKey);
188        }
189        if nonce.len() != 7 {
190            return Err(CryptoError::InvalidNonce);
191        }
192        let cipher = AesGcm256::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
193        let mut nonce_prefix = [0u8; 7];
194        nonce_prefix.copy_from_slice(nonce);
195        Ok(Self {
196            cipher: Some(cipher),
197            nonce_prefix,
198            counter: 0,
199            aad: aad.to_vec(),
200            pending: Vec::new(),
201            mode: StreamMode::Encrypting,
202        })
203    }
204
205    /// Encrypt one chunk.
206    ///
207    /// The **previously supplied** chunk (from the last `encrypt_update` call)
208    /// is encrypted into `out` with a non-final nonce.  The supplied `chunk`
209    /// is buffered for the next call.  On the first call, only buffering
210    /// occurs; `out` receives 0 bytes.
211    ///
212    /// `out` must be large enough to hold the previous chunk's ciphertext +
213    /// 16-byte tag (i.e. `prev_chunk.len() + 16`).
214    fn encrypt_update(&mut self, chunk: &[u8], out: &mut [u8]) -> Result<usize, CryptoError> {
215        if self.mode != StreamMode::Encrypting {
216            return Err(CryptoError::BadInput);
217        }
218        let cipher = self.cipher.as_ref().ok_or(CryptoError::BadInput)?;
219
220        if self.pending.is_empty() {
221            // First call: buffer the incoming chunk, emit nothing.
222            self.pending = chunk.to_vec();
223            return Ok(0);
224        }
225
226        // Encrypt the buffered chunk with flag=0x00 (non-final).
227        let nonce = self.current_nonce(false);
228        let prev = core::mem::replace(&mut self.pending, chunk.to_vec());
229        let written = stream_seal_chunk::<_, 12>(cipher, &nonce, &self.aad, &prev, out)?;
230        self.advance_counter()?;
231        Ok(written)
232    }
233
234    /// Finalize encryption by flushing the buffered last chunk with flag=0x01.
235    ///
236    /// `out` must hold at least `last_buffered_chunk.len() + 16` bytes.
237    /// Returns the 16-byte final authentication tag (also embedded in `out`).
238    fn encrypt_finalize(mut self, out: &mut [u8]) -> Result<[u8; 16], CryptoError> {
239        if self.mode != StreamMode::Encrypting {
240            return Err(CryptoError::BadInput);
241        }
242        self.mode = StreamMode::Finished;
243        let cipher = self.cipher.take().ok_or(CryptoError::BadInput)?;
244
245        let nonce = self.current_nonce(true);
246        let last = self.pending.clone();
247        let written = stream_seal_chunk::<_, 12>(&cipher, &nonce, &self.aad, &last, out)?;
248
249        // Extract the 16-byte tag from the end of what was written.
250        let tag_start = written - 16;
251        let mut tag = [0u8; 16];
252        tag.copy_from_slice(&out[tag_start..written]);
253        Ok(tag)
254    }
255
256    /// Decrypt one chunk.
257    ///
258    /// Buffers the supplied ciphertext chunk.  If a previous chunk is pending,
259    /// it is decrypted (with non-final nonce) into `out`.
260    fn decrypt_update(&mut self, chunk: &[u8], out: &mut [u8]) -> Result<usize, CryptoError> {
261        if self.mode != StreamMode::Decrypting {
262            // Allow switching to decrypt mode on first decrypt call.
263            if self.mode == StreamMode::Encrypting && self.counter == 0 && self.pending.is_empty() {
264                self.mode = StreamMode::Decrypting;
265            } else {
266                return Err(CryptoError::BadInput);
267            }
268        }
269        let cipher = self.cipher.as_ref().ok_or(CryptoError::BadInput)?;
270
271        if self.pending.is_empty() {
272            self.pending = chunk.to_vec();
273            return Ok(0);
274        }
275
276        // Decrypt the buffered chunk with flag=0x00 (non-final).
277        let nonce = self.current_nonce(false);
278        let prev = core::mem::replace(&mut self.pending, chunk.to_vec());
279        let written = stream_open_chunk::<_, 12>(cipher, &nonce, &self.aad, &prev, out)?;
280        self.advance_counter()?;
281        Ok(written)
282    }
283
284    /// Verify and finalize decryption of the buffered last chunk.
285    ///
286    /// `expected_tag` is the 16-byte tag from the last ciphertext chunk.
287    /// The buffered chunk must already contain `ciphertext || tag`.
288    fn decrypt_finalize(mut self, expected_tag: &[u8]) -> Result<(), CryptoError> {
289        if self.mode != StreamMode::Decrypting {
290            return Err(CryptoError::BadInput);
291        }
292        self.mode = StreamMode::Finished;
293        let cipher = self.cipher.take().ok_or(CryptoError::BadInput)?;
294
295        // The buffered last chunk already contains ct || tag.
296        // But decrypt_finalize's expected_tag is provided externally.
297        // We verify the buffered pending chunk includes that tag.
298        let pending = self.pending.clone();
299        let tag_len = 16usize;
300        if pending.len() < tag_len {
301            return Err(CryptoError::BadInput);
302        }
303
304        // Check expected_tag matches what's in the pending buffer.
305        let embedded_tag = &pending[pending.len() - tag_len..];
306        if !bool::from(expected_tag.ct_eq(embedded_tag)) {
307            return Err(CryptoError::InvalidTag);
308        }
309
310        let nonce = self.current_nonce(true);
311        let mut pt = alloc::vec![0u8; pending.len() - tag_len];
312        stream_open_chunk::<_, 12>(&cipher, &nonce, &self.aad, &pending, &mut pt).map(|_| ())
313    }
314
315    /// Reset the stream to its initial uninitialized state.
316    fn reset(&mut self) {
317        self.counter = 0;
318        self.pending.clear();
319        self.mode = StreamMode::Encrypting;
320        self.cipher = None;
321    }
322}
323
324// ── ChaCha20-Poly1305 STREAM ──────────────────────────────────────────────────
325
326/// STREAM chunked AEAD using XChaCha20-Poly1305.
327///
328/// Nonce layout: 19-byte prefix ‖ 4-byte counter (big-endian) ‖ 1-byte flag.
329///
330/// Provide a 19-byte prefix to `init`.
331pub struct ChaCha20Poly1305Stream {
332    cipher: Option<XChaCha20Poly1305>,
333    nonce_prefix: [u8; 19],
334    counter: u32,
335    aad: Vec<u8>,
336    pending: Vec<u8>,
337    mode: StreamMode,
338}
339
340impl core::fmt::Debug for ChaCha20Poly1305Stream {
341    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
342        f.debug_struct("ChaCha20Poly1305Stream")
343            .field("mode", &self.mode)
344            .field("counter", &self.counter)
345            .field("pending_len", &self.pending.len())
346            .finish()
347    }
348}
349
350impl ChaCha20Poly1305Stream {
351    fn current_nonce(&self, is_final: bool) -> [u8; 24] {
352        build_nonce(&self.nonce_prefix, self.counter, is_final)
353    }
354
355    fn advance_counter(&mut self) -> Result<(), CryptoError> {
356        self.counter = self
357            .counter
358            .checked_add(1)
359            .ok_or(CryptoError::Internal("STREAM counter overflow"))?;
360        Ok(())
361    }
362}
363
364impl StreamingAead for ChaCha20Poly1305Stream {
365    /// Initialise the stream.
366    ///
367    /// `nonce` must be exactly 19 bytes (XChaCha20 nonce prefix).
368    fn init(key: &[u8], nonce: &[u8], aad: &[u8]) -> Result<Self, CryptoError> {
369        if key.len() != 32 {
370            return Err(CryptoError::InvalidKey);
371        }
372        if nonce.len() != 19 {
373            return Err(CryptoError::InvalidNonce);
374        }
375        let cipher = XChaCha20Poly1305::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
376        let mut nonce_prefix = [0u8; 19];
377        nonce_prefix.copy_from_slice(nonce);
378        Ok(Self {
379            cipher: Some(cipher),
380            nonce_prefix,
381            counter: 0,
382            aad: aad.to_vec(),
383            pending: Vec::new(),
384            mode: StreamMode::Encrypting,
385        })
386    }
387
388    fn encrypt_update(&mut self, chunk: &[u8], out: &mut [u8]) -> Result<usize, CryptoError> {
389        if self.mode != StreamMode::Encrypting {
390            return Err(CryptoError::BadInput);
391        }
392        let cipher = self.cipher.as_ref().ok_or(CryptoError::BadInput)?;
393
394        if self.pending.is_empty() {
395            self.pending = chunk.to_vec();
396            return Ok(0);
397        }
398
399        let nonce = self.current_nonce(false);
400        let prev = core::mem::replace(&mut self.pending, chunk.to_vec());
401        let written = stream_seal_chunk::<_, 24>(cipher, &nonce, &self.aad, &prev, out)?;
402        self.advance_counter()?;
403        Ok(written)
404    }
405
406    fn encrypt_finalize(mut self, out: &mut [u8]) -> Result<[u8; 16], CryptoError> {
407        if self.mode != StreamMode::Encrypting {
408            return Err(CryptoError::BadInput);
409        }
410        self.mode = StreamMode::Finished;
411        let cipher = self.cipher.take().ok_or(CryptoError::BadInput)?;
412
413        let nonce = self.current_nonce(true);
414        let last = self.pending.clone();
415        let written = stream_seal_chunk::<_, 24>(&cipher, &nonce, &self.aad, &last, out)?;
416
417        let tag_start = written - 16;
418        let mut tag = [0u8; 16];
419        tag.copy_from_slice(&out[tag_start..written]);
420        Ok(tag)
421    }
422
423    fn decrypt_update(&mut self, chunk: &[u8], out: &mut [u8]) -> Result<usize, CryptoError> {
424        if self.mode != StreamMode::Decrypting {
425            if self.mode == StreamMode::Encrypting && self.counter == 0 && self.pending.is_empty() {
426                self.mode = StreamMode::Decrypting;
427            } else {
428                return Err(CryptoError::BadInput);
429            }
430        }
431        let cipher = self.cipher.as_ref().ok_or(CryptoError::BadInput)?;
432
433        if self.pending.is_empty() {
434            self.pending = chunk.to_vec();
435            return Ok(0);
436        }
437
438        let nonce = self.current_nonce(false);
439        let prev = core::mem::replace(&mut self.pending, chunk.to_vec());
440        let written = stream_open_chunk::<_, 24>(cipher, &nonce, &self.aad, &prev, out)?;
441        self.advance_counter()?;
442        Ok(written)
443    }
444
445    fn decrypt_finalize(mut self, expected_tag: &[u8]) -> Result<(), CryptoError> {
446        if self.mode != StreamMode::Decrypting {
447            return Err(CryptoError::BadInput);
448        }
449        self.mode = StreamMode::Finished;
450        let cipher = self.cipher.take().ok_or(CryptoError::BadInput)?;
451
452        let pending = self.pending.clone();
453        let tag_len = 16usize;
454        if pending.len() < tag_len {
455            return Err(CryptoError::BadInput);
456        }
457
458        let embedded_tag = &pending[pending.len() - tag_len..];
459        if !bool::from(expected_tag.ct_eq(embedded_tag)) {
460            return Err(CryptoError::InvalidTag);
461        }
462
463        let nonce = self.current_nonce(true);
464        let mut pt = alloc::vec![0u8; pending.len() - tag_len];
465        stream_open_chunk::<_, 24>(&cipher, &nonce, &self.aad, &pending, &mut pt).map(|_| ())
466    }
467
468    fn reset(&mut self) {
469        self.counter = 0;
470        self.pending.clear();
471        self.mode = StreamMode::Encrypting;
472        self.cipher = None;
473    }
474}
475
476// ── Tests ─────────────────────────────────────────────────────────────────────
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    const KEY_256: [u8; 32] = [0x42u8; 32];
483    const NONCE_PREFIX_7: [u8; 7] = [0x24u8; 7];
484    const NONCE_PREFIX_19: [u8; 19] = [0x24u8; 19];
485    const AAD: &[u8] = b"stream aad";
486    const TAG_LEN: usize = 16;
487
488    /// Encrypt `chunks` using `Aes256GcmStream`, returning `(Vec<ct_chunks>, final_tag)`.
489    ///
490    /// Each element of the returned `Vec` is `ciphertext || 16-byte tag` for
491    /// one chunk.  The last element corresponds to the final (flag=1) chunk.
492    fn encrypt_chunks_aes256(chunks: &[&[u8]]) -> (Vec<Vec<u8>>, [u8; 16]) {
493        assert!(!chunks.is_empty());
494        let mut enc = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init enc");
495        let mut ct_chunks: Vec<Vec<u8>> = Vec::new();
496
497        // encrypt_update(chunk_i) emits the encryption of chunk_{i-1} (look-ahead buffering).
498        // The output buffer must be large enough for the *previous* (pending) chunk + tag.
499        // We use the largest possible chunk size across all chunks for simplicity.
500        let max_chunk_len = chunks.iter().map(|c| c.len()).max().unwrap_or(0);
501        let buf_cap = max_chunk_len + TAG_LEN;
502
503        for chunk in chunks {
504            let mut buf = alloc::vec![0u8; buf_cap];
505            let written = enc.encrypt_update(chunk, &mut buf).expect("encrypt_update");
506            if written > 0 {
507                ct_chunks.push(buf[..written].to_vec());
508            }
509        }
510
511        // Finalize flushes the last buffered chunk with flag=1.
512        let last = *chunks.last().unwrap();
513        let mut final_buf = alloc::vec![0u8; last.len() + TAG_LEN];
514        let tag = enc
515            .encrypt_finalize(&mut final_buf)
516            .expect("encrypt_finalize");
517        ct_chunks.push(final_buf[..last.len() + TAG_LEN].to_vec());
518        (ct_chunks, tag)
519    }
520
521    /// Decrypt `ct_chunks` with `Aes256GcmStream`, returning the concatenated plaintext.
522    ///
523    /// `final_tag` is the tag returned by `encrypt_finalize` / embedded in the last chunk.
524    fn decrypt_chunks_aes256(ct_chunks: &[Vec<u8>], final_tag: &[u8; 16]) -> Vec<u8> {
525        let mut dec = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init dec");
526        dec.mode = StreamMode::Decrypting;
527        let mut plaintext: Vec<u8> = Vec::new();
528
529        // Feed all ct chunks via decrypt_update.
530        // The first call just buffers; subsequent calls decrypt the prior buffered chunk.
531        for ct in ct_chunks {
532            let buf_cap = ct.len(); // plaintext is at most ct.len() - TAG_LEN
533            let mut buf = alloc::vec![0u8; buf_cap];
534            let written = dec.decrypt_update(ct, &mut buf).expect("decrypt_update");
535            plaintext.extend_from_slice(&buf[..written]);
536        }
537
538        // Finalize: decrypt the last buffered ct chunk (with flag=1 nonce) and verify tag.
539        dec.decrypt_finalize(final_tag).expect("decrypt_finalize");
540        // The last chunk's plaintext is not returned by decrypt_finalize in our trait.
541        // We recover it by subtracting the tag from the last ct chunk.
542        // (decrypt_finalize internally verifies; plaintext was recovered into the internal buffer.)
543        // We need to extract it separately — re-decrypt the final chunk manually.
544        let last_ct = ct_chunks.last().unwrap();
545        let pt_len = last_ct.len().saturating_sub(TAG_LEN);
546        // Re-run a fresh decrypt just to extract the final plaintext bytes.
547        let mut dec2 = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init dec2");
548        dec2.mode = StreamMode::Decrypting;
549        for ct in ct_chunks {
550            let mut buf = alloc::vec![0u8; ct.len()];
551            let written = dec2.decrypt_update(ct, &mut buf).expect("decrypt_update2");
552            if written > 0 {
553                // These chunks were already added by the first pass; skip here.
554                let _ = written;
555            }
556        }
557        // After all chunks fed, the last chunk is pending in dec2.
558        // We can't call decrypt_finalize twice on the same stream (it's consumed).
559        // Instead, decrypt the last ct chunk directly using the known key/nonce.
560        let nonce_counter = (ct_chunks.len() as u32).wrapping_sub(1);
561        let nonce: [u8; 12] = build_nonce(&NONCE_PREFIX_7, nonce_counter, true);
562        let cipher = aes_gcm::Aes256Gcm::new_from_slice(&KEY_256).expect("cipher");
563        let nonce_ga = aead::generic_array::GenericArray::from_slice(nonce.as_ref());
564        let mut last_pt = last_ct[..pt_len].to_vec();
565        let tag_bytes = &last_ct[pt_len..];
566        let tag_ga = aead::Tag::<aes_gcm::Aes256Gcm>::clone_from_slice(tag_bytes);
567        cipher
568            .decrypt_in_place_detached(nonce_ga, AAD, &mut last_pt, &tag_ga)
569            .expect("last chunk decrypt");
570        plaintext.extend_from_slice(&last_pt);
571        plaintext
572    }
573
574    #[test]
575    fn aes256gcm_stream_three_chunks() {
576        let chunks: &[&[u8]] = &[b"chunk-one---", b"chunk-two---", b"chunk-three"];
577        let expected: Vec<u8> = chunks.iter().flat_map(|c| c.iter().copied()).collect();
578
579        let (ct_chunks, final_tag) = encrypt_chunks_aes256(chunks);
580        let recovered = decrypt_chunks_aes256(&ct_chunks, &final_tag);
581        assert_eq!(recovered, expected, "three-chunk round-trip failed");
582    }
583
584    #[test]
585    fn aes256gcm_stream_single_chunk() {
586        let chunk = b"only one chunk";
587
588        let mut enc = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init");
589        // First update: buffers the chunk, emits nothing.
590        let mut buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
591        let written = enc.encrypt_update(chunk, &mut buf).expect("update");
592        assert_eq!(written, 0);
593
594        // Finalize: encrypts the buffered chunk with flag=1.
595        let mut final_buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
596        let tag = enc.encrypt_finalize(&mut final_buf).expect("finalize");
597        assert_eq!(final_buf.len(), chunk.len() + TAG_LEN);
598
599        // Decrypt: first update buffers the ct chunk.
600        let mut dec = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init dec");
601        dec.mode = StreamMode::Decrypting;
602        let mut pt_buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
603        let w = dec
604            .decrypt_update(&final_buf, &mut pt_buf)
605            .expect("decrypt_update");
606        assert_eq!(w, 0, "first update must buffer, not emit");
607        // Finalize decrypts the buffered final chunk.
608        dec.decrypt_finalize(&tag).expect("decrypt_finalize");
609    }
610
611    #[test]
612    fn aes256gcm_stream_tamper_middle_chunk_fails() {
613        // Encrypt 3 chunks so we get a middle ct chunk to tamper with.
614        let chunks: &[&[u8]] = &[b"chunk-A-data---", b"chunk-B-tamper-", b"chunk-C-final--"];
615        let (mut ct_chunks, final_tag) = encrypt_chunks_aes256(chunks);
616
617        // Tamper with the middle chunk (index 0 of ct_chunks corresponds to chunk-A
618        // because of the look-ahead: encrypt_update(chunk-A) emits nothing,
619        // encrypt_update(chunk-B) emits ct of chunk-A,
620        // encrypt_update(chunk-C) emits ct of chunk-B,
621        // encrypt_finalize emits ct of chunk-C).
622        //
623        // ct_chunks[0] = ct of chunk-A (flag=0, counter=0)
624        // ct_chunks[1] = ct of chunk-B (flag=0, counter=1)
625        // ct_chunks[2] = ct of chunk-C (flag=1, counter=2)
626        ct_chunks[1][0] ^= 0xFF; // tamper chunk-B ciphertext
627
628        let mut dec = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init dec");
629        dec.mode = StreamMode::Decrypting;
630
631        // Feed ct_chunks[0]: buffers (returns 0).
632        let mut pt_buf = alloc::vec![0u8; ct_chunks[0].len()];
633        let w0 = dec
634            .decrypt_update(&ct_chunks[0], &mut pt_buf)
635            .expect("update0");
636        assert_eq!(w0, 0);
637
638        // Feed ct_chunks[1]: decrypts buffered ct_chunks[0], buffers ct_chunks[1] (tampered).
639        // Should succeed (ct_chunks[0] is not tampered).
640        let mut pt_buf1 = alloc::vec![0u8; ct_chunks[0].len()];
641        let w1 = dec
642            .decrypt_update(&ct_chunks[1], &mut pt_buf1)
643            .expect("update1");
644        assert!(w1 > 0, "should have emitted decrypted chunk-A");
645
646        // Feed ct_chunks[2]: decrypts the tampered ct_chunks[1] — should fail.
647        let mut pt_buf2 = alloc::vec![0u8; ct_chunks[1].len()];
648        let result = dec.decrypt_update(&ct_chunks[2], &mut pt_buf2);
649        assert!(
650            matches!(result, Err(CryptoError::InvalidTag)),
651            "expected InvalidTag on tampered chunk, got: {:?}",
652            result
653        );
654        // final_tag is not needed since we expect failure before decrypt_finalize.
655        let _ = final_tag;
656    }
657
658    #[test]
659    fn aes256gcm_stream_tamper_final_tag_fails() {
660        let chunks: &[&[u8]] = &[b"single"];
661        let (ct_chunks, mut final_tag) = encrypt_chunks_aes256(chunks);
662
663        // Tamper with the final tag.
664        final_tag[0] ^= 0xFF;
665
666        let mut dec = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init dec");
667        dec.mode = StreamMode::Decrypting;
668        let mut pt_buf = alloc::vec![0u8; ct_chunks[0].len()];
669        dec.decrypt_update(&ct_chunks[0], &mut pt_buf)
670            .expect("update");
671        let result = dec.decrypt_finalize(&final_tag);
672        assert!(
673            matches!(result, Err(CryptoError::InvalidTag)),
674            "expected InvalidTag, got: {:?}",
675            result
676        );
677    }
678
679    #[test]
680    fn aes256gcm_stream_reject_update_after_finalize() {
681        // After encrypt_finalize (which consumes enc), you cannot call encrypt_update.
682        // This is enforced at compile time by consuming `self` in encrypt_finalize.
683        // We just verify the finalize path works correctly.
684        let chunk = b"data";
685        let mut enc = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init");
686        let mut buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
687        enc.encrypt_update(chunk, &mut buf).expect("update");
688        let mut final_buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
689        let _tag = enc.encrypt_finalize(&mut final_buf).expect("finalize");
690        // enc is moved/consumed; further calls would not compile.
691    }
692
693    #[test]
694    fn chacha20poly1305_stream_single_chunk_round_trip() {
695        let chunk = b"xchacha20 stream chunk";
696
697        let mut enc = ChaCha20Poly1305Stream::init(&KEY_256, &NONCE_PREFIX_19, AAD).expect("init");
698        let mut buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
699        let w = enc.encrypt_update(chunk, &mut buf).expect("update");
700        assert_eq!(w, 0);
701
702        let mut final_buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
703        let tag = enc.encrypt_finalize(&mut final_buf).expect("finalize");
704
705        let mut dec =
706            ChaCha20Poly1305Stream::init(&KEY_256, &NONCE_PREFIX_19, AAD).expect("init dec");
707        dec.mode = StreamMode::Decrypting;
708        let mut pt_buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
709        let _w = dec
710            .decrypt_update(&final_buf, &mut pt_buf)
711            .expect("decrypt_update");
712        dec.decrypt_finalize(&tag).expect("decrypt_finalize");
713    }
714
715    #[test]
716    fn aes256gcm_stream_wrong_nonce_prefix_length() {
717        let result = Aes256GcmStream::init(&KEY_256, &[0u8; 12], AAD);
718        assert!(
719            matches!(result, Err(CryptoError::InvalidNonce)),
720            "expected InvalidNonce, got: {:?}",
721            result.as_ref().map(|_| ()).map_err(|e| format!("{e:?}"))
722        );
723    }
724
725    #[test]
726    fn chacha20poly1305_stream_wrong_nonce_prefix_length() {
727        let result = ChaCha20Poly1305Stream::init(&KEY_256, &[0u8; 12], AAD);
728        assert!(
729            matches!(result, Err(CryptoError::InvalidNonce)),
730            "expected InvalidNonce, got: {:?}",
731            result.as_ref().map(|_| ()).map_err(|e| format!("{e:?}"))
732        );
733    }
734
735    #[test]
736    fn aes256gcm_stream_reset_clears_state() {
737        // Initialise, feed one chunk (buffers it), then reset.
738        // After reset, counter must be 0, pending must be empty,
739        // and the mode must be back to Encrypting.
740        let chunk = b"some data";
741        let mut enc = Aes256GcmStream::init(&KEY_256, &NONCE_PREFIX_7, AAD).expect("init");
742        assert_eq!(enc.counter, 0, "initial counter");
743        assert!(enc.pending.is_empty(), "initial pending");
744        assert_eq!(enc.mode, StreamMode::Encrypting, "initial mode");
745
746        let mut buf = alloc::vec![0u8; chunk.len() + TAG_LEN];
747        let w = enc.encrypt_update(chunk, &mut buf).expect("encrypt_update");
748        assert_eq!(w, 0, "first update buffers; emits nothing");
749        assert!(!enc.pending.is_empty(), "pending filled after update");
750
751        // Reset clears cipher, counter, pending, and sets mode to Encrypting.
752        enc.reset();
753        assert_eq!(enc.counter, 0, "counter after reset");
754        assert!(enc.pending.is_empty(), "pending after reset");
755        assert_eq!(enc.mode, StreamMode::Encrypting, "mode after reset");
756        assert!(enc.cipher.is_none(), "cipher cleared after reset");
757    }
758}