Skip to main content

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}