umsh_core/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! Core wire-format types and packet construction/parsing utilities for UMSH.
4//!
5//! > Note: This reference implementation is a work in progress and was developed
6//! > with the assistance of an LLM. It should be considered experimental.
7//!
8//! This crate stays focused on byte layout, typed header fields, and zero-copy
9//! parsing. It does not perform any cryptographic operations or I/O.
10//!
11//! The main entry points are:
12//!
13//! - [`PacketHeader::parse`] to inspect a received frame.
14//! - [`PacketBuilder`] to construct outbound frames with typestate guards.
15//! - [`feed_aad`] to stream canonical authenticated data into a MAC state.
16//! - [`options::OptionEncoder`] and [`options::OptionDecoder`] for CoAP-style
17//!   option blocks used throughout the protocol.
18//!
19//! # Example
20//!
21//! ```rust
22//! use umsh_core::{MicSize, NodeHint, PacketBuilder, PacketHeader, PublicKey};
23//!
24//! let mut buf = [0u8; 128];
25//! let src = PublicKey([0x11; 32]);
26//! let dst = NodeHint([0xAA, 0xBB, 0xCC]);
27//!
28//! let packet = PacketBuilder::new(&mut buf)
29//!     .unicast(dst)
30//!     .source_full(&src)
31//!     .frame_counter(7)
32//!     .encrypted()
33//!     .mic_size(MicSize::Mic16)
34//!     .payload(b"hello")
35//!     .build()
36//!     .unwrap();
37//!
38//! let header = PacketHeader::parse(packet.as_bytes()).unwrap();
39//! assert_eq!(header.dst, Some(dst));
40//! assert_eq!(header.body_range.len(), 5);
41//! ```
42
43mod builder;
44mod error;
45pub mod options;
46mod packet;
47
48pub use builder::{
49    BlindUnicastBuilder, BroadcastBuilder, MacAckBuilder, MulticastBuilder, PacketBuilder,
50    UnicastBuilder, state,
51};
52pub use error::{BuildError, EncodeError, ParseError};
53pub use packet::{
54    ChannelId, ChannelKey, Fcf, FloodHops, MicSize, NodeHint, OptionNumber, PacketHeader,
55    PacketType, ParsedOptions, PayloadType, PublicKey, RouterHint, Scf, SecInfo, SourceAddr,
56    SourceAddrRef, UMSH_VERSION, UnsealedPacket, feed_aad, iter_options,
57};
58
59#[cfg(test)]
60mod tests {
61    use crate::{
62        Fcf, MicSize, NodeHint, OptionNumber, PacketBuilder, PacketHeader, PacketType, PublicKey,
63        Scf, SecInfo, SourceAddrRef, feed_aad,
64        options::{OptionDecoder, OptionEncoder},
65    };
66
67    #[test]
68    fn option_codec_round_trip() {
69        let mut buf = [0u8; 32];
70        let mut enc = OptionEncoder::new(&mut buf);
71        enc.put(1, &[0x78, 0x53]).unwrap();
72        enc.put(2, &[]).unwrap();
73        enc.end_marker().unwrap();
74        let len = enc.finish();
75
76        let mut decoder = OptionDecoder::new(&buf[..len]);
77        assert_eq!(decoder.next().unwrap().unwrap(), (1, &[0x78, 0x53][..]));
78        assert_eq!(decoder.next().unwrap().unwrap(), (2, &[][..]));
79        assert!(decoder.next().is_none());
80    }
81
82    #[test]
83    fn secinfo_round_trip() {
84        let sec = SecInfo {
85            scf: Scf::new(true, MicSize::Mic16, true),
86            frame_counter: 42,
87            salt: Some(0x1234),
88        };
89        let mut buf = [0u8; 7];
90        let len = sec.encode(&mut buf).unwrap();
91        assert_eq!(len, 7);
92        assert_eq!(SecInfo::decode(&buf).unwrap(), sec);
93    }
94
95    #[test]
96    fn parse_broadcast_beacon() {
97        let bytes = [0xC0, 0xA1, 0xB2, 0x03];
98        let header = PacketHeader::parse(&bytes).unwrap();
99        assert_eq!(header.packet_type(), PacketType::Broadcast);
100        assert!(header.is_beacon());
101    }
102
103    #[test]
104    fn builder_and_parser_for_unicast_match() {
105        let mut buf = [0u8; 128];
106        let src = PublicKey([0xA1; 32]);
107        let dst = NodeHint([0xC3, 0xD4, 0x25]);
108        let packet = PacketBuilder::new(&mut buf)
109            .unicast(dst)
110            .source_full(&src)
111            .frame_counter(42)
112            .encrypted()
113            .mic_size(MicSize::Mic16)
114            .payload(b"hello")
115            .build()
116            .unwrap();
117
118        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
119        assert_eq!(header.packet_type(), PacketType::Unicast);
120        assert_eq!(header.dst, Some(dst));
121        assert_eq!(header.body_range.len(), 5);
122    }
123
124    #[test]
125    fn blind_unicast_builder_and_parser_match() {
126        let mut buf = [0u8; 128];
127        let src = PublicKey([0xA1; 32]);
128        let dst = NodeHint([0xC3, 0xD4, 0x25]);
129        let channel = crate::ChannelId([0x7E, 0x5F]);
130        let packet = PacketBuilder::new(&mut buf)
131            .blind_unicast(channel, dst)
132            .source_full(&src)
133            .frame_counter(5)
134            .payload(b"hello")
135            .build()
136            .unwrap();
137
138        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
139        assert_eq!(header.packet_type(), PacketType::BlindUnicast);
140        assert_eq!(header.channel, Some(channel));
141        assert_eq!(packet.blind_addr().unwrap().len(), 35);
142        assert_eq!(header.body_range.len(), 5);
143    }
144
145    #[test]
146    fn unencrypted_blind_unicast_builder_and_parser_match() {
147        let mut buf = [0u8; 128];
148        let src = PublicKey([0xA1; 32]);
149        let dst = NodeHint([0xC3, 0xD4, 0x25]);
150        let channel = crate::ChannelId([0x7E, 0x5F]);
151        let packet = PacketBuilder::new(&mut buf)
152            .blind_unicast(channel, dst)
153            .source_full(&src)
154            .frame_counter(5)
155            .unencrypted()
156            .payload(b"hello")
157            .build()
158            .unwrap();
159
160        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
161        assert_eq!(header.packet_type(), PacketType::BlindUnicast);
162        assert_eq!(header.channel, Some(channel));
163        assert_eq!(header.dst, Some(dst));
164        assert_eq!(
165            header.source,
166            SourceAddrRef::FullKeyAt {
167                offset: header.body_range.start - 32
168            }
169        );
170        assert!(!header.sec_info.unwrap().scf.encrypted());
171        assert_eq!(header.body_range.len(), 5);
172    }
173
174    #[test]
175    fn builder_encodes_incremental_options_with_correct_deltas() {
176        let mut buf = [0u8; 128];
177        let src = NodeHint([0xA1, 0xB2, 0x03]);
178        let dst = NodeHint([0xC3, 0xD4, 0x25]);
179        let packet = PacketBuilder::new(&mut buf)
180            .unicast(dst)
181            .source_hint(src)
182            .frame_counter(10)
183            .encrypted()
184            .trace_route()
185            .region_code([0x78, 0x53])
186            .payload(b"hey")
187            .build()
188            .unwrap();
189
190        // New layout: FCF DST(3) SRC(3) SECINFO(5) OPTIONS...
191        // Options start at byte 12: trace_route(0x20) + region_code(0x92,0x78,0x53) + 0xFF
192        assert_eq!(&packet.as_bytes()[12..17], &[0x20, 0x92, 0x78, 0x53, 0xFF]);
193    }
194
195    #[test]
196    fn aad_excludes_dynamic_options() {
197        // New layout for unicast with hint source, encrypted, MIC8, no fhops:
198        // FCF | DST(3) | SRC(3) | SECINFO(5) | OPTIONS(5) | 0xFF | payload(3) | MIC(8)
199        // Total = 1 + 3 + 3 + 5 + 4 + 1 + 3 + 8 = 28 bytes
200        // Options: trace route (2, len 0) + region code (11, len 2) — both dynamic, excluded from AAD
201        let mut bytes = [0u8; 64];
202        bytes[0] = Fcf::new(PacketType::Unicast, false, false).0;
203        bytes[1..4].copy_from_slice(&[0xC3, 0xD4, 0x25]); // DST
204        bytes[4..7].copy_from_slice(&[0xA1, 0xB2, 0x03]); // SRC hint
205        bytes[7] = Scf::new(true, MicSize::Mic8, false).0; // SCF
206        bytes[8..12].copy_from_slice(&42u32.to_be_bytes()); // frame counter
207        bytes[12] = 0x20; // trace route: delta=2, len=0
208        bytes[13] = 0x92; // region code: delta=9, len=2
209        bytes[14] = 0x78;
210        bytes[15] = 0x53;
211        bytes[16] = 0xFF; // end marker
212        bytes[17..20].copy_from_slice(b"hey"); // payload
213        bytes[20..28].fill(0x11); // MIC
214        let header = PacketHeader::parse(&bytes[..28]).unwrap();
215        let mut aad = [0u8; 18];
216        let mut aad_len = 0usize;
217        feed_aad(&header, &bytes[..28], |chunk| {
218            let next_len = aad_len + chunk.len();
219            aad[aad_len..next_len].copy_from_slice(chunk);
220            aad_len = next_len;
221        });
222        assert_eq!(
223            &aad[..aad_len],
224            &[
225                bytes[0],
226                0xC3,
227                0xD4,
228                0x25,
229                0xA1,
230                0xB2,
231                0x03,
232                Scf::new(true, MicSize::Mic8, false).0,
233                0x00,
234                0x00,
235                0x00,
236                0x2A,
237            ]
238        );
239    }
240
241    #[test]
242    fn aad_encodes_static_option_tl_as_u16_be_pairs() {
243        let mut buf = [0u8; 96];
244        let packet = PacketBuilder::new(&mut buf)
245            .unicast(NodeHint([0xC3, 0xD4, 0x25]))
246            .source_hint(NodeHint([0xA1, 0xB2, 0x03]))
247            .frame_counter(42)
248            .encrypted()
249            .option(OptionNumber::Unknown(300), &[0xAA])
250            .payload(b"hey")
251            .build()
252            .unwrap();
253        let bytes = packet.as_bytes().to_vec();
254        let header = PacketHeader::parse(&bytes).unwrap();
255        let mut aad = [0u8; 32];
256        let mut aad_len = 0usize;
257
258        feed_aad(&header, &bytes, |chunk| {
259            let next_len = aad_len + chunk.len();
260            aad[aad_len..next_len].copy_from_slice(chunk);
261            aad_len = next_len;
262        });
263
264        assert_eq!(&aad[1..6], &[0x01, 0x2C, 0x00, 0x01, 0xAA]);
265    }
266
267    #[test]
268    fn parse_blind_unicast_tracks_secinfo_range() {
269        // New layout: FCF | CHANNEL(2) | SECINFO(5) | 0xFF | ENC_DST_SRC(6) | payload(5) | MIC(4)
270        // 0xFF is required because body follows options
271        let bytes = [
272            0xF0, 0x7E, 0x5F, 0x80, 0x00, 0x00, 0x00, 0x05, 0xFF, 0xC3, 0xD4, 0x25, 0xA1, 0xB2,
273            0x03, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x11, 0x22, 0x33, 0x44,
274        ];
275        let header = PacketHeader::parse(&bytes).unwrap();
276        assert_eq!(header.sec_info.unwrap().wire_len(), 5);
277    }
278}