aescrypt_rs/decryption/stream/versions.rs
1//! src/decryption/stream/versions.rs
2//! Version-aware streaming CBC decryption + final-block / HMAC trailer handling.
3
4use crate::aliases::HmacSha256;
5use crate::aliases::{Aes256Key32, Iv16, Trailer32};
6use crate::decryption::stream::context::DecryptionContext;
7use crate::decryption::stream::trailer::{
8 extract_hmac_scattered, extract_hmac_simple, write_final_modulo, write_final_pkcs7,
9};
10use crate::error::AescryptError;
11use aes::cipher::KeyInit;
12use aes::Aes256Dec;
13use hmac::Mac;
14use secure_gate::{ConstantTimeEq, RevealSecret};
15use std::io::{Read, Write};
16
17fn verify_payload_hmac(hmac: HmacSha256, expected: &Trailer32) -> Result<(), AescryptError> {
18 let computed = hmac.finalize().into_bytes();
19 let computed_fixed =
20 Trailer32::try_from(computed.as_ref()).expect("computed hmac is 32 bytes");
21 if !computed_fixed.ct_eq(expected) {
22 return Err(AescryptError::Header("HMAC verification failed".into()));
23 }
24 Ok(())
25}
26
27/// Per-version configuration for [`decrypt_ciphertext_stream`].
28///
29/// Selects the padding scheme and trailer layout to use after the streaming
30/// CBC loop has consumed the ciphertext. Construct this from the `(version,
31/// modulo_or_reserved)` tuple returned by
32/// [`crate::decryption::read_file_version`].
33///
34/// # Format
35///
36/// | Variant | Padding scheme | Trailer length | Trailer layout |
37/// | ------- | -------------- | :------------: | --------------------------------- |
38/// | [`V0`](Self::V0) | legacy modulo (low nibble of `reserved_modulo`) | 32 B | contiguous HMAC tag |
39/// | [`V1`](Self::V1) | legacy modulo (last buffered byte) | 33 B | modulo byte then HMAC tag |
40/// | [`V2`](Self::V2) | legacy modulo (last buffered byte) | 33 B | modulo byte then HMAC tag |
41/// | [`V3`](Self::V3) | PKCS#7 (1..=16) | 32 B | contiguous HMAC tag |
42///
43/// # Security
44///
45/// `V0`/`V1`/`V2` exist only for read compatibility; this crate never produces
46/// them. Use [`V3`](Self::V3) for any new file.
47#[derive(Clone, Copy)]
48pub enum StreamConfig {
49 /// AES Crypt v0 — legacy modulo padding, 32-byte contiguous HMAC trailer.
50 ///
51 /// `reserved_modulo` is the 5th header byte (the v0 modulo byte) and
52 /// determines the final-block length: `len = (reserved_modulo & 0x0F)`,
53 /// or `16` when that nibble is zero.
54 V0 {
55 /// 5th header byte; low nibble is the final-block byte count.
56 reserved_modulo: u8,
57 },
58 /// AES Crypt v1 — legacy modulo padding with the modulo byte embedded in
59 /// a 33-byte scattered trailer.
60 V1,
61 /// AES Crypt v2 — same trailer/padding shape as v1, plus header
62 /// extensions before the encrypted session block.
63 V2,
64 /// AES Crypt v3 — PKCS#7 padding and a 32-byte contiguous HMAC-SHA256
65 /// trailer. The only format this crate writes.
66 V3,
67}
68
69/// Streams ciphertext from `input_reader` through AES-256-CBC decryption,
70/// writes the recovered plaintext to `output_writer`, and verifies the
71/// version-appropriate HMAC trailer.
72///
73/// `decrypt_ciphertext_stream` is the per-block worker for [`crate::decrypt()`].
74/// It consumes the encrypted payload (everything after the encrypted session
75/// block on disk), decrypts each 16-byte CBC block into the
76/// [`crate::decryption`] ring buffer, and finally validates the trailer:
77///
78/// - [`StreamConfig::V0`] / [`StreamConfig::V3`]: 32-byte contiguous
79/// HMAC-SHA256 tag.
80/// - [`StreamConfig::V1`] / [`StreamConfig::V2`]: 33-byte trailer (modulo
81/// byte plus HMAC-SHA256 tag).
82///
83/// # Errors
84///
85/// - [`AescryptError::Io`] — reader or writer error during the streaming loop
86/// or trailer write.
87/// - [`AescryptError::Header`] — trailer length mismatch
88/// (`"v0: expected 32-byte HMAC trailer"`,
89/// `"v1/v2: expected 33-byte trailer"`,
90/// `"v3: expected 32-byte HMAC trailer"`),
91/// payload-HMAC mismatch (`"HMAC verification failed"`),
92/// or invalid v3 PKCS#7 padding (`"v3: invalid PKCS#7 padding"`).
93///
94/// # Panics
95///
96/// Never panics on valid input. The internal `expect("computed hmac is 32 bytes")`
97/// is a structural invariant of HMAC-SHA256.
98///
99/// # Security
100///
101/// - **Decrypt-then-verify**. Plaintext blocks are written to `output_writer`
102/// as they are produced. The HMAC tag is checked **after** the stream ends,
103/// so partial unauthenticated plaintext may already be on `output_writer`
104/// when this function returns an error. See [`crate::decrypt()`] for the
105/// caller contract.
106/// - HMAC and PKCS#7 padding comparisons use [`secure-gate`]'s
107/// `ConstantTimeEq`.
108/// - All session keys, IVs, ring-buffer slots, and trailers live in
109/// [`secure-gate`] aliases that zeroize on drop.
110///
111/// # Compatibility
112///
113/// - `V0`/`V1`/`V2` are read-only legacy-format support.
114/// - `V3` is bit-identical to ciphertext produced by [`crate::encrypt()`] and
115/// the official AES Crypt v3 reference implementation.
116///
117/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
118#[inline(always)]
119pub fn decrypt_ciphertext_stream<R, W>(
120 mut input_reader: R,
121 mut output_writer: W,
122 initial_vector: &Iv16,
123 encryption_key: &Aes256Key32,
124 config: StreamConfig,
125) -> Result<(), AescryptError>
126where
127 R: Read,
128 W: Write,
129{
130 let cipher = encryption_key.with_secret(|key| Aes256Dec::new(key.into()));
131
132 // This is the exact same construction used in encrypt_stream
133 let mut hmac = encryption_key.with_secret(|key| {
134 <HmacSha256 as Mac>::new_from_slice(key)
135 .expect("encryption_key is always 32 bytes — valid HMAC key")
136 });
137
138 let mut ctx = DecryptionContext::new_with_iv(initial_vector);
139 ctx.decrypt_cbc_loop(&mut input_reader, &mut output_writer, &cipher, &mut hmac)?;
140
141 ctx.advance_tail();
142 let remaining = ctx.remaining();
143
144 match config {
145 StreamConfig::V0 { reserved_modulo } => {
146 if remaining != 32 {
147 return Err(AescryptError::Header(
148 "v0: expected 32-byte HMAC trailer".into(),
149 ));
150 }
151
152 let expected_hmac = extract_hmac_simple(&ctx);
153 verify_payload_hmac(hmac, &expected_hmac)?;
154 write_final_modulo(&ctx, &mut output_writer, reserved_modulo)?;
155 }
156
157 StreamConfig::V1 | StreamConfig::V2 => {
158 if remaining != 33 {
159 return Err(AescryptError::Header(
160 "v1/v2: expected 33-byte trailer".into(),
161 ));
162 }
163
164 let (expected_hmac, modulo_byte) = extract_hmac_scattered(&ctx);
165 verify_payload_hmac(hmac, &expected_hmac)?;
166 write_final_modulo(&ctx, &mut output_writer, modulo_byte)?;
167 }
168
169 StreamConfig::V3 => {
170 if remaining != 32 {
171 return Err(AescryptError::Header(
172 "v3: expected 32-byte HMAC trailer".into(),
173 ));
174 }
175
176 let expected_hmac = extract_hmac_simple(&ctx);
177 verify_payload_hmac(hmac, &expected_hmac)?;
178 write_final_pkcs7(&ctx, &mut output_writer)?;
179 }
180 }
181
182 Ok(())
183}