fix(audit): address C2, C3, M4, M5 from 2026-05-25 audit

C2: Add EncryptingTransport wrapper — all media I/O now goes through
ChaChaSession encrypt/decrypt before hitting the QUIC datagram path.
cli.rs run_live/run_silence/run_file_mode accept Arc<dyn MediaTransport>
and receive a wrapped transport after the handshake.

C3: Wire VideoScorer::observe() into both plain and trunked forwarding
loops in room.rs. Packets from participants with Abusive verdict are
dropped before forwarding. last_bwe_kbps tracked from quality reports.

M4: Widen FEC repair symbol index from u8 to u16 throughout
(FecEncoder::generate_repair, FecDecoder::add_symbol, all call sites in
call.rs, bench.rs, pipeline.rs, wzp-android). Eliminates theoretical
wrapping when num_source + repair_count > 255.

M5: Track last_encrypt_timestamp in ChaChaSession. debug_assert in
encrypt() that timestamp is non-decreasing across calls (including post-
rekey). complete_rekey() explicitly preserves last_encrypt_timestamp to
prevent accidental timestamp reset regressions.

583 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-05-25 06:20:05 +04:00
parent 15af58a95d
commit 52a6f5e048
13 changed files with 299 additions and 29 deletions

View File

@@ -33,6 +33,8 @@ pub struct ChaChaSession {
sas_code: Option<u32>,
/// Per-stream anti-replay windows, keyed by (stream_id, media_type).
anti_replay: HashMap<(u8, MediaType), AntiReplayWindow>,
/// Last timestamp seen in encrypt() — used to assert monotonicity across rekeys.
last_encrypt_timestamp: Option<u32>,
}
impl ChaChaSession {
@@ -55,6 +57,7 @@ impl ChaChaSession {
pending_rekey_secret: None,
sas_code: None,
anti_replay: HashMap::new(),
last_encrypt_timestamp: None,
}
}
@@ -122,6 +125,18 @@ impl CryptoSession for ChaChaSession {
out.extend_from_slice(&ciphertext);
self.send_seq = self.send_seq.wrapping_add(1); // packet counter for rekey trigger only
// M5: assert timestamp_ms is non-decreasing across calls (including post-rekey).
// Timestamps are u32 and wrap at 2^32 ms (~49 days); allow wrapping.
debug_assert!(
self.last_encrypt_timestamp
.map_or(true, |last| header.timestamp.wrapping_sub(last) < u32::MAX / 2),
"encrypt: timestamp must not decrease (last={:?}, now={})",
self.last_encrypt_timestamp,
header.timestamp,
);
self.last_encrypt_timestamp = Some(header.timestamp);
Ok(())
}
@@ -189,7 +204,9 @@ impl CryptoSession for ChaChaSession {
.perform_rekey(peer_ephemeral_pub, secret, total_packets);
self.install_key(new_key);
// Reset sequence counters after rekey for nonce uniqueness
// Reset sequence counters after rekey for nonce uniqueness.
// last_encrypt_timestamp is intentionally NOT reset — spec requires
// timestamp_ms to be monotonic across rekeys.
self.send_seq = 0;
self.recv_seq = 0;