Skip to main content

pim_protocol/
transport_frame.rs

1//! Outer authenticated transport envelope exchanged on direct peer links.
2
3use bytes::{Buf, BufMut, BytesMut};
4
5use pim_core::{FrameCodec, PimError};
6
7use crate::frame_type::FrameType;
8
9/// Magic bytes identifying a PIM transport frame: "PM" (0x504D).
10pub const MAGIC: u16 = 0x504D;
11
12/// Current protocol version.
13pub const VERSION: u8 = 1;
14
15/// Maximum payload size (1 MB).
16pub const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
17
18/// The outermost frame on the wire between directly connected peers.
19///
20/// Layout:
21/// - magic: u16 (0x504D)
22/// - version: u8
23/// - frame_type: u8
24/// - length: u32 (payload length)
25/// - nonce: [u8; 12]
26/// - payload: [u8; length] (encrypted)
27/// - tag: [u8; 16]
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TransportFrame {
30    /// Payload discriminator for `payload`.
31    pub frame_type: FrameType,
32    /// AES-GCM nonce used for the encrypted payload.
33    pub nonce: [u8; 12],
34    /// Encrypted inner frame bytes.
35    pub payload: bytes::Bytes,
36    /// Authentication tag for the encrypted payload.
37    pub tag: [u8; 16],
38}
39
40/// Header size: magic(2) + version(1) + frame_type(1) + length(4) + nonce(12) = 20
41const HEADER_SIZE: usize = 20;
42/// Tag size: 16
43const TAG_SIZE: usize = 16;
44
45impl FrameCodec for TransportFrame {
46    fn encode(&self, buf: &mut BytesMut) {
47        buf.put_u16(MAGIC);
48        buf.put_u8(VERSION);
49        buf.put_u8(self.frame_type as u8);
50        buf.put_u32(self.payload.len() as u32);
51        buf.put_slice(&self.nonce);
52        buf.put_slice(&self.payload);
53        buf.put_slice(&self.tag);
54    }
55
56    fn decode(buf: &mut BytesMut) -> Result<Self, PimError> {
57        if buf.len() < HEADER_SIZE {
58            return Err(PimError::Protocol("frame too short for header".into()));
59        }
60
61        let magic = (&buf[0..2]).get_u16();
62        if magic != MAGIC {
63            return Err(PimError::Protocol(format!(
64                "invalid magic: 0x{magic:04X}, expected 0x{MAGIC:04X}"
65            )));
66        }
67
68        let version = buf[2];
69        if version != VERSION {
70            return Err(PimError::Protocol(format!(
71                "unsupported version: {version}, expected {VERSION}"
72            )));
73        }
74
75        let frame_type = FrameType::from_u8(buf[3])?;
76        let length = (&buf[4..8]).get_u32();
77
78        if length > MAX_PAYLOAD_SIZE {
79            return Err(PimError::Protocol(format!(
80                "payload too large: {length} bytes, max {MAX_PAYLOAD_SIZE}"
81            )));
82        }
83
84        let total_size = HEADER_SIZE + length as usize + TAG_SIZE;
85        if buf.len() < total_size {
86            return Err(PimError::Protocol(format!(
87                "frame truncated: need {total_size} bytes, have {}",
88                buf.len()
89            )));
90        }
91
92        let mut nonce = [0u8; 12];
93        nonce.copy_from_slice(&buf[8..20]);
94
95        buf.advance(HEADER_SIZE);
96        let payload = buf.split_to(length as usize).freeze();
97
98        let mut tag = [0u8; 16];
99        tag.copy_from_slice(&buf[0..TAG_SIZE]);
100
101        buf.advance(TAG_SIZE);
102
103        Ok(TransportFrame {
104            frame_type,
105            nonce,
106            payload,
107            tag,
108        })
109    }
110}
111
112#[cfg(test)]
113mod tests;