diff --git a/ostp-core/src/crypto/obfuscation.rs b/ostp-core/src/crypto/obfuscation.rs index 6493ad9..8caeb76 100644 --- a/ostp-core/src/crypto/obfuscation.rs +++ b/ostp-core/src/crypto/obfuscation.rs @@ -23,14 +23,15 @@ pub fn derive_psk(access_key: &[u8]) -> [u8; 32] { psk } -/// Derives a unique 4-byte session_id mask using HMAC-SHA256(key, nonce). -/// Used strictly for handshake phase obfuscation. -fn derive_session_mask(key: &[u8; 8], nonce: u64) -> [u8; 4] { +/// Derives a 6-byte handshake mask using HMAC-SHA256(key, nonce). +/// Covers session_id (4 bytes) + noise_len (2 bytes) to prevent +/// DPI from seeing a constant length field in the handshake header. +fn derive_handshake_mask(key: &[u8; 8], nonce: u64) -> [u8; 6] { let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length"); mac.update(&nonce.to_be_bytes()); let result = mac.finalize().into_bytes(); - let mut mask = [0u8; 4]; - mask.copy_from_slice(&result[..4]); + let mut mask = [0u8; 6]; + mask.copy_from_slice(&result[..6]); mask } @@ -61,10 +62,10 @@ pub fn obfuscate_packet_inplace(raw: &mut [u8], key: &[u8; 8], is_handshake: boo raw[i] ^= mask_result[i]; } } - } else if raw.len() >= 4 { - // Handshake packets: mask session_id with a fixed handshake-phase mask - let mask = derive_session_mask(key, u64::MAX); - for i in 0..4 { + } else if raw.len() >= 6 { + // Handshake: mask session_id (4 bytes) + noise_len (2 bytes) + let mask = derive_handshake_mask(key, u64::MAX); + for i in 0..6 { raw[i] ^= mask[i]; } } @@ -90,8 +91,8 @@ pub fn deobfuscate_header_inplace( header[i] ^= mask_result[i]; } } else { - let mask = derive_session_mask(key, u64::MAX); - for i in 0..4 { + let mask = derive_handshake_mask(key, u64::MAX); + for i in 0..header.len().min(6) { header[i] ^= mask[i]; } } @@ -104,9 +105,9 @@ pub fn deobfuscate_packet_inplace(raw: &mut [u8], key: &[u8; 8], is_handshake: b header.copy_from_slice(header_slice); deobfuscate_header_inplace(&mut header, ciphertext, key, is_handshake); header_slice.copy_from_slice(&header); - } else if raw.len() >= 4 { - let mask = derive_session_mask(key, u64::MAX); - for i in 0..4 { + } else if raw.len() >= 6 { + let mask = derive_handshake_mask(key, u64::MAX); + for i in 0..6 { raw[i] ^= mask[i]; } } diff --git a/ostp-core/src/protocol.rs b/ostp-core/src/protocol.rs index 289f6e5..11f41c9 100644 --- a/ostp-core/src/protocol.rs +++ b/ostp-core/src/protocol.rs @@ -214,8 +214,20 @@ impl ProtocolMachine { } if self.state == OstpState::Handshaking { + // Wire format: [session_id:4][noise_len:2][noise_payload:N][random_padding:*] + // Extract noise_len to pass exactly the right bytes to snow + if raw_vec.len() < 6 { + return Err(ProtocolError::Framing("handshake too short for length prefix".to_string())); + } + let noise_len = u16::from_be_bytes([raw_vec[4], raw_vec[5]]) as usize; + if raw_vec.len() < 6 + noise_len { + return Err(ProtocolError::Framing(format!( + "handshake truncated: expected {} noise bytes, got {}", + noise_len, raw_vec.len() - 6 + ))); + } let mut read_out = vec![0_u8; 1024]; - let n = self.noise.read_handshake(&raw_vec[4..], &mut read_out)?; + let n = self.noise.read_handshake(&raw_vec[6..6 + noise_len], &mut read_out)?; let response = match self.role { NoiseRole::Responder => { @@ -380,15 +392,17 @@ impl ProtocolMachine { // Anti-DPI: add random padding after the Noise payload to prevent // size fingerprinting. Without this, every handshake is exactly 52 bytes // which is trivially detectable by TSPU/DPI systems. + // + // Wire format: [session_id:4][noise_len:2][noise_payload:N][random_padding:32-128] let pad_len: usize = rand::thread_rng().gen_range(32..=128); let mut pad = vec![0u8; pad_len]; rand::thread_rng().fill(&mut pad[..]); - let mut out = Vec::with_capacity(4 + noise_payload.len() + 2 + pad_len); + let noise_len = noise_payload.len() as u16; + let mut out = Vec::with_capacity(4 + 2 + noise_payload.len() + pad_len); out.extend_from_slice(&self.session_id.to_be_bytes()); + out.extend_from_slice(&noise_len.to_be_bytes()); out.extend_from_slice(noise_payload); - // 2-byte padding length prefix so receiver can strip it - out.extend_from_slice(&(pad_len as u16).to_be_bytes()); out.extend_from_slice(&pad); crate::crypto::obfuscate_packet_inplace(&mut out, &self.obfuscation_key, true); Ok(Bytes::from(out)) diff --git a/ostp-server/src/dispatcher.rs b/ostp-server/src/dispatcher.rs index 9e6ed26..e37c6b0 100644 --- a/ostp-server/src/dispatcher.rs +++ b/ostp-server/src/dispatcher.rs @@ -158,10 +158,12 @@ impl Dispatcher { let psk = ostp_core::crypto::derive_psk(candidate_key.as_bytes()); // Decode the session_id using this key's obfuscation - let mut header = [0u8; 4]; - header.copy_from_slice(&packet[0..4]); + // Handshake header is 6 bytes: [session_id:4][noise_len:2] + let mut header = [0u8; 6]; + if packet.len() < 6 { continue; } + header.copy_from_slice(&packet[0..6]); ostp_core::crypto::deobfuscate_packet_inplace(&mut header, &obf_key, true); - let candidate_session_id = u32::from_be_bytes(header); + let candidate_session_id = u32::from_be_bytes([header[0], header[1], header[2], header[3]]); let mut cfg = self.machine_config.clone(); cfg.session_id = candidate_session_id;