crypt-io 0.8.0

AEAD encryption (ChaCha20-Poly1305, AES-256-GCM), hashing (BLAKE3, SHA-2), MAC (HMAC, BLAKE3 keyed), and KDF (HKDF, Argon2id) for Rust. Algorithm-agile. RustCrypto-backed primitives with REPS discipline. Simple API. Sub-microsecond throughput.
Documentation
# v0.7.0 — Streaming & File Encryption

**Release date:** 2026-05-22
**Status:** pre-1.0 milestone. The public API is allowed to evolve in
breaking ways through the `0.x` series; 1.0 freezes it.

---

## What this release is

Encryption for data that doesn't fit in memory. A new
`crypt_io::stream` module ships a chunked-AEAD frame format with
the [STREAM construction] — the same shape AGE uses — plus two
file-to-file convenience helpers. Same security guarantees as the
single-shot `Crypt` API, plus detection of truncation, reordering,
and chunk duplication.

[STREAM construction]: https://eprint.iacr.org/2015/189.pdf

This is also the phase that fills the previously-empty
[`examples/`](../../examples) directory with five runnable
end-to-end programs.

---

## Highlights

- **`StreamEncryptor` / `StreamDecryptor`** — symmetric streaming
  types. Both buffer internally; callers feed arbitrary-sized
  byte chunks via `update()` and call `finalize()` when the input
  is exhausted. No need to track chunk boundaries.
- **`stream::encrypt_file` / `stream::decrypt_file`** — the common
  "encrypt this file into that file" workflow. Reads
  the algorithm from the stream header on decrypt — caller
  doesn't need to know which algorithm was used to encrypt.
- **STREAM-construction nonces.** Per-chunk nonce is
  `prefix(7) || counter(4 BE) || last_flag(1)`. The counter
  defeats reordering and duplication; the last-flag byte defeats
  truncation. Header bytes are AAD on every chunk, so tampering
  with the algorithm byte or chunk-size byte fails verification
  on the first chunk.
- **Final-chunk-always invariant.** The encryptor always emits a
  final chunk (even if zero plaintext remains) and the final
  chunk is always strictly smaller than `chunk_size + 16` bytes.
  This makes EOF detection unambiguous: short read → final
  chunk; full read → expect more.
- **25 attack-surface integration tests.** Wrong key, body tamper,
  tag tamper, three flavours of truncation, swapped chunks,
  duplicated chunk, three flavours of header tamper, wrong key
  length, distinct nonce prefixes per stream, byte-by-byte feed
  on both sides, file round-trip for both algorithms.
- **5 runnable `examples/`**`aead_round_trip`, `password_hash`,
  `derive_subkeys`, `mac_authenticate`, `encrypt_file`. The
  `examples/` directory was empty before this phase; it now
  covers the major use cases end-to-end.

---

## API at a glance

In-memory round-trip:

```rust
use crypt_io::Algorithm;
use crypt_io::stream::{StreamEncryptor, StreamDecryptor};

let key = [0u8; 32];
let plaintext = b"the quick brown fox jumps over the lazy dog".repeat(1000);

// ---- Encrypt ----
let (mut enc, header) = StreamEncryptor::new(&key, Algorithm::ChaCha20Poly1305)?;
let mut wire = header.to_vec();
wire.extend(enc.update(&plaintext)?);
wire.extend(enc.finalize()?);

// ---- Decrypt ----
let mut dec = StreamDecryptor::new(&key, &wire[..24])?;
let mut recovered = dec.update(&wire[24..])?;
recovered.extend(dec.finalize()?);

assert_eq!(recovered, plaintext);
# Ok::<(), crypt_io::Error>(())
```

File round-trip:

```rust,no_run
use crypt_io::Algorithm;
use crypt_io::stream;

let key = [0u8; 32];
stream::encrypt_file("input.bin", "output.enc", &key, Algorithm::ChaCha20Poly1305)?;
stream::decrypt_file("output.enc", "decrypted.bin", &key)?;
# Ok::<(), crypt_io::Error>(())
```

Custom chunk size (default is 64 KiB; range is 1 KiB..16 MiB):

```rust
# use crypt_io::Algorithm;
# use crypt_io::stream::StreamEncryptor;
let key = [0u8; 32];
let (enc, _header) =
    StreamEncryptor::new_with_chunk_size(&key, Algorithm::ChaCha20Poly1305, 20)?; // 1 MiB chunks
assert_eq!(enc.chunk_size(), 1 << 20);
# Ok::<(), crypt_io::Error>(())
```

---

## Wire format

```
Header (24 bytes):
   [0..8]   magic = b"\x89CRYPTIO"
   [8]      version = 0x01
   [9]      algorithm (0x00 ChaCha20-Poly1305, 0x01 AES-256-GCM)
   [10]     chunk_size_log2 (default 16 = 64 KiB)
   [11..16] reserved (zero)
   [16..23] nonce_prefix (7 random bytes)
   [23]     reserved (zero)

Body:
   [chunk_0 (chunk_size + 16 B)]      ── non-final, last_flag = 0
   [chunk_1 (chunk_size + 16 B)]      ── non-final, last_flag = 0
   ...
   [chunk_N-1 (chunk_size + 16 B)]    ── non-final, last_flag = 0
   [chunk_N (< chunk_size + 16 B)]    ── final, last_flag = 1

Per-chunk nonce (12 bytes):
   [0..7]   nonce_prefix (from header)
   [7..11]  counter (u32 big-endian, starts at 0)
   [11]     last_flag (0x00 for non-final, 0x01 for final)
```

