Skip to main content

aescrypt_rs/decryption/
session.rs

1//! src/decryption/session.rs
2//! Session-block recovery for the AES Crypt v0–v3 read path.
3//!
4//! Every buffer that touches ciphertext, IV, or key material is wrapped in a
5//! [`secure-gate`] auto-zeroizing alias — including the ciphertext and HMAC
6//! tag read from disk — so no plaintext key bytes survive the call frame.
7//!
8//! [`secure-gate`]: https://github.com/Slurp9187/secure-gate
9
10use crate::aliases::{Aes256Key32, Block16, EncryptedSessionBlock48, Iv16, SessionHmacTag32};
11use crate::decryption::read_exact_span;
12use crate::{aliases::HmacSha256, error::AescryptError, utilities::xor_blocks};
13use aes::cipher::{BlockDecrypt, KeyInit};
14use aes::{Aes256Dec, Block as AesBlock};
15use hmac::Mac;
16use secure_gate::{ConstantTimeEq, RevealSecret, RevealSecretMut};
17use std::io::Read;
18
19/// Recovers the session IV and session key from the file header into the
20/// caller's pre-allocated [`secure-gate`] buffers.
21///
22/// The behavior depends on `file_version`:
23///
24/// - **v0**: the setup key *is* the session key; `session_iv_out` is set to
25///   `public_iv`, `session_key_out` to `setup_key`. No HMAC, no decryption.
26/// - **v1/v2**: reads a 48-byte AES-256-CBC encrypted session block plus a
27///   32-byte HMAC-SHA256 tag, verifies the tag with constant-time equality,
28///   then CBC-decrypts the block under `setup_key` chained off `public_iv`.
29/// - **v3**: same as v1/v2, but the version byte (`0x03`) is folded into the
30///   session HMAC after the encrypted block, matching the v3 spec.
31///
32/// # Errors
33///
34/// - [`AescryptError::Io`] — reader error while consuming the encrypted block
35///   or HMAC tag.
36/// - [`AescryptError::Header`] — session HMAC mismatch
37///   (`"session data corrupted or tampered (HMAC mismatch)"`).
38///
39/// # Panics
40///
41/// Never panics on valid input. The internal `expect` calls on `setup_key`
42/// (`"setup_key is always 32 bytes"`) and on `computed_hmac`
43/// (`"computed hmac is 32 bytes"`) are structural invariants of
44/// [`Aes256Key32`](crate::aliases::Aes256Key32) and HMAC-SHA256.
45///
46/// # Security
47///
48/// - HMAC verification uses [`secure-gate`]'s `ConstantTimeEq`.
49/// - Encrypted session block, HMAC tag, and CBC working buffers are all
50///   [`secure-gate`] aliases that zeroize on drop.
51/// - For `file_version == 0`, `session_key_out` is overwritten with a copy of
52///   `setup_key`; both buffers continue to zeroize independently.
53///
54/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
55#[inline(always)]
56pub fn extract_session_data<R>(
57    reader: &mut R,
58    file_version: u8,
59    public_iv: &Iv16,
60    setup_key: &Aes256Key32,
61    session_iv_out: &mut Iv16,
62    session_key_out: &mut Aes256Key32,
63) -> Result<(), AescryptError>
64where
65    R: Read,
66{
67    // v0: direct secure copy — no encryption, no HMAC
68    if file_version == 0 {
69        public_iv.with_secret(|iv| *session_iv_out = Iv16::from(*iv));
70        setup_key.with_secret(|key| *session_key_out = Aes256Key32::from(*key));
71        return Ok(());
72    }
73
74    // Read encrypted session block and HMAC tag — both wrapped for auto-zeroing
75    let encrypted_block: EncryptedSessionBlock48 = read_exact_span(reader)?;
76    let expected_hmac: SessionHmacTag32 = read_exact_span(reader)?;
77
78    // HMAC verification — exact same pattern as encryption side
79    let mut mac = setup_key.with_secret(|key| {
80        <HmacSha256 as hmac::Mac>::new_from_slice(key).expect("setup_key is always 32 bytes")
81    });
82
83    encrypted_block.with_secret(|block| mac.update(block));
84    if file_version >= 3 {
85        mac.update(&[file_version]); // v3 spec: version byte included in session HMAC
86    }
87
88    let computed_hmac = mac.finalize().into_bytes();
89    let computed_hmac_fixed =
90        SessionHmacTag32::try_from(computed_hmac.as_ref()).expect("computed hmac is 32 bytes");
91    let hmac_valid = computed_hmac_fixed.ct_eq(&expected_hmac);
92    if !hmac_valid {
93        return Err(AescryptError::Header(
94            "session data corrupted or tampered (HMAC mismatch)".into(),
95        ));
96    }
97
98    // Decrypt directly into secure output buffers
99    let cipher = setup_key.with_secret(|key| Aes256Dec::new(key.into()));
100
101    let mut previous_block: Block16 = public_iv.with_secret(|iv| Block16::new(*iv));
102
103    encrypted_block.with_secret(|encrypted| {
104        for (i, chunk) in encrypted.chunks_exact(16).enumerate() {
105            let chunk_array: [u8; 16] = chunk.try_into().expect("chunk is exactly 16 bytes");
106            let chunk_block = Block16::from(chunk_array);
107            chunk_block.with_secret(|cb| {
108                let mut block = AesBlock::from(*cb);
109                cipher.decrypt_block(&mut block);
110
111                let xor_pb = previous_block.with_secret(|pb| *pb);
112                match i {
113                    0 => session_iv_out
114                        .with_secret_mut(|siv| xor_blocks(block.as_ref(), &xor_pb, siv)),
115                    1 => session_key_out
116                        .with_secret_mut(|sk| xor_blocks(block.as_ref(), &xor_pb, &mut sk[0..16])),
117                    2 => session_key_out
118                        .with_secret_mut(|sk| xor_blocks(block.as_ref(), &xor_pb, &mut sk[16..32])),
119                    _ => return,
120                };
121
122                // Update previous ciphertext block for next iteration
123                previous_block = chunk_block.with_secret(|cb| Block16::new(*cb));
124            });
125        }
126    });
127
128    Ok(())
129}