#[cfg(test)]
mod crypto_tests {
use crate::crypto::*;
#[test]
fn test_x25519_shared_secret_agreement() {
let alice = X25519KeyPair::generate().expect("Alice keygen");
let bob = X25519KeyPair::generate().expect("Bob keygen");
let alice_pk = alice.public_key_bytes;
let bob_pk = bob.public_key_bytes;
let alice_ss = alice.diffie_hellman(&bob_pk).expect("Alice DH");
let bob_ss = bob.diffie_hellman(&alice_pk).expect("Bob DH");
assert_eq!(
alice_ss.0, bob_ss.0,
"X25519: shared secrets must be equal"
);
assert_ne!(alice_ss.0, vec![0u8; alice_ss.0.len()], "Shared secret must not be all-zeros");
}
#[test]
fn test_x25519_keys_are_unique() {
let kp1 = X25519KeyPair::generate().expect("keygen 1");
let kp2 = X25519KeyPair::generate().expect("keygen 2");
assert_ne!(
kp1.public_key_bytes, kp2.public_key_bytes,
"Two independently generated X25519 key pairs must differ"
);
}
#[test]
fn test_x25519_single_use_enforced() {
let alice = X25519KeyPair::generate().expect("keygen");
let bob = X25519KeyPair::generate().expect("keygen");
let bob_pk = bob.public_key_bytes;
alice.diffie_hellman(&bob_pk).expect("first DH must succeed");
}
#[test]
fn test_x25519_wrong_peer_gives_different_secret() {
let alice = X25519KeyPair::generate().expect("keygen");
let bob = X25519KeyPair::generate().expect("keygen");
let eve = X25519KeyPair::generate().expect("keygen");
let alice_pk = alice.public_key_bytes;
let bob_with_alice = bob.diffie_hellman(&alice_pk).expect("Bob × Alice");
let eve_with_alice = eve.diffie_hellman(&alice_pk).expect("Eve × Alice");
assert_ne!(
bob_with_alice.0, eve_with_alice.0,
"ECDH with different private keys must yield different shared secrets"
);
}
#[test]
fn test_mlkem_encap_decap_roundtrip() {
let kp = MlKemKeyPair::generate().expect("MlKem keygen");
let pk_bytes = kp.public_key_bytes();
assert_eq!(pk_bytes.len(), MLKEM_PK_LEN, "ML-KEM-768 PK must be {MLKEM_PK_LEN} bytes");
let (ciphertext, encap_ss) = mlkem_encapsulate(&pk_bytes).expect("encapsulate");
assert_eq!(ciphertext.len(), MLKEM_CT_LEN, "ML-KEM-768 ciphertext must be {MLKEM_CT_LEN} bytes");
let decap_ss = kp.decapsulate(&ciphertext).expect("decapsulate");
assert_eq!(
encap_ss.0, decap_ss.0,
"ML-KEM: encap and decap shared secrets must be equal"
);
}
#[test]
fn test_mlkem_encapsulation_is_randomized() {
let kp = MlKemKeyPair::generate().expect("keygen");
let pk = kp.public_key_bytes();
let (ct1, _) = mlkem_encapsulate(&pk).expect("encap 1");
let (ct2, _) = mlkem_encapsulate(&pk).expect("encap 2");
assert_ne!(ct1, ct2, "Each encapsulation must produce a fresh ciphertext");
}
#[test]
fn test_mlkem_tampered_ciphertext_yields_wrong_secret() {
let kp = MlKemKeyPair::generate().expect("keygen");
let pk = kp.public_key_bytes();
let (mut ciphertext, correct_ss) = mlkem_encapsulate(&pk).expect("encap");
ciphertext[MLKEM_CT_LEN / 2] ^= 0xFF;
let wrong_ss = kp.decapsulate(&ciphertext).expect("decap of tampered CT");
assert_ne!(
correct_ss.0, wrong_ss.0,
"Tampered ciphertext must not yield the correct shared secret"
);
}
#[test]
fn test_mlkem_encapsulate_wrong_pk_length_errors() {
let short_pk = vec![0u8; 100]; let result = mlkem_encapsulate(&short_pk);
assert!(
matches!(result, Err(CryptoError::InvalidPublicKey)),
"Expected InvalidPublicKey error for short PK"
);
}
#[test]
fn test_mlkem_decapsulate_wrong_ct_length_errors() {
let kp = MlKemKeyPair::generate().expect("keygen");
let bad_ct = vec![0u8; 42]; let result = kp.decapsulate(&bad_ct);
assert!(
matches!(result, Err(CryptoError::InvalidCiphertext)),
"Expected InvalidCiphertext error for short CT"
);
}
#[test]
fn test_hkdf_combiner_is_deterministic() {
let classical = SecretBytes(vec![0xAA; 32]);
let pq = SecretBytes(vec![0xBB; 32]);
let key1 = derive_session_key(&classical, &pq).expect("derive 1");
let key2 = derive_session_key(&classical, &pq).expect("derive 2");
assert_eq!(key1.0, key2.0, "HKDF must be deterministic for identical inputs");
}
#[test]
fn test_hkdf_combiner_input_order_matters() {
let a = SecretBytes(vec![0xAA; 32]);
let b = SecretBytes(vec![0xBB; 32]);
let key_ab = derive_session_key(&a, &b).expect("a||b");
let key_ba = derive_session_key(&b, &a).expect("b||a");
assert_ne!(key_ab.0, key_ba.0, "Input order must matter for the hybrid combiner");
}
#[test]
fn test_hkdf_combiner_different_inputs_yield_different_keys() {
let classical1 = SecretBytes(vec![0x11; 32]);
let classical2 = SecretBytes(vec![0x22; 32]);
let pq = SecretBytes(vec![0xBB; 32]);
let key1 = derive_session_key(&classical1, &pq).expect("key1");
let key2 = derive_session_key(&classical2, &pq).expect("key2");
assert_ne!(key1.0, key2.0, "Different classical secrets must yield different session keys");
}
#[test]
fn test_cookie_is_deterministic() {
let secret: [u8; 32] = [0x42; 32];
let mac: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
let seq: u32 = 0xDEADBEEF;
let c1 = compute_cookie(&secret, &mac, seq);
let c2 = compute_cookie(&secret, &mac, seq);
assert_eq!(c1, c2);
}
#[test]
fn test_cookie_verify_correct() {
let secret: [u8; 32] = [0x42; 32];
let mac: [u8; 6] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
let seq: u32 = 1234;
let cookie = compute_cookie(&secret, &mac, seq);
assert!(verify_cookie(&secret, &mac, seq, &cookie), "Correct cookie must verify");
}
#[test]
fn test_cookie_verify_wrong_cookie() {
let secret: [u8; 32] = [0x42; 32];
let mac: [u8; 6] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
let seq: u32 = 1234;
let mut bad_cookie = compute_cookie(&secret, &mac, seq);
bad_cookie[0] ^= 0xFF; assert!(!verify_cookie(&secret, &mac, seq, &bad_cookie), "Tampered cookie must not verify");
}
#[test]
fn test_cookie_bound_to_mac() {
let secret: [u8; 32] = [0x42; 32];
let mac1: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
let mac2: [u8; 6] = [0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA];
let seq: u32 = 99;
let cookie_for_mac1 = compute_cookie(&secret, &mac1, seq);
assert!(
!verify_cookie(&secret, &mac2, seq, &cookie_for_mac1),
"Cookie from mac1 must not verify for mac2"
);
}
#[test]
fn test_cookie_bound_to_sequence_id() {
let secret: [u8; 32] = [0x42; 32];
let mac: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
let cookie_seq1 = compute_cookie(&secret, &mac, 1);
assert!(
!verify_cookie(&secret, &mac, 2, &cookie_seq1),
"Cookie from seq=1 must not verify for seq=2"
);
}
#[test]
fn test_session_key_ct_eq_equal() {
let k1 = SessionKey([0xAB; SESSION_KEY_LEN]);
let k2 = SessionKey([0xAB; SESSION_KEY_LEN]);
assert!(k1.ct_eq(&k2));
}
#[test]
fn test_session_key_ct_eq_not_equal() {
let k1 = SessionKey([0xAB; SESSION_KEY_LEN]);
let k2 = SessionKey([0xCD; SESSION_KEY_LEN]);
assert!(!k1.ct_eq(&k2));
}
}
#[cfg(test)]
mod fragmentation_tests {
use crate::network::*;
use crate::crypto::HMAC_LEN;
fn dummy_cookie() -> [u8; HMAC_LEN] { [0xCC; HMAC_LEN] }
#[test]
fn test_fragment_count_for_mlkem_ciphertext() {
let payload = vec![0xAB; 1088];
let frames = fragment_payload(&payload, 42, &dummy_cookie());
assert_eq!(frames.len(), 3, "1088-byte payload must split into 3 fragments");
}
#[test]
fn test_fragment_count_for_mlkem_public_key() {
let payload = vec![0xCD; 1184];
let frames = fragment_payload(&payload, 1, &dummy_cookie());
assert_eq!(frames.len(), 3, "1184-byte payload must split into 3 fragments");
}
#[test]
fn test_no_fragment_exceeds_max_payload() {
let payload = vec![0x55; 1184];
let frames = fragment_payload(&payload, 7, &dummy_cookie());
for frame in &frames {
assert!(
frame.payload.len() <= FRAG_PAYLOAD_MAX,
"Fragment {} exceeds FRAG_PAYLOAD_MAX ({} > {})",
frame.header.frag_index,
frame.payload.len(),
FRAG_PAYLOAD_MAX
);
}
}
#[test]
fn test_cookie_only_in_first_fragment() {
let cookie = dummy_cookie();
let frames = fragment_payload(&vec![0u8; 1088], 5, &cookie);
assert_eq!(frames[0].cookie.as_slice(), cookie.as_ref(), "Cookie must appear in frag 0");
for frame in &frames[1..] {
assert_eq!(
frame.cookie.as_slice(), [0u8; HMAC_LEN].as_ref(),
"Cookie in frag {} must be all-zeros", frame.header.frag_index
);
}
}
#[test]
fn test_all_fragments_share_sequence_id() {
let seq_id = 0xCAFEBABE;
let frames = fragment_payload(&vec![0u8; 1088], seq_id, &dummy_cookie());
for frame in &frames {
assert_eq!(frame.header.sequence_id, seq_id);
}
}
#[test]
fn test_fragment_indices_are_correct() {
let frames = fragment_payload(&vec![0u8; 1088], 1, &dummy_cookie());
let total = frames.len() as u8;
for (i, frame) in frames.iter().enumerate() {
assert_eq!(frame.header.frag_index, i as u8);
assert_eq!(frame.header.frag_total, total);
}
}
#[test]
fn test_payload_len_header_matches_actual() {
let frames = fragment_payload(&vec![0xAA; 1088], 3, &dummy_cookie());
for frame in &frames {
assert_eq!(
frame.header.payload_len as usize,
frame.payload.len(),
"payload_len mismatch on frag {}", frame.header.frag_index
);
}
}
#[test]
fn test_reassembly_in_order() {
let original: Vec<u8> = (0u8..=255u8).cycle().take(1088).collect();
let frames = fragment_payload(&original, 100, &dummy_cookie());
let reassembled = reassemble_fragments(&frames).expect("reassembly must succeed");
assert_eq!(reassembled, original, "Reassembled payload must equal original");
}
#[test]
fn test_reassembly_out_of_order() {
let original: Vec<u8> = (0u8..=255u8).cycle().take(1088).collect();
let mut frames = fragment_payload(&original, 200, &dummy_cookie());
frames.reverse();
let reassembled = reassemble_fragments(&frames).expect("out-of-order reassembly must succeed");
assert_eq!(reassembled, original);
}
#[test]
fn test_reassembly_incomplete_returns_none() {
let original = vec![0u8; 1088];
let mut frames = fragment_payload(&original, 300, &dummy_cookie());
frames.pop(); let result = reassemble_fragments(&frames);
assert!(result.is_none(), "Incomplete fragments must return None");
}
#[test]
fn test_reassembly_empty_returns_none() {
assert!(reassemble_fragments(&[]).is_none());
}
#[test]
fn test_single_fragment_payload() {
let original = vec![0xBE; 128]; let frames = fragment_payload(&original, 1, &dummy_cookie());
assert_eq!(frames.len(), 1, "128-byte payload must produce 1 fragment");
let reassembled = reassemble_fragments(&frames).unwrap();
assert_eq!(reassembled, original);
}
#[test]
fn test_reassembly_rejects_mixed_sequence_ids() {
let original = vec![0u8; 1088];
let mut frames = fragment_payload(&original, 1, &dummy_cookie());
frames.last_mut().unwrap().header.sequence_id = 9999;
let result = reassemble_fragments(&frames);
assert!(result.is_none(), "Mixed sequence IDs must be rejected");
}
}
#[cfg(test)]
mod handshake_tests {
use crate::network::*;
use rand_core::RngCore;
const AP_MAC: [u8; 6] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
const STATION_MAC: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
fn random_seq_id() -> u32 {
let mut b = [0u8; 4];
rand_core::OsRng.fill_bytes(&mut b);
u32::from_be_bytes(b)
}
#[test]
fn test_full_handshake_produces_matching_keys() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let ap_x25519_pk = ap.x25519_public_key_bytes().unwrap();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let (pq_frames, station_pq_ss) =
station.build_pq_fragments(&ap_mlkem_pk, seq_id, &cookie).unwrap();
let mut ap_key: Option<crate::crypto::SessionKey> = None;
for frame in &pq_frames {
ap_key = ap.process_fragment(frame, &STATION_MAC, &station_x25519_pk).unwrap();
}
let ap_session_key = ap_key.expect("AP must have derived session key");
let station_session_key =
station.complete_handshake(&ap_x25519_pk, station_pq_ss).unwrap();
assert!(
ap_session_key.ct_eq(&station_session_key),
"AP and Station session keys must be identical after successful handshake"
);
}
#[test]
fn test_two_handshakes_produce_different_keys() {
fn run_handshake() -> [u8; 32] {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let ap_x25519_pk = ap.x25519_public_key_bytes().unwrap();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let (pq_frames, station_pq_ss) =
station.build_pq_fragments(&ap_mlkem_pk, seq_id, &cookie).unwrap();
let mut ap_key: Option<crate::crypto::SessionKey> = None;
for frame in &pq_frames {
ap_key = ap.process_fragment(frame, &STATION_MAC, &station_x25519_pk).unwrap();
}
let ap_sk = ap_key.unwrap();
let _sta_sk = station.complete_handshake(&ap_x25519_pk, station_pq_ss).unwrap();
ap_sk.0
}
let key1 = run_handshake();
let key2 = run_handshake();
assert_ne!(key1, key2, "Two independent handshakes must produce different session keys");
}
#[test]
fn test_fast_link_frame_bad_magic_rejected() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let mut frame = station.build_fast_link_frame().unwrap();
frame.magic = [0x00, 0x00, 0x00, 0x00];
let result = ap.process_fast_link_frame(&frame, 1);
assert!(result.is_err(), "Frame with bad magic must be rejected");
}
#[test]
fn test_fast_link_frame_bad_version_rejected() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let mut frame = station.build_fast_link_frame().unwrap();
frame.version = 99;
let result = ap.process_fast_link_frame(&frame, 1);
assert!(result.is_err(), "Frame with wrong version must be rejected");
}
#[test]
fn test_ap_stateless_before_valid_cookie() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let _cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let bad_cookie: [u8; crate::crypto::HMAC_LEN] = [0xFF; crate::crypto::HMAC_LEN];
let (pq_frames, _station_pq_ss) =
station.build_pq_fragments(&ap_mlkem_pk, seq_id, &bad_cookie).unwrap();
let result = ap.process_fragment(&pq_frames[0], &STATION_MAC, &station_x25519_pk);
assert!(
matches!(result, Err(NetworkError::InvalidCookie)),
"AP must reject frag-0 with invalid cookie"
);
}
}
#[cfg(test)]
mod security_tests {
use crate::network::*;
use crate::crypto::HMAC_LEN;
use rand_core::RngCore;
const AP_MAC: [u8; 6] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
const STATION_MAC: [u8; 6] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
const EVE_MAC: [u8; 6] = [0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE];
fn random_seq_id() -> u32 {
let mut b = [0u8; 4];
rand_core::OsRng.fill_bytes(&mut b);
u32::from_be_bytes(b)
}
#[test]
fn test_fragment_without_prior_state_rejected() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let (pq_frames, _) = station
.build_pq_fragments(&ap_mlkem_pk, seq_id, &cookie)
.unwrap();
let result = ap.process_fragment(&pq_frames[1], &STATION_MAC, &station_x25519_pk);
assert!(
matches!(result, Err(NetworkError::UnknownStation)),
"frag-1 before frag-0 must fail with UnknownStation"
);
}
#[test]
fn test_zero_cookie_rejected() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let _cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let zero_cookie = [0u8; HMAC_LEN];
let (pq_frames, _) = station
.build_pq_fragments(&ap_mlkem_pk, seq_id, &zero_cookie)
.unwrap();
let result = ap.process_fragment(&pq_frames[0], &STATION_MAC, &station_x25519_pk);
assert!(
matches!(result, Err(NetworkError::InvalidCookie)),
"All-zeros cookie must be rejected"
);
}
#[test]
fn test_wrong_mlkem_pk_produces_mismatched_keys() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let eve_kp = crate::crypto::MlKemKeyPair::generate().unwrap();
let wrong_mlkem_pk = eve_kp.public_key_bytes();
let ap_x25519_pk = ap.x25519_public_key_bytes().unwrap();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let (pq_frames, station_pq_ss) = station
.build_pq_fragments(&wrong_mlkem_pk, seq_id, &cookie)
.unwrap();
let mut ap_key: Option<crate::crypto::SessionKey> = None;
for frame in &pq_frames {
let _ = ap.process_fragment(frame, &STATION_MAC, &station_x25519_pk);
}
if let Some(ap_sk) = ap_key {
let sta_sk = station.complete_handshake(&ap_x25519_pk, station_pq_ss).unwrap();
assert!(
!ap_sk.ct_eq(&sta_sk),
"Wrong ML-KEM PK: keys must NOT match"
);
}
}
#[test]
fn test_cookie_not_transferable_across_stations() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let eve_station = Station::new(EVE_MAC).expect("Eve setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let eve_x25519_pk = eve_station.x25519_public_key_bytes().unwrap();
let (eve_frames, _) = eve_station
.build_pq_fragments(&ap_mlkem_pk, seq_id, &cookie)
.unwrap();
let result = ap.process_fragment(&eve_frames[0], &EVE_MAC, &eve_x25519_pk);
assert!(
matches!(result, Err(NetworkError::InvalidCookie)),
"Station A's cookie must not be accepted for Station B"
);
}
#[test]
fn test_replayed_sequence_id_rejected() {
let mut ap = AccessPoint::new(AP_MAC).expect("AP setup");
let station = Station::new(STATION_MAC).expect("Station setup");
let seq_id = random_seq_id();
let fast_link = station.build_fast_link_frame().unwrap();
let cookie = ap.process_fast_link_frame(&fast_link, seq_id).unwrap();
let ap_mlkem_pk = ap.mlkem_public_key_bytes();
let station_x25519_pk = station.x25519_public_key_bytes().unwrap();
let (pq_frames, _) = station
.build_pq_fragments(&ap_mlkem_pk, seq_id, &cookie)
.unwrap();
for frame in &pq_frames {
let _ = ap.process_fragment(frame, &STATION_MAC, &station_x25519_pk);
}
let replay_result =
ap.process_fragment(&pq_frames[0], &STATION_MAC, &station_x25519_pk);
match replay_result {
Ok(Some(_)) => panic!("Replayed handshake must NOT produce a new session key without fresh cookie"),
_ => { }
}
}
}
#[cfg(test)]
mod zeroize_tests {
use crate::crypto::{SecretBytes, SessionKey, SESSION_KEY_LEN};
use zeroize::Zeroize;
#[test]
fn test_secret_bytes_zeroize_on_demand() {
let mut secret = SecretBytes(vec![0xAB; 64]);
assert!(secret.0.iter().all(|&b| b == 0xAB), "Pre-zeroize: should be 0xAB");
secret.zeroize();
assert!(secret.0.iter().all(|&b| b == 0x00), "Post-zeroize: all bytes must be 0x00");
}
#[test]
fn test_session_key_zeroize_on_demand() {
let mut key = SessionKey([0xDE; SESSION_KEY_LEN]);
assert!(key.0.iter().all(|&b| b == 0xDE));
key.zeroize();
assert!(key.0.iter().all(|&b| b == 0x00), "SessionKey bytes must be zeroed after zeroize()");
}
#[test]
fn test_session_key_debug_is_redacted() {
let key = SessionKey([0xFF; SESSION_KEY_LEN]);
let debug_str = format!("{:?}", key);
assert!(
!debug_str.contains("ff") && !debug_str.contains("FF") && !debug_str.contains("255"),
"SessionKey Debug output must not expose key bytes: got '{}'", debug_str
);
assert!(
debug_str.contains("REDACTED"),
"SessionKey Debug must say REDACTED"
);
}
}