#![forbid(unsafe_code)]
use siphasher::sip::SipHasher13;
use std::hash::Hasher;
use subtle::ConstantTimeEq;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub conn_tag: u64,
pub object_id: u16,
pub payload_id: [u8; 4],
pub flags: u8,
pub payload: Vec<u8>,
}
pub const HEADER_LEN: usize = 15;
pub const TAG_LEN: usize = 8;
pub const MIN_FRAME: usize = HEADER_LEN + TAG_LEN;
fn write_header(frame: &Frame) -> [u8; HEADER_LEN] {
let mut out = [0u8; HEADER_LEN];
out[0..8].copy_from_slice(&frame.conn_tag.to_be_bytes());
out[8..10].copy_from_slice(&frame.object_id.to_be_bytes());
out[10..14].copy_from_slice(&frame.payload_id);
out[14] = frame.flags;
out
}
fn read_header(bytes: &[u8; HEADER_LEN]) -> (u64, u16, [u8; 4], u8) {
let conn_tag = u64::from_be_bytes(bytes[0..8].try_into().expect("8 bytes"));
let object_id = u16::from_be_bytes(bytes[8..10].try_into().expect("2 bytes"));
let payload_id: [u8; 4] = bytes[10..14].try_into().expect("4 bytes");
let flags = bytes[14];
(conn_tag, object_id, payload_id, flags)
}
#[inline]
fn auth_tag(auth_key: &[u8; 16], header: &[u8; HEADER_LEN], payload: &[u8]) -> [u8; TAG_LEN] {
let mut hasher = SipHasher13::new_with_key(auth_key);
hasher.write(header);
hasher.write(payload);
hasher.finish().to_be_bytes()
}
#[inline]
fn header_mask(hp_key: &[u8; 16], sample: &[u8]) -> [u8; HEADER_LEN] {
let mut out = [0u8; HEADER_LEN];
let mut h0 = SipHasher13::new_with_key(hp_key);
h0.write(sample);
h0.write(&0u32.to_be_bytes());
out[0..8].copy_from_slice(&h0.finish().to_be_bytes());
let mut h1 = SipHasher13::new_with_key(hp_key);
h1.write(sample);
h1.write(&1u32.to_be_bytes());
out[8..15].copy_from_slice(&h1.finish().to_be_bytes()[..7]);
out
}
fn xor_in_place(buf: &mut [u8], mask: &[u8]) {
for (b, m) in buf.iter_mut().zip(mask.iter()) {
*b ^= *m;
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum WireError {
#[error("authentication failed")]
AuthFailed,
#[error("malformed datagram")]
Malformed,
}
pub struct Codec {
auth_key: [u8; 16],
hp_key: [u8; 16],
}
impl Codec {
pub fn new(auth_key: [u8; 16], hp_key: [u8; 16]) -> Self {
Self { auth_key, hp_key }
}
}
pub trait WireCodec {
fn frame(&self, frame: &Frame) -> Vec<u8>;
fn deframe(&self, datagram: &[u8]) -> Result<Frame, WireError>;
}
impl WireCodec for Codec {
fn frame(&self, frame: &Frame) -> Vec<u8> {
let header = write_header(frame);
let tag = auth_tag(&self.auth_key, &header, &frame.payload);
let mask = header_mask(&self.hp_key, &tag);
let mut out = Vec::with_capacity(HEADER_LEN + frame.payload.len() + TAG_LEN);
out.extend_from_slice(&header);
xor_in_place(&mut out[..HEADER_LEN], &mask);
out.extend_from_slice(&frame.payload);
out.extend_from_slice(&tag);
out
}
fn deframe(&self, datagram: &[u8]) -> Result<Frame, WireError> {
if datagram.len() < MIN_FRAME {
return Err(WireError::Malformed);
}
let tag = &datagram[datagram.len() - TAG_LEN..];
let mask = header_mask(&self.hp_key, tag);
let mut header = [0u8; HEADER_LEN];
header.copy_from_slice(&datagram[..HEADER_LEN]);
xor_in_place(&mut header, &mask);
let payload = &datagram[HEADER_LEN..datagram.len() - TAG_LEN];
let expected = auth_tag(&self.auth_key, &header, payload);
if expected.ct_eq(tag).unwrap_u8() != 1 {
return Err(WireError::AuthFailed);
}
let (conn_tag, object_id, payload_id, flags) = read_header(&header);
Ok(Frame {
conn_tag,
object_id,
payload_id,
flags,
payload: payload.to_vec(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_tag_is_keyed_and_covers_input() {
let k1 = [1u8; 16];
let k2 = [2u8; 16];
let h1 = [0x11u8; HEADER_LEN];
let h2 = [0x22u8; HEADER_LEN];
let a = auth_tag(&k1, &h1, b"hello");
let b = auth_tag(&k1, &h1, b"hello");
let c = auth_tag(&k1, &h1, b"hellp"); let d = auth_tag(&k2, &h1, b"hello"); let e = auth_tag(&k1, &h2, b"hello"); assert_eq!(a, b, "deterministic for same key+input");
assert_ne!(a, c, "changes when payload bytes change");
assert_ne!(a, d, "changes when key changes");
assert_ne!(a, e, "changes when header bytes change");
}
#[test]
fn auth_tag_multi_slice_matches_contiguous() {
let key = [0x42u8; 16];
let header = [0x33u8; HEADER_LEN];
let payload = vec![0x77u8; 1200];
let tag = auth_tag(&key, &header, &payload);
let mut ref_hasher = SipHasher13::new_with_key(&key);
let mut contiguous = Vec::with_capacity(HEADER_LEN + payload.len());
contiguous.extend_from_slice(&header);
contiguous.extend_from_slice(&payload);
ref_hasher.write(&contiguous);
let expected = ref_hasher.finish().to_be_bytes();
assert_eq!(
tag, expected,
"streaming multi-slice matches contiguous hash"
);
}
#[test]
fn frame_carries_object_id() {
let frame = Frame {
conn_tag: 7,
object_id: 42,
payload_id: [0; 4],
flags: 0,
payload: vec![1, 2, 3],
};
assert_eq!(frame.object_id, 42);
}
#[test]
fn header_mask_masks_reversibly_and_hides_constants() {
let hp = [3u8; 16];
let sample = [0xAAu8; TAG_LEN];
let mut header = [0u8; HEADER_LEN]; let mask = header_mask(&hp, &sample);
assert_eq!(mask.len(), HEADER_LEN);
xor_in_place(&mut header, &mask);
assert_ne!(
header, [0u8; HEADER_LEN],
"constant header is hidden after masking"
);
xor_in_place(&mut header, &mask);
assert_eq!(header, [0u8; HEADER_LEN], "masking is reversible");
let mask2 = header_mask(&hp, &[0xBBu8; TAG_LEN]);
assert_ne!(mask, mask2);
}
#[test]
fn header_mask_consecutive_blocks_are_distinct() {
let hp = [7u8; 16];
let sample = [0x55u8; TAG_LEN];
let mask = header_mask(&hp, &sample);
assert_eq!(mask.len(), HEADER_LEN);
assert_ne!(
&mask[..7],
&mask[8..15],
"block-0 (counter=0) and block-1 (counter=1) must differ"
);
}
#[test]
fn min_frame_equals_header_plus_tag() {
assert_eq!(
MIN_FRAME,
HEADER_LEN + TAG_LEN,
"MIN_FRAME must be HEADER_LEN + TAG_LEN"
);
}
#[test]
fn codec_accepts_exact_min_frame_datagram() {
let codec = Codec::new([9u8; 16], [10u8; 16]);
let frame = Frame {
conn_tag: 0xCAFE_BABE_0000_0001,
object_id: 3,
payload_id: [0xAB, 0xCD, 0xEF, 0x01],
flags: 0x42,
payload: vec![], };
let wire = codec.frame(&frame);
assert_eq!(
wire.len(),
MIN_FRAME,
"empty-payload frame must be exactly MIN_FRAME bytes"
);
assert_eq!(
codec.deframe(&wire).unwrap(),
frame,
"exact-MIN_FRAME datagram must be accepted"
);
}
#[test]
fn codec_roundtrips_a_frame() {
let codec = Codec::new([4u8; 16], [5u8; 16]);
let frame = Frame {
conn_tag: 0xDEAD_BEEF_0000_0001,
object_id: 7,
payload_id: [1, 2, 3, 4],
flags: 0b0000_0011,
payload: b"the quick brown fox".to_vec(),
};
let wire = codec.frame(&frame);
assert!(wire.len() >= MIN_FRAME);
assert_eq!(codec.deframe(&wire).unwrap(), frame);
}
#[test]
fn codec_rejects_tampered_frame() {
let codec = Codec::new([4u8; 16], [5u8; 16]);
let frame = Frame {
conn_tag: 1,
object_id: 1,
payload_id: [0; 4],
flags: 0,
payload: b"payload".to_vec(),
};
let mut wire = codec.frame(&frame);
let last = wire.len() - 1;
wire[last] ^= 0x01; assert_eq!(codec.deframe(&wire), Err(WireError::AuthFailed));
}
#[test]
fn codec_rejects_short_datagram() {
let codec = Codec::new([4u8; 16], [5u8; 16]);
assert_eq!(
codec.deframe(&[0u8; MIN_FRAME - 1]),
Err(WireError::Malformed)
);
}
#[test]
fn codec_has_no_constant_header_bytes() {
let codec = Codec::new([4u8; 16], [5u8; 16]);
let base = Frame {
conn_tag: 0,
object_id: 0,
payload_id: [0; 4],
flags: 0,
payload: vec![],
};
let wire = codec.frame(&base);
assert_ne!(&wire[..HEADER_LEN], &[0u8; HEADER_LEN]);
}
#[test]
fn header_roundtrips() {
let frame = Frame {
conn_tag: 0x0102_0304_0506_0708,
object_id: 0xABCD,
payload_id: [9, 8, 7, 6],
flags: 0x5A,
payload: vec![],
};
let bytes = write_header(&frame);
assert_eq!(bytes.len(), HEADER_LEN);
let (conn_tag, object_id, payload_id, flags) = read_header(&bytes);
assert_eq!(conn_tag, frame.conn_tag);
assert_eq!(object_id, frame.object_id);
assert_eq!(payload_id, frame.payload_id);
assert_eq!(flags, frame.flags);
}
}