aescrypt_rs/encryption/stream.rs
1// src/encryption/stream.rs
2
3//! v3 streaming AES-256-CBC payload encryption with HMAC-SHA256 trailer.
4
5use crate::aliases::HmacSha256;
6use crate::aliases::{Aes256Key32, Block16, Iv16};
7use crate::error::AescryptError;
8use crate::utilities::{read_until_full, xor_blocks};
9use aes::cipher::{BlockEncrypt, KeyInit};
10use aes::{Aes256Enc, Block as AesBlock};
11use hmac::Mac;
12use secure_gate::{RevealSecret, RevealSecretMut};
13use std::io::{Read, Write};
14
15/// Encrypts the payload stream of an AES Crypt v3 file with PKCS#7 padding and
16/// appends a 32-byte HMAC-SHA256 trailer.
17///
18/// `encrypt_stream` reads `source` until EOF, encrypts each 16-byte plaintext
19/// block in CBC mode using `session_key` chained off `session_iv`, writes the
20/// resulting ciphertext to `destination`, and finishes with a 32-byte
21/// HMAC-SHA256 tag computed over every ciphertext block. The final block is
22/// always padded with PKCS#7; even an empty or 16-aligned input emits one full
23/// pad block.
24///
25/// This is the streaming primitive called by [`crate::encrypt()`] after the
26/// header, public IV, encrypted session block, and session HMAC have already
27/// been written.
28///
29/// # Format
30///
31/// - Block cipher: AES-256 in CBC mode (`session_key`, `session_iv`).
32/// - Padding: PKCS#7 (1..=16 bytes), always present.
33/// - Authentication: HMAC-SHA256 keyed with `session_key` over the ciphertext;
34/// the tag is appended after the last ciphertext block.
35///
36/// # Errors
37///
38/// - [`AescryptError::Io`] — `source.read` or `destination.write_all` returned
39/// an error.
40///
41/// # Panics
42///
43/// Never panics on valid input. The internal `try_into().unwrap()` is over a
44/// slice that is always exactly 16 bytes by construction.
45///
46/// # Security
47///
48/// - `session_key` is consumed only inside scoped [`secure-gate`] reveals; it
49/// never escapes a `with_secret` closure.
50/// - `session_iv` **must** be unique per file. [`crate::encrypt()`] generates
51/// it via the [`secure-gate`] CSPRNG (`Iv16::from_random`).
52/// - PKCS#7 padding is always applied so the ciphertext length cannot leak the
53/// true plaintext length modulo 16.
54/// - HMAC verification on the read side uses constant-time equality.
55///
56/// # See also
57///
58/// - [`crate::encrypt()`] — high-level API that wraps this function.
59/// - [`crate::decryption::decrypt_ciphertext_stream`] — read-side counterpart.
60///
61/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
62#[inline(always)]
63pub fn encrypt_stream<R, W>(
64 mut source: R,
65 mut destination: W,
66 session_iv: &Iv16,
67 session_key: &Aes256Key32,
68) -> Result<(), AescryptError>
69where
70 R: Read,
71 W: Write,
72{
73 let cipher = session_key.with_secret(|sk| Aes256Enc::new(sk.into()));
74 let mut hmac = session_key.with_secret(|sk| {
75 <HmacSha256 as Mac>::new_from_slice(sk)
76 .expect("session_key is always 32 bytes — valid HMAC key")
77 });
78
79 // previous ciphertext block — secure from birth
80 let mut prev_block = session_iv.with_secret(|siv| Block16::new(*siv));
81
82 let mut plaintext_block = Block16::new([0u8; 16]);
83
84 loop {
85 // Read up to 16 bytes, accumulating partial `read()` results until the buffer is full
86 // or the source returns 0 (EOF). A single `read()` may return fewer than requested even
87 // when more data exist (sockets, pipes); treating that as EOF would silently truncate.
88 let n = plaintext_block
89 .with_secret_mut(|pb| read_until_full(&mut source, pb))
90 .map_err(AescryptError::Io)?;
91 let is_final = n < 16;
92
93 if is_final {
94 let pad = (16 - n) as u8;
95 plaintext_block.with_secret_mut(|pb| pb[n..].fill(pad));
96 }
97
98 // XOR with previous ciphertext
99 let mut xor_output = Block16::new([0u8; 16]);
100 plaintext_block.with_secret(|pb| {
101 prev_block
102 .with_secret(|pb_prev| xor_output.with_secret_mut(|xo| xor_blocks(pb, pb_prev, xo)))
103 });
104
105 // Encrypt
106 let mut aes_block = xor_output.with_secret(|xo| AesBlock::from(*xo));
107 cipher.encrypt_block(&mut aes_block);
108 let ct_slice = aes_block.as_ref(); // &[u8]
109
110 // HMAC + write ciphertext
111 hmac.update(ct_slice);
112 destination.write_all(ct_slice)?;
113
114 // Update previous block for next iteration
115 prev_block = Block16::new(ct_slice.try_into().unwrap());
116
117 if is_final {
118 break;
119 }
120 }
121
122 destination.write_all(hmac.finalize().into_bytes().as_ref())?;
123 Ok(())
124}