# v0.2.0 — AEAD Foundation (ChaCha20-Poly1305)
**Release date:** 2026-05-21
**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
The first working encryption layer in `crypt-io`. Round-trip
authenticated encryption with ChaCha20-Poly1305 — the default AEAD —
shipped behind the simple `Crypt::encrypt` / `Crypt::decrypt` surface
the crate was designed around. Built on the RustCrypto
`chacha20poly1305` crate, with `mod-rand` Tier 3 (OS-backed CSPRNG)
producing every nonce.
If you wanted a one-line, hard-to-misuse AEAD in Rust and were tired
of wiring up nonce generation, `KeyInit`, and `Payload` by hand,
this is the release that gives you that.
---
## Highlights
- **`Crypt::encrypt(key, plaintext)`** — encrypt a byte slice under a
32-byte key. Returns `nonce || ciphertext || tag` as a single
`Vec<u8>`. Fresh nonce generated per call via the OS CSPRNG.
- **`Crypt::decrypt(key, ciphertext)`** — verify and decrypt the
buffer produced by `encrypt`. Authentication failure is a single
opaque error: wrong key, tampered ciphertext, tampered tag,
truncated tag, and AAD mismatches all surface as
`Error::AuthenticationFailed`. That is deliberate — the variant is
not subdivided because exposing the failure mode tells an attacker
how close they are to a forgery.
- **`encrypt_with_aad` / `decrypt_with_aad`** — variants that
authenticate additional data without encrypting it.
- **Algorithm-agile design.** `Crypt::new()` returns the
ChaCha20-Poly1305 default; `Crypt::with_algorithm(Algorithm)`
lets you pick. The `Algorithm` enum is `#[non_exhaustive]` so
AES-256-GCM can join in 0.3.0 without breaking matches.
- **RFC 8439 §2.8.2 known-answer test** verifies the underlying
primitive is wired in byte-for-byte correctly.
---
## API at a glance
```rust
use crypt_io::Crypt;
let crypt = Crypt::new(); // ChaCha20-Poly1305
let key = [0u8; 32]; // your 256-bit key
let ciphertext = crypt.encrypt(&key, b"attack at dawn")?;
let recovered = crypt.decrypt(&key, &ciphertext)?;
assert_eq!(&*recovered, b"attack at dawn");
// With associated data — authenticated, not encrypted.
let ct = crypt.encrypt_with_aad(&key, b"body", b"context")?;
let pt = crypt.decrypt_with_aad(&key, &ct, b"context")?;
# Ok::<(), crypt_io::Error>(())
```
### Wire layout
The buffer returned by `encrypt` / `encrypt_with_aad` is exactly:
```
+---------------+----------------------+--------------+
```
Total size: `plaintext.len() + 28` bytes. The nonce is generated
internally and stored at the front of the buffer so `decrypt` only
needs the key plus the buffer.
---
## What's NOT in 0.2.0
The roadmap groups these into later phases. Documented here so you
can plan around them:
- **AES-256-GCM** — arrives in 0.3.0 alongside hardware-acceleration
verification (AES-NI on x86, crypto extensions on ARM).
- **Hashing (BLAKE3 / SHA-2)** — phase 0.4.0.
- **MAC (HMAC, BLAKE3 keyed)** — phase 0.5.0.
- **KDF (HKDF, Argon2id)** — phase 0.6.0.
- **Stream / file encryption** — phase 0.7.0.
- **Benchmark suite + tuning** — phase 0.8.0.
- **Fuzz testing** — phase 0.9.0.
- **`Zeroizing<Vec<u8>>` return on `decrypt`** — currently returns
`Vec<u8>`. Wrap the result with `zeroize::Zeroizing::new(...)`
if you need zero-on-drop for the recovered plaintext, or layer
this crate on top of `key-vault` so plaintext never touches a
raw `Vec`.
---
## Security notes
- **Never reuse a nonce with the same key.** This API cannot —
every `encrypt` call draws a fresh 12-byte nonce from the OS
CSPRNG via `mod-rand::tier3::fill_bytes`. The 96-bit space has a
birthday bound of ~2⁴⁸, comfortably beyond any realistic message
volume per key.
- **No raw key bytes in errors.** `Error::InvalidKey` carries only
the expected vs. actual *lengths*. No variant of `Error` ever
contains plaintext, ciphertext, nonces, or key material.
- **Constant-time tag verification** is preserved by deferring to
the upstream `chacha20poly1305` crate — this wrapper performs no
equality comparisons on tag bytes itself.
- **`AuthenticationFailed` is opaque.** It does not distinguish
wrong-key from tampered-ciphertext from AAD-mismatch.
---
## Compatibility & build
- **MSRV bumped from 1.75 → 1.85.** Required by `edition = "2024"`
(Cargo ≥ 1.84 rejects the previous combination). CI matrix
updated.
- **Edition 2024.** No code change required on consumer side.
- **Default features:** `std`, `zeroize`, `aead-chacha20`,
`hash-blake3`, `mac-hmac`, `kdf-hkdf`. The defaults give you the
ChaCha20-Poly1305 path with zeroize support.
---
## Installation
```toml
[dependencies]
crypt-io = "0.2"
```
Or:
```bash
cargo add crypt-io
```
---
## Verification
| `cargo fmt --all -- --check` | clean |
| `cargo clippy --all-targets --all-features -- -D warnings` | clean |
| `cargo test --all-features` | 22 unit + 1 smoke + 3 doctest — all passing |
| RFC 8439 §2.8.2 known-answer | byte-exact match |
| MSRV (1.85) build | clean |
---
## Acknowledgements
`crypt-io` does not implement cryptographic primitives. The math in
0.2.0 comes from:
- [`chacha20poly1305`](https://crates.io/crates/chacha20poly1305) —
RustCrypto's ChaCha20-Poly1305 AEAD.
- [`chacha20`](https://crates.io/crates/chacha20) — the underlying
stream cipher.
- [`poly1305`](https://crates.io/crates/poly1305) — the underlying
MAC primitive.
Plus the portfolio:
- [`mod-rand`](https://crates.io/crates/mod-rand) — Tier 3 OS-backed
CSPRNG for nonce generation.
---
## What's next
Phase 0.3.0: AES-256-GCM and algorithm selection. Same `Crypt`
API, additional `Algorithm::Aes256Gcm` variant, NIST SP 800-38D
known-answer tests, hardware acceleration verified on x86 (AES-NI)
and ARM (crypto extensions).