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.5.0 — MAC: HMAC-SHA256, HMAC-SHA512, BLAKE3 Keyed

**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

The authentication-tag surface that pairs with 0.4.0's hashing. A
new `crypt_io::mac` module ships three MACs behind a consistent
compute / verify / streaming triad — and verification is **always**
constant-time, by design.

If you ever wrote `if tag == expected_tag` against a secret value,
this module is the fix. The `*_verify` paths and the streaming
hashers' `verify` methods route through the upstream constant-time
comparators (`hmac::verify_slice`, `blake3::Hash::eq`), so the time
your code takes to reject a tampered tag does not depend on how
many leading bytes happened to match.

---

## Highlights

- **Three algorithms, one API shape.**
  - `mac::hmac_sha256` / `hmac_sha512` — universal interop (JWT,
    TLS PRF, AWS SigV4, anything spec'd to HMAC).
  - `mac::blake3_keyed` — fastest of the three on modern hardware,
    typically 4–10× faster than HMAC-SHA256 at the same security
    level.
- **Compute + verify, never `==`.** Every algorithm has a
  `*_verify` variant that takes `(key, data, expected_tag)` and
  returns `bool` (or `Result<bool>` for HMAC) via constant-time
  comparison. Module documentation explicitly forbids `tag ==
  expected` and points callers at these.
- **Streaming hashers**`HmacSha256` / `HmacSha512` / `Blake3Mac`
  with chainable `update` and consuming `finalize` / `verify`. For
  inputs that arrive in chunks or don't fit in memory.
- **Spec-pinned KATs.** RFC 4231 Test Cases 1 + 2 for both
  HMAC-SHA256 and HMAC-SHA512 (4 vectors). BLAKE3 keyed empty-input
  under the official 32-byte ASCII key, pinned as a byte-array
  constant.
- **Verify-rejection coverage.** Every algorithm has explicit
  tests for wrong-key, wrong-data, wrong-tag, truncated-tag, and
  (BLAKE3) oversized-tag — wrong-length tags surface as a `false`
  return, not a panic.

---

## API at a glance

One-shot (HMAC returns `Result` because the upstream trait
signature is fallible; in practice HMAC accepts any key length):

```rust
use crypt_io::mac;

let key = b"shared secret";
let data = b"message to authenticate";

let tag = mac::hmac_sha256(key, data)?;
assert!(mac::hmac_sha256_verify(key, data, &tag)?);
# Ok::<(), crypt_io::Error>(())
```

BLAKE3 keyed is infallible because the key is typed:

```rust
use crypt_io::mac;

let key = [0x42u8; 32];                        // 32-byte typed key
let tag = mac::blake3_keyed(&key, b"message"); // never fails

assert!(mac::blake3_keyed_verify(&key, b"message", &tag));
assert!(!mac::blake3_keyed_verify(&key, b"tampered", &tag));
```

Streaming:

```rust
use crypt_io::mac::Blake3Mac;

let key = [0x42u8; 32];
let mut m = Blake3Mac::new(&key);
m.update(b"first chunk ");
m.update(b"second chunk");
let tag = m.finalize();
assert_eq!(tag.len(), 32);
```

Streaming verify:

```rust
# #[cfg(feature = "mac-hmac")] {
use crypt_io::mac::{hmac_sha256, HmacSha256};

let key = b"k";
let tag = hmac_sha256(key, b"msg")?;

let mut m = HmacSha256::new(key)?;
m.update(b"msg");
assert!(m.verify(&tag));
# }
# Ok::<(), crypt_io::Error>(())
```

---

## Choosing a MAC

| You want… | Pick |
|---|---|
| JWT (HS256), TLS PRF, AWS request signing, anywhere a spec names HMAC-SHA256 | `mac::hmac_sha256` |
| 64-byte tag for spec compliance | `mac::hmac_sha512` |
| Maximum throughput, you control both sides of the wire | `mac::blake3_keyed` |
| Type-checked fixed-size key | `mac::blake3_keyed` (`&[u8; 32]`) |
| Variable-length key handled internally | `mac::hmac_*` (accepts any length) |

All three are safe at 256-bit symmetric strength.

---

## What's NOT in 0.5.0

- **`subtle` as a direct dep.** Constant-time comparison is provided
  by the upstream `hmac` and `blake3` crates, both of which
  document the property and route through `subtle` internally.
  We do not re-export `subtle` from this crate; if you want it,
  add it to your own `Cargo.toml`.
- **Empirical timing verification** (`dudect`-style). That belongs
  in the security-hardening phase. For 0.5.0 we trust the upstream
  documentation — `hmac::verify_slice` is constant-time,
  `blake3::Hash::eq` is constant-time, both are widely audited.
- **MAC benchmarks.** Deferred to Phase 0.8.0 alongside the AEAD
  and hash benchmark suites.

---

## Security notes

- **Hash-vs-MAC separation preserved.** `Blake3Hasher` (in `hash`)
  is key-free and does not expose `with_key`. The only way to
  produce a BLAKE3 keyed tag through this crate is `Blake3Mac` (in
  `mac`). This avoids the "I used a raw hash as a MAC" footgun.
- **Verify is the only authentication path.** Tag comparison via
  `==` is documented as a security mistake in the module overview;
  the `*_verify` and streaming `verify` methods exist precisely so
  callers don't write that code.
- **Tag-length variation is a rejection, not a panic.** Wrong-length
  `expected_tag` arguments return `false` (or `Ok(false)`), not a
  panic on a length-mismatched compare.
- **No raw key bytes in errors.** `Error::Mac` carries only a
  `&'static str` reason — never key material, data, or tag bytes.
- **HMAC `Result` is a contract artifact, not a runtime concern.**
  HMAC accepts any key length in practice (long keys are hashed
  down internally per RFC 2104). The wrapper returns `Result`
  because the upstream `KeyInit::new_from_slice` is fallible by
  trait signature, but the `Err` arm is unreachable for HMAC.

---

## Compatibility & build

- **Default features extended.** `default` now includes `mac-blake3`
  in addition to `mac-hmac`. A fresh `cargo add crypt-io` ships
  with all three MACs available.
- **MSRV** unchanged: Rust 1.85 (edition 2024).
- **New `Error::Mac` variant.** `Error` is `#[non_exhaustive]`, so
  match sites with a wildcard arm compile unchanged. The variant
  is unreachable in practice.

---

## Installation

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

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` | 95 unit + 1 smoke + 21 doctest — all passing |
| `cargo doc --no-deps --all-features` (with `RUSTDOCFLAGS="-D warnings"`) | clean |
| HMAC-SHA256 RFC 4231 cases 1 + 2 | byte-exact match |
| HMAC-SHA512 RFC 4231 cases 1 + 2 | byte-exact match |
| BLAKE3 keyed empty-input KAT | byte-exact match |
| Verify-rejection (wrong-key / wrong-data / wrong-tag / wrong-length) | all reject across all three MACs |
| Streaming-equals-one-shot | verified per algorithm at multiple chunk boundaries |
| MSRV (1.85) build | clean |

---

## Acknowledgements

`crypt-io` does not implement cryptographic primitives. The math
new in 0.5.0 comes from:

- [`hmac`]https://crates.io/crates/hmac — RustCrypto's generic
  HMAC implementation, including the constant-time `verify_slice`.
- [`sha2`]https://crates.io/crates/sha2 — the SHA-256 / SHA-512
  primitives that drive HMAC.
- [`blake3`]https://crates.io/crates/blake3 — official BLAKE3
  implementation, including keyed mode and constant-time
  `Hash::eq`.

Carried forward: `chacha20poly1305`, `aes-gcm`, `mod-rand`.

---

## What's next

Phase 0.6.0: KDF. HKDF-SHA256, HKDF-SHA512 for deriving keys from
a master, plus Argon2id for password-derived keys — both with
RFC 5869 / known-answer tests and intentionally-slow parameter
defaults for Argon2id.