# 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:

```
+---------------+----------------------+--------------+
| 12-byte nonce | ciphertext (N bytes) | 16-byte tag  |
+---------------+----------------------+--------------+
```

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

| Check | Result |
|---|---|
| `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).
