aescrypt_rs/encryption/write.rs
1//! Low-level v3 header / trailer writers for custom encryption flows.
2//!
3//! Most callers should use [`crate::encrypt()`] which composes these helpers
4//! into a complete v3 file. The writers in this module are exposed for
5//! advanced use cases that need to interleave the AES Crypt header with their
6//! own framing — for example, embedding ciphertext inside another container.
7//!
8//! All writers in this module reject pre-v3 versions with
9//! [`AescryptError::UnsupportedVersion`]. This crate does not produce legacy
10//! formats; see the [crate-level Security Model](crate#security-model) for
11//! rationale.
12
13use crate::aliases::{HmacSha256, Iv16};
14use crate::constants::{PBKDF2_MAX_ITER, PBKDF2_MIN_ITER};
15use crate::error::AescryptError;
16use hmac::Mac;
17use secure_gate::RevealSecret;
18use std::io::Write;
19
20/// Writes `data` to `writer` as a single contiguous run.
21///
22/// Thin wrapper over [`Write::write_all`] that converts I/O failures into
23/// [`AescryptError::Io`]. Used internally by every other writer in this module
24/// and by [`crate::encryption::encrypt_session_block`] / payload streaming.
25///
26/// # Errors
27///
28/// - [`AescryptError::Io`] — `writer.write_all` returned an error.
29///
30/// # Panics
31///
32/// Never panics on its own. Panics in `writer` propagate normally.
33#[doc(hidden)]
34#[inline]
35pub fn write_octets<W: Write>(writer: &mut W, data: &[u8]) -> Result<(), AescryptError> {
36 writer.write_all(data).map_err(AescryptError::Io)
37}
38
39/// Writes the 5-byte AES Crypt v3 file header `b"AES" || version || 0x00`.
40///
41/// # Format
42///
43/// ```text
44/// 'A' 'E' 'S' version 0x00
45/// 0 1 2 3 4
46/// ```
47///
48/// # Errors
49///
50/// - [`AescryptError::UnsupportedVersion`] — `version < 3`. This crate only
51/// writes v3.
52/// - [`AescryptError::Io`] — `writer` returned an error.
53#[inline]
54pub fn write_header<W: Write>(writer: &mut W, version: u8) -> Result<(), AescryptError> {
55 if version < 3 {
56 return Err(AescryptError::UnsupportedVersion(version));
57 }
58 write_octets(writer, &[b'A', b'E', b'S', version, 0x00])
59}
60
61/// Writes the v3 extension-block section, terminated by a zero-length record.
62///
63/// If `extensions` is `Some(bytes)`, those bytes are written verbatim. If
64/// `None`, the canonical "no extensions" terminator `[0x00, 0x00]` is written.
65///
66/// # Format
67///
68/// Each extension is a `u16` big-endian length followed by `length` payload
69/// bytes; a length of `0` ends the section. When `extensions` is `Some`, the
70/// caller is responsible for emitting any payload extensions and the trailing
71/// `[0x00, 0x00]` terminator.
72///
73/// # Errors
74///
75/// - [`AescryptError::UnsupportedVersion`] — `version < 3`.
76/// - [`AescryptError::Io`] — `writer` returned an error.
77#[inline]
78pub fn write_extensions<W: Write>(
79 writer: &mut W,
80 version: u8,
81 extensions: Option<&[u8]>,
82) -> Result<(), AescryptError> {
83 if version < 3 {
84 return Err(AescryptError::UnsupportedVersion(version));
85 }
86 let data = extensions.unwrap_or(&[0x00, 0x00]);
87 write_octets(writer, data)
88}
89
90/// Writes the v3 PBKDF2 iteration count as 4 big-endian bytes.
91///
92/// # Format
93///
94/// `iterations.to_be_bytes()`, written immediately after the extensions block
95/// and immediately before the public IV.
96///
97/// # Errors
98///
99/// - [`AescryptError::UnsupportedVersion`] — `version < 3` (v0/v1/v2 do not
100/// carry an iteration count in the header).
101/// - [`AescryptError::Header`] — `iterations` is outside
102/// [`PBKDF2_MIN_ITER`](crate::constants::PBKDF2_MIN_ITER) `..=`
103/// [`PBKDF2_MAX_ITER`](crate::constants::PBKDF2_MAX_ITER).
104/// - [`AescryptError::Io`] — `writer` returned an error.
105///
106/// # Security
107///
108/// The iteration count gates PBKDF2 cost and is therefore the primary
109/// password-cracking-resistance knob. Use
110/// [`DEFAULT_PBKDF2_ITERATIONS`](crate::constants::DEFAULT_PBKDF2_ITERATIONS)
111/// for new files unless you have measured your platform.
112#[inline]
113pub fn write_iterations<W: Write>(
114 writer: &mut W,
115 iterations: u32,
116 version: u8,
117) -> Result<(), AescryptError> {
118 if version < 3 {
119 return Err(AescryptError::UnsupportedVersion(version));
120 }
121 if !(PBKDF2_MIN_ITER..=PBKDF2_MAX_ITER).contains(&iterations) {
122 return Err(AescryptError::Header("invalid KDF iterations".into()));
123 }
124 write_octets(writer, &iterations.to_be_bytes())
125}
126
127/// Writes the 16-byte public IV after revealing it from its [`secure-gate`]
128/// wrapper.
129///
130/// The public IV is the per-file salt fed to PBKDF2 (and the CBC IV for the
131/// session-block encryption). It is generated with the [`secure-gate`] CSPRNG
132/// inside [`crate::encrypt()`]; downstream callers writing custom flows must
133/// generate a fresh, unpredictable IV per file.
134///
135/// # Errors
136///
137/// - [`AescryptError::Io`] — `writer` returned an error.
138///
139/// # Security
140///
141/// The public IV is **not** secret; it is written in the clear and read back
142/// during decryption. It must be **unique and unpredictable** per file. Reusing
143/// a public IV with the same password yields the same setup key and breaks the
144/// uniqueness of the encrypted session block.
145///
146/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
147#[inline]
148pub fn write_public_iv<W: Write>(writer: &mut W, iv: &Iv16) -> Result<(), AescryptError> {
149 iv.with_secret(|i| write_octets(writer, i))
150}
151
152/// Finalizes `hmac` and writes the resulting 32-byte HMAC-SHA256 tag.
153///
154/// Consumes `hmac` (it is no longer reusable) and writes the 32-byte tag
155/// produced by [`Mac::finalize`]. Used to seal both the encrypted session block
156/// (after [`crate::encryption::encrypt_session_block`]) and, separately, the
157/// payload stream (inside [`crate::encryption::encrypt_stream`]).
158///
159/// # Errors
160///
161/// - [`AescryptError::Io`] — `writer` returned an error while writing the
162/// 32-byte tag.
163///
164/// # Security
165///
166/// HMAC-SHA256 with a 32-byte key derived from PBKDF2-HMAC-SHA512 (the "setup
167/// key" for the session block, the session key for the payload). Verification
168/// on the read side uses constant-time equality via [`secure-gate`]'s
169/// `ConstantTimeEq`.
170///
171/// [`secure-gate`]: https://github.com/Slurp9187/secure-gate
172#[inline]
173pub fn write_hmac<W: Write>(writer: &mut W, hmac: HmacSha256) -> Result<(), AescryptError> {
174 write_octets(writer, hmac.finalize().into_bytes().as_ref())
175}