fix(wzp-crypto): derive AEAD nonces from MediaHeader.seq, not recv_seq

The previous scheme built ChaCha20-Poly1305 nonces from an internal
recv_seq counter that incremented once per decrypt() call. Under
in-order delivery recv_seq stayed in sync with the sender's send_seq,
but any out-of-order or lost packet caused them to diverge permanently —
every subsequent packet then used the wrong nonce and AEAD decryption
failed for the rest of the session.

Fix: parse the MediaHeader at the top of both encrypt() and decrypt()
and use header.seq as the nonce input. Both sides now derive the nonce
from the same wire field, surviving reordering by construction.

send_seq / recv_seq are kept as pure packet counters for the rekey
interval trigger; they no longer affect nonce derivation.

All tests updated to pass valid v2 MediaHeader bytes instead of raw
byte literals (the new code requires a parseable header for nonce
derivation). New test decrypt_survives_out_of_order_delivery encrypts
5 packets and delivers them out of order (indices 0,2,1,4,3); this
test would have failed under the old counter-based scheme.

Fixes audit finding C1 from AUDIT-2026-05-25.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-05-25 06:00:01 +04:00
parent f78794f4b6
commit 12b0d9738f
5 changed files with 210 additions and 56 deletions

View File

@@ -263,17 +263,36 @@ pub fn bench_encrypt_decrypt() -> CryptoResult {
})
.collect();
let header = b"bench-header";
// Build valid v2 MediaHeader bytes — encrypt/decrypt now derive nonces from
// header.seq and require a parseable MediaHeader (WIRE_SIZE bytes minimum).
use wzp_proto::packet::MediaHeader;
use wzp_proto::{CodecId, MediaType};
let mut total_bytes: usize = 0;
let start = Instant::now();
for payload in &payloads {
for (i, payload) in payloads.iter().enumerate() {
let hdr = MediaHeader {
version: 2,
flags: 0,
media_type: MediaType::Audio,
codec_id: CodecId::Opus24k,
stream_id: 0,
fec_ratio: 0,
seq: i as u32,
timestamp: (i as u32).wrapping_mul(20),
fec_block: 0,
};
let mut header_bytes = Vec::with_capacity(MediaHeader::WIRE_SIZE);
hdr.write_to(&mut header_bytes);
let mut ciphertext = Vec::with_capacity(payload.len() + 16);
encryptor.encrypt(header, payload, &mut ciphertext).unwrap();
encryptor
.encrypt(&header_bytes, payload, &mut ciphertext)
.unwrap();
let mut plaintext = Vec::with_capacity(payload.len());
decryptor
.decrypt(header, &ciphertext, &mut plaintext)
.decrypt(&header_bytes, &ciphertext, &mut plaintext)
.unwrap();
total_bytes += payload.len();

View File

@@ -99,31 +99,52 @@ async fn full_handshake_both_sides_derive_same_session() {
assert_eq!(chosen_profile, wzp_proto::QualityProfile::GOOD);
// Verify both sides can communicate: client encrypts, relay decrypts.
let header = b"test-header";
// encrypt/decrypt derive nonces from MediaHeader.seq, so we need valid headers.
use wzp_proto::packet::MediaHeader;
use wzp_proto::{CodecId, MediaType};
let make_hdr = |seq: u32| {
let h = MediaHeader {
version: 2,
flags: 0,
media_type: MediaType::Audio,
codec_id: CodecId::Opus24k,
stream_id: 0,
fec_ratio: 0,
seq,
timestamp: seq.wrapping_mul(20),
fec_block: 0,
};
let mut b = Vec::new();
h.write_to(&mut b);
b
};
let header = make_hdr(0);
let plaintext = b"hello from client to relay";
let mut ciphertext = Vec::new();
client_session
.encrypt(header, plaintext, &mut ciphertext)
.encrypt(&header, plaintext, &mut ciphertext)
.expect("client encrypt should succeed");
let mut decrypted = Vec::new();
relay_session
.decrypt(header, &ciphertext, &mut decrypted)
.decrypt(&header, &ciphertext, &mut decrypted)
.expect("relay decrypt should succeed");
assert_eq!(&decrypted[..], plaintext);
// Verify reverse direction: relay encrypts, client decrypts.
let header2 = make_hdr(0); // relay's send_seq starts at 0
let plaintext2 = b"hello from relay to client";
let mut ciphertext2 = Vec::new();
relay_session
.encrypt(header, plaintext2, &mut ciphertext2)
.encrypt(&header2, plaintext2, &mut ciphertext2)
.expect("relay encrypt should succeed");
let mut decrypted2 = Vec::new();
client_session
.decrypt(header, &ciphertext2, &mut decrypted2)
.decrypt(&header2, &ciphertext2, &mut decrypted2)
.expect("client decrypt should succeed");
assert_eq!(&decrypted2[..], plaintext2);