use crate::crypto::{
compute_cookie, derive_session_key, mlkem_encapsulate, verify_cookie, CryptoError, MlKemKeyPair,
SecretBytes, SessionKey, X25519KeyPair, HMAC_LEN, MLKEM_CT_LEN, X25519_PK_LEN,
};
use rand_core::RngCore;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const FRAG_PAYLOAD_MAX: usize = 400;
pub const MLKEM_PK_FRAG_COUNT: u8 = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FastLinkFrame {
pub magic: [u8; 4],
pub version: u8,
pub frame_type: u8,
pub x25519_public_key: [u8; X25519_PK_LEN],
pub station_mac: [u8; 6],
}
impl FastLinkFrame {
pub const MAGIC: [u8; 4] = *b"WPAN";
pub const FRAME_TYPE: u8 = 0x01;
pub fn new(x25519_pk: [u8; X25519_PK_LEN], station_mac: [u8; 6]) -> Self {
FastLinkFrame {
magic: Self::MAGIC,
version: 1,
frame_type: Self::FRAME_TYPE,
x25519_public_key: x25519_pk,
station_mac,
}
}
pub fn is_valid(&self) -> bool {
self.magic == Self::MAGIC && self.version == 1 && self.frame_type == Self::FRAME_TYPE
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentHeader {
pub magic: [u8; 4],
pub frame_type: u8,
pub sequence_id: u32,
pub frag_index: u8,
pub frag_total: u8,
pub payload_len: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentedPQFrame {
pub header: FragmentHeader,
pub cookie: Vec<u8>,
pub payload: Vec<u8>,
}
impl FragmentedPQFrame {
pub const FRAME_TYPE: u8 = 0x02;
}
pub fn fragment_payload(
payload: &[u8],
sequence_id: u32,
cookie: &[u8; HMAC_LEN],
) -> Vec<FragmentedPQFrame> {
let chunks: Vec<&[u8]> = payload.chunks(FRAG_PAYLOAD_MAX).collect();
let frag_total = chunks.len() as u8;
let mut frames = Vec::with_capacity(chunks.len());
for (idx, chunk) in chunks.iter().enumerate() {
let frag_index = idx as u8;
let frame_cookie = if frag_index == 0 { cookie.to_vec() } else { vec![0u8; HMAC_LEN] };
frames.push(FragmentedPQFrame {
header: FragmentHeader {
magic: FastLinkFrame::MAGIC,
frame_type: FragmentedPQFrame::FRAME_TYPE,
sequence_id,
frag_index,
frag_total,
payload_len: chunk.len() as u16,
},
cookie: frame_cookie,
payload: chunk.to_vec(),
});
}
frames
}
pub fn reassemble_fragments(frames: &[FragmentedPQFrame]) -> Option<Vec<u8>> {
if frames.is_empty() {
return None;
}
let frag_total = frames[0].header.frag_total as usize;
if frames.len() != frag_total {
return None; }
let mut sorted = frames.to_vec();
sorted.sort_by_key(|f| f.header.frag_index);
let seq_id = sorted[0].header.sequence_id;
for (expected_idx, frame) in sorted.iter().enumerate() {
if frame.header.frag_index as usize != expected_idx {
return None; }
if frame.header.sequence_id != seq_id {
return None; }
}
let mut reassembled = Vec::new();
for frame in &sorted {
reassembled.extend_from_slice(&frame.payload[..frame.header.payload_len as usize]);
}
Some(reassembled)
}
#[derive(Debug)]
struct StationHandshakeState {
x25519_pk: [u8; X25519_PK_LEN],
fragments: Vec<FragmentedPQFrame>,
frag_total: u8,
}
pub struct AccessPoint {
#[allow(dead_code)]
pub mac: [u8; 6],
mlkem_kp: MlKemKeyPair,
x25519_kp: Option<X25519KeyPair>,
cookie_secret: [u8; 32],
station_state: HashMap<u64, StationHandshakeState>,
}
impl AccessPoint {
pub fn new(mac: [u8; 6]) -> Result<Self, NetworkError> {
let mut secret = [0u8; 32];
rand_core::OsRng.fill_bytes(&mut secret);
Ok(AccessPoint {
mac,
mlkem_kp: MlKemKeyPair::generate().map_err(NetworkError::Crypto)?,
x25519_kp: Some(X25519KeyPair::generate().map_err(NetworkError::Crypto)?),
cookie_secret: secret,
station_state: HashMap::new(),
})
}
pub fn mlkem_public_key_bytes(&self) -> Vec<u8> {
self.mlkem_kp.public_key_bytes()
}
pub fn x25519_public_key_bytes(&self) -> Option<[u8; X25519_PK_LEN]> {
self.x25519_kp.as_ref().map(|kp| kp.public_key_bytes)
}
pub fn build_cookie(&self, station_mac: &[u8; 6], sequence_id: u32) -> [u8; HMAC_LEN] {
compute_cookie(&self.cookie_secret, station_mac, sequence_id)
}
pub fn process_fast_link_frame(
&mut self,
frame: &FastLinkFrame,
sequence_id: u32,
) -> Result<[u8; HMAC_LEN], NetworkError> {
if !frame.is_valid() {
return Err(NetworkError::InvalidFrame("FastLinkFrame magic/version check failed"));
}
println!(
"[AP] Received FastLinkFrame from station {:02X?} — issuing cookie challenge",
frame.station_mac
);
let cookie = self.build_cookie(&frame.station_mac, sequence_id);
Ok(cookie)
}
pub fn process_fragment(
&mut self,
frame: &FragmentedPQFrame,
station_mac: &[u8; 6],
station_x25519_pk: &[u8; X25519_PK_LEN],
) -> Result<Option<SessionKey>, NetworkError> {
let station_id = mac_to_u64(station_mac);
let seq_id = frame.header.sequence_id;
if frame.header.frag_index == 0 {
let cookie_arr: &[u8; HMAC_LEN] = frame.cookie.as_slice().try_into()
.map_err(|_| NetworkError::InvalidCookie)?;
if !verify_cookie(&self.cookie_secret, station_mac, seq_id, cookie_arr) {
println!("[AP] Cookie verification FAILED for station {:02X?} — dropping", station_mac);
return Err(NetworkError::InvalidCookie);
}
println!("[AP] Cookie verified for station {:02X?} — allocating reassembly state", station_mac);
self.station_state.insert(
station_id,
StationHandshakeState {
x25519_pk: *station_x25519_pk,
fragments: vec![frame.clone()],
frag_total: frame.header.frag_total,
},
);
return Ok(None); }
let state = self
.station_state
.get_mut(&station_id)
.ok_or(NetworkError::UnknownStation)?;
state.fragments.push(frame.clone());
if state.fragments.len() < state.frag_total as usize {
println!(
"[AP] Fragment {}/{} received for station {:02X?}",
state.fragments.len(),
state.frag_total,
station_mac
);
return Ok(None); }
println!("[AP] All {} fragments received — reassembling ML-KEM ciphertext", state.frag_total);
let ciphertext = reassemble_fragments(&state.fragments)
.ok_or(NetworkError::ReassemblyFailed)?;
if ciphertext.len() != MLKEM_CT_LEN {
return Err(NetworkError::InvalidFrame("Reassembled payload length mismatch"));
}
let pq_ss = self
.mlkem_kp
.decapsulate(&ciphertext)
.map_err(NetworkError::Crypto)?;
println!("[AP] ML-KEM-768 decapsulation successful");
let x25519_kp = self
.x25519_kp
.take()
.ok_or(NetworkError::X25519Consumed)?;
let classical_ss = x25519_kp
.diffie_hellman(&state.x25519_pk)
.map_err(NetworkError::Crypto)?;
println!("[AP] X25519 ECDH successful");
let session_key = derive_session_key(&classical_ss, &pq_ss)
.map_err(NetworkError::Crypto)?;
println!("[AP] Session key derived via HKDF-SHA384 hybrid combiner");
self.station_state.remove(&station_id);
Ok(Some(session_key))
}
}
pub struct Station {
pub mac: [u8; 6],
x25519_kp: Option<X25519KeyPair>,
}
impl Station {
pub fn new(mac: [u8; 6]) -> Result<Self, NetworkError> {
Ok(Station {
mac,
x25519_kp: Some(X25519KeyPair::generate().map_err(NetworkError::Crypto)?),
})
}
pub fn build_fast_link_frame(&self) -> Result<FastLinkFrame, NetworkError> {
let pk = self
.x25519_kp
.as_ref()
.ok_or(NetworkError::X25519Consumed)?
.public_key_bytes;
Ok(FastLinkFrame::new(pk, self.mac))
}
pub fn x25519_public_key_bytes(&self) -> Result<[u8; X25519_PK_LEN], NetworkError> {
self.x25519_kp
.as_ref()
.map(|kp| kp.public_key_bytes)
.ok_or(NetworkError::X25519Consumed)
}
pub fn build_pq_fragments(
&self,
ap_mlkem_pk: &[u8],
sequence_id: u32,
cookie: &[u8; HMAC_LEN],
) -> Result<(Vec<FragmentedPQFrame>, SecretBytes), NetworkError> {
let (ciphertext, pq_ss) =
mlkem_encapsulate(ap_mlkem_pk).map_err(NetworkError::Crypto)?;
println!(
"[Station] ML-KEM-768 encapsulation successful — ciphertext {} bytes",
ciphertext.len()
);
let frames = fragment_payload(&ciphertext, sequence_id, cookie);
println!(
"[Station] Ciphertext split into {} fragments (max {} bytes each)",
frames.len(),
FRAG_PAYLOAD_MAX
);
Ok((frames, pq_ss))
}
pub fn complete_handshake(
mut self,
ap_x25519_pk: &[u8; X25519_PK_LEN],
pq_ss: SecretBytes,
) -> Result<SessionKey, NetworkError> {
let x25519_kp = self.x25519_kp.take().ok_or(NetworkError::X25519Consumed)?;
let classical_ss = x25519_kp
.diffie_hellman(ap_x25519_pk)
.map_err(NetworkError::Crypto)?;
println!("[Station] X25519 ECDH successful");
let session_key = derive_session_key(&classical_ss, &pq_ss)
.map_err(NetworkError::Crypto)?;
println!("[Station] Session key derived via HKDF-SHA384 hybrid combiner");
Ok(session_key)
}
}
fn mac_to_u64(mac: &[u8; 6]) -> u64 {
let mut buf = [0u8; 8];
buf[2..8].copy_from_slice(mac);
u64::from_be_bytes(buf)
}
#[derive(Debug, thiserror::Error)]
pub enum NetworkError {
#[error("Cryptographic operation failed: {0}")]
Crypto(#[from] CryptoError),
#[error("Invalid frame: {0}")]
InvalidFrame(&'static str),
#[error("DoS cookie verification failed")]
InvalidCookie,
#[error("Fragment reassembly failed — incomplete or inconsistent fragments")]
ReassemblyFailed,
#[error("Unknown station — received non-initial fragment without prior state")]
UnknownStation,
#[error("X25519 key pair already consumed (single-use)")]
X25519Consumed,
}