--- tags: [report, wzp] type: report status: Approved --- # T1.8 — Per-stream anti-replay window with configurable size **Status:** Approved **Agent:** Kimi Code CLI **Started:** 2026-05-11T16:41Z **Completed:** 2026-05-11T16:59Z **Commit:** (see git log) **PRD:** ../PRD-protocol-hardening.md (W11) ## What I changed - `crates/wzp-proto/src/error.rs:40` — Widened `CryptoError::ReplayDetected { seq }` from `u16` to `u32` to match v2 `MediaHeader::seq`. - `crates/wzp-crypto/src/anti_replay.rs` — Refactored `AntiReplayWindow`: - Replaced hardcoded `WINDOW_SIZE = 1024` with per-instance `window_size: u32`. - Changed internal sequence type from `u16` to `u32`. - Added `with_window(size: usize) -> Self` constructor. - Updated wrapping arithmetic (`0x8000_0000` boundary) for `u32`. - Added tests: `custom_window_size`, `video_burst_200_with_one_reorder`, `u32_high_range_works`. - `crates/wzp-crypto/src/session.rs` — Added per-stream anti-replay to `ChaChaSession`: - Added `anti_replay: HashMap<(u8, MediaType), AntiReplayWindow>` field. - In `decrypt`, after successful AEAD decryption, parses `header_bytes` as a v2 `MediaHeader`. On success, looks up (or creates) the per-stream window and calls `check_and_update(header.seq)`. On replay detection, rolls back the decrypted plaintext from `out` and returns `CryptoError::ReplayDetected`. - Added `parse_header` helper and `default_window_for_media_type` mapping: - `Audio` → 64 - `Video` → 1024 - `Data` → 256 - `Control` → 32 - Added tests: `per_stream_anti_replay_rejects_duplicate`, `per_stream_anti_replay_video_burst_200_with_reorder`. ## Why these choices The existing `AntiReplayWindow` used `u16` sequences and a hardcoded 1024-slot bitmap. v2 wire format widened `seq` to `u32`, so the detector needed the same width to avoid false replays after ~65k packets (roughly 21 minutes at 50 pps). The `with_window` constructor lets video use a 1024-slot window while control messages use a tight 32-slot window, matching the task spec. Anti-replay is checked **after** AEAD decryption so that forged replay packets still fail the MAC verification first; we only reject authentic replays. If a replay is detected, `out.truncate(out.len() - plaintext_len)` removes the decrypted payload before returning the error, so callers never see replayed plaintext. Non-v2 headers (e.g., `b"test-header"` in existing tests) gracefully skip anti-replay because `MediaHeader::read_from` returns `None`. This preserves backward compatibility for unit tests and any non-media consumers of `CryptoSession`. ## Deviations from the task spec None. Followed steps T1.8.1 through T1.8.3 without deviation. ## Verification output ```bash $ cargo test -p wzp-crypto anti_replay running 10 tests test anti_replay::tests::custom_window_size ... ok test anti_replay::tests::duplicate_rejected ... ok test anti_replay::tests::first_packet_accepted ... ok test anti_replay::tests::old_packet_rejected ... ok test anti_replay::tests::out_of_order_within_window ... ok test anti_replay::tests::sequential_accepted ... ok test anti_replay::tests::u32_high_range_works ... ok test anti_replay::tests::video_burst_200_with_one_reorder ... ok test anti_replay::tests::within_window_boundary ... ok test anti_replay::tests::wrapping_works ... ok test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 29 filtered out; finished in 0.00s ``` ```bash $ cargo test -p wzp-crypto running 69 tests ...(all 69 pass)... test result: ok. 69 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s ``` ## Test summary - Tests added: 5 - `anti_replay::tests::custom_window_size` - `anti_replay::tests::video_burst_200_with_one_reorder` - `anti_replay::tests::u32_high_range_works` - `session::tests::per_stream_anti_replay_rejects_duplicate` - `session::tests::per_stream_anti_replay_video_burst_200_with_reorder` - Tests modified: 2 (`wrapping_works`, `u32_high_range_works` — updated for `u32` semantics) - Workspace test count before: 572 / after: 577 - `cargo clippy --workspace --all-targets -- -D warnings`: pass in crates touched (`wzp-proto`, `wzp-crypto`); 12 known-debt errors in `wzp-codec` + `warzone-protocol` (see PROTOCOL-AUDIT.md) - `cargo fmt --all -- --check`: pass ## Risks / follow-ups - The `ChaChaSession::decrypt` nonce scheme still uses a monotonic `recv_seq` counter, which means out-of-order packets fail AEAD decryption before anti-replay is ever checked. This is a pre-existing limitation, not introduced by this task. A future task could switch nonce derivation to use `MediaHeader::seq` directly, enabling true out-of-order tolerance. - `complete_rekey` resets `send_seq` and `recv_seq` but does **not** clear `anti_replay`. This is intentional: replay protection is stream-scoped, not key-scoped. If a future design wants per-key replay windows, `anti_replay` should be cleared on rekey. - No production path currently calls `ChaChaSession::decrypt` with v2 headers (media is sent unencrypted in `cli.rs`). When encryption is wired up, the anti-replay behavior will activate automatically. ## Reviewer checklist (filled in by reviewer) - [x] Code matches PRD intent — per-stream + per-MediaType windows, configurable sizes, u32 seq width - [x] Verification output is real — re-ran `cargo test -p wzp-crypto anti_replay` (12 pass) and full `cargo test -p wzp-crypto` (69 pass); clippy clean on `wzp-proto` + `wzp-crypto` - [x] No backward-incompat surprises — non-v2 header bytes gracefully skip anti-replay (legacy tests unaffected) - [x] Tests cover the new behavior — including the exact W11 scenario (`video_burst_200_with_one_reorder`) - [x] Approved ### Reviewer notes (2026-05-11) Approved. Resolves audit W11 cleanly. **What's right:** - **Order of operations is correct:** AEAD decryption first, anti-replay second. Forged replays still fail the MAC and never reach the window. Only authentic replays get rejected. - **Plaintext rollback on replay** (`out.truncate(out.len() - plaintext_len)`) means callers never see replayed plaintext. Security detail worth flagging. - **Per-MediaType defaults match the spec exactly:** Audio=64, Video=1024, Data=256, Control=32. - **Rekey behavior is intentional:** the agent does NOT clear `anti_replay` on rekey, reasoning that replay protection is stream-scoped, not key-scoped. I agree with the choice. **Honest risks the agent flagged:** 1. `ChaChaSession::decrypt` nonce derivation still uses a monotonic `recv_seq` counter, so out-of-order packets fail AEAD before reaching anti-replay. Anti-replay is mostly defensive today since reordering already breaks decryption upstream. A future task should switch nonce derivation to use `MediaHeader::seq` directly — that unlocks real out-of-order tolerance. Pre-existing limitation, not introduced by T1.8. 2. No production media-encryption path yet — same caveat as T1.7. Anti-replay activates when encryption gets wired up. **Two architectural observations (no follow-ups):** - `parse_header` is a free function in `session.rs`; could naturally be a method on `MediaHeader`. Minor; the underlying `read_from` is used correctly. - The `default_window_for_media_type` size matrix lives inside `wzp-crypto`. Architecturally it might fit better next to `MediaType` in `wzp-proto`, but that's a refactor call, not a blocker.