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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# Changelog

All notable changes to `crypt-io` will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [Unreleased]

### Added

### Changed

### Fixed

### Security

---

## [0.8.0] - 2026-05-22

### Added

- **Five criterion benchmark suites** under `benches/`  `aead.rs`, `hash.rs`, `mac.rs`, `kdf.rs`, `stream.rs`. Each
  exercises every shipped algorithm at the canonical input sizes
  (64 B / 1 KiB / 64 KiB / 1 MiB for byte-stream ops, 32/64/128 B
  for HKDF output length, OWASP-default + fast-params for
  Argon2id). All five are wired as `[[bench]]` entries with
  `harness = false` and run via `cargo bench --bench <name>`.
- **`docs/PERFORMANCE.md`** — methodology + reference-machine
  specs (AMD Ryzen 9 9950X3D, AES-NI + SHA-NI + AVX-512, WSL2
  Ubuntu, Rust 1.85.0) + measured throughput tables for every
  operation + a contract-check matrix comparing measured numbers
  to the 1.0 performance targets + a "choosing parameters for
  your hardware" guide.
- **Wrapping-overhead analysis** in PERFORMANCE.md — comparison
  of our measured numbers against upstream RustCrypto's published
  benches for each primitive. Most operations are within
  measurement noise of upstream; the per-call `Vec` allocation
  in our encrypt path is the only material overhead and is
  documented for a post-1.0 zero-allocation variant.

### Changed

- **Replaced placeholder `benches/crypt_bench.rs`** with the five
  real bench files. The `[[bench]] name = "crypt_bench"` entry in
  `Cargo.toml` is gone; replaced with five entries
  (`aead`, `hash`, `mac`, `kdf`, `stream`).
- **BLAKE3 1 KiB performance target revised.** The < 500 ns
  target set at scaffold time was over-optimistic — BLAKE3
  small-input cost is dominated by per-call setup overhead
  before its tree-parallel SIMD path engages. Measured: 1.07 µs
  at 1 KiB on Zen 5. BLAKE3 dominates at ≥ 4 KiB (11+ GiB/s at
  64 KiB). PERFORMANCE.md documents the actual shape; the
  contract will be re-stated for 1.0.
- **Argon2id OWASP-defaults cost note.** Measured at ~9 ms per
  hash on this Zen 5 chip — ~11× faster than the "100 ms on a
  modern CPU" design intent. PERFORMANCE.md flags this with a
  warning and points callers at `argon2_hash_with_params` for
  raising `t_cost` / `m_cost` on fast hardware to maintain the
  brute-force-resistance budget.
- **Stream encrypt 1 GiB/s target** measured marginal at 1 MiB
  plaintext (932 MiB/s ChaCha20, 999 MiB/s AES). Within
  measurement noise of the 1 GiB/s target; well over for
  decrypt (1.19-1.30 GiB/s). PERFORMANCE.md documents the
  allocation pressure that's the bottleneck and flags
  zero-allocation streaming as post-1.0 work.

### Security

- **No security-surface changes.** The bench suite exercises the
  same public API as the integration tests; it does not weaken
  any verification path or expose any new surface.

[0.8.0]: https://github.com/jamesgober/crypt-io/compare/v0.7.0...v0.8.0

---

## [0.7.0] - 2026-05-22

### Added

- **`crypt_io::stream` module** — chunked AEAD with a
  [STREAM-construction] frame format for encrypting data that
  doesn't fit in memory.
  - `StreamEncryptor` — buffers plaintext, emits encrypted chunks
    of `chunk_size + 16` bytes each. `new()` + `update()` +
    `finalize()` triad; `new_with_chunk_size()` for tuning chunk
    size (10..=24 log2).
  - `StreamDecryptor` — symmetric inverse. Parses the header,
    buffers ciphertext, emits decrypted plaintext as chunks
    complete.
  - `stream::encrypt_file` / `stream::decrypt_file` — file-to-file
    helpers using `BufReader` / `BufWriter` and the streaming
    types. Available under `std`.
  - **Frame format** documented in [`stream::frame`]:
    24-byte header (magic + version + algorithm + chunk_size_log2 +
    nonce_prefix) + N-1 non-final chunks + 1 final chunk strictly
    smaller than `chunk_size + 16` bytes. STREAM-construction
    per-chunk nonces (`prefix || counter_u32_be || last_flag`)
    defeat truncation, reordering, and duplication; header bytes
    are AAD for every chunk, so header tampering surfaces as
    authentication failure on the first chunk.
