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        assert_eq!(&packet.as_bytes()[1..6], &[0x20, 0x92, 0x78, 0x53, 0xFF]);
191    }
192
193    #[test]
194    fn aad_excludes_dynamic_options() {
195        let mut bytes = [0u8; 64];
196        bytes[0] = Fcf::new(PacketType::Unicast, false, true, false).0;
197        bytes[1] = 0x20;
198        bytes[2] = 0x92;
199        bytes[3] = 0x78;
200        bytes[4] = 0x53;
201        bytes[5] = 0xFF;
202        bytes[6..9].copy_from_slice(&[0xC3, 0xD4, 0x25]);
203        bytes[9..12].copy_from_slice(&[0xA1, 0xB2, 0x03]);
204        bytes[12] = Scf::new(true, MicSize::Mic8, false).0;
205        bytes[13..17].copy_from_slice(&42u32.to_be_bytes());
206        bytes[17..20].copy_from_slice(b"hey");
207        bytes[20..28].fill(0x11);
208        let header = PacketHeader::parse(&bytes[..28]).unwrap();
209        let mut aad = [0u8; 18];
210        let mut aad_len = 0usize;
211        feed_aad(&header, &bytes[..28], |chunk| {
212            let next_len = aad_len + chunk.len();
213            aad[aad_len..next_len].copy_from_slice(chunk);
214            aad_len = next_len;
215        });
216        assert_eq!(
217            &aad[..aad_len],
218            &[
219                bytes[0],
220                0xC3,
221                0xD4,
222                0x25,
223                0xA1,
224                0xB2,
225                0x03,
226                Scf::new(true, MicSize::Mic8, false).0,
227                0x00,
228                0x00,
229                0x00,
230                0x2A,
231            ]
232        );
233    }
234
235    #[test]
236    fn aad_encodes_static_option_tl_as_u16_be_pairs() {
237        let mut buf = [0u8; 96];
238        let packet = PacketBuilder::new(&mut buf)
239            .unicast(NodeHint([0xC3, 0xD4, 0x25]))
240            .source_hint(NodeHint([0xA1, 0xB2, 0x03]))
241            .frame_counter(42)
242            .encrypted()
243            .option(OptionNumber::Unknown(300), &[0xAA])
244            .payload(b"hey")
245            .build()
246            .unwrap();
247        let bytes = packet.as_bytes().to_vec();
248        let header = PacketHeader::parse(&bytes).unwrap();
249        let mut aad = [0u8; 32];
250        let mut aad_len = 0usize;
251
252        feed_aad(&header, &bytes, |chunk| {
253            let next_len = aad_len + chunk.len();
254            aad[aad_len..next_len].copy_from_slice(chunk);
255            aad_len = next_len;
256        });
257
258        assert_eq!(&aad[1..6], &[0x01, 0x2C, 0x00, 0x01, 0xAA]);
259    }
260
261    #[test]
262    fn parse_blind_unicast_tracks_secinfo_range() {
263        let bytes = [
264            0xF0, 0x7E, 0x5F, 0x80, 0x00, 0x00, 0x00, 0x05, 0xC3, 0xD4, 0x25, 0xA1, 0xB2, 0x03,
265            0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x11, 0x22, 0x33, 0x44,
266        ];
267        let header = PacketHeader::parse(&bytes).unwrap();
268        assert_eq!(header.sec_info.unwrap().wire_len(), 5);
269    }
270}