Skip to main content

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}