- **Public constants** in `crypt_io::stream`: `HEADER_LEN = 24`,
  `TAG_LEN = 16`, `DEFAULT_CHUNK_SIZE_LOG2 = 16` (64 KiB),
  `MIN_CHUNK_SIZE_LOG2 = 10`, `MAX_CHUNK_SIZE_LOG2 = 24`.
- **Integration test suite** `tests/stream.rs` — 25 tests
  covering:
  - Round-trip across both algorithms, multiple chunk sizes,
    empty / 1-byte / exact-chunk / chunk+1 / many-chunk / 10 MiB
    inputs, byte-by-byte feeding on both sides.
  - Attack surface: wrong key, tampered chunk body, tampered tag,
    truncation (to zero / mid-tag / dropped final chunk), swapped
    chunks, duplicated chunk, tampered algorithm byte, tampered
    nonce prefix, tampered magic, wrong key length, distinct
    nonce prefixes per stream.
  - File round-trip for both algorithms.
- **`examples/` directory populated** — 5 runnable examples
  covering the main use cases:
  - `aead_round_trip.rs``Crypt::encrypt` / `decrypt`, both
    algorithms, with-AAD variant.
  - `password_hash.rs` — Argon2id hash + verify, custom params.
  - `derive_subkeys.rs` — HKDF for splitting one master into many
    purpose-specific subkeys with domain separation.
  - `mac_authenticate.rs` — HMAC-SHA256 + verify, BLAKE3 keyed,
    streaming MAC.
  - `encrypt_file.rs``stream::encrypt_file` /
    `stream::decrypt_file` round-trip plus a tamper-detection demo.

### Changed

- **Default features extended.** `default` now includes `stream`
  alongside the AEAD / hash / MAC / KDF baselines. A fresh
  `cargo add crypt-io` ships with the streaming surface
  available.
- **`stream` feature dependencies broadened** from `aead-chacha20`
  to `aead-chacha20 + aead-aes-gcm`, so the streaming types can
  switch algorithms at runtime without an extra feature flag.
- **`lib.rs` module wiring.** The `stream` module is exposed when
  the `stream` feature is enabled.

### Security

- **Truncation, reordering, and duplication detection.** The
  STREAM construction's per-chunk nonces include both a counter
  and a `last_flag` byte. Any of these attacks produces a nonce
  mismatch on the affected chunk → `AuthenticationFailed`.
- **Header binding.** Every encrypted chunk uses the 24-byte
  header as AAD, so tampering with the algorithm byte, chunk
  size, or nonce prefix shows up as authentication failure on
  the first chunk.
- **Final-chunk-always invariant.** The encryptor always emits a
  final chunk (even if it carries zero plaintext bytes), so the
  decryptor can detect end-of-stream unambiguously by length —
  a stream that ends mid-chunk or after a non-final chunk fails
  to verify.
- **Opaque `AuthenticationFailed`.** Wrong key, tampered chunk,
  tampered tag, header tampering, truncation, reordering, and
  duplication all surface as the same single variant. The
  classification is intentionally not exposed.
- **File-decrypt failure cleanup.** `decrypt_file` documents that
  callers must delete the partially-written output file on error
  — earlier chunks may have been written to disk before a later
  chunk failed to verify. The documentation is explicit because
  this is a footgun in every chunked-AEAD design.

[STREAM-construction]: https://eprint.iacr.org/2015/189.pdf
[`stream::frame`]: crate::stream::frame
[0.7.0]: https://github.com/jamesgober/crypt-io/compare/v0.6.0...v0.7.0

---

## [0.6.0] - 2026-05-22

### Added

- **`crypt_io::kdf` module** — two algorithms for deriving keys,
  each addressing a different threat model:
  - **HKDF** ([RFC 5869]):
    - `kdf::hkdf_sha256(ikm, salt, info, len) -> Result<Vec<u8>>`      extract-then-expand HKDF with SHA-256 underneath. Accepts
      an optional `salt`, an `info` context string, and an output
      length up to `255 * 32 = 8160` bytes.
    - `kdf::hkdf_sha512(...)` — same shape, SHA-512 digest, output
      up to `255 * 64 = 16320` bytes.
    - Output-length bounds enforced and surfaced as
      [`Error::Kdf`] when exceeded.
    - Feature: `kdf-hkdf` (default on).
  - **Argon2id** ([RFC 9106]):
    - `kdf::argon2_hash(password) -> Result<String>` — hashes with
      the OWASP-recommended parameter set (~100 ms on a modern
      CPU). Salt is generated fresh via `mod_rand::tier3::fill_bytes`
      and embedded in the returned PHC string. No salt management
      required from callers.
    - `kdf::argon2_hash_with_params(password, params)` — same but
      with caller-supplied [`Argon2Params`].
    - `kdf::argon2_verify(phc, password) -> Result<bool>` —
      constant-time verification against a PHC-encoded hash string.
    - Feature: `kdf-argon2` (default on in 0.6.0+).
