Skip to main content

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}