Files
nym/common/nym-lp/src/codec.rs
T

1250 lines
45 KiB
Rust

// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::LpError;
use crate::message::{LpMessage, MessageType};
use crate::packet::{LpHeader, LpPacket, OuterHeader, TRAILER_LEN};
use bytes::{BufMut, BytesMut};
use chacha20poly1305::{
ChaCha20Poly1305, Key, Nonce, Tag,
aead::{AeadInPlace, KeyInit},
};
use zeroize::{Zeroize, ZeroizeOnDrop};
/// Size of outer header (receiver_idx + counter) - always cleartext
pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes
/// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode
const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
/// Outer AEAD key for LP packet encryption.
///
/// Derived from PSK using Blake3 KDF with domain separation.
/// Used for opportunistic encryption: before PSK packets are cleartext,
/// after PSK packets have encrypted payload and authenticated header.
///
/// # Security: Nonce Reuse Prevention
///
/// ChaCha20-Poly1305 requires unique nonces per key. The counter starts at 0
/// for each session, which is safe because:
///
/// 1. **PSK is always fresh**: Each handshake uses PSQ
/// with a client-generated random salt. This ensures a unique
/// PSK for every session, even between the same client-gateway pair.
///
/// 2. **Key derivation**: `outer_key = Blake3_KDF("lp-outer-aead", PSK)`.
/// Different PSK → different outer_key → nonce reuse impossible.
///
/// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions.
/// Each connection performs fresh KKT+PSQ handshake.
///
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct OuterAeadKey {
key: [u8; 32],
}
impl OuterAeadKey {
/// KDF context for outer AEAD key derivation (domain separation)
const KDF_CONTEXT: &'static str = "lp-outer-aead";
/// Derive outer AEAD key from PSK.
///
/// Uses Blake3 KDF with domain separation to avoid key reuse
/// between the outer AEAD layer and the inner Noise layer.
pub fn from_psk(psk: &[u8; 32]) -> Self {
let key = nym_crypto::kdf::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]);
Self { key }
}
/// Get reference to the raw key bytes.
pub fn as_bytes(&self) -> &[u8; 32] {
&self.key
}
}
impl std::fmt::Debug for OuterAeadKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OuterAeadKey")
.field("key", &"[REDACTED]")
.finish()
}
}
/// Build 12-byte nonce from 8-byte counter (zero-padded).
///
/// Format: counter (8 bytes LE) || 0x00000000 (4 bytes)
fn build_nonce(counter: u64) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[..8].copy_from_slice(&counter.to_le_bytes());
// bytes 8..12 remain zero (zero-padding)
nonce
}
/// Parse message from raw type and content bytes.
///
/// Used when decrypting outer-encrypted packets where the message type
/// was encrypted along with the content.
fn parse_message_from_type_and_content(
msg_type_raw: u32,
content: &[u8],
) -> Result<LpMessage, LpError> {
let message_type = MessageType::from_u32(msg_type_raw)
.ok_or_else(|| LpError::invalid_message_type(msg_type_raw))?;
LpMessage::decode_content(content, message_type)
}
/// Parse only the outer header from raw packet bytes.
///
/// Used for routing before session lookup. The outer header (receiver_idx + counter)
/// is always cleartext at bytes 0-12 in the unified packet format.
///
/// # Arguments
/// * `src` - Raw packet bytes (at least OuterHeader::SIZE bytes)
///
/// # Errors
/// * `LpError::InsufficientBufferSize` - Packet too small for outer header
pub fn parse_lp_header_only(src: &[u8]) -> Result<OuterHeader, LpError> {
OuterHeader::parse(src)
}
/// Parses a complete Lewes Protocol packet from a byte slice (e.g., a UDP datagram payload).
///
/// ## Unified Packet Format
///
/// Both cleartext and encrypted packets have the same structure:
/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext
/// - Inner payload: proto(1) + reserved(3) + msg_type(4) + content - cleartext or encrypted
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
///
/// # Arguments
/// * `src` - Raw packet bytes
/// * `outer_key` - None for cleartext parsing, Some for AEAD decryption
///
/// # Errors
/// * `LpError::AeadTagMismatch` - Tag verification failed (when outer_key provided)
/// * `LpError::InsufficientBufferSize` - Packet too small
pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<LpPacket, LpError> {
// Minimum size check: OuterHeader + InnerPrefix + MsgType + Trailer (for 0-payload message)
// 12 + 4 + 2 + 16 = 34 bytes
let min_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 2 + TRAILER_LEN;
if src.len() < min_size {
return Err(LpError::InsufficientBufferSize);
}
// Parse outer header (always cleartext at bytes 0-12)
let outer_header = OuterHeader::parse(src)?;
// Extract trailer (potential AEAD tag)
let trailer_start = src.len() - TRAILER_LEN;
let mut trailer = [0u8; TRAILER_LEN];
trailer.copy_from_slice(&src[trailer_start..]);
// Inner payload is everything between outer header and trailer
let inner_bytes = &src[OUTER_HEADER_SIZE..trailer_start];
// Handle decryption if outer key provided
match outer_key {
None => {
// Cleartext mode - parse inner directly
// Inner format: proto(1) + reserved(3) + msg_type(4) + content
if inner_bytes.len() < INNER_PREFIX_SIZE + 4 {
return Err(LpError::InsufficientBufferSize);
}
let protocol_version = inner_bytes[0];
// reserved bytes [1..4] are ignored
let msg_type = u32::from_le_bytes([
inner_bytes[4],
inner_bytes[5],
inner_bytes[6],
inner_bytes[7],
]);
let message_content = &inner_bytes[8..];
let header = LpHeader {
protocol_version,
reserved: [0u8; 3],
receiver_idx: outer_header.receiver_idx,
counter: outer_header.counter,
};
let message = parse_message_from_type_and_content(msg_type, message_content)?;
Ok(LpPacket {
header,
message,
trailer,
})
}
Some(key) => {
// AEAD decryption mode
// AAD is the outer header (12 bytes)
let nonce = build_nonce(outer_header.counter);
let aad = &src[..OUTER_HEADER_SIZE];
// Copy inner payload for in-place decryption
let mut decrypted = inner_bytes.to_vec();
// Convert trailer to Tag
let tag = Tag::from_slice(&trailer);
// Decrypt and verify
let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
cipher
.decrypt_in_place_detached(Nonce::from_slice(&nonce), aad, &mut decrypted, tag)
.map_err(|_| LpError::AeadTagMismatch)?;
// Decrypted format: proto(1) + reserved(3) + msg_type(4) + content
if decrypted.len() < INNER_PREFIX_SIZE + 4 {
return Err(LpError::InsufficientBufferSize);
}
let protocol_version = decrypted[0];
// reserved bytes [1..4] are ignored
let msg_type =
u32::from_le_bytes([decrypted[4], decrypted[5], decrypted[6], decrypted[7]]);
let message_content = &decrypted[8..];
let header = LpHeader {
protocol_version,
reserved: [0u8; 3],
receiver_idx: outer_header.receiver_idx,
counter: outer_header.counter,
};
let message = parse_message_from_type_and_content(msg_type, message_content)?;
Ok(LpPacket {
header,
message,
trailer,
})
}
}
}
/// Serializes an LpPacket into the provided BytesMut buffer.
///
/// ## Unified Packet Format
///
/// Both cleartext and encrypted packets have the same structure:
/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext
/// - Inner payload: proto(1) + reserved(3) + msg_type(4) + content - cleartext or encrypted
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
///
/// # Arguments
/// * `item` - Packet to serialize
/// * `dst` - Output buffer
/// * `outer_key` - None for cleartext, Some for AEAD encryption
pub fn serialize_lp_packet(
item: &LpPacket,
dst: &mut BytesMut,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), LpError> {
// Total size: outer_header(12) + inner_prefix(4) + msg_type(4) + content + trailer(16)
let total_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 4 + item.message.len() + TRAILER_LEN;
dst.reserve(total_size);
// 1. Write outer header (always cleartext) - 12 bytes
let outer_header = OuterHeader::new(item.header.receiver_idx, item.header.counter);
outer_header.encode_into(dst);
match outer_key {
None => {
// Cleartext mode
// 2. Write inner prefix: proto(1) + reserved(3)
dst.put_u8(item.header.protocol_version);
dst.put_slice(&item.header.reserved); // reserved
// 3. Write message type (4B) + content
dst.put_slice(&(item.message.typ() as u32).to_le_bytes());
item.message.encode_content(dst);
// 4. Write zeros trailer
dst.put_slice(&[0u8; TRAILER_LEN]);
Ok(())
}
Some(key) => {
// AEAD encryption mode
// AAD is the outer header (first 12 bytes)
let aad = outer_header.encode();
// 2. Build plaintext: proto(1) + reserved(3) + msg_type(4) + content
let mut plaintext = BytesMut::new();
plaintext.put_u8(item.header.protocol_version);
plaintext.put_slice(&item.header.reserved); // reserved
plaintext.put_slice(&(item.message.typ() as u32).to_le_bytes());
item.message.encode_content(&mut plaintext);
// 3. Copy plaintext to dst for in-place encryption
let payload_start = dst.len();
dst.put_slice(&plaintext);
// 4. Build nonce from counter
let nonce = build_nonce(item.header.counter);
// 5. Encrypt payload in-place
let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
let tag = cipher
.encrypt_in_place_detached(
Nonce::from_slice(&nonce),
&aad,
&mut dst[payload_start..],
)
.map_err(|_| LpError::Internal("AEAD encryption failed".to_string()))?;
// 6. Append tag as trailer
dst.put_slice(&tag);
Ok(())
}
}
}
// Add a new error variant for invalid message types (Moved from previous impl LpError block)
impl LpError {
pub fn invalid_message_type(message_type: u32) -> Self {
LpError::InvalidMessageType(message_type)
}
}
#[cfg(test)]
mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
// Import standalone functions
use super::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
// Keep necessary imports
use crate::LpError;
use crate::message::{EncryptedDataPayload, HandshakeData, LpMessage, MessageType};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use bytes::BytesMut;
use nym_crypto::asymmetric::{ed25519, x25519};
use rand::thread_rng;
// With unified format, outer header (receiver_idx + counter) is always first
// and is the only cleartext portion for encrypted packets
const OUTER_HDR: usize = super::OUTER_HEADER_SIZE; // 12 bytes
// === Cleartext Encode/Decode Tests ===
#[test]
fn test_serialize_parse_busy() {
let mut dst = BytesMut::new();
// Create a Busy packet
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 123,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
// Serialize the packet (cleartext)
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet (cleartext)
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
assert!(matches!(decoded.message, LpMessage::Busy));
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
}
#[test]
fn test_serialize_parse_handshake() {
let mut dst = BytesMut::new();
// Create a Handshake message packet
let payload = vec![42u8; 80]; // Example payload size
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 123,
},
message: LpMessage::Handshake(HandshakeData(payload.clone())),
trailer: [0; TRAILER_LEN],
};
// Serialize the packet (cleartext)
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet (cleartext)
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
// Verify message type and data
match decoded.message {
LpMessage::Handshake(decoded_payload) => {
assert_eq!(decoded_payload, HandshakeData(payload));
}
_ => panic!("Expected Handshake message"),
}
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
}
#[test]
fn test_serialize_parse_encrypted_data() {
let mut dst = BytesMut::new();
// Create an EncryptedData message packet
let payload = vec![43u8; 124]; // Example payload size
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 123,
},
message: LpMessage::EncryptedData(EncryptedDataPayload(payload.clone())),
trailer: [0; TRAILER_LEN],
};
// Serialize the packet (cleartext)
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet (cleartext)
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
// Verify message type and data
match decoded.message {
LpMessage::EncryptedData(decoded_payload) => {
assert_eq!(decoded_payload, EncryptedDataPayload(payload));
}
_ => panic!("Expected EncryptedData message"),
}
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
}
// === Incomplete Data Tests ===
#[test]
fn test_parse_incomplete_header() {
// Create a buffer with incomplete header
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Only 4 bytes, not enough for LpHeader::SIZE
// Attempt to parse - expect error
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
LpError::InsufficientBufferSize
));
}
#[test]
fn test_parse_incomplete_message_type() {
// Create a buffer with complete header but incomplete message type
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&[0]); // Only 1 byte of message type (need 2)
// Buffer length = 16 + 1 = 17. Min size = 16 + 2 + 16 = 34.
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
LpError::InsufficientBufferSize
));
}
#[test]
fn test_parse_incomplete_message_data() {
// Create a buffer simulating Handshake but missing trailer and maybe partial payload
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // Handshake type
buf.extend_from_slice(&[42; 40]); // 40 bytes of payload data
// Buffer length = 16 + 2 + 40 = 58. Min size = 16 + 2 + 16 = 34.
// Payload size calculated as 58 - 34 = 24.
// Trailer expected at index 16 + 2 + 24 = 42.
// Trailer read attempts src[42..58].
// This *should* parse successfully based on the logic, but the trailer is garbage.
// Let's rethink: parse_lp_packet assumes the *entire slice* is the packet.
// If the slice doesn't end exactly where the trailer should, it's an error.
// In this case, total length is 58. OuterHdr(12) + InnerPrefix(4) + Type(2) + Trailer(16) = 34. Payload = 58-34=24.
// Trailer starts at 16+2+24 = 42. Ends at 42+16=58. It fits exactly.
// This test *still* doesn't test incompleteness correctly for the datagram parser.
// Let's test a buffer that's *too short* even for header+type+trailer+min_payload
// Note: Buffer order doesn't matter for this test since we fail on minimum size check
let mut buf_too_short = BytesMut::new();
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx (outer header)
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // counter (outer header)
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // version + reserved (inner prefix)
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // msg type
// No payload, no trailer. Length = 12+4+2=18. Min size = 34.
let result_too_short = parse_lp_packet(&buf_too_short, None);
assert!(result_too_short.is_err());
assert!(matches!(
result_too_short.unwrap_err(),
LpError::InsufficientBufferSize
));
// Test a buffer missing PART of the trailer
let mut buf_partial_trailer = BytesMut::new();
buf_partial_trailer.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf_partial_trailer.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf_partial_trailer.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf_partial_trailer.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // Handshake type
let payload = vec![42u8; 20]; // Assume 20 byte payload
buf_partial_trailer.extend_from_slice(&payload);
buf_partial_trailer.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer
// Total length = 16 + 2 + 20 + 15 = 53. Min size = 34. This passes.
// Payload size = 53 - 34 = 19. <--- THIS IS WRONG. The parser assumes the length dictates payload.
// Let's fix the parser logic slightly.
// The point is, parse_lp_packet expects a COMPLETE datagram. Providing less bytes
// than LpHeader + Type + Trailer should fail. Providing *more* is also an issue unless
// the length calculation works out perfectly. The most direct test is just < min_size.
// Renaming test to reflect this.
}
#[test]
fn test_parse_buffer_smaller_than_minimum() {
// Test a buffer that's smaller than the smallest possible packet (LpHeader+Type+Trailer)
let mut buf_too_short = BytesMut::new();
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf_too_short.extend_from_slice(&MessageType::Busy.to_u32().to_le_bytes()); // Type
buf_too_short.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer
// Length = 16 + 2 + 15 = 33. Min Size = 34.
let result_too_short = parse_lp_packet(&buf_too_short, None);
assert!(
result_too_short.is_err(),
"Expected error for buffer size 33, min 34"
);
assert!(matches!(
result_too_short.unwrap_err(),
LpError::InsufficientBufferSize
));
}
#[test]
fn test_parse_invalid_message_type() {
// Create a buffer with invalid message type
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
// Need payload and trailer to meet min_size requirement
let payload_size = 10; // Arbitrary
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Attempt to parse
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::InvalidMessageType(255)) => {} // Expected error
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
Ok(_) => panic!("Expected error, but got Ok"),
}
}
#[test]
fn test_parse_incorrect_payload_size_for_busy() {
// Create a Busy packet but *with* a payload (which is invalid)
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::Busy.to_u32().to_le_bytes()); // Busy type
buf.extend_from_slice(&[42; 1]); // <<< Invalid 1-byte payload for Busy
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Total size = 16 + 2 + 1 + 16 = 35. Min size = 34.
// Calculated payload size = 35 - 34 = 1.
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
LpError::InvalidPayloadSize {
expected: 0,
actual: 1
}
));
}
// Test multiple packets simulation isn't relevant for datagram parsing
// #[test]
// fn test_multiple_packets_in_buffer() { ... }
// === ClientHello Serialization Tests ===
#[test]
fn test_serialize_parse_client_hello() {
use crate::message::ClientHelloData;
let mut rng = thread_rng();
let valid_ed25519 = ed25519::KeyPair::new(&mut rng);
let mut dst = BytesMut::new();
// Create ClientHelloData
let client_key = x25519::PublicKey::from_byte_array(&[42u8; 32]);
let client_ed25519_key = *valid_ed25519.public_key();
let salt = [99u8; 32];
let hello_data = ClientHelloData {
receiver_index: 12345,
client_lp_public_key: client_key,
client_ed25519_public_key: client_ed25519_key,
salt,
};
// Create a ClientHello message packet
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 123,
},
message: LpMessage::ClientHello(hello_data.clone()),
trailer: [0; TRAILER_LEN],
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
// Verify message type and data
match decoded.message {
LpMessage::ClientHello(decoded_data) => {
assert_eq!(decoded_data.client_lp_public_key, client_key);
assert_eq!(decoded_data.salt, salt);
}
_ => panic!("Expected ClientHello message"),
}
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
}
#[test]
fn test_serialize_parse_client_hello_with_fresh_salt() {
use crate::message::ClientHelloData;
let mut dst = BytesMut::new();
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time before UNIX epoch")
.as_secs();
// Create ClientHelloData with fresh salt
let mut rng = thread_rng();
let valid_ed25519 = ed25519::KeyPair::new(&mut rng);
let client_key = x25519::PublicKey::from_byte_array(&[7u8; 32]);
let client_ed25519_key = *valid_ed25519.public_key();
let hello_data =
ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
// Create a ClientHello message packet
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 100,
counter: 200,
},
message: LpMessage::ClientHello(hello_data.clone()),
trailer: [55; TRAILER_LEN],
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify message type and data
match decoded.message {
LpMessage::ClientHello(decoded_data) => {
assert_eq!(decoded_data.client_lp_public_key, client_key);
assert_eq!(decoded_data.salt, hello_data.salt);
// Verify timestamp can be extracted
let timestamp = decoded_data.extract_timestamp();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// Timestamp should be within 2 seconds of now
assert!((timestamp as i64 - now as i64).abs() <= 2);
}
_ => panic!("Expected ClientHello message"),
}
}
#[test]
fn test_parse_client_hello_malformed_data() {
// Create a buffer with ClientHello message type but invalid bincode data
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::ClientHello.to_u32().to_le_bytes()); // ClientHello type
// Add data that does not equal to ClientHelloData::LEN
buf.extend_from_slice(&[0xFF; 50]); // invalid data
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Attempt to parse
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::DeserializationError(_)) => {} // Expected error
Err(e) => panic!("Expected DeserializationError, got {:?}", e),
Ok(_) => panic!("Expected error, but got Ok"),
}
}
#[test]
fn test_parse_client_hello_incomplete_bincode() {
// Create a buffer with ClientHello but truncated bincode data
let mut buf = BytesMut::new();
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::ClientHello.to_u32().to_le_bytes()); // ClientHello type
// Add incomplete bincode data (only partial ClientHelloData)
buf.extend_from_slice(&[0; 20]); // Too few bytes for full ClientHelloData
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Attempt to parse
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::DeserializationError(_)) => {} // Expected error
Err(e) => panic!("Expected DeserializationError, got {:?}", e),
Ok(_) => panic!("Expected error, but got Ok"),
}
}
#[test]
fn test_forward_packet_encode_decode_roundtrip() {
let mut dst = BytesMut::new();
let forward_data = crate::message::ForwardPacketData {
target_gateway_identity: [77u8; 32],
target_lp_address: "1.2.3.4:41264".to_string(),
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 999,
counter: 555,
},
message: LpMessage::ForwardPacket(forward_data),
trailer: [0xff; TRAILER_LEN],
};
// Serialize
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse back
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify LP protocol handling works correctly
assert_eq!(decoded.header.receiver_idx, 999);
assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket));
if let LpMessage::ForwardPacket(data) = decoded.message {
assert_eq!(data.target_gateway_identity, [77u8; 32]);
assert_eq!(data.target_lp_address, "1.2.3.4:41264");
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
} else {
panic!("Expected ForwardPacket message");
}
}
// === Outer AEAD Tests ===
#[test]
fn test_aead_roundtrip_with_key() {
// Test that encrypt/decrypt roundtrip works with an AEAD key
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Parse back with the same key
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.receiver_idx, 12345);
assert_eq!(decoded.header.counter, 999);
assert!(matches!(decoded.message, LpMessage::Busy));
}
#[test]
fn test_aead_ciphertext_differs_from_plaintext() {
// Verify that encrypted payload differs from plaintext
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::EncryptedData(crate::message::EncryptedDataPayload(vec![
0xAA, 0xBB, 0xCC, 0xDD,
])),
trailer: [0; TRAILER_LEN],
};
let mut cleartext = BytesMut::new();
serialize_lp_packet(&packet, &mut cleartext, None).unwrap();
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Outer header (receiver_idx + counter) should be the same - always cleartext
assert_eq!(&cleartext[..OUTER_HDR], &encrypted[..OUTER_HDR]);
// Inner payload (proto + reserved + msg_type + content) should differ (encrypted)
let payload_start = OUTER_HDR;
let payload_end_cleartext = cleartext.len() - TRAILER_LEN;
let payload_end_encrypted = encrypted.len() - TRAILER_LEN;
assert_ne!(
&cleartext[payload_start..payload_end_cleartext],
&encrypted[payload_start..payload_end_encrypted],
"Encrypted payload should differ from plaintext"
);
// Trailer should differ (zeros vs AEAD tag)
assert_ne!(
&cleartext[payload_end_cleartext..],
&encrypted[payload_end_encrypted..],
"Encrypted trailer should be a tag, not zeros"
);
}
#[test]
fn test_aead_tampered_tag_fails() {
// Verify that tampering with the tag causes decryption failure
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Tamper with the tag (last byte)
let last_idx = encrypted.len() - 1;
encrypted[last_idx] ^= 0xFF;
// Parsing should fail with AeadTagMismatch
let result = parse_lp_packet(&encrypted, Some(&outer_key));
assert!(matches!(result, Err(LpError::AeadTagMismatch)));
}
#[test]
fn test_aead_tampered_header_fails() {
// Verify that tampering with the header (AAD) causes decryption failure
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Tamper with the outer header AAD (flip a bit in counter at byte 4)
// New format: [receiver_idx(0-3), counter(4-11)], so byte 4 is counter's LSB
encrypted[4] ^= 0x01;
// Parsing should fail with AeadTagMismatch
let result = parse_lp_packet(&encrypted, Some(&outer_key));
assert!(matches!(result, Err(LpError::AeadTagMismatch)));
}
#[test]
fn test_aead_different_counters_produce_different_ciphertext() {
// Verify that different counters (nonces) produce different ciphertexts
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet1 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 1,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let packet2 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 2, // Different counter
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut encrypted1, Some(&outer_key)).unwrap();
let mut encrypted2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut encrypted2, Some(&outer_key)).unwrap();
// The encrypted inner payloads should differ even though the message is the same
// (because nonce is different). Inner payload starts after outer header.
let payload_start = OUTER_HDR;
assert_ne!(
&encrypted1[payload_start..],
&encrypted2[payload_start..],
"Different counters should produce different ciphertexts"
);
}
#[test]
fn test_aead_wrong_key_fails() {
// Verify that decryption with wrong key fails
let psk1 = [42u8; 32];
let psk2 = [43u8; 32]; // Different PSK
let outer_key1 = OuterAeadKey::from_psk(&psk1);
let outer_key2 = OuterAeadKey::from_psk(&psk2);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key1)).unwrap();
// Parsing with wrong key should fail
let result = parse_lp_packet(&encrypted, Some(&outer_key2));
assert!(matches!(result, Err(LpError::AeadTagMismatch)));
}
#[test]
fn test_aead_encrypted_data_message_roundtrip() {
// Test AEAD with EncryptedData message type (larger payload)
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let payload_data = vec![0xDE; 100];
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 54321,
counter: 12345678,
},
message: LpMessage::EncryptedData(crate::message::EncryptedDataPayload(
payload_data.clone(),
)),
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
match decoded.message {
LpMessage::EncryptedData(data) => {
assert_eq!(data.0, payload_data);
}
_ => panic!("Expected EncryptedData message"),
}
}
#[test]
fn test_aead_handshake_message_roundtrip() {
// Test AEAD with Handshake message type
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let handshake_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 99999,
counter: 2,
},
message: LpMessage::Handshake(HandshakeData(handshake_data.clone())),
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
match decoded.message {
LpMessage::Handshake(data) => {
assert_eq!(data.0, handshake_data);
}
_ => panic!("Expected Handshake message"),
}
}
// === Subsession Message Tests ===
#[test]
fn test_serialize_parse_subsession_request() {
let mut dst = BytesMut::new();
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 100,
},
message: LpMessage::SubsessionRequest,
trailer: [0; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 100);
assert!(matches!(decoded.message, LpMessage::SubsessionRequest));
}
#[test]
fn test_serialize_parse_subsession_kk1() {
use crate::message::SubsessionKK1Data;
let mut dst = BytesMut::new();
let kk1_data = SubsessionKK1Data {
payload: vec![0xAA; 50], // 50 bytes KK payload
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 123,
counter: 456,
},
message: LpMessage::SubsessionKK1(kk1_data.clone()),
trailer: [0; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
assert_eq!(decoded.header.receiver_idx, 123);
match decoded.message {
LpMessage::SubsessionKK1(data) => {
assert_eq!(data.payload, kk1_data.payload);
}
_ => panic!("Expected SubsessionKK1 message"),
}
}
#[test]
fn test_serialize_parse_subsession_kk2() {
use crate::message::SubsessionKK2Data;
let mut dst = BytesMut::new();
let kk2_data = SubsessionKK2Data {
payload: vec![0x11; 60], // 60 bytes KK response payload
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 789,
counter: 1000,
},
message: LpMessage::SubsessionKK2(kk2_data.clone()),
trailer: [0; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
assert_eq!(decoded.header.receiver_idx, 789);
match decoded.message {
LpMessage::SubsessionKK2(data) => {
assert_eq!(data.payload, kk2_data.payload);
}
_ => panic!("Expected SubsessionKK2 message"),
}
}
#[test]
fn test_serialize_parse_subsession_ready() {
use crate::message::SubsessionReadyData;
let mut dst = BytesMut::new();
let ready_data = SubsessionReadyData {
receiver_index: 99999,
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 200,
},
message: LpMessage::SubsessionReady(ready_data.clone()),
trailer: [0; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
assert_eq!(decoded.header.receiver_idx, 42);
match decoded.message {
LpMessage::SubsessionReady(data) => {
assert_eq!(data.receiver_index, 99999);
}
_ => panic!("Expected SubsessionReady message"),
}
}
#[test]
fn test_subsession_request_with_payload_fails() {
// SubsessionRequest should have no payload
let mut buf = BytesMut::new();
buf.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx
buf.extend_from_slice(&123u64.to_le_bytes()); // counter
buf.extend_from_slice(&[1, 0, 0, 0]); // version + reserved
buf.extend_from_slice(&MessageType::SubsessionRequest.to_u32().to_le_bytes());
buf.extend_from_slice(&[0xFF]); // Invalid payload for SubsessionRequest
buf.extend_from_slice(&[0; TRAILER_LEN]);
let result = parse_lp_packet(&buf, None);
assert!(matches!(
result,
Err(LpError::InvalidPayloadSize {
expected: 0,
actual: 1
})
));
}
#[test]
fn test_aead_subsession_roundtrip() {
use crate::message::SubsessionKK1Data;
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let kk1_data = SubsessionKK1Data {
payload: vec![0xDE; 48], // 48 bytes KK payload
};
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 54321,
counter: 999,
},
message: LpMessage::SubsessionKK1(kk1_data.clone()),
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
match decoded.message {
LpMessage::SubsessionKK1(data) => {
assert_eq!(data.payload, kk1_data.payload);
}
_ => panic!("Expected SubsessionKK1 message"),
}
}
}