- **`Argon2Params`** struct exposing `m_cost` (memory in KiB),
  `t_cost` (iterations), `p_cost` (lanes), `output_len`. `Default`
  matches the OWASP recommendations (19 MiB / 2 / 1 / 32 bytes).
- **Public constants** in `crypt_io::kdf`:
  `HKDF_MAX_OUTPUT_SHA256 = 8160`, `HKDF_MAX_OUTPUT_SHA512 = 16320`,
  `ARGON2_DEFAULT_OUTPUT_LEN = 32`, `ARGON2_DEFAULT_SALT_LEN = 16`
  (each feature-gated).
- **`Error::Kdf(&'static str)`** variant for KDF-specific failures
  (HKDF output-length overflow, Argon2 parameter validation, PHC
  parse failures).
- **RFC 5869 known-answer tests** — Test Case 1 (full HKDF-SHA256
  with salt + info) and Test Case 3 (no salt, no info). Both pinned
  as byte arrays.
- **HKDF-SHA512 wrapper round-trip** — RFC 5869 only ships SHA-256
  / SHA-1 vectors, so for SHA-512 we cross-check the wrapper output
  against a direct call into the upstream `hkdf` crate. Catches any
  wrapper-level mistake without committing to a specific vector
  we'd have to maintain.
- **Argon2id functional tests** (with reduced parameters so the
  suite stays fast): round-trip hash/verify, wrong-password
  rejection, two-hashes-of-same-password-differ (salt randomness
  proof), unparseable-PHC rejection, tampered-PHC rejection,
  empty-password edge case, 1 KiB-password edge case, custom
  params honoured in the PHC string, default params match OWASP,
  invalid-params rejected, redaction-clean error rendering
  (passwords never appear in `Error` Display / Debug).

### Changed

- **Default features extended.** `default` now includes
  `kdf-argon2` in addition to `kdf-hkdf`. A fresh `cargo add
  crypt-io` ships with the full symmetric-crypto + KDF surface.
- **`lib.rs` module wiring.** The `kdf` module is exposed when
  either `kdf-hkdf` or `kdf-argon2` is enabled.

### Security

- **HKDF is not for passwords.** Module overview documents that
  HKDF expects high-entropy input keying material (a master key, a
  DH shared secret, a token). Feeding it a password is a security
  mistake — the module points callers at Argon2id for that case.
- **Argon2id defaults follow OWASP.** 19 MiB memory, 2 iterations,
  1 lane, 32-byte output — sized for ~100 ms per hash on a modern
  CPU. Reducing any parameter reduces resistance to brute force.
- **Salt is generated, not provided.** `argon2_hash` calls
  `mod_rand::tier3::fill_bytes` for every hash, so each PHC string
  carries a fresh random salt. Salt reuse cannot happen through
  the public API.
- **No password bytes in errors.** Verified by an explicit test
  that round-trips a known password through the unparseable-PHC
  failure path and asserts neither `Display` nor `Debug` rendering
  of the resulting `Error` contains the password.
- **PHC-string parse failures surface as `Error::Kdf`.** A
  correctly-formatted but wrong-password hash returns
  `Ok(false)`; only malformed inputs produce an error. The
  distinction matters because applications should log parse
  failures differently from authentication failures.

[RFC 5869]: https://datatracker.ietf.org/doc/html/rfc5869
[RFC 9106]: https://datatracker.ietf.org/doc/html/rfc9106
[`Error::Kdf`]: crate::Error
[0.6.0]: https://github.com/jamesgober/crypt-io/compare/v0.5.0...v0.6.0

---

## [0.5.0] - 2026-05-22

### Added

