aescrypt_rs/decryption/read.rs
1//! src/decryption/read.rs
2//! Header / extension / iteration-count parsers for the AES Crypt v0–v3 read path.
3//!
4//! Every parser in this module reads into a [`SpanBuffer<N>`](crate::aliases::SpanBuffer),
5//! the [`secure-gate`] auto-zeroizing fixed-size buffer, so that even
6//! pre-authentication header bytes never linger on the stack after the call
7//! returns. The parsers are sequenced by [`crate::decrypt()`]; they are
8//! exposed publicly so that callers driving custom containers can rebuild the
9//! read pipeline themselves.
10//!
11//! # Security
12//!
13//! These parsers run **before** the session HMAC is verified, so any allocation
14//! or work they perform is attacker-influenceable. They are deliberately kept
15//! to fixed-size reads with hard-coded upper bounds (e.g. `MAX_EXTENSIONS`,
16//! [`PBKDF2_MAX_ITER`](crate::constants::PBKDF2_MAX_ITER)).
17//!
18//! [`secure-gate`]: https://github.com/Slurp9187/secure-gate
19
20use crate::aliases::SpanBuffer;
21use crate::error::AescryptError;
22use secure_gate::{RevealSecret, RevealSecretMut};
23use std::io::Read;
24
25/// Reads exactly `N` bytes from `reader` into a fresh auto-zeroizing
26/// [`SpanBuffer<N>`](crate::aliases::SpanBuffer).
27///
28/// `read_exact_span` is the primary stream reader for the constant-memory
29/// decryption path. The returned buffer lives on the stack inside a
30/// [`secure-gate`] wrapper so its contents are wiped on drop — important
31/// because pre-authentication header bytes pass through this function.
32///
33/// # Errors
34///
35/// - [`AescryptError::Io`] — `reader.read_exact` returned an error, including
36/// premature EOF.
37///
38/// # Panics
39///
40/// Never panics. EOF is surfaced as [`AescryptError::Io`], not a panic.
41///
42/// # Security
43///
44/// Output buffer auto-zeroizes via [`secure-gate`] regardless of which
45/// branch of the caller eventually returns.
46///
47/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
48#[inline(always)]
49pub fn read_exact_span<R, const N: usize>(reader: &mut R) -> Result<SpanBuffer<N>, AescryptError>
50where
51 R: Read,
52{
53 let mut buf = SpanBuffer::new([0u8; N]);
54 buf.with_secret_mut(|b| reader.read_exact(b))
55 .map_err(AescryptError::Io)?;
56 Ok(buf)
57}
58
59/// Reads and validates the 5-byte AES Crypt file header.
60///
61/// Returns `(version, modulo_or_reserved)` where:
62///
63/// - `version` is the file format version (`0..=3`).
64/// - `modulo_or_reserved` is the 5th header byte:
65/// - For v0: the **modulo** byte (any value; passed to
66/// [`StreamConfig::V0`](crate::decryption::StreamConfig::V0) for final
67/// plaintext-length recovery).
68/// - For v1–v3: the **reserved** byte; an error is returned unless it is
69/// `0x00`.
70///
71/// This is the strict counterpart to [`crate::read_version`], which only
72/// reads as many bytes as needed and is permissive about short v0 stubs.
73///
74/// # Errors
75///
76/// - [`AescryptError::Io`] — premature EOF or other reader error.
77/// - [`AescryptError::Header`] — magic bytes are not `b"AES"`, or the v1–v3
78/// reserved byte is not `0x00`.
79/// - [`AescryptError::UnsupportedVersion`] — version byte is `> 3`.
80///
81/// # Security
82///
83/// Reads exactly 5 bytes regardless of input, capping pre-authentication
84/// effort. The output is plain `(u8, u8)` — there is nothing secret to
85/// zeroize.
86#[inline(always)]
87pub fn read_file_version<R>(reader: &mut R) -> Result<(u8, u8), AescryptError>
88where
89 R: Read,
90{
91 let header = read_exact_span::<_, 4>(reader)?;
92 let is_aes = header.with_secret(|h| &h[..3] == b"AES");
93 if !is_aes {
94 return Err(AescryptError::Header(
95 "invalid magic header (expected 'AES')".into(),
96 ));
97 }
98 let version = header.with_secret(|h| h[3]);
99 if version > 3 {
100 return Err(AescryptError::UnsupportedVersion(version));
101 }
102 let modulo_or_reserved = read_exact_span::<_, 1>(reader)?.with_secret(|b| b[0]);
103 if version >= 1 && modulo_or_reserved != 0x00 {
104 return Err(AescryptError::Header(
105 "invalid header: reserved byte must be 0x00 for v1–v3".into(),
106 ));
107 }
108 Ok((version, modulo_or_reserved))
109}
110
111/// Maximum number of extensions accepted before returning an error.
112///
113/// A crafted file with an unbounded number of small extensions could consume
114/// proportional CPU/IO before the HMAC check. Limiting to 256 extensions is
115/// well above any legitimate use while capping the pre-auth work.
116const MAX_EXTENSIONS: usize = 256;
117
118/// Consumes all v2/v3 extension blocks from `reader`, stopping at the
119/// zero-length terminator.
120///
121/// For `version < 2`, this is a no-op (v0/v1 files have no extension section).
122/// For v2/v3, each extension is parsed as a `u16` big-endian length followed by
123/// `length` payload bytes, and is discarded. The loop stops when a zero-length
124/// extension is encountered.
125///
126/// # Errors
127///
128/// - [`AescryptError::Io`] — reader error or premature EOF inside an
129/// extension.
130/// - [`AescryptError::Header`] — more than 256 extension blocks encountered
131/// (`"too many extensions (limit: 256)"`).
132///
133/// # Security
134///
135/// Capped at 256 extensions to bound CPU/I/O on attacker-controlled files.
136/// The discard buffer is fixed at 256 bytes and reused across reads.
137#[inline(always)]
138pub fn consume_all_extensions<R>(reader: &mut R, version: u8) -> Result<(), AescryptError>
139where
140 R: Read,
141{
142 if version < 2 {
143 return Ok(());
144 }
145
146 let mut count = 0usize;
147 loop {
148 if count >= MAX_EXTENSIONS {
149 return Err(AescryptError::Header(
150 "too many extensions (limit: 256)".into(),
151 ));
152 }
153
154 let len_bytes = read_exact_span::<_, 2>(reader)?;
155 let len = len_bytes.with_secret(|lb| u16::from_be_bytes(*lb));
156
157 if len == 0 {
158 break; // end of extensions
159 }
160
161 // Safe skip — no allocation needed
162 let mut discard = [0u8; 256]; // reuse buffer for small extensions
163 let mut remaining = len as usize;
164
165 while remaining > 0 {
166 let to_read = remaining.min(discard.len());
167 reader
168 .read_exact(&mut discard[..to_read])
169 .map_err(AescryptError::Io)?;
170 remaining -= to_read;
171 }
172 count += 1;
173 }
174 Ok(())
175}
176
177/// Reads the 4-byte big-endian PBKDF2 iteration count from a v3 file header.
178///
179/// Returns `0` for `version < 3` (v0/v1/v2 do not store an iteration count;
180/// they use the fixed [`ACKDF_ITERATIONS`](crate::kdf::ackdf::ACKDF_ITERATIONS)
181/// instead). For v3, the value is validated against an internal upper bound of
182/// 5 000 000 iterations (matching
183/// [`PBKDF2_MAX_ITER`](crate::constants::PBKDF2_MAX_ITER)) and rejected if
184/// zero.
185///
186/// # Errors
187///
188/// - [`AescryptError::Io`] — reader error or premature EOF.
189/// - [`AescryptError::Header`] — iteration count is `0`
190/// (`"KDF iterations cannot be zero"`) or exceeds 5 000 000
191/// (`"KDF iterations unreasonably high (>5M)"`).
192///
193/// # Security
194///
195/// The 5 000 000 ceiling is enforced before any password-dependent work to
196/// prevent denial-of-service via crafted headers with `iterations = u32::MAX`.
197#[inline(always)]
198pub fn read_kdf_iterations<R>(reader: &mut R, version: u8) -> Result<u32, AescryptError>
199where
200 R: Read,
201{
202 if version < 3 {
203 return Ok(0);
204 }
205
206 let iter_bytes = read_exact_span::<_, 4>(reader)?;
207 let iterations = iter_bytes.with_secret(|ib| u32::from_be_bytes(*ib));
208
209 if iterations == 0 {
210 return Err(AescryptError::Header(
211 "KDF iterations cannot be zero".into(),
212 ));
213 }
214 if iterations > 5_000_000 {
215 return Err(AescryptError::Header(
216 "KDF iterations unreasonably high (>5M)".into(),
217 ));
218 }
219
220 Ok(iterations)
221}