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}