lora_packet/types.rs
1//! Strong typed primitives for `LoRaWAN` packets.
2//!
3//! Three groups of types live here:
4//!
5//! 1. **Message metadata enums:** [`MType`], [`Direction`], [`LorawanVersion`].
6//! 2. **Bitfield byte wrappers:** [`Mhdr`], [`FCtrl`], [`DlSettings`].
7//! 3. **Strong newtypes for identifiers and keys**: [`DevAddr`], [`DevEui`],
8//! [`AppEui`] (alias [`JoinEui`]), [`NetId`], [`DevNonce`], [`AppNonce`]
9//! (alias [`JoinNonce`]), [`AppKey`], [`NwkKey`], [`AppSKey`], [`NwkSKey`],
10//! [`FNwkSIntKey`], [`SNwkSIntKey`], [`NwkSEncKey`], [`JSIntKey`],
11//! [`JSEncKey`], [`RootWorSKey`], [`WorSIntKey`], [`WorSEncKey`].
12//!
13//! Identifier newtypes hold `Copy` byte arrays and impl `Debug`. Key newtypes
14//! do *not* implement `Copy` and have a redacted `Debug` impl
15//! (`AppKey(***)`); their bytes are wiped on drop via `zeroize::ZeroizeOnDrop`.
16//! Pass keys by reference; clone only when ownership is genuinely required.
17//!
18//! ## Endianness
19//!
20//! All identifier newtypes store bytes in display order (big-endian,
21//! left-to-right as you would print them). [`crate::LoraPacket::from_wire`]
22//! and [`crate::LoraPacket::to_wire`] reverse byte order for you when reading
23//! or writing the little-endian wire format.
24
25use crate::error::{Error, Result};
26
27/// `LoRaWAN` message types as encoded in the high 3 bits of the MHDR byte.
28///
29/// Use [`MType::from_mhdr`] to extract from a raw MHDR byte, or
30/// [`Mhdr::m_type`] when you already have a [`Mhdr`] wrapper.
31///
32/// All variants are routed through the matching [`crate::Payload`] variant
33/// when you parse with [`crate::LoraPacket::from_wire`].
34///
35/// # Examples
36///
37/// ```
38/// use lora_packet::MType;
39///
40/// assert_eq!(MType::from_mhdr(0x40).unwrap(), MType::UnconfirmedDataUp);
41/// assert_eq!(MType::from_mhdr(0xE0).unwrap(), MType::Proprietary);
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[repr(u8)]
46pub enum MType {
47 /// Device join request (OTAA, MHDR bits = `000`).
48 JoinRequest = 0b000,
49 /// Server response to a join request (MHDR bits = `001`).
50 JoinAccept = 0b001,
51 /// Uplink data without acknowledgment (MHDR bits = `010`).
52 UnconfirmedDataUp = 0b010,
53 /// Downlink data without acknowledgment (MHDR bits = `011`).
54 UnconfirmedDataDown = 0b011,
55 /// Uplink data with acknowledgment (MHDR bits = `100`).
56 ConfirmedDataUp = 0b100,
57 /// Downlink data with acknowledgment (MHDR bits = `101`).
58 ConfirmedDataDown = 0b101,
59 /// Rejoin request (`LoRaWAN` 1.1 only, MHDR bits = `110`).
60 RejoinRequest = 0b110,
61 /// Proprietary message body (MHDR bits = `111`).
62 Proprietary = 0b111,
63}
64
65impl MType {
66 /// Parse the 3-bit `MType` field from an MHDR byte.
67 ///
68 /// # Errors
69 /// Returns [`Error::InvalidMType`] when the field does not match any defined value.
70 /// All 3-bit patterns are currently defined, so this never fails in practice,
71 /// but the signature is kept fallible for forward compatibility.
72 pub const fn from_mhdr(mhdr: u8) -> Result<Self> {
73 match (mhdr >> 5) & 0b111 {
74 0b000 => Ok(Self::JoinRequest),
75 0b001 => Ok(Self::JoinAccept),
76 0b010 => Ok(Self::UnconfirmedDataUp),
77 0b011 => Ok(Self::UnconfirmedDataDown),
78 0b100 => Ok(Self::ConfirmedDataUp),
79 0b101 => Ok(Self::ConfirmedDataDown),
80 0b110 => Ok(Self::RejoinRequest),
81 0b111 => Ok(Self::Proprietary),
82 n => Err(Error::InvalidMType(n)),
83 }
84 }
85}
86
87/// Direction of a `LoRaWAN` data frame.
88///
89/// Set automatically when you parse with [`crate::LoraPacket::from_wire`]
90/// (from the `MType`) or when you call [`crate::LoraPacketBuilder::data`].
91/// Read it back on [`crate::Data::direction`].
92///
93/// Direction selects which key is used for `FRMPayload` and `FOpts` crypt
94/// (see [`crate::Data::decrypt_payload`]) and which CMAC block layout is
95/// used for the MIC (see [`crate::LoraPacket::calculate_mic_v1_0`]).
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub enum Direction {
99 /// Device to network server.
100 Uplink,
101 /// Network server to device.
102 Downlink,
103}
104
105/// `LoRaWAN` protocol version, used to pick the right MIC and crypto path.
106///
107/// 1.0 and 1.1 use the same AES-CMAC primitive but different key roles, B
108/// blocks, and (for uplinks) a dual-MIC construction. The version is implicit
109/// in which method you call:
110///
111/// - [`crate::LoraPacket::verify_mic_v1_0`] +
112/// [`crate::V1_0MicKeys`] for 1.0.x.
113/// - [`crate::LoraPacket::verify_mic_v1_1`] +
114/// [`crate::V1_1MicKeys`] for 1.1.
115///
116/// This enum is exposed for callers that route or log by version; it does not
117/// itself dispatch.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub enum LorawanVersion {
121 /// `LoRaWAN` 1.0.x.
122 V1_0,
123 /// `LoRaWAN` 1.1.
124 V1_1,
125}
126
127/// MHDR byte: 3 bits `MType`, 3 bits RFU, 2 bits Major.
128///
129/// Wraps the leading byte of every `PHYPayload`. Build with
130/// [`Mhdr::from_parts`] or wrap an existing byte with [`Mhdr::new`].
131///
132/// # Examples
133///
134/// ```
135/// use lora_packet::{Mhdr, MType};
136///
137/// let m = Mhdr::from_parts(MType::UnconfirmedDataUp, 0);
138/// assert_eq!(m.as_byte(), 0x40);
139/// assert_eq!(m.m_type().unwrap(), MType::UnconfirmedDataUp);
140/// assert_eq!(m.major(), 0);
141/// ```
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub struct Mhdr(pub u8);
145
146impl Mhdr {
147 /// Construct from a raw byte.
148 pub const fn new(b: u8) -> Self {
149 Self(b)
150 }
151
152 /// Build MHDR from `MType` and major version (default major = 0).
153 pub const fn from_parts(m_type: MType, major: u8) -> Self {
154 Self(((m_type as u8) << 5) | (major & 0b11))
155 }
156
157 /// Decode the `MType`.
158 ///
159 /// # Errors
160 /// Returns [`Error::InvalidMType`] if the field is not a defined value
161 /// (currently all patterns are defined).
162 pub const fn m_type(&self) -> Result<MType> {
163 MType::from_mhdr(self.0)
164 }
165
166 /// Lower 2 bits, the major version. Only `0b00` is defined.
167 pub const fn major(&self) -> u8 {
168 self.0 & 0b11
169 }
170
171 /// Raw byte for serialization.
172 pub const fn as_byte(&self) -> u8 {
173 self.0
174 }
175}
176
177/// `FCtrl` byte in a data-frame FHDR.
178///
179/// Bit layout (uplink and downlink differ on bit 4):
180/// - Bit 7: ADR
181/// - Bit 6: `ADRACKReq` (uplink) / RFU (downlink)
182/// - Bit 5: ACK
183/// - Bit 4: `ClassB` (uplink) / `FPending` (downlink)
184/// - Bits 3..0: `FOptsLen`
185///
186/// # Examples
187///
188/// ```
189/// use lora_packet::FCtrl;
190///
191/// let c = FCtrl(0b1010_0110);
192/// assert!(c.adr());
193/// assert!(c.ack());
194/// assert_eq!(c.f_opts_len(), 6);
195/// ```
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198pub struct FCtrl(pub u8);
199
200impl FCtrl {
201 /// Construct from a raw byte.
202 pub const fn new(b: u8) -> Self {
203 Self(b)
204 }
205
206 /// ADR bit.
207 pub const fn adr(&self) -> bool {
208 self.0 & 0b1000_0000 != 0
209 }
210
211 /// `ADRACKReq` bit (uplink only).
212 pub const fn adr_ack_req(&self) -> bool {
213 self.0 & 0b0100_0000 != 0
214 }
215
216 /// ACK bit.
217 pub const fn ack(&self) -> bool {
218 self.0 & 0b0010_0000 != 0
219 }
220
221 /// `FPending` bit (downlink only; same position as `ClassB` on uplink).
222 pub const fn f_pending(&self) -> bool {
223 self.0 & 0b0001_0000 != 0
224 }
225
226 /// `ClassB` bit (uplink only; same position as `FPending` on downlink).
227 pub const fn class_b(&self) -> bool {
228 self.0 & 0b0001_0000 != 0
229 }
230
231 /// `FOpts` length in bytes (0..=15).
232 pub const fn f_opts_len(&self) -> u8 {
233 self.0 & 0b0000_1111
234 }
235
236 /// Raw byte for serialization.
237 pub const fn as_byte(&self) -> u8 {
238 self.0
239 }
240}
241
242/// `DLSettings` byte in a Join Accept.
243///
244/// Bit layout:
245/// - Bit 7: `OptNeg` (`LoRaWAN` 1.1 only)
246/// - Bits 6..4: `RX1DRoffset`
247/// - Bits 3..0: `RX2DataRate`
248///
249/// `OptNeg = 1` signals that the device should operate in 1.1 mode
250/// (dual-MIC, separate JS keys, etc.); `OptNeg = 0` keeps the session in
251/// 1.0 compatibility mode.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
253#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
254pub struct DlSettings(pub u8);
255
256impl DlSettings {
257 /// Construct from a raw byte.
258 pub const fn new(b: u8) -> Self {
259 Self(b)
260 }
261
262 /// RX1 data-rate offset (3 bits).
263 pub const fn rx1_dr_offset(&self) -> u8 {
264 (self.0 >> 4) & 0b111
265 }
266
267 /// RX2 data rate (4 bits).
268 pub const fn rx2_data_rate(&self) -> u8 {
269 self.0 & 0b1111
270 }
271
272 /// `OptNeg` bit. When set, the device is operating in `LoRaWAN` 1.1 mode.
273 pub const fn opt_neg(&self) -> bool {
274 self.0 & 0b1000_0000 != 0
275 }
276
277 /// Raw byte for serialization.
278 pub const fn as_byte(&self) -> u8 {
279 self.0
280 }
281}
282
283/// Internal macro: declare a Copy newtype wrapping a fixed-size byte array.
284macro_rules! id_newtype {
285 ($(#[$meta:meta])* $name:ident, $len:expr) => {
286 $(#[$meta])*
287 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
289 pub struct $name(pub [u8; $len]);
290
291 impl $name {
292 /// Construct from a fixed-size array.
293 pub const fn new(bytes: [u8; $len]) -> Self {
294 Self(bytes)
295 }
296
297 /// Construct from a slice, validating the length.
298 ///
299 /// # Errors
300 /// Returns [`Error::InvalidIdentifierLength`] when the slice length
301 /// does not match the expected size.
302 pub const fn from_slice(s: &[u8]) -> Result<Self> {
303 if s.len() != $len {
304 return Err(Error::InvalidIdentifierLength { expected: $len, got: s.len() });
305 }
306 let mut arr = [0u8; $len];
307 arr.copy_from_slice(s);
308 Ok(Self(arr))
309 }
310
311 /// Borrow the underlying bytes.
312 pub const fn as_bytes(&self) -> &[u8; $len] {
313 &self.0
314 }
315 }
316
317 #[cfg(feature = "hex_base64")]
318 impl $name {
319 /// Construct from a hex string.
320 ///
321 /// # Errors
322 /// [`Error::Hex`] if the input is not valid hex.
323 /// [`Error::InvalidIdentifierLength`] if the decoded byte length is wrong.
324 pub fn from_hex(s: &str) -> Result<Self> {
325 let bytes = hex::decode(s)?;
326 Self::from_slice(&bytes)
327 }
328
329 /// Construct from a standard base64 string.
330 ///
331 /// # Errors
332 /// [`Error::Base64`] if the input is not valid base64.
333 /// [`Error::InvalidIdentifierLength`] if the decoded byte length is wrong.
334 pub fn from_base64(s: &str) -> Result<Self> {
335 use base64::Engine as _;
336 let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
337 Self::from_slice(&bytes)
338 }
339 }
340 };
341}
342
343id_newtype!(
344 /// Device address (4 bytes, big-endian display order).
345 ///
346 /// Assigned by the network during join, then carried in every Data frame.
347 /// Wire bytes are little-endian; the struct keeps display order so
348 /// `DevAddr([0x49, 0xBE, 0x7D, 0xF1])` prints as `49be7df1`.
349 DevAddr, 4
350);
351id_newtype!(
352 /// Device EUI (8 bytes, big-endian display order).
353 ///
354 /// IEEE EUI-64 globally unique device identifier. Carried in Join Request
355 /// and Rejoin Request frames; the network uses it to look up the device.
356 DevEui, 8
357);
358id_newtype!(
359 /// Application EUI in `LoRaWAN` 1.0 / Join EUI in 1.1 (8 bytes).
360 ///
361 /// Identifies the Join Server responsible for the device. See alias
362 /// [`JoinEui`].
363 AppEui, 8
364);
365/// `LoRaWAN` 1.1 spec alias for [`AppEui`]. The 1.1 spec renamed the field
366/// to `JoinEUI`; the bytes are the same.
367pub use AppEui as JoinEui;
368
369id_newtype!(
370 /// Network ID (3 bytes).
371 ///
372 /// Identifies the home network. Carried in Join Accept and in the B0 block
373 /// for some MIC calculations.
374 NetId, 3
375);
376id_newtype!(
377 /// Device nonce (2 bytes).
378 ///
379 /// Per-join random value generated by the device; used together with
380 /// `AppNonce` for session-key derivation. The device must not reuse a
381 /// `DevNonce` per the spec.
382 DevNonce, 2
383);
384id_newtype!(
385 /// Application nonce (1.0) / Join nonce (1.1), 3 bytes.
386 ///
387 /// Per-join random value generated by the network. See alias
388 /// [`JoinNonce`].
389 AppNonce, 3
390);
391/// `LoRaWAN` 1.1 spec alias for [`AppNonce`]. The 1.1 spec renamed the field
392/// to `JoinNonce`; the bytes are the same.
393pub use AppNonce as JoinNonce;
394
395/// Internal macro: declare a 16-byte key newtype with redacted Debug,
396/// explicit `Zeroize`, and the standard constructor/accessor surface.
397///
398/// Keys deliberately do not implement `Copy`. Copying secret material around
399/// the stack defeats `ZeroizeOnDrop`: every implicit copy leaves a residue
400/// no destructor can find. Callers borrow keys (`&key`) and explicitly clone
401/// only when ownership is required.
402macro_rules! key_newtype {
403 ($(#[$meta:meta])* $name:ident) => {
404 $(#[$meta])*
405 #[derive(Clone, PartialEq, Eq, Hash, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
406 pub struct $name([u8; 16]);
407
408 impl $name {
409 /// Construct from a 16-byte array.
410 pub const fn new(bytes: [u8; 16]) -> Self {
411 Self(bytes)
412 }
413
414 /// Construct from a slice, validating the length.
415 ///
416 /// # Errors
417 /// Returns [`Error::InvalidKeyLength`] when the slice is not 16 bytes.
418 pub const fn from_slice(s: &[u8]) -> Result<Self> {
419 if s.len() != 16 {
420 return Err(Error::InvalidKeyLength { expected: 16, got: s.len() });
421 }
422 let mut arr = [0u8; 16];
423 arr.copy_from_slice(s);
424 Ok(Self(arr))
425 }
426
427 /// Borrow the raw key bytes.
428 pub const fn as_bytes(&self) -> &[u8; 16] {
429 &self.0
430 }
431 }
432
433 impl core::fmt::Debug for $name {
434 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
435 write!(f, concat!(stringify!($name), "(***)"))
436 }
437 }
438
439 #[cfg(feature = "hex_base64")]
440 impl $name {
441 /// Construct from a hex string (32 hex chars for 16 bytes).
442 ///
443 /// # Errors
444 /// [`Error::Hex`] if the input is not valid hex.
445 /// [`Error::InvalidKeyLength`] if the decoded byte length is not 16.
446 pub fn from_hex(s: &str) -> Result<Self> {
447 let bytes = hex::decode(s)?;
448 Self::from_slice(&bytes)
449 }
450
451 /// Construct from a standard base64 string.
452 ///
453 /// # Errors
454 /// [`Error::Base64`] if the input is not valid base64.
455 /// [`Error::InvalidKeyLength`] if the decoded byte length is not 16.
456 pub fn from_base64(s: &str) -> Result<Self> {
457 use base64::Engine as _;
458 let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
459 Self::from_slice(&bytes)
460 }
461 }
462
463 #[cfg(feature = "serde")]
464 impl serde::Serialize for $name {
465 fn serialize<S: serde::Serializer>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error> {
466 serializer.serialize_str(&hex_encode_16(&self.0))
467 }
468 }
469
470 #[cfg(feature = "serde")]
471 impl<'de> serde::Deserialize<'de> for $name {
472 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> core::result::Result<Self, D::Error> {
473 let s = <alloc::string::String as serde::Deserialize>::deserialize(deserializer)?;
474 let bytes = hex_decode_16(&s).map_err(serde::de::Error::custom)?;
475 Ok(Self(bytes))
476 }
477 }
478 };
479}
480
481#[cfg(feature = "serde")]
482fn hex_encode_16(b: &[u8; 16]) -> alloc::string::String {
483 let mut s = alloc::string::String::with_capacity(32);
484 for byte in b {
485 use core::fmt::Write;
486 let _ = write!(s, "{byte:02x}");
487 }
488 s
489}
490
491#[cfg(feature = "serde")]
492fn hex_decode_16(s: &str) -> core::result::Result<[u8; 16], &'static str> {
493 if s.len() != 32 {
494 return Err("expected 32 hex characters for a 16-byte key");
495 }
496 let mut out = [0u8; 16];
497 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
498 let hi = hex_nibble(chunk[0])?;
499 let lo = hex_nibble(chunk[1])?;
500 out[i] = (hi << 4) | lo;
501 }
502 Ok(out)
503}
504
505#[cfg(feature = "serde")]
506const fn hex_nibble(b: u8) -> core::result::Result<u8, &'static str> {
507 match b {
508 b'0'..=b'9' => Ok(b - b'0'),
509 b'a'..=b'f' => Ok(b - b'a' + 10),
510 b'A'..=b'F' => Ok(b - b'A' + 10),
511 _ => Err("invalid hex character"),
512 }
513}
514
515key_newtype!(
516 /// Root application key (`LoRaWAN` 1.0; also used for 1.1 `AppSKey`
517 /// derivation).
518 ///
519 /// Provisioned in the device; never sent over the air. In 1.0 it derives
520 /// both `AppSKey` and `NwkSKey` via [`crate::SessionKeys10::derive`]. In
521 /// 1.1 it derives `AppSKey` via [`crate::SessionKeys11::derive`].
522 AppKey
523);
524key_newtype!(
525 /// Root network key (`LoRaWAN` 1.1).
526 ///
527 /// Provisioned in the device; never sent over the air. Used to derive
528 /// `FNwkSIntKey`, `SNwkSIntKey`, `NwkSEncKey`, `JSIntKey`, and `JSEncKey`.
529 /// Pass to [`crate::SessionKeys11::derive`] and
530 /// [`crate::JoinServerKeys::derive`].
531 NwkKey
532);
533key_newtype!(
534 /// Application session key.
535 ///
536 /// Used to encrypt and decrypt `FRMPayload` when `FPort > 0` (the common
537 /// case for application data). See [`crate::Data::decrypt_payload`].
538 AppSKey
539);
540key_newtype!(
541 /// Network session key (`LoRaWAN` 1.0).
542 ///
543 /// Used for:
544 /// - Data MIC computation (see [`crate::LoraPacket::verify_mic_v1_0`]).
545 /// - `FRMPayload` crypt when `FPort = 0` (MAC commands in `FRMPayload`).
546 ///
547 /// In 1.1, the equivalent roles split into `FNwkSIntKey`, `SNwkSIntKey`,
548 /// and `NwkSEncKey`.
549 NwkSKey
550);
551key_newtype!(
552 /// Forwarding network session integrity key (`LoRaWAN` 1.1).
553 ///
554 /// Computes the lower 2 bytes of the dual-MIC for uplink Data frames.
555 /// See [`crate::V1_1MicKeys`].
556 FNwkSIntKey
557);
558key_newtype!(
559 /// Serving network session integrity key (`LoRaWAN` 1.1).
560 ///
561 /// Computes the upper 2 bytes of the uplink dual-MIC and the full
562 /// downlink MIC; also used for Rejoin types 0 and 2.
563 SNwkSIntKey
564);
565key_newtype!(
566 /// Network session encryption key (`LoRaWAN` 1.1).
567 ///
568 /// Encrypts `FOpts` MAC commands and `FRMPayload` with `FPort = 0`.
569 /// See [`crate::Data::encrypt_fopts`].
570 NwkSEncKey
571);
572key_newtype!(
573 /// Join Server integrity key (`LoRaWAN` 1.1).
574 ///
575 /// MIC key for Join Accept and Rejoin Type 1. Derived from `NwkKey` and
576 /// `DevEui` via [`crate::JoinServerKeys::derive`].
577 JSIntKey
578);
579key_newtype!(
580 /// Join Server encryption key (`LoRaWAN` 1.1).
581 ///
582 /// Re-encrypts the Join Accept body sent to a rejoining device. Derived
583 /// from `NwkKey` and `DevEui` via [`crate::JoinServerKeys::derive`].
584 JSEncKey
585);
586key_newtype!(
587 /// Root key for Relay / Wake-On-Radio (WOR) sessions.
588 ///
589 /// Derived from `NwkSKey` via [`crate::WorKeys::root`]. Pass to
590 /// [`crate::WorKeys::session`] together with a `DevAddr` to produce a
591 /// `WorSessionKeys` pair.
592 RootWorSKey
593);
594key_newtype!(
595 /// WOR session integrity key.
596 ///
597 /// One half of the pair produced by [`crate::WorKeys::session`].
598 WorSIntKey
599);
600key_newtype!(
601 /// WOR session encryption key.
602 ///
603 /// One half of the pair produced by [`crate::WorKeys::session`].
604 WorSEncKey
605);
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn mtype_from_mhdr_unconfirmed_up() {
613 assert_eq!(MType::from_mhdr(0x40).unwrap(), MType::UnconfirmedDataUp);
614 }
615
616 #[test]
617 fn mtype_from_mhdr_join_request() {
618 assert_eq!(MType::from_mhdr(0x00).unwrap(), MType::JoinRequest);
619 }
620
621 #[test]
622 fn mtype_from_mhdr_join_accept() {
623 assert_eq!(MType::from_mhdr(0x20).unwrap(), MType::JoinAccept);
624 }
625
626 #[test]
627 fn mtype_from_mhdr_proprietary() {
628 assert_eq!(MType::from_mhdr(0xE0).unwrap(), MType::Proprietary);
629 }
630
631 #[test]
632 fn mtype_from_mhdr_ignores_low_bits() {
633 assert_eq!(MType::from_mhdr(0b010_00011).unwrap(), MType::UnconfirmedDataUp);
634 }
635
636 #[test]
637 fn mhdr_from_parts_data_up() {
638 let m = Mhdr::from_parts(MType::UnconfirmedDataUp, 0);
639 assert_eq!(m.as_byte(), 0x40);
640 assert_eq!(m.m_type().unwrap(), MType::UnconfirmedDataUp);
641 assert_eq!(m.major(), 0);
642 }
643
644 #[test]
645 fn fctrl_bits() {
646 let c = FCtrl(0b1010_0110);
647 assert!(c.adr());
648 assert!(!c.adr_ack_req());
649 assert!(c.ack());
650 assert!(!c.f_pending());
651 assert_eq!(c.f_opts_len(), 6);
652 }
653
654 #[test]
655 fn dlsettings_layout() {
656 let d = DlSettings(0b1011_0010);
657 assert!(d.opt_neg());
658 assert_eq!(d.rx1_dr_offset(), 0b011);
659 assert_eq!(d.rx2_data_rate(), 0b0010);
660 }
661
662 #[test]
663 fn dev_addr_from_slice_ok() {
664 let a = DevAddr::from_slice(&[0x49, 0xBE, 0x7D, 0xF1]).unwrap();
665 assert_eq!(a.as_bytes(), &[0x49, 0xBE, 0x7D, 0xF1]);
666 }
667
668 #[test]
669 fn dev_addr_from_slice_wrong_length() {
670 let e = DevAddr::from_slice(&[0x49, 0xBE, 0x7D]).unwrap_err();
671 match e {
672 Error::InvalidIdentifierLength { expected, got } => {
673 assert_eq!(expected, 4);
674 assert_eq!(got, 3);
675 }
676 _ => panic!("wrong error variant"),
677 }
678 }
679
680 #[test]
681 fn dev_eui_round_trip() {
682 let bytes = [0x33, 0x31, 0x38, 0x32, 0x74, 0x35, 0x69, 0x05];
683 let e = DevEui::new(bytes);
684 assert_eq!(e.as_bytes(), &bytes);
685 }
686
687 #[test]
688 fn join_eui_is_app_eui_alias() {
689 let a: AppEui = JoinEui::new([1, 2, 3, 4, 5, 6, 7, 8]);
690 assert_eq!(a.as_bytes(), &[1, 2, 3, 4, 5, 6, 7, 8]);
691 }
692
693 #[test]
694 fn dev_nonce_two_bytes() {
695 let n = DevNonce::from_slice(&[0xF1, 0x8E]).unwrap();
696 assert_eq!(n.as_bytes(), &[0xF1, 0x8E]);
697 }
698
699 use alloc::format;
700
701 #[test]
702 fn app_key_from_slice_ok() {
703 let k = AppKey::from_slice(&[0u8; 16]).unwrap();
704 assert_eq!(k.as_bytes(), &[0u8; 16]);
705 }
706
707 #[test]
708 fn app_key_from_slice_wrong_length() {
709 let e = AppKey::from_slice(&[0u8; 15]).unwrap_err();
710 match e {
711 Error::InvalidKeyLength { expected, got } => {
712 assert_eq!(expected, 16);
713 assert_eq!(got, 15);
714 }
715 _ => panic!("wrong error variant"),
716 }
717 }
718
719 #[test]
720 fn key_debug_is_redacted() {
721 let k = AppSKey::new([0xAB; 16]);
722 let s = format!("{k:?}");
723 assert_eq!(s, "AppSKey(***)");
724 assert!(!s.contains("ab"));
725 }
726
727 #[test]
728 fn key_zeroize_wipes_bytes() {
729 use zeroize::Zeroize;
730 let mut k = NwkSKey::new([0xFFu8; 16]);
731 k.zeroize();
732 assert_eq!(k.as_bytes(), &[0u8; 16]);
733 }
734
735 #[test]
736 fn key_zeroize_on_drop() {
737 // Direct verification of the drop-time wipe is not possible from safe
738 // Rust: once a value is dropped its storage may be reused. The presence
739 // of `zeroize::ZeroizeOnDrop` in the derives (see the macro) is what
740 // gives the guarantee. As a smoke test, confirm that explicit
741 // `Zeroize::zeroize` clears the bytes; the same impl runs on drop.
742 use zeroize::Zeroize;
743 let mut k = NwkSKey::new([0xff; 16]);
744 k.zeroize();
745 assert_eq!(k.as_bytes(), &[0u8; 16]);
746 }
747}