- **`crypt_io::mac` module** — three message-authentication-code
  algorithms with a consistent compute / verify / streaming
  surface:
  - **HMAC-SHA256** ([RFC 2104] + [RFC 4231] test vectors):
    - `mac::hmac_sha256(key, data) -> Result<[u8; 32]>` — one-shot.
    - `mac::hmac_sha256_verify(key, data, expected_tag) -> Result<bool>`      constant-time tag comparison via the upstream `hmac` crate's
      `verify_slice`.
    - `HmacSha256` streaming hasher (`new``update``finalize`
      or `verify`).
    - Feature: `mac-hmac` (default on).
  - **HMAC-SHA512**:
    - `mac::hmac_sha512(key, data) -> Result<[u8; 64]>` + matching
      `mac::hmac_sha512_verify(...)`.
    - `HmacSha512` streaming hasher.
    - Feature: `mac-hmac`.
  - **BLAKE3 keyed mode**:
    - `mac::blake3_keyed(key: &[u8; 32], data) -> [u8; 32]`      infallible (typed key, no runtime length check).
    - `mac::blake3_keyed_verify(...)` — constant-time tag comparison
      via BLAKE3's `Hash::eq` (the upstream crate documents this as
      constant time).
    - `Blake3Mac` streaming MAC.
    - Feature: `mac-blake3` (default on in 0.5.0+).
- **Output-length and key-length constants** in `crypt_io::mac`:
  `HMAC_SHA256_OUTPUT_LEN = 32`, `HMAC_SHA512_OUTPUT_LEN = 64`,
  `BLAKE3_MAC_OUTPUT_LEN = 32`, `BLAKE3_MAC_KEY_LEN = 32` (each
  feature-gated).
- **`Error::Mac(&'static str)`** variant for MAC construction
  failures. Unreachable in practice (HMAC accepts any key length;
  BLAKE3 keyed takes a typed `[u8; 32]`), but the variant exists
  because the upstream `Mac` trait surface is fallible by
  signature.
- **RFC 4231 known-answer tests**:
  - HMAC-SHA256 Test Case 1 (20-byte `0x0b` key, `"Hi There"`).
  - HMAC-SHA256 Test Case 2 (4-byte `"Jefe"` key, `"what do ya want..."`).
  - HMAC-SHA512 Test Case 1 and Test Case 2 (same key/data inputs).
- **BLAKE3 keyed KAT** — empty-input tag under the official 32-byte
  ASCII key `"whats the Elvish word for friend"`, pinned as a
  byte-array constant.
- **Verify-rejection tests** for every algorithm: wrong-tag,
  wrong-key, wrong-data, truncated-tag, oversized-tag (BLAKE3).
- **Streaming-equivalence tests** for every algorithm at multiple
  chunk boundaries.
- **Streaming-verify tests** for every algorithm (constant-time
  accept on match, reject on tamper).
- **Doctests** for every public entry point in the new module.

### Changed

- **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. Drop `mac-blake3` if you want HMAC-only.
- **`lib.rs` module wiring.** The `mac` module is exposed when
  either `mac-hmac` or `mac-blake3` is enabled.

### Security

- **Constant-time verification is the only verification path.** The
  `*_verify` free functions and the streaming hashers' `verify`
  methods all use upstream constant-time comparators. The module
  documentation explicitly forbids `tag == expected` and points
  callers at the `verify` paths.
- **Hash-vs-MAC separation preserved.** Keyed-hash semantics live in
  this module; the `hash` module remains key-free. The `Blake3Hasher`
  in `hash` does **not** expose `with_key``Blake3Mac` in `mac`
  is the only way to produce a BLAKE3 keyed tag through this crate.
- **No raw key bytes in errors.** `Error::Mac` carries only a
  `&'static str` reason — never key material.
- **Tag-length variation is a rejection, not a panic.** All
  `*_verify` functions return `false` when `expected_tag` is the
  wrong length, rather than panicking on a length-mismatched compare.

[RFC 2104]: https://datatracker.ietf.org/doc/html/rfc2104
[RFC 4231]: https://datatracker.ietf.org/doc/html/rfc4231
[0.5.0]: https://github.com/jamesgober/crypt-io/compare/v0.4.0...v0.5.0

---

## [0.4.0] - 2026-05-22

### Added

