Skip to main content

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}