1#![forbid(unsafe_code)]
5
6use siphasher::sip::SipHasher13;
7use std::hash::Hasher;
8use subtle::ConstantTimeEq;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Frame {
13 pub conn_tag: u64,
15 pub object_id: u16,
17 pub payload_id: [u8; 4],
19 pub flags: u8,
21 pub payload: Vec<u8>,
23}
24
25pub const HEADER_LEN: usize = 15;
27pub const TAG_LEN: usize = 8;
29pub const MIN_FRAME: usize = HEADER_LEN + TAG_LEN;
31
32fn write_header(frame: &Frame) -> [u8; HEADER_LEN] {
34 let mut out = [0u8; HEADER_LEN];
35 out[0..8].copy_from_slice(&frame.conn_tag.to_be_bytes());
36 out[8..10].copy_from_slice(&frame.object_id.to_be_bytes());
37 out[10..14].copy_from_slice(&frame.payload_id);
38 out[14] = frame.flags;
39 out
40}
41
42fn read_header(bytes: &[u8; HEADER_LEN]) -> (u64, u16, [u8; 4], u8) {
44 let conn_tag = u64::from_be_bytes(bytes[0..8].try_into().expect("8 bytes"));
45 let object_id = u16::from_be_bytes(bytes[8..10].try_into().expect("2 bytes"));
46 let payload_id: [u8; 4] = bytes[10..14].try_into().expect("4 bytes");
47 let flags = bytes[14];
48 (conn_tag, object_id, payload_id, flags)
49}
50
51#[inline]
55fn auth_tag(auth_key: &[u8; 16], header: &[u8; HEADER_LEN], payload: &[u8]) -> [u8; TAG_LEN] {
56 let mut hasher = SipHasher13::new_with_key(auth_key);
57 hasher.write(header);
58 hasher.write(payload);
59 hasher.finish().to_be_bytes()
60}
61
62#[inline]
67fn header_mask(hp_key: &[u8; 16], sample: &[u8]) -> [u8; HEADER_LEN] {
68 let mut out = [0u8; HEADER_LEN];
69
70 let mut h0 = SipHasher13::new_with_key(hp_key);
72 h0.write(sample);
73 h0.write(&0u32.to_be_bytes());
74 out[0..8].copy_from_slice(&h0.finish().to_be_bytes());
75
76 let mut h1 = SipHasher13::new_with_key(hp_key);
78 h1.write(sample);
79 h1.write(&1u32.to_be_bytes());
80 out[8..15].copy_from_slice(&h1.finish().to_be_bytes()[..7]);
81
82 out
83}
84
85fn xor_in_place(buf: &mut [u8], mask: &[u8]) {
87 for (b, m) in buf.iter_mut().zip(mask.iter()) {
88 *b ^= *m;
89 }
90}
91
92#[derive(Debug, thiserror::Error, PartialEq, Eq)]
94pub enum WireError {
95 #[error("authentication failed")]
97 AuthFailed,
98 #[error("malformed datagram")]
100 Malformed,
101}
102
103pub struct Codec {
106 auth_key: [u8; 16],
107 hp_key: [u8; 16],
108}
109
110impl Codec {
111 pub fn new(auth_key: [u8; 16], hp_key: [u8; 16]) -> Self {
113 Self { auth_key, hp_key }
114 }
115}
116
117pub trait WireCodec {
119 fn frame(&self, frame: &Frame) -> Vec<u8>;
121 fn deframe(&self, datagram: &[u8]) -> Result<Frame, WireError>;
123}
124
125impl WireCodec for Codec {
126 fn frame(&self, frame: &Frame) -> Vec<u8> {
127 let header = write_header(frame);
128 let tag = auth_tag(&self.auth_key, &header, &frame.payload);
129 let mask = header_mask(&self.hp_key, &tag);
130
131 let mut out = Vec::with_capacity(HEADER_LEN + frame.payload.len() + TAG_LEN);
132 out.extend_from_slice(&header);
133 xor_in_place(&mut out[..HEADER_LEN], &mask);
134 out.extend_from_slice(&frame.payload);
135 out.extend_from_slice(&tag);
136 out
137 }
138
139 fn deframe(&self, datagram: &[u8]) -> Result<Frame, WireError> {
140 if datagram.len() < MIN_FRAME {
141 return Err(WireError::Malformed);
142 }
143 let tag = &datagram[datagram.len() - TAG_LEN..];
144 let mask = header_mask(&self.hp_key, tag);
146 let mut header = [0u8; HEADER_LEN];
147 header.copy_from_slice(&datagram[..HEADER_LEN]);
148 xor_in_place(&mut header, &mask);
149 let payload = &datagram[HEADER_LEN..datagram.len() - TAG_LEN];
150
151 let expected = auth_tag(&self.auth_key, &header, payload);
153 if expected.ct_eq(tag).unwrap_u8() != 1 {
154 return Err(WireError::AuthFailed);
155 }
156 let (conn_tag, object_id, payload_id, flags) = read_header(&header);
157 Ok(Frame {
158 conn_tag,
159 object_id,
160 payload_id,
161 flags,
162 payload: payload.to_vec(),
163 })
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn auth_tag_is_keyed_and_covers_input() {
173 let k1 = [1u8; 16];
174 let k2 = [2u8; 16];
175 let h1 = [0x11u8; HEADER_LEN];
176 let h2 = [0x22u8; HEADER_LEN];
177 let a = auth_tag(&k1, &h1, b"hello");
178 let b = auth_tag(&k1, &h1, b"hello");
179 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");
183 assert_ne!(a, c, "changes when payload bytes change");
184 assert_ne!(a, d, "changes when key changes");
185 assert_ne!(a, e, "changes when header bytes change");
186 }
187
188 #[test]
189 fn auth_tag_multi_slice_matches_contiguous() {
190 let key = [0x42u8; 16];
191 let header = [0x33u8; HEADER_LEN];
192 let payload = vec![0x77u8; 1200];
193 let tag = auth_tag(&key, &header, &payload);
194
195 let mut ref_hasher = SipHasher13::new_with_key(&key);
196 let mut contiguous = Vec::with_capacity(HEADER_LEN + payload.len());
197 contiguous.extend_from_slice(&header);
198 contiguous.extend_from_slice(&payload);
199 ref_hasher.write(&contiguous);
200 let expected = ref_hasher.finish().to_be_bytes();
201
202 assert_eq!(
203 tag, expected,
204 "streaming multi-slice matches contiguous hash"
205 );
206 }
207
208 #[test]
209 fn frame_carries_object_id() {
210 let frame = Frame {
211 conn_tag: 7,
212 object_id: 42,
213 payload_id: [0; 4],
214 flags: 0,
215 payload: vec![1, 2, 3],
216 };
217 assert_eq!(frame.object_id, 42);
218 }
219
220 #[test]
221 fn header_mask_masks_reversibly_and_hides_constants() {
222 let hp = [3u8; 16];
223 let sample = [0xAAu8; TAG_LEN];
224 let mut header = [0u8; HEADER_LEN]; let mask = header_mask(&hp, &sample);
226 assert_eq!(mask.len(), HEADER_LEN);
227 xor_in_place(&mut header, &mask);
228 assert_ne!(
229 header, [0u8; HEADER_LEN],
230 "constant header is hidden after masking"
231 );
232 xor_in_place(&mut header, &mask);
234 assert_eq!(header, [0u8; HEADER_LEN], "masking is reversible");
235 let mask2 = header_mask(&hp, &[0xBBu8; TAG_LEN]);
237 assert_ne!(mask, mask2);
238 }
239
240 #[test]
243 fn header_mask_consecutive_blocks_are_distinct() {
244 let hp = [7u8; 16];
245 let sample = [0x55u8; TAG_LEN];
246 let mask = header_mask(&hp, &sample);
247 assert_eq!(mask.len(), HEADER_LEN);
248 assert_ne!(
249 &mask[..7],
250 &mask[8..15],
251 "block-0 (counter=0) and block-1 (counter=1) must differ"
252 );
253 }
254
255 #[test]
260 fn min_frame_equals_header_plus_tag() {
261 assert_eq!(
262 MIN_FRAME,
263 HEADER_LEN + TAG_LEN,
264 "MIN_FRAME must be HEADER_LEN + TAG_LEN"
265 );
266 }
267
268 #[test]
273 fn codec_accepts_exact_min_frame_datagram() {
274 let codec = Codec::new([9u8; 16], [10u8; 16]);
275 let frame = Frame {
276 conn_tag: 0xCAFE_BABE_0000_0001,
277 object_id: 3,
278 payload_id: [0xAB, 0xCD, 0xEF, 0x01],
279 flags: 0x42,
280 payload: vec![], };
282 let wire = codec.frame(&frame);
283 assert_eq!(
284 wire.len(),
285 MIN_FRAME,
286 "empty-payload frame must be exactly MIN_FRAME bytes"
287 );
288 assert_eq!(
289 codec.deframe(&wire).unwrap(),
290 frame,
291 "exact-MIN_FRAME datagram must be accepted"
292 );
293 }
294
295 #[test]
296 fn codec_roundtrips_a_frame() {
297 let codec = Codec::new([4u8; 16], [5u8; 16]);
298 let frame = Frame {
299 conn_tag: 0xDEAD_BEEF_0000_0001,
300 object_id: 7,
301 payload_id: [1, 2, 3, 4],
302 flags: 0b0000_0011,
303 payload: b"the quick brown fox".to_vec(),
304 };
305 let wire = codec.frame(&frame);
306 assert!(wire.len() >= MIN_FRAME);
307 assert_eq!(codec.deframe(&wire).unwrap(), frame);
308 }
309
310 #[test]
311 fn codec_rejects_tampered_frame() {
312 let codec = Codec::new([4u8; 16], [5u8; 16]);
313 let frame = Frame {
314 conn_tag: 1,
315 object_id: 1,
316 payload_id: [0; 4],
317 flags: 0,
318 payload: b"payload".to_vec(),
319 };
320 let mut wire = codec.frame(&frame);
321 let last = wire.len() - 1;
322 wire[last] ^= 0x01; assert_eq!(codec.deframe(&wire), Err(WireError::AuthFailed));
324 }
325
326 #[test]
327 fn codec_rejects_short_datagram() {
328 let codec = Codec::new([4u8; 16], [5u8; 16]);
329 assert_eq!(
330 codec.deframe(&[0u8; MIN_FRAME - 1]),
331 Err(WireError::Malformed)
332 );
333 }
334
335 #[test]
336 fn codec_has_no_constant_header_bytes() {
337 let codec = Codec::new([4u8; 16], [5u8; 16]);
340 let base = Frame {
341 conn_tag: 0,
342 object_id: 0,
343 payload_id: [0; 4],
344 flags: 0,
345 payload: vec![],
346 };
347 let wire = codec.frame(&base);
348 assert_ne!(&wire[..HEADER_LEN], &[0u8; HEADER_LEN]);
350 }
351
352 #[test]
353 fn header_roundtrips() {
354 let frame = Frame {
355 conn_tag: 0x0102_0304_0506_0708,
356 object_id: 0xABCD,
357 payload_id: [9, 8, 7, 6],
358 flags: 0x5A,
359 payload: vec![],
360 };
361 let bytes = write_header(&frame);
362 assert_eq!(bytes.len(), HEADER_LEN);
363 let (conn_tag, object_id, payload_id, flags) = read_header(&bytes);
364 assert_eq!(conn_tag, frame.conn_tag);
365 assert_eq!(object_id, frame.object_id);
366 assert_eq!(payload_id, frame.payload_id);
367 assert_eq!(flags, frame.flags);
368 }
369}