- **`crypt_io::hash` module** — three cryptographic hash functions
  exposed through a consistent free-function API plus matching
  streaming hashers:
  - **BLAKE3** ([`blake3::hash`]https://github.com/BLAKE3-team/BLAKE3):
    - `hash::blake3(data) -> [u8; 32]` — one-shot, 32-byte digest.
    - `hash::blake3_long(data, len) -> Vec<u8>` — one-shot, any
      output length via the extendable-output (XOF) mode.
    - `Blake3Hasher` — streaming, with `update` / `finalize` /
      `finalize_xof`.
    - Feature: `hash-blake3` (default on).
  - **SHA-256** (NIST FIPS 180-4):
    - `hash::sha256(data) -> [u8; 32]`.
    - `Sha256Hasher` — streaming, with `update` / `finalize`.
    - Feature: `hash-sha2` (default on).
  - **SHA-512** (NIST FIPS 180-4):
    - `hash::sha512(data) -> [u8; 64]`.
    - `Sha512Hasher` — streaming, with `update` / `finalize`.
    - Feature: `hash-sha2` (default on).
- **Output-length constants** in `crypt_io::hash`:
  `BLAKE3_OUTPUT_LEN = 32`, `SHA256_OUTPUT_LEN = 32`,
  `SHA512_OUTPUT_LEN = 64` (each feature-gated).
- **Known-answer tests** verifying byte-exact output against the
  spec references:
  - SHA-256: FIPS 180-4 B.1 (`abc`), B.2 (the 56-byte two-block
    input), empty-input.
  - SHA-512: FIPS 180-4 C.1 (`abc`), C.2 (the 112-byte two-block
    input), empty-input.
  - BLAKE3: empty-input + `"IETF"` against the upstream crate's
    output. Both pinned as byte-array constants so any future
    wrapper-level mistake (wrong endianness, wrong slicing) is
    caught immediately.
- **Streaming-equivalence tests** for every algorithm: feeding the
  same data in three different chunk boundaries to the streaming
  hasher produces a bit-identical digest to the one-shot path.
- **BLAKE3 XOF tests** verifying:
  - Output length always matches the requested `len`.
  - Output is deterministic in the input.
  - The first 32 bytes of an extended-output digest equal the
    default 32-byte digest of the same input.
- **Doctests** for the module overview, all six entry points, and
  both streaming-hasher constructors.

### Changed

- **Default features extended.** `default` now includes `hash-sha2`
  in addition to `hash-blake3`, so a fresh `cargo add crypt-io`
  ships with all three hash functions available. Drop
  `hash-sha2` if you want BLAKE3-only.
- **`lib.rs` module wiring.** The `hash` module is exposed when
  either `hash-blake3` or `hash-sha2` is enabled (or both).
- **`aead` module gate** widened to fire when either AEAD feature
  is enabled (was: only `aead-chacha20`); makes the module reachable
  in AES-only configurations.

### Security

- **No key/MAC surface.** This module is hash-only. Keyed BLAKE3 and
  HMAC-SHA2 live in the upcoming `crypt_io::mac` module (Phase
  0.5.0) where the authentication-tag semantics get their own,
  separate API. Using a raw hash function as a MAC is a security
  mistake; the absence of `with_key` on `Blake3Hasher` /
  `Sha256Hasher` / `Sha512Hasher` is deliberate.

[0.4.0]: https://github.com/jamesgober/crypt-io/compare/v0.3.0...v0.4.0

---

## [0.3.0] - 2026-05-21

### Added

- **`Algorithm::Aes256Gcm` variant** — AES-256-GCM ([NIST SP 800-38D])
  joins ChaCha20-Poly1305 as a peer in the `Algorithm` enum. Same
  32-byte key, same 12-byte nonce, same 16-byte tag, same wire
  layout (`nonce || ciphertext || tag`) — only the primitive
  changes. The enum is still `#[non_exhaustive]`.
- **`Crypt::aes_256_gcm()`** — feature-gated convenience constructor.
  Equivalent to `Crypt::with_algorithm(Algorithm::Aes256Gcm)`; the
  separate constructor makes call sites read like deliberate
  choices, which they should be.
- **AES-256-GCM dispatch path** in `Crypt::encrypt_with_aad` /
  `decrypt_with_aad`. When the `aead-aes-gcm` feature is enabled,
  selecting `Algorithm::Aes256Gcm` routes through the new
  `aes_gcm` backend module; when the feature is disabled, an
  `Error::AlgorithmNotEnabled("aead-aes-gcm")` is returned.
- **NIST GCM Test Cases 14 + 15 known-answer tests** verifying the
  upstream `aes-gcm` primitive produces the spec-mandated ciphertext
  and tag bytes for known inputs. Mirrors the RFC 8439 KAT shipped
  for ChaCha20-Poly1305 in 0.2.0.
- **AES-256-GCM end-to-end tests** through the `Crypt` surface:
  algorithm metadata, constructor, round-trip (empty / short /
  1 MiB), nonce-uniqueness, wrong-key, body tamper, tag tamper,
  truncation rejection, AAD round-trip, AAD mismatch, invalid key
  length. 13 new `Crypt`-level tests.
- **Cross-algorithm integration tests** (active when both
  `aead-chacha20` and `aead-aes-gcm` features are enabled):
  - Ciphertext from one algorithm fails authentication when
    decrypted with the other.
  - `Algorithm::name()` values are distinct across all shipped
    variants.
- **Public constants** `AES_GCM_NONCE_LEN = 12` and
  `AES_GCM_TAG_LEN = 16` in `crypt_io::aead` for callers
  pre-sizing buffers without conditional compilation.

### Changed

- **Default features extended.** The crate's default feature set now
  includes `aead-aes-gcm` so a vanilla `cargo add crypt-io` ships
  with both AEADs available. Drop the default and select
  `["std", "zeroize", "aead-chacha20", "hash-blake3", "mac-hmac", "kdf-hkdf"]`
  if you want the 0.2.0 surface (ChaCha20-Poly1305 only).
- **`Algorithm` accessors** (`name`, `key_len`, `nonce_len`,
  `tag_len`) now handle the new `Aes256Gcm` variant. Behaviour for
  `ChaCha20Poly1305` is unchanged.
- **`aead/mod.rs` doc-comment header** updated to introduce both
  algorithms and document the "when to pick which" decision tree
  (ChaCha20 is the default; AES-256-GCM is the deliberate choice
  for AES-NI hardware or for spec interop).
- **`clippy.toml` `doc-valid-idents` whitelist** extended with
  `ARMv8`, `AArch64`, `CLMUL`, `Graviton`, `GHASH`, `JWE`,
  `A256GCM`, `x86_64`, `Silicon`, `SoCs`. These appear in the new
  AES-GCM doc comments and need to be on the whitelist so pedantic
  `clippy::doc_markdown` doesn't trip them.

### Security

- **`AuthenticationFailed` opacity preserved across algorithms.**
  AES-256-GCM and ChaCha20-Poly1305 both surface every cryptographic
  failure mode (wrong key, tampered ciphertext, tampered tag, AAD
  mismatch) as the single `Error::AuthenticationFailed` variant.
  Switching algorithms does not change the error-classification
  surface an attacker can observe.
- **Constant-time tag verification** preserved by deferring to the
  upstream `aes-gcm` crate — no equality comparisons on tag bytes
  in this wrapper.
- **Nonce policy is per-call**, identical to the ChaCha20 path. AES-GCM
  is *especially* sensitive to nonce reuse — repeating a `(key,
  nonce)` pair leaks the XOR of the two plaintexts and the GHASH
  authentication key, which is catastrophic. This API draws a fresh
  nonce from `mod-rand::tier3::fill_bytes` for every encrypt call,
  so the failure mode cannot happen through the public surface.
- **No bytes in errors.** `aes_gcm.rs` follows the same redaction
  contract as `chacha20.rs`: no plaintext, ciphertext, nonces, or
  key material appears in any `Error` variant.

[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
[0.3.0]: https://github.com/jamesgober/crypt-io/compare/v0.2.0...v0.3.0

---

## [0.2.0] - 2026-05-21

### Added

- **AEAD foundation — ChaCha20-Poly1305 (RFC 8439).** First working
  encryption layer for the crate:
  - `Algorithm` enum (`#[non_exhaustive]`) — currently `ChaCha20Poly1305`.
    `Default` selects ChaCha20-Poly1305. The enum exposes `name()`,
    `key_len()`, `nonce_len()`, and `tag_len()` accessors.
  - `Crypt` struct — algorithm-agile encryption handle.
    `Crypt::new()` defaults to ChaCha20-Poly1305;
    `Crypt::with_algorithm(Algorithm)` for explicit selection.
  - `Crypt::encrypt(key, plaintext) -> Vec<u8>` and
    `Crypt::decrypt(key, ciphertext) -> Vec<u8>` — round-trip AEAD
    with nonce-prepended wire layout `nonce || ciphertext || tag`.
  - `Crypt::encrypt_with_aad(key, plaintext, aad)` and
    `Crypt::decrypt_with_aad(key, ciphertext, aad)` — variants that
    authenticate associated data.
  - Public constants `CHACHA20_NONCE_LEN = 12`,
    `CHACHA20_TAG_LEN = 16`, `KEY_LEN = 32` (in `crypt_io::aead`).
- **`Error` enum + `Result` type alias** with `Display` + `Error`
  impls. Variants: `InvalidKey { expected, actual }`,
  `InvalidCiphertext(String)`, `AuthenticationFailed`,
  `AlgorithmNotEnabled(&'static str)`, `RandomFailure(&'static str)`.
  `#[non_exhaustive]` — match sites need a wildcard arm. All
  variants are redaction-clean by design: no key bytes, no
  plaintext, no nonces, no ciphertext are ever included in error
  rendering.
- **RFC 8439 §2.8.2 known-answer test** verifying the upstream
  primitive integration is byte-exact against the official vector.
- **Round-trip + tamper-detection + AAD-mismatch test suite.**
  Unit-test coverage for: empty plaintext, 1 MiB plaintext,
  wrong-key authentication failure, tampered ciphertext rejection,
  tampered tag rejection, truncated-buffer rejection, AAD round-trip,
  AAD-mismatch rejection, encrypt-with-aad / decrypt-without-aad
  rejection, invalid key length rejection on both sides.
- **Doctests** for `crypt_io::Crypt::encrypt`,
  `crypt_io::Crypt::decrypt`, and the `aead` module overview.
- **Nonce generation via `mod-rand` Tier 3** (OS-backed CSPRNG —
  `getrandom` on Linux, `getentropy` on macOS, `BCryptGenRandom`
  on Windows).

### Changed

- **MSRV bumped from 1.75 to 1.85** to match the existing
  `edition = "2024"` declaration in `Cargo.toml`. Cargo ≥ 1.84
  refuses to parse the previous combination. CI matrix updated.
- **`src/lib.rs` lint block** extended to the REPS canonical set:
  adds `#![deny(clippy::unreachable)]`, `#![warn(clippy::pedantic)]`,
  and `#![allow(clippy::module_name_repetitions)]`. `extern crate
  alloc;` declared to support the `no_std` build path.
- **`clippy.toml` MSRV synced to 1.85** and `doc-valid-idents`
  whitelist added covering domain terms (`RustCrypto`, `BLAKE3`,
  `ChaCha20`, `Poly1305`, `AES-NI`, etc.).
- Crate skeleton expanded to `src/aead/`, `src/aead/chacha20.rs`,
  `src/error.rs`.

### Security

- **Authentication failures collapse to a single opaque variant.**
  `Error::AuthenticationFailed` is returned for wrong-key,
  tampered-ciphertext, tampered-tag, AAD-mismatch, and truncated-tag
  inputs. The variant is deliberately not subtype-discriminated:
  exposing which mode failed would tell an attacker how close they
  are to a successful forgery.
- **Constant-time tag verification** is preserved by deferring to
  the upstream `chacha20poly1305` crate — no equality comparisons
  on tag bytes happen in this wrapper.
- **No raw key bytes in errors.** `Error::InvalidKey` carries only
  the expected vs. actual *lengths*, never the bytes themselves.
- **Fresh nonce per call**`mod-rand` Tier 3 fills a new 12-byte
  buffer for every `encrypt` / `encrypt_with_aad`. Nonce reuse on
  the same key cannot happen via this API.

[0.2.0]: https://github.com/jamesgober/crypt-io/compare/v0.1.0...v0.2.0

---

## [0.1.0] - 2026-05-18

### Added

- Initial scaffold and repository bootstrap.
- REPS compliance baseline.
- CI for Linux/macOS/Windows on stable and MSRV (1.75).
- Project documentation framework (PROMPT, DIRECTIVES, ROADMAP).
- Feature flags for AEAD (chacha20, aes-gcm), hashing (blake3, sha2), MAC (hmac, blake3 keyed), KDF (hkdf, argon2), stream encryption.
- Dependencies wired: `mod-rand` for CSPRNG, `error-forge` for errors, optional `log-io` and `metrics-lib`.

[Unreleased]: https://github.com/jamesgober/crypt-io/compare/v0.8.0...HEAD
[0.1.0]: https://github.com/jamesgober/crypt-io/releases/tag/v0.1.0