Steganography hides that a message exists. AEAD (authenticated encryption with associated data) hides what it says — and cryptographically binds the envelope's metadata so a tamperer can't swap scheme, density, or body length without the recipient noticing. rsteg's default is XChaCha20-Poly1305 sealed with a key derived from the user's passphrase via Argon2id.
Overhead: 58 bytes per payload regardless of plaintext size.
The whole thing becomes the body of a
PayloadHeader-framed message with flags.encrypted = 1 and
crypto_fourcc = b"XCA1".
PBKDF2 is HMAC in a loop — CPU-bound. A modern GPU or ASIC can run tens of millions of PBKDF2-SHA256 candidates per second, because HMAC uses almost no memory. A 600 000-iteration PBKDF2 defence (OWASP 2023) collapses to minutes against a moderately-resourced attacker with a dictionary.
Memory-hard KDFs require the attacker to allocate large per-guess working memory that can't be amortized across guesses. scrypt was the first mainstream design; Argon2 (RFC 9106) is the PHC winner that superseded it.
Within Argon2: the -id variant is the OWASP default. Argon2d has side-channel leaks (memory access pattern depends on secrets — fine on a server, bad on a shared client). Argon2i resists side-channels but has weaker time-memory tradeoffs. Argon2id runs one i-pass then a d-pass, and gets the best of both for password hashing.
| parameter | value | note |
|---|---|---|
| m_cost | 65536 | 64 MiB memory per derivation — dominates cost |
| t_cost | 3 | three iterations (passes over memory) |
| p_cost | 1 | single-lane; we don't parallelize a user password |
| salt | 16 B OS entropy | distinct per-file; prevents rainbow tables |
| output | 32 B | XChaCha20 key |
At 64 MiB × 3 passes on a modern CPU, one KDF call takes ~300–500 ms. A brute-force attacker pays that per candidate — a 10⁶-word dictionary is days on one machine, weeks on a small cluster. That's the right ballpark for file-at-rest steganography.
AES-GCM is a fine AEAD. It's fast on CPUs with AES-NI, it's standardized, it's what TLS uses. But it has a nonce-reuse cliff: the same (key, nonce) pair with different plaintexts leaks the XOR of the plaintexts and destroys the MAC. GCM nonces are 96 bits; birthday-collision on random 96-bit values shows up around 2⁴⁸ messages — reachable for a busy server, not reachable for a steganography tool, but not robust to buggy callers.
XChaCha20 uses a 192-bit extended nonce. Birthday bound is 2⁹⁶ — practically unreachable. You can generate a fresh random nonce per seal from the OS RNG with no counter, no state, no collision worry. That's the right property for a library that might be called from a shell pipeline with no persistent storage.
XChaCha20 is also a stream cipher, so there's no block
padding and no padding-oracle class of bug. Poly1305 is a 128-bit
universal-hash MAC computed over ciphertext concatenated with AAD,
constant-time verified via subtle::ConstantTimeEq in the
RustCrypto implementation. All four properties — large nonce, no padding,
fast on commodity CPUs, audited implementation — make it the conservative
pick for this use case.
The associated data for Poly1305 is:
aad = outer_payload_header (32 B)
|| inner_crypto_header (42 B: kdf_id, kdf_version, salt, nonce)
This is the whole point. Consider the attacks AAD defends against:
scheme_fourcc
from BLSP (permuted) to BLSL (linear), hoping the
victim re-extracts in linear order and silently gets garbage that
statistically leaks something. With AAD binding: extracting with any
non-writer scheme fourcc fails Poly1305 verification → BadPassphrase.
body_len to a
smaller value to truncate the output. Tag fails.
open(ciphertext, passphrase, outer_header) runs the seal flow in
reverse: parse the 42-byte AEAD preamble, reject unknown
kdf_id/kdf_version, reconstruct AAD exactly, run
Argon2id with the parsed salt, decrypt-and-verify with XChaCha20-Poly1305.
Any failure — wrong passphrase, tampered ciphertext, tampered AAD,
truncated input, tampered nonce — returns the same error,
Error::BadPassphrase. No subdivision. The spec 06
invariant: the caller cannot distinguish "you typed the password wrong"
from "someone flipped a bit in the header". That distinction would leak a
side channel for the adversary to tell whether their tamper landed on a
signed region or not.
Steganographic concealment: the AEAD envelope still looks like a 58-byte preamble followed by N + 16 bytes. A strong steganalyser with access to the plaintext cover can still detect that something has been written to the LSB plane — no AEAD fixes that. It's the permuted walk plus reasonable density choice that handles concealment. AEAD handles confidentiality and tamper resistance given concealment.
Post-compromise: if the passphrase leaks, every past file encrypted with that passphrase is recoverable. rsteg does not implement forward secrecy — which would require per-file asymmetric crypto and is out of scope for a local-file tool.