The header bytes are passed as **Additional Authenticated Data**
to every chunk's AEAD operation, so tampering with the algorithm
or chunk-size byte breaks verification on the first chunk.

---

## What's NOT in 0.7.0

- **Resumable streaming** (checkpoint encryptor state, resume
  after a process restart). Deferred post-1.0 — needs a
  serialisable state format and more design.
- **1 GiB stress test in `cargo test`** — would add ~30 s × OS
  matrix to every CI run. Deferred to Phase 0.9.0 (fuzz/soak)
  where it can run as an opt-in `#[ignore]`d test.
- **Async file helpers** — Phase 1.x.
- **Stream benchmarks** — Phase 0.8.0 alongside the rest of the
  criterion suite.

---

## Security notes

- **`AuthenticationFailed` is opaque.** Wrong key, tampered chunk
  body, tampered tag, tampered header (algorithm / chunk size /
  nonce prefix), truncation, reordering, duplication — all
  surface as the same single variant. The classification is
  intentionally not exposed.
- **Truncation detection is at the protocol level.** The
  `last_flag` byte in the per-chunk nonce means a chunk encrypted
  as non-final cannot be verified as final (and vice versa). Cut
  the final chunk off the stream and the decoder fails the
  next-to-last chunk's verification.
- **Reorder / duplication detection.** The 32-bit chunk counter
  is part of the nonce. Swap or duplicate any chunk and the
  counter mismatch breaks verification.
- **Header binding.** The 24-byte header is AAD for every chunk.
  Tampering with any field of the header that the AEAD will use
  later (algorithm, chunk size) shows up as authentication
  failure on the first chunk.
- **File-decrypt error → delete the output.** `decrypt_file`
  writes plaintext chunks to disk as they verify. If a later
  chunk fails to authenticate, earlier chunks have already been
  written. The function documentation is explicit: callers must
  delete the output file on error to avoid leaking plaintext.

---

## Compatibility & build

- **Default features extended.** `default` now includes `stream`
  alongside the AEAD / hash / MAC / KDF baselines.
- **`stream` feature now pulls both AEAD backends** (`aead-chacha20`
  + `aead-aes-gcm`). The streaming API offers a runtime algorithm
  choice; requiring callers to also flip `aead-aes-gcm`
  separately was a footgun.
- **MSRV** unchanged: Rust 1.85 (edition 2024).
- **No breaking changes to `Crypt` / `Algorithm` / `Error` /
  `Result` / `hash::*` / `mac::*` / `kdf::*`.** The 0.6.0 surface
  carries forward unchanged.

---

## Installation

```toml
[dependencies]
crypt-io = "0.7"
```

Or:

```bash
cargo add crypt-io
```

---

## Verification

| Check | Result |
|---|---|
| `cargo fmt --all -- --check` | clean |
| `cargo clippy --all-targets --all-features -- -D warnings` | clean |
| `cargo test --all-features` | 126 unit + 1 smoke + 25 stream-integration + 31 doctest — all passing |
| `cargo doc --no-deps --all-features` (with `RUSTDOCFLAGS="-D warnings"`) | clean |
| Round-trip across both algorithms / 6 plaintext shapes / 2 chunk sizes | all pass |
| 10 MiB round-trip | passes in < 1 s |
| Wrong-key / body-tamper / tag-tamper rejection | all reject |
| Truncation (3 flavours) rejection | all reject |
| Swapped chunks / duplicated chunk rejection | reject |
| Header-tamper (algorithm / nonce-prefix / magic) rejection | reject |
| File round-trip (ChaCha20 + AES-GCM) | passes |
| All 5 examples run | passes |
| MSRV (1.85) build | clean |

---

## Acknowledgements

`crypt-io` does not implement cryptographic primitives. The
streaming layer's per-chunk AEAD primitives are still the
[`chacha20poly1305`](https://crates.io/crates/chacha20poly1305)
and [`aes-gcm`](https://crates.io/crates/aes-gcm) crates from
RustCrypto, called with caller-supplied STREAM nonces. The
nonce-prefix randomness comes from
[`mod-rand`](https://crates.io/crates/mod-rand) Tier 3 (OS
CSPRNG).

The STREAM construction itself is from "Online
Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance"
by Hoang, Reyhanitabar, Rogaway, and Vizár (2015).

---

## What's next

Phase 0.8.0: Performance verification. The criterion benchmark
suite that backs every "<X µs / <Y ns" claim in the README and
ROADMAP with measured numbers on a known reference machine.
AEAD / hash / MAC / KDF / streaming throughput across both
algorithms and the typical input sizes, plus a comparison
against the upstream RustCrypto crates so any wrapping overhead
is visible.