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
43pub mod base58;
44mod builder;
45mod error;
46pub mod options;
47mod packet;
48#[cfg(feature = "region-codec")]
49mod region;
50
51pub use builder::{
52    BlindUnicastBuilder, BroadcastBuilder, MacAckBuilder, MulticastBuilder, PacketBuilder,
53    UnicastBuilder, state,
54};
55pub use error::{AddressParseError, BuildError, EncodeError, ParseError};
56pub use packet::{
57    ChannelId, ChannelKey, ChannelTag, Fcf, FloodHops, MicSize, NodeHint, OptionNumber,
58    PacketHeader, PacketType, ParsedOptions, PayloadType, PublicKey, RouterHint, Scf, SecInfo,
59    SourceAddr, SourceAddrRef, UMSH_VERSION, UnsealedPacket, feed_aad, iter_options,
60};
61#[cfg(feature = "region-codec")]
62pub use region::{RegionCode, RegionCodeError};
63
64#[cfg(test)]
65mod tests {
66    use crate::{
67        Fcf, MicSize, NodeHint, OptionNumber, PacketBuilder, PacketHeader, PacketType, PublicKey,
68        Scf, SecInfo, SourceAddrRef, feed_aad,
69        options::{OptionDecoder, OptionEncoder},
70    };
71
72    #[test]
73    fn option_codec_round_trip() {
74        let mut buf = [0u8; 32];
75        let mut enc = OptionEncoder::new(&mut buf);
76        enc.put(1, &[0x78, 0x53]).unwrap();
77        enc.put(2, &[]).unwrap();
78        enc.end_marker().unwrap();
79        let len = enc.finish();
80
81        let mut decoder = OptionDecoder::new(&buf[..len]);
82        assert_eq!(decoder.next().unwrap().unwrap(), (1, &[0x78, 0x53][..]));
83        assert_eq!(decoder.next().unwrap().unwrap(), (2, &[][..]));
84        assert!(decoder.next().is_none());
85    }
86
87    #[test]
88    fn secinfo_round_trip() {
89        let sec = SecInfo {
90            scf: Scf::new(true, MicSize::Mic16, true),
91            frame_counter: 42,
92            salt: Some(0x1234),
93        };
94        let mut buf = [0u8; 7];
95        let len = sec.encode(&mut buf).unwrap();
96        assert_eq!(len, 7);
97        assert_eq!(SecInfo::decode(&buf).unwrap(), sec);
98    }
99
100    #[test]
101    fn parse_broadcast_beacon() {
102        let bytes = [0xC0, 0xA1, 0xB2, 0x03];
103        let header = PacketHeader::parse(&bytes).unwrap();
104        assert_eq!(header.packet_type(), PacketType::Broadcast);
105        assert!(header.is_beacon());
106    }
107
108    #[test]
109    fn empty_payload_emits_no_end_of_options_marker() {
110        // A beacon is a broadcast with an empty payload. Nothing follows the
111        // options block, so the `0xFF` marker must not be emitted — it would
112        // be a wasted byte on every beacon.
113        let mut buf = [0u8; 64];
114        let beacon = PacketBuilder::new(&mut buf)
115            .broadcast()
116            .source_hint(NodeHint([0x8B, 0x60, 0xF5]))
117            .flood_hops(5)
118            .payload(&[])
119            .build()
120            .unwrap();
121        assert_eq!(beacon, &[0xC1, 0x50, 0x8B, 0x60, 0xF5]);
122        let header = PacketHeader::parse(beacon).unwrap();
123        assert!(header.is_beacon());
124
125        // The marker returns as soon as there is a payload to delimit.
126        let mut buf = [0u8; 64];
127        let with_payload = PacketBuilder::new(&mut buf)
128            .broadcast()
129            .source_hint(NodeHint([0x8B, 0x60, 0xF5]))
130            .payload(b"hi")
131            .build()
132            .unwrap();
133        assert_eq!(with_payload, &[0xC0, 0x8B, 0x60, 0xF5, 0xFF, b'h', b'i']);
134
135        // The same holds for a secured type whose body is empty.
136        let mut buf = [0u8; 128];
137        let unicast = PacketBuilder::new(&mut buf)
138            .unicast(NodeHint([0xC3, 0xD4, 0x25]))
139            .source_hint(NodeHint([0xA1, 0xB2, 0x03]))
140            .frame_counter(7)
141            .encrypted()
142            .mic_size(MicSize::Mic8)
143            .payload(&[])
144            .build()
145            .unwrap();
146        // FCF | DST(3) | SRC(3) | SECINFO(5) | no options, no marker | MIC(8)
147        let bytes = unicast.as_bytes();
148        assert_eq!(
149            bytes,
150            &[
151                0xD0, 0xC3, 0xD4, 0x25, 0xA1, 0xB2, 0x03, 0xA0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
152                0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
153            ],
154        );
155        let header = PacketHeader::parse(bytes).unwrap();
156        assert!(header.options_range.is_empty());
157        assert!(header.body_range.is_empty());
158        assert_eq!(header.mic_range.len(), 8);
159    }
160
161    #[test]
162    fn builder_and_parser_for_unicast_match() {
163        let mut buf = [0u8; 128];
164        let src = PublicKey([0xA1; 32]);
165        let dst = NodeHint([0xC3, 0xD4, 0x25]);
166        let packet = PacketBuilder::new(&mut buf)
167            .unicast(dst)
168            .source_full(&src)
169            .frame_counter(42)
170            .encrypted()
171            .mic_size(MicSize::Mic16)
172            .payload(b"hello")
173            .build()
174            .unwrap();
175
176        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
177        assert_eq!(header.packet_type(), PacketType::Unicast);
178        assert_eq!(header.dst, Some(dst));
179        assert_eq!(header.body_range.len(), 5);
180    }
181
182    #[test]
183    fn blind_unicast_builder_and_parser_match() {
184        let mut buf = [0u8; 128];
185        let src = PublicKey([0xA1; 32]);
186        let dst = NodeHint([0xC3, 0xD4, 0x25]);
187        let channel = crate::ChannelId([0x7E, 0x5F]);
188        let packet = PacketBuilder::new(&mut buf)
189            .blind_unicast(channel, dst)
190            .source_full(&src)
191            .frame_counter(5)
192            .payload(b"hello")
193            .build()
194            .unwrap();
195
196        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
197        assert_eq!(header.packet_type(), PacketType::BlindUnicast);
198        assert_eq!(header.channel, Some(channel));
199        assert_eq!(packet.blind_addr().unwrap().len(), 35);
200        assert_eq!(header.body_range.len(), 5);
201    }
202
203    #[test]
204    fn unencrypted_blind_unicast_builder_and_parser_match() {
205        let mut buf = [0u8; 128];
206        let src = PublicKey([0xA1; 32]);
207        let dst = NodeHint([0xC3, 0xD4, 0x25]);
208        let channel = crate::ChannelId([0x7E, 0x5F]);
209        let packet = PacketBuilder::new(&mut buf)
210            .blind_unicast(channel, dst)
211            .source_full(&src)
212            .frame_counter(5)
213            .unencrypted()
214            .payload(b"hello")
215            .build()
216            .unwrap();
217
218        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
219        assert_eq!(header.packet_type(), PacketType::BlindUnicast);
220        assert_eq!(header.channel, Some(channel));
221        assert_eq!(header.dst, Some(dst));
222        assert_eq!(
223            header.source,
224            SourceAddrRef::FullKeyAt {
225                offset: header.body_range.start - 32
226            }
227        );
228        assert!(!header.sec_info.unwrap().scf.encrypted());
229        assert_eq!(header.body_range.len(), 5);
230    }
231
232    #[test]
233    fn builder_encodes_incremental_options_with_correct_deltas() {
234        let mut buf = [0u8; 128];
235        let src = NodeHint([0xA1, 0xB2, 0x03]);
236        let dst = NodeHint([0xC3, 0xD4, 0x25]);
237        let packet = PacketBuilder::new(&mut buf)
238            .unicast(dst)
239            .source_hint(src)
240            .frame_counter(10)
241            .encrypted()
242            .trace_route()
243            .region_code([0x78, 0x53])
244            .payload(b"hey")
245            .build()
246            .unwrap();
247
248        // New layout: FCF DST(3) SRC(3) SECINFO(5) OPTIONS...
249        // Options start at byte 12: trace_route(0x20) + region_code(0x92,0x78,0x53) + 0xFF
250        assert_eq!(&packet.as_bytes()[12..17], &[0x20, 0x92, 0x78, 0x53, 0xFF]);
251    }
252
253    #[test]
254    fn aad_excludes_dynamic_options() {
255        // New layout for unicast with hint source, encrypted, MIC8, no fhops:
256        // FCF | DST(3) | SRC(3) | SECINFO(5) | OPTIONS(5) | 0xFF | payload(3) | MIC(8)
257        // Total = 1 + 3 + 3 + 5 + 4 + 1 + 3 + 8 = 28 bytes
258        // Options: trace route (2, len 0) + region code (11, len 2) — both dynamic, excluded from AAD
259        let mut bytes = [0u8; 64];
260        bytes[0] = Fcf::new(PacketType::Unicast, false, false).0;
261        bytes[1..4].copy_from_slice(&[0xC3, 0xD4, 0x25]); // DST
262        bytes[4..7].copy_from_slice(&[0xA1, 0xB2, 0x03]); // SRC hint
263        bytes[7] = Scf::new(true, MicSize::Mic8, false).0; // SCF
264        bytes[8..12].copy_from_slice(&42u32.to_be_bytes()); // frame counter
265        bytes[12] = 0x20; // trace route: delta=2, len=0
266        bytes[13] = 0x92; // region code: delta=9, len=2
267        bytes[14] = 0x78;
268        bytes[15] = 0x53;
269        bytes[16] = 0xFF; // end marker
270        bytes[17..20].copy_from_slice(b"hey"); // payload
271        bytes[20..28].fill(0x11); // MIC
272        let header = PacketHeader::parse(&bytes[..28]).unwrap();
273        let mut aad = [0u8; 18];
274        let mut aad_len = 0usize;
275        feed_aad(&header, &bytes[..28], |chunk| {
276            let next_len = aad_len + chunk.len();
277            aad[aad_len..next_len].copy_from_slice(chunk);
278            aad_len = next_len;
279        });
280        assert_eq!(
281            &aad[..aad_len],
282            &[
283                bytes[0],
284                0xC3,
285                0xD4,
286                0x25,
287                0xA1,
288                0xB2,
289                0x03,
290                Scf::new(true, MicSize::Mic8, false).0,
291                0x00,
292                0x00,
293                0x00,
294                0x2A,
295            ]
296        );
297    }
298
299    #[test]
300    fn aad_encodes_static_option_tl_as_u16_be_pairs() {
301        let mut buf = [0u8; 96];
302        let packet = PacketBuilder::new(&mut buf)
303            .unicast(NodeHint([0xC3, 0xD4, 0x25]))
304            .source_hint(NodeHint([0xA1, 0xB2, 0x03]))
305            .frame_counter(42)
306            .encrypted()
307            .option(OptionNumber::Unknown(300), &[0xAA])
308            .payload(b"hey")
309            .build()
310            .unwrap();
311        let bytes = packet.as_bytes().to_vec();
312        let header = PacketHeader::parse(&bytes).unwrap();
313        let mut aad = [0u8; 32];
314        let mut aad_len = 0usize;
315
316        feed_aad(&header, &bytes, |chunk| {
317            let next_len = aad_len + chunk.len();
318            aad[aad_len..next_len].copy_from_slice(chunk);
319            aad_len = next_len;
320        });
321
322        assert_eq!(&aad[1..6], &[0x01, 0x2C, 0x00, 0x01, 0xAA]);
323    }
324
325    #[test]
326    fn parse_blind_unicast_tracks_secinfo_range() {
327        // New layout: FCF | CHANNEL(2) | SECINFO(5) | 0xFF | ENC_DST_SRC(6) | payload(5) | MIC(4)
328        // 0xFF is required because body follows options
329        let bytes = [
330            0xF0, 0x7E, 0x5F, 0x80, 0x00, 0x00, 0x00, 0x05, 0xFF, 0xC3, 0xD4, 0x25, 0xA1, 0xB2,
331            0x03, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x11, 0x22, 0x33, 0x44,
332        ];
333        let header = PacketHeader::parse(&bytes).unwrap();
334        assert_eq!(header.sec_info.unwrap().wire_len(), 5);
335    }
336}