crypt_io/stream/decryptor.rs
1//! Streaming AEAD decryptor.
2
3use alloc::vec::Vec;
4
5use crate::aead::Algorithm;
6use crate::error::{Error, Result};
7
8use super::aead::decrypt_chunk;
9use super::frame::{HEADER_LEN, NONCE_PREFIX_LEN, build_nonce, chunk_size_from_log2, parse_header};
10
11/// Streaming AEAD decryptor — the inverse of [`super::StreamEncryptor`].
12///
13/// Construct from the 24-byte header, feed encrypted chunk bytes via
14/// [`update`](Self::update), and finalise with
15/// [`finalize`](Self::finalize). The decryptor buffers exactly enough
16/// bytes to know whether the next chunk is final, so callers don't
17/// need to track chunk boundaries — only "this is all the bytes" (via
18/// `finalize`).
19///
20/// Authentication failures (tampered ciphertext, wrong key, tampered
21/// header, truncated stream, reordered chunks, duplicated chunks) all
22/// surface as [`Error::AuthenticationFailed`]. The variant is
23/// intentionally opaque — exposing which mode failed would leak
24/// information to an attacker.
25///
26/// # Example
27///
28/// See [`super::StreamEncryptor`] for a round-trip example.
29#[derive(Debug)]
30pub struct StreamDecryptor {
31 algorithm: Algorithm,
32 key: [u8; 32],
33 nonce_prefix: [u8; NONCE_PREFIX_LEN],
34 aad: [u8; HEADER_LEN],
35 counter: u32,
36 chunk_size: usize,
37 chunk_size_log2: u8,
38 /// Encrypted bytes awaiting decryption. Always holds at most
39 /// `chunk_size + 16` bytes after each `update` returns.
40 buffer: Vec<u8>,
41}
42
43impl StreamDecryptor {
44 /// Construct a decryptor by parsing `header_bytes` (must be at
45 /// least 24 bytes — only the first 24 are read).
46 ///
47 /// # Errors
48 ///
49 /// - [`Error::InvalidKey`] if `key` is not 32 bytes.
50 /// - [`Error::InvalidCiphertext`] if the header is malformed
51 /// (wrong magic, unsupported version, unknown algorithm,
52 /// out-of-range chunk size).
53 pub fn new(key: &[u8], header_bytes: &[u8]) -> Result<Self> {
54 if key.len() != 32 {
55 return Err(Error::InvalidKey {
56 expected: 32,
57 actual: key.len(),
58 });
59 }
60 let parsed = parse_header(header_bytes)?;
61 let chunk_size = chunk_size_from_log2(parsed.chunk_size_log2);
62
63 let mut key_arr = [0u8; 32];
64 key_arr.copy_from_slice(key);
65
66 Ok(Self {
67 algorithm: parsed.algorithm,
68 key: key_arr,
69 nonce_prefix: parsed.nonce_prefix,
70 aad: parsed.raw,
71 counter: 0,
72 chunk_size,
73 chunk_size_log2: parsed.chunk_size_log2,
74 // capacity = one non-final chunk's worth
75 buffer: Vec::with_capacity(chunk_size + 16),
76 })
77 }
78
79 /// Chunk size in bytes for this decryptor (read from the header).
80 #[must_use]
81 pub fn chunk_size(&self) -> usize {
82 self.chunk_size
83 }
84
85 /// Log2 of the chunk size (read from the header).
86 #[must_use]
87 pub fn chunk_size_log2(&self) -> u8 {
88 self.chunk_size_log2
89 }
90
91 /// Algorithm encoded in the header.
92 #[must_use]
93 pub fn algorithm(&self) -> Algorithm {
94 self.algorithm
95 }
96
97 /// Feed encrypted-stream bytes. Returns zero or more decrypted
98 /// plaintext bytes as complete non-final chunks are processed.
99 ///
100 /// The decryptor holds at most `chunk_size + 16` bytes in its
101 /// internal buffer between calls — that's exactly one full
102 /// non-final chunk, held in case it turns out to be the final
103 /// chunk (signalled by the next `update` having nothing to add or
104 /// `finalize` being called).
105 ///
106 /// # Errors
107 ///
108 /// - [`Error::AuthenticationFailed`] for any cryptographic
109 /// failure: tampered ciphertext, wrong key, tampered header,
110 /// chunk-counter desync, etc.
111 pub fn update(&mut self, data: &[u8]) -> Result<Vec<u8>> {
112 if data.is_empty() {
113 return Ok(Vec::new());
114 }
115
116 self.buffer.extend_from_slice(data);
117
118 let chunk_frame = self.chunk_size + 16;
119 let mut out = Vec::new();
120
121 // Process any chunks for which we know they are non-final:
122 // a chunk is non-final iff there is more than `chunk_frame`
123 // bytes in the buffer (because the encryptor guarantees the
124 // final chunk is strictly < `chunk_frame` bytes).
125 while self.buffer.len() > chunk_frame {
126 let chunk_bytes: Vec<u8> = self.buffer.drain(..chunk_frame).collect();
127 let nonce = build_nonce(&self.nonce_prefix, self.counter, false);
128 let pt = decrypt_chunk(self.algorithm, &self.key, &nonce, &chunk_bytes, &self.aad)?;
129 out.extend_from_slice(&pt);
130 self.counter = self.counter.checked_add(1).ok_or(Error::InvalidCiphertext(
131 alloc::string::String::from("stream chunk counter overflow"),
132 ))?;
133 }
134
135 Ok(out)
136 }
137
138 /// Flush. Treats whatever is in the buffer as the final encrypted
139 /// chunk and decrypts it. Returns the final plaintext bytes.
140 ///
141 /// # Errors
142 ///
143 /// - [`Error::InvalidCiphertext`] if the buffer is shorter than 16
144 /// bytes (cannot contain a tag) — typically caused by a stream
145 /// that lost its final chunk entirely.
146 /// - [`Error::AuthenticationFailed`] if the buffered bytes do not
147 /// verify as the final chunk under the expected nonce. This
148 /// covers truncation (a buffered chunk that the encoder wrote
149 /// as non-final being treated as final by the decoder),
150 /// tampering, and wrong key.
151 pub fn finalize(self) -> Result<Vec<u8>> {
152 let chunk_frame = self.chunk_size + 16;
153 if self.buffer.len() > chunk_frame {
154 // The update loop holds at most chunk_frame bytes; we
155 // shouldn't reach here.
156 return Err(Error::InvalidCiphertext(alloc::format!(
157 "stream finalize buffer too large ({} bytes, max {chunk_frame})",
158 self.buffer.len()
159 )));
160 }
161 if self.buffer.len() < 16 {
162 return Err(Error::InvalidCiphertext(alloc::format!(
163 "stream finalize buffer too short ({} bytes, need at least 16 for tag)",
164 self.buffer.len()
165 )));
166 }
167
168 let nonce = build_nonce(&self.nonce_prefix, self.counter, true);
169 decrypt_chunk(self.algorithm, &self.key, &nonce, &self.buffer, &self.aad)
170 }
171}