aescrypt_rs/decryption/decrypt.rs
1//! src/core/decryption/decrypt.rs
2//! High-level [`decrypt`] entry point: streams an AES Crypt v0–v3 file to plaintext.
3
4use crate::decryption::read::{
5 consume_all_extensions, read_exact_span, read_file_version, read_kdf_iterations,
6};
7use crate::decryption::session::extract_session_data;
8use crate::decryption::stream::{decrypt_ciphertext_stream, StreamConfig};
9
10use crate::aliases::{Aes256Key32, Iv16, PasswordString};
11use crate::error::AescryptError;
12use crate::{derive_ackdf_key, derive_pbkdf2_key};
13use std::io::{Read, Write};
14
15/// Decrypts an AES Crypt v0–v3 file streamed from `input` and writes the
16/// recovered plaintext to `output`.
17///
18/// `decrypt` parses the header (auto-detecting the format version), derives
19/// the setup key (PBKDF2-HMAC-SHA512 for v3, ACKDF-SHA-256 for v0/v1/v2),
20/// recovers the session IV/key, and runs the version-appropriate streaming
21/// CBC loop. Only the version dispatched at the top of the function is
22/// observable on output — the internal `StreamConfig` mapping is mediated by
23/// [`crate::decryption::decrypt_ciphertext_stream`].
24///
25/// # Format
26///
27/// - v0: legacy modulo padding, 32-byte trailing HMAC, ACKDF setup key.
28/// - v1/v2: scattered modulo + 32-byte HMAC, ACKDF setup key.
29/// - v3: PKCS#7 padding, 32-byte HMAC, PBKDF2-HMAC-SHA512 setup key, version
30/// byte folded into the session HMAC.
31///
32/// See [`crate::decryption`] for the full per-stage compatibility matrix.
33///
34/// # Errors
35///
36/// - [`AescryptError::Io`] — `input.read` or `output.write_all` returned an
37/// I/O error.
38/// - [`AescryptError::Header`] — invalid magic, non-zero v1–v3 reserved byte,
39/// too many extensions, malformed iteration count, session-HMAC mismatch
40/// (`"session data corrupted or tampered"`), payload-HMAC mismatch
41/// (`"HMAC verification failed"`), or invalid v3 PKCS#7 padding.
42/// - [`AescryptError::UnsupportedVersion`] — version byte is `> 3`.
43/// - [`AescryptError::Crypto`] — KDF failure (PBKDF2 or ACKDF, including
44/// non-UTF-8 password bytes for v0–v2).
45///
46/// # Panics
47///
48/// Never panics on valid or malformed input. The internal `unreachable!` is
49/// guarded by [`crate::decryption::read_file_version`], which clamps the
50/// version to `0..=3`.
51///
52/// # Security
53///
54/// **Decrypt-then-verify**. For payloads larger than roughly two AES blocks
55/// (≈32 bytes of ciphertext after the session block), decrypted data is
56/// written to `output` **incrementally** as blocks are processed. The payload
57/// HMAC and v3 PKCS#7 validation run only after the ciphertext stream has been
58/// read.
59///
60/// If this function returns an error — for example
61/// `"HMAC verification failed"` or a v3 padding error — `output` may already
62/// contain **partial, unauthenticated plaintext**. Callers **must** discard or
63/// overwrite `output` on error and must not treat its contents as secret or
64/// trustworthy.
65///
66/// ```no_run
67/// use aescrypt_rs::{decrypt, PasswordString};
68/// use std::io::Cursor;
69///
70/// # let reader = Cursor::new(vec![]);
71/// # let password = PasswordString::new("pw".to_string());
72/// let mut plaintext = Vec::new();
73/// if decrypt(reader, &mut plaintext, &password).is_err() {
74/// plaintext.clear(); // mandatory when using an accumulating buffer
75/// }
76/// ```
77///
78/// Other security properties:
79///
80/// - Setup, session, and intermediate keys live in [`secure-gate`] aliases and
81/// zeroize on drop. The [`PasswordString`] never appears in plain form
82/// outside scoped reveals.
83/// - HMAC and PKCS#7 padding are compared in constant time
84/// (`secure-gate`'s `ConstantTimeEq`).
85/// - Pre-authentication parsing is bounded: the header has fixed sizes,
86/// extensions are capped at 256 entries, and the iteration count is clamped
87/// to [`PBKDF2_MAX_ITER`](crate::constants::PBKDF2_MAX_ITER).
88///
89/// # Compatibility — empty password
90///
91/// Unlike [`crate::encrypt()`], this function does not reject an empty
92/// password. An empty password against a file encrypted with a non-empty
93/// password will fail at HMAC verification. This asymmetry is intentional:
94/// third-party AES Crypt tools may produce files encrypted with an empty
95/// password, and `decrypt` must be able to handle them.
96///
97/// # Thread Safety
98///
99/// `decrypt` is `Send` whenever its `R`/`W` are. There is no shared mutable
100/// state, so multiple threads may call `decrypt` concurrently on independent
101/// inputs/outputs.
102///
103/// # Examples
104///
105/// ```no_run
106/// use aescrypt_rs::{decrypt, PasswordString};
107/// use std::io::Cursor;
108///
109/// let password = PasswordString::new("secret".to_string());
110/// let ciphertext: &[u8] = b""; // contents of a .aes file
111///
112/// let mut plaintext = Vec::new();
113/// match decrypt(Cursor::new(ciphertext), &mut plaintext, &password) {
114/// Ok(()) => { /* plaintext is now authenticated */ }
115/// Err(_) => plaintext.clear(),
116/// }
117/// ```
118///
119/// Threaded usage:
120///
121/// ```no_run
122/// use aescrypt_rs::{decrypt, PasswordString};
123/// use std::io::Cursor;
124/// use std::thread;
125///
126/// let password = PasswordString::new("secret".to_string());
127/// let encrypted = b"encrypted data...";
128///
129/// let handle = thread::spawn(move || {
130/// let mut plaintext = Vec::new();
131/// decrypt(Cursor::new(encrypted), &mut plaintext, &password)
132/// });
133///
134/// let _result = handle.join().unwrap();
135/// ```
136///
137/// # See also
138///
139/// - [`crate::encrypt()`] — inverse operation.
140/// - [`crate::read_version`] — header-only version triage.
141///
142/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
143/// [`PasswordString`]: crate::PasswordString
144#[inline(always)]
145pub fn decrypt<R, W>(
146 mut input: R,
147 mut output: W,
148 password: &PasswordString,
149) -> Result<(), AescryptError>
150where
151 R: Read,
152 W: Write,
153{
154 let (file_version, reserved_modulo) = read_file_version(&mut input)?;
155 consume_all_extensions(&mut input, file_version)?;
156
157 let kdf_iterations = read_kdf_iterations(&mut input, file_version)?;
158
159 // Public IV — secure from the start
160 let public_iv: Iv16 = Iv16::from(read_exact_span(&mut input)?);
161
162 // Setup key — secure buffer from birth
163 let mut setup_key = Aes256Key32::new([0u8; 32]);
164
165 if file_version <= 2 {
166 derive_ackdf_key(password, &public_iv, &mut setup_key)?;
167 } else {
168 derive_pbkdf2_key(password, &public_iv, kdf_iterations, &mut setup_key)?;
169 }
170
171 // Session key/IV — secure buffers from birth
172 let mut session_iv = Iv16::new([0u8; 16]);
173 let mut session_key = Aes256Key32::new([0u8; 32]);
174
175 extract_session_data(
176 &mut input,
177 file_version,
178 &public_iv,
179 &setup_key,
180 &mut session_iv,
181 &mut session_key,
182 )?;
183
184 let stream_config = match file_version {
185 0 => StreamConfig::V0 { reserved_modulo },
186 1 => StreamConfig::V1,
187 2 => StreamConfig::V2,
188 3 => StreamConfig::V3,
189 _ => unreachable!("file_version validated in read_file_version"),
190 };
191
192 decrypt_ciphertext_stream(
193 &mut input,
194 &mut output,
195 &session_iv,
196 &session_key,
197 stream_config,
198 )?;
199
200 Ok(())
201}