Files
wz-phone/crates/wzp-crypto/src/rekey.rs
Siavash Sameni 51e893590c feat: WarzonePhone lossy VoIP protocol — Phase 1 complete
Rust workspace with 7 crates implementing a custom VoIP protocol
designed for extremely lossy connections (5-70% loss, 100-500kbps,
300-800ms RTT). 89 tests passing across all crates.

Crates:
- wzp-proto: Wire format, traits, adaptive quality controller, jitter buffer, session FSM
- wzp-codec: Opus encoder/decoder (audiopus), Codec2 stubs, adaptive switching, resampling
- wzp-fec: RaptorQ fountain codes, interleaving, block management (proven 30-70% loss recovery)
- wzp-crypto: X25519+ChaCha20-Poly1305, Warzone identity compatible, anti-replay, rekeying
- wzp-transport: QUIC via quinn with DATAGRAM frames, path monitoring, signaling streams
- wzp-relay: Integration stub (Phase 2)
- wzp-client: Integration stub (Phase 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:45:07 +04:00

133 lines
4.2 KiB
Rust

//! Rekeying state machine for forward secrecy.
//!
//! Triggers rekeying every 2^16 packets. Uses HKDF to mix the old key
//! with the new DH result, then zeroizes the old key material.
use hkdf::Hkdf;
use sha2::Sha256;
use x25519_dalek::{PublicKey, StaticSecret};
/// Rekeying interval: every 2^16 packets.
const REKEY_INTERVAL: u64 = 1 << 16;
/// Manages rekeying decisions and key evolution.
pub struct RekeyManager {
/// Current symmetric key material (32 bytes).
current_key: [u8; 32],
/// Packet count at which last rekey occurred.
last_rekey_at: u64,
}
impl RekeyManager {
/// Create a new `RekeyManager` with the initial session key.
pub fn new(initial_key: [u8; 32]) -> Self {
Self {
current_key: initial_key,
last_rekey_at: 0,
}
}
/// Check whether rekeying should occur based on packet count.
pub fn should_rekey(&self, packet_count: u64) -> bool {
packet_count.saturating_sub(self.last_rekey_at) >= REKEY_INTERVAL
}
/// Perform rekeying: mix old key + new DH shared secret via HKDF.
///
/// The old key is zeroized after the new key is derived.
/// Returns the new 32-byte symmetric key.
pub fn perform_rekey(
&mut self,
new_peer_pub: &[u8; 32],
our_new_secret: StaticSecret,
packet_count: u64,
) -> [u8; 32] {
let peer_public = PublicKey::from(*new_peer_pub);
let new_dh = our_new_secret.diffie_hellman(&peer_public);
// Mix old key (as salt) with new DH result (as IKM) via HKDF
let hk = Hkdf::<Sha256>::new(Some(&self.current_key), new_dh.as_bytes());
let mut new_key = [0u8; 32];
hk.expand(b"warzone-rekey", &mut new_key)
.expect("HKDF expand should not fail for 32 bytes");
// Zeroize old key for forward secrecy
self.current_key.fill(0);
// Install new key
self.current_key = new_key;
self.last_rekey_at = packet_count;
new_key
}
/// Get a reference to the current key.
pub fn current_key(&self) -> &[u8; 32] {
&self.current_key
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::OsRng;
#[test]
fn should_rekey_at_interval() {
let mgr = RekeyManager::new([0xAA; 32]);
assert!(!mgr.should_rekey(0));
assert!(!mgr.should_rekey(65535));
assert!(mgr.should_rekey(65536));
assert!(mgr.should_rekey(100_000));
}
#[test]
fn rekey_produces_different_key() {
let initial = [0xBB; 32];
let mut mgr = RekeyManager::new(initial);
let secret = StaticSecret::random_from_rng(OsRng);
let peer_secret = StaticSecret::random_from_rng(OsRng);
let peer_pub = PublicKey::from(&peer_secret).to_bytes();
let new_key = mgr.perform_rekey(&peer_pub, secret, 65536);
assert_ne!(new_key, initial);
}
#[test]
fn old_key_zeroized_after_rekey() {
let initial = [0xCC; 32];
let mut mgr = RekeyManager::new(initial);
let secret = StaticSecret::random_from_rng(OsRng);
let peer_secret = StaticSecret::random_from_rng(OsRng);
let peer_pub = PublicKey::from(&peer_secret).to_bytes();
// Save pointer to check zeroization
let _new_key = mgr.perform_rekey(&peer_pub, secret, 65536);
// The old key slot should now contain the new key, not the initial
assert_ne!(*mgr.current_key(), initial);
}
#[test]
fn consistent_rekey_with_same_inputs() {
// Two managers with same initial key, same DH inputs, should get same result
let initial = [0xDD; 32];
let mut mgr1 = RekeyManager::new(initial);
let mut mgr2 = RekeyManager::new(initial);
// Use StaticSecret so we can clone the key bytes
let secret_bytes = [0x42u8; 32];
let secret1 = StaticSecret::from(secret_bytes);
let secret2 = StaticSecret::from(secret_bytes);
let peer_bytes = [0x77u8; 32];
let peer_secret = StaticSecret::from(peer_bytes);
let peer_pub = PublicKey::from(&peer_secret).to_bytes();
let k1 = mgr1.perform_rekey(&peer_pub, secret1, 65536);
let k2 = mgr2.perform_rekey(&peer_pub, secret2, 65536);
assert_eq!(k1, k2);
}
}