# T1.8 — Per-stream anti-replay window with configurable size **Status:** Pending Review **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) - [ ] Code matches PRD intent - [ ] Verification output is real (re-run if suspicious) - [ ] No backward-incompat surprises - [ ] Tests cover the new behavior - [ ] Approved