Skip to main content

lora_packet/
codec.rs

1//! Wire-format codec for `LoRaWAN` packets.
2//!
3//! Two halves to the API:
4//!
5//! - **Parsing**: [`LoraPacket::from_wire`] takes raw bytes and returns a
6//!   typed [`LoraPacket`]. Match on [`LoraPacket::payload`] (a [`Payload`]
7//!   enum) to dispatch on the message variant.
8//! - **Building**: [`LoraPacket::builder`] returns a [`LoraPacketBuilder`]
9//!   that finalises with [`LoraPacketBuilder::sign_and_encrypt`] (Data),
10//!   [`LoraPacketBuilder::sign_join_request`] (Join Request 1.0),
11//!   [`LoraPacketBuilder::sign_join_request_v1_1`] (Join Request 1.1), or
12//!   [`LoraPacketBuilder::sign_join_accept`] (Join Accept).
13//!
14//! All wire bytes are little-endian; struct fields display in big-endian
15//! order. The codec reverses bytes for you on both parse and serialise.
16
17use alloc::vec::Vec;
18
19use crate::types::{AppEui, AppNonce, DevAddr, DevEui, DevNonce, Direction, DlSettings, FCtrl, MType, Mhdr, NetId};
20
21/// A `LoRaWAN` `PHYPayload`, parsed into structured fields.
22///
23/// `LoraPacket` is always exactly one of the five message types described by
24/// [`Payload`]. The variant carries every field that is meaningful for that
25/// message type; fields that do not apply are not representable.
26///
27/// Construct from wire bytes with [`from_wire`](Self::from_wire), or compose
28/// from fields with [`builder`](Self::builder).
29///
30/// # Examples
31///
32/// Parse and dispatch on the message variant:
33///
34/// ```
35/// use lora_packet::{LoraPacket, Payload};
36///
37/// let bytes = hex::decode("40f17dbe4900020001954378762b11ff0d")?;
38/// let packet = LoraPacket::from_wire(&bytes)?;
39///
40/// match &packet.payload {
41///   Payload::Data(d) => {
42///     assert_eq!(d.dev_addr.as_bytes(), &[0x49, 0xbe, 0x7d, 0xf1]);
43///     assert_eq!(d.f_cnt(), 2);
44///   }
45///   Payload::JoinRequest(_) => unreachable!(),
46///   Payload::JoinAccept(_) => unreachable!(),
47///   Payload::RejoinRequest(_) => unreachable!(),
48///   Payload::Proprietary(_) => unreachable!(),
49/// }
50/// # Ok::<(), Box<dyn std::error::Error>>(())
51/// ```
52#[derive(Debug, Clone, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct LoraPacket {
55  /// Full wire bytes (MHDR + `MACPayload` + MIC).
56  ///
57  /// Kept around so that MIC re-computation does not need to re-serialise.
58  /// [`to_wire`](Self::to_wire) rebuilds these bytes from the typed fields.
59  pub phy_payload: Vec<u8>,
60  /// MAC header byte.
61  pub mhdr: Mhdr,
62  /// 4-byte message integrity code (the last 4 bytes of `phy_payload`).
63  pub mic: [u8; 4],
64  /// Type-specific payload fields. See [`Payload`].
65  pub payload: Payload,
66}
67
68/// Discriminated union over the five `LoRaWAN` message variants.
69///
70/// The variant is always determined by [`LoraPacket::m_type`] / the MHDR.
71/// Use the [`LoraPacket::as_data`] / [`LoraPacket::as_join_request`]
72/// helpers when you want to peek without an exhaustive match.
73///
74/// # Examples
75///
76/// ```
77/// use lora_packet::{LoraPacket, Payload};
78///
79/// let bytes = hex::decode("0039363463336913aa05693574323831338ef1c1d5ec6c")?;
80/// let packet = LoraPacket::from_wire(&bytes)?;
81///
82/// if let Payload::JoinRequest(jr) = &packet.payload {
83///   assert_eq!(jr.dev_nonce.as_bytes(), &[0xf1, 0x8e]);
84/// }
85/// # Ok::<(), Box<dyn std::error::Error>>(())
86/// ```
87#[derive(Debug, Clone, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub enum Payload {
90  /// OTAA Join Request.
91  JoinRequest(JoinRequest),
92  /// Server-issued Join Accept (plaintext fields; the wire form is encrypted).
93  JoinAccept(JoinAccept),
94  /// Confirmed or unconfirmed Data frame, uplink or downlink.
95  Data(Data),
96  /// `LoRaWAN` 1.1 Rejoin Request (one of three types).
97  RejoinRequest(RejoinRequest),
98  /// Proprietary message body (opaque bytes).
99  Proprietary(Vec<u8>),
100}
101
102/// Fields of an OTAA Join Request.
103///
104/// Returned by [`LoraPacket::from_wire`] when the MHDR indicates
105/// [`MType::JoinRequest`]; the MIC is verified separately with
106/// [`LoraPacket::verify_mic_v1_0`] (using `AppKey`) or
107/// [`LoraPacket::verify_mic_v1_1`] (using `NwkKey`).
108#[derive(Debug, Clone, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct JoinRequest {
111  /// Join EUI (`AppEUI` in 1.0 nomenclature, `JoinEUI` in 1.1).
112  pub join_eui: AppEui,
113  /// Device EUI (IEEE EUI-64).
114  pub dev_eui: DevEui,
115  /// Device-generated random nonce; must not repeat per the spec.
116  pub dev_nonce: DevNonce,
117}
118
119/// Fields of a Join Accept (plaintext, after decrypt).
120///
121/// On the wire, the body of a Join Accept is AES-ECB encrypted. Use
122/// [`JoinAccept::decrypt_from_wire`] to turn wire bytes into this struct,
123/// or [`JoinAccept::encrypt_for_wire`] to go the other direction (server
124/// side).
125#[derive(Debug, Clone, PartialEq, Eq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct JoinAccept {
128  /// Server-generated random nonce (`AppNonce` in 1.0, `JoinNonce` in 1.1).
129  pub join_nonce: AppNonce,
130  /// Network ID.
131  pub net_id: NetId,
132  /// `DevAddr` assigned to the device.
133  pub dev_addr: DevAddr,
134  /// Downlink settings (RX1 offset, RX2 data rate, `OptNeg`).
135  pub dl_settings: DlSettings,
136  /// RX1 delay, in seconds.
137  pub rx_delay: u8,
138  /// Optional Channel-Frequency List (16 bytes); only present in some
139  /// regional plans.
140  pub cf_list: Option<[u8; 16]>,
141  /// 1.1 only: `JoinReqType` byte threaded into the 1.1 Join Accept MIC.
142  /// `None` on parsed packets; set explicitly when re-signing a 1.1 frame.
143  pub join_req_type: Option<u8>,
144}
145
146/// Fields of a Data message (confirmed or unconfirmed, uplink or downlink).
147///
148/// `FRMPayload` is encrypted on the wire; call
149/// [`Data::decrypt_payload`] to recover the plaintext. MAC commands in
150/// `FOpts` are encrypted only in `LoRaWAN` 1.1; call
151/// [`Data::decrypt_fopts`] when applicable.
152///
153/// # Frame counters
154///
155/// The wire carries only the lower 16 bits of the 32-bit `FCnt`. The caller
156/// tracks the upper 16 bits and passes it to [`Data::f_cnt_32`] and to
157/// every crypt / MIC call. Pass `0` for sessions that never wrap.
158#[derive(Debug, Clone, PartialEq, Eq)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160pub struct Data {
161  /// Frame direction, inferred from `MType`.
162  pub direction: Direction,
163  /// `true` for `ConfirmedData{Up,Down}`.
164  pub confirmed: bool,
165  /// Device address.
166  pub dev_addr: DevAddr,
167  /// Frame-control byte.
168  pub f_ctrl: FCtrl,
169  /// Lower 16 bits of `FCnt` as on the wire (little-endian).
170  ///
171  /// Use [`Data::f_cnt`] for a `u16` value or [`Data::f_cnt_32`] for the
172  /// full 32-bit counter with a caller-supplied upper half.
173  pub f_cnt: [u8; 2],
174  /// MAC commands carried in `FOpts` (empty when none); up to 15 bytes.
175  /// Encrypted with `NwkSEncKey` in 1.1; plaintext in 1.0.
176  pub f_opts: Vec<u8>,
177  /// `FPort` byte. `Some(0)` means `FRMPayload` carries MAC commands;
178  /// `Some(p)` with `p > 0` means application data; `None` when neither
179  /// `FPort` nor `FRMPayload` is present.
180  pub f_port: Option<u8>,
181  /// `FRMPayload`. Encrypted on the wire; replace with plaintext after
182  /// [`Data::decrypt_payload`].
183  pub frm_payload: Option<Vec<u8>>,
184}
185
186/// Rejoin Request body (`LoRaWAN` 1.1).
187///
188/// Type 0/2 trigger a rejoin to the same network; type 1 triggers a rejoin
189/// to a different `JoinEUI` (a roaming hand-off). Each type uses a different
190/// MIC key (see [`LoraPacket::calculate_mic_v1_1`]).
191#[derive(Debug, Clone, PartialEq, Eq)]
192#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
193pub enum RejoinRequest {
194  /// Type 0: `NetID || DevEUI || RJCount0`. MIC key: `SNwkSIntKey`.
195  Type0 {
196    /// Network ID.
197    net_id: NetId,
198    /// Device EUI.
199    dev_eui: DevEui,
200    /// Rejoin counter 0.
201    rj_count_0: [u8; 2],
202  },
203  /// Type 1: `JoinEUI || DevEUI || RJCount1`. MIC key: `JSIntKey`.
204  Type1 {
205    /// Join EUI.
206    join_eui: AppEui,
207    /// Device EUI.
208    dev_eui: DevEui,
209    /// Rejoin counter 1.
210    rj_count_1: [u8; 2],
211  },
212  /// Type 2: `NetID || DevEUI || RJCount0`. MIC key: `SNwkSIntKey`.
213  Type2 {
214    /// Network ID.
215    net_id: NetId,
216    /// Device EUI.
217    dev_eui: DevEui,
218    /// Rejoin counter 0.
219    rj_count_0: [u8; 2],
220  },
221}
222
223impl LoraPacket {
224  /// Message type from the MHDR.
225  ///
226  /// # Panics
227  /// Never panics on a `LoraPacket` produced by [`from_wire`](Self::from_wire)
228  /// or the builder; both reject invalid `MType` bytes up front. When a
229  /// `LoraPacket` is struct-constructed directly with an invalid `Mhdr` byte,
230  /// this method will panic. Prefer construction via `from_wire` or
231  /// `builder()`.
232  pub fn m_type(&self) -> MType {
233    self.mhdr.m_type().expect("LoraPacket MHDR always has a valid MType")
234  }
235
236  /// `true` for any data frame (confirmed or unconfirmed, uplink or downlink).
237  pub const fn is_data(&self) -> bool {
238    matches!(self.payload, Payload::Data(_))
239  }
240
241  /// `true` for `ConfirmedDataUp` or `ConfirmedDataDown`.
242  pub fn is_confirmed(&self) -> bool {
243    matches!(self.m_type(), MType::ConfirmedDataUp | MType::ConfirmedDataDown)
244  }
245
246  /// `true` for Join Request.
247  pub const fn is_join_request(&self) -> bool {
248    matches!(self.payload, Payload::JoinRequest(_))
249  }
250
251  /// `true` for Join Accept.
252  pub const fn is_join_accept(&self) -> bool {
253    matches!(self.payload, Payload::JoinAccept(_))
254  }
255
256  /// `true` for Rejoin Request.
257  pub const fn is_rejoin_request(&self) -> bool {
258    matches!(self.payload, Payload::RejoinRequest(_))
259  }
260
261  /// Borrow as [`Data`] if this is a data frame, else `None`.
262  pub const fn as_data(&self) -> Option<&Data> {
263    if let Payload::Data(d) = &self.payload {
264      Some(d)
265    } else {
266      None
267    }
268  }
269
270  /// Mutably borrow as [`Data`] if this is a data frame.
271  pub const fn as_data_mut(&mut self) -> Option<&mut Data> {
272    if let Payload::Data(d) = &mut self.payload {
273      Some(d)
274    } else {
275      None
276    }
277  }
278
279  /// Borrow as [`JoinRequest`] if applicable, else `None`.
280  pub const fn as_join_request(&self) -> Option<&JoinRequest> {
281    if let Payload::JoinRequest(j) = &self.payload {
282      Some(j)
283    } else {
284      None
285    }
286  }
287
288  /// Borrow as [`JoinAccept`] if applicable, else `None`.
289  pub const fn as_join_accept(&self) -> Option<&JoinAccept> {
290    if let Payload::JoinAccept(j) = &self.payload {
291      Some(j)
292    } else {
293      None
294    }
295  }
296
297  /// Borrow as [`RejoinRequest`] if applicable, else `None`.
298  pub const fn as_rejoin_request(&self) -> Option<&RejoinRequest> {
299    if let Payload::RejoinRequest(r) = &self.payload {
300      Some(r)
301    } else {
302      None
303    }
304  }
305
306  /// Direction of this packet on the `LoRaWAN` network.
307  ///
308  /// - [`JoinRequest`] is always uplink (device to network server).
309  /// - [`JoinAccept`] is always downlink (network server to device).
310  /// - [`Data`] returns the direction encoded in its `MType`.
311  /// - [`RejoinRequest`] is always uplink.
312  /// - [`Payload::Proprietary`] has no protocol-defined direction;
313  ///   this method returns `None` in that case.
314  ///
315  /// # Examples
316  ///
317  /// ```
318  /// use lora_packet::{LoraPacket, Direction};
319  ///
320  /// let bytes = hex::decode("40f17dbe4900020001954378762b11ff0d").unwrap();
321  /// let packet = LoraPacket::from_wire(&bytes).unwrap();
322  /// assert_eq!(packet.direction(), Some(Direction::Uplink));
323  /// ```
324  pub const fn direction(&self) -> Option<Direction> {
325    match &self.payload {
326      Payload::JoinRequest(_) | Payload::RejoinRequest(_) => Some(Direction::Uplink),
327      Payload::JoinAccept(_) => Some(Direction::Downlink),
328      Payload::Data(d) => Some(d.direction),
329      Payload::Proprietary(_) => None,
330    }
331  }
332
333  /// Verify the MIC using the `LoRaWAN` 1.0 key set.
334  ///
335  /// Compares against `self.mic` in constant time
336  /// (via `subtle::ConstantTimeEq`). Returns `Ok(true)` on match,
337  /// `Ok(false)` on mismatch, and an error only if a required key is
338  /// missing from `keys`.
339  ///
340  /// # Errors
341  /// [`crate::Error::MissingKey`] if a required key for the message type is
342  /// not in `keys` (e.g. `nwk_s_key` for a Data frame).
343  ///
344  /// # Examples
345  ///
346  /// ```
347  /// use lora_packet::{LoraPacket, NwkSKey, V1_0MicKeys};
348  ///
349  /// let bytes = hex::decode("40f17dbe4900020001954378762b11ff0d")?;
350  /// let packet = LoraPacket::from_wire(&bytes)?;
351  /// let nwk_s_key = NwkSKey::from_slice(&hex::decode("44024241ed4ce9a68c6a8bc055233fd3")?)?;
352  /// let keys = V1_0MicKeys { nwk_s_key: Some(&nwk_s_key), ..Default::default() };
353  /// assert!(packet.verify_mic_v1_0(&keys)?);
354  /// # Ok::<(), Box<dyn std::error::Error>>(())
355  /// ```
356  pub fn verify_mic_v1_0(&self, keys: &crate::mic::V1_0MicKeys<'_>) -> crate::Result<bool> {
357    let calculated = self.calculate_mic_v1_0(keys)?;
358    Ok(crate::mic::mic_eq(calculated, self.mic))
359  }
360
361  /// Verify the MIC using the `LoRaWAN` 1.1 key set.
362  ///
363  /// 1.1 uplinks use a dual-MIC construction with both `FNwkSIntKey` and
364  /// `SNwkSIntKey`; downlinks use `SNwkSIntKey` only. Join Accept frames
365  /// also need `join_eui`, `dev_nonce`, and `join_req_type` in the keyset.
366  ///
367  /// # Errors
368  /// [`crate::Error::MissingKey`] if a required key for the message type is
369  /// not in `keys`.
370  pub fn verify_mic_v1_1(&self, keys: &crate::mic::V1_1MicKeys<'_>) -> crate::Result<bool> {
371    let calculated = self.calculate_mic_v1_1(keys)?;
372    Ok(crate::mic::mic_eq(calculated, self.mic))
373  }
374
375  /// Calculate the MIC under `LoRaWAN` 1.0 without overwriting `self.mic`.
376  ///
377  /// Useful when you want to compare against an expected value without
378  /// running [`Self::verify_mic_v1_0`], or when you need to record both the
379  /// computed MIC and the received MIC for debugging.
380  ///
381  /// # Errors
382  /// - [`crate::Error::MissingKey`] if a required key for the message type
383  ///   is not in `keys`.
384  /// - [`crate::Error::UnsupportedForVersion`] if the payload is a Rejoin
385  ///   Request or Proprietary frame (both 1.1-only).
386  pub fn calculate_mic_v1_0(&self, keys: &crate::mic::V1_0MicKeys<'_>) -> crate::Result<[u8; 4]> {
387    match &self.payload {
388      Payload::Data(_) => {
389        let key = keys
390          .nwk_s_key
391          .ok_or(crate::Error::MissingKey("nwk_s_key required for Data MIC"))?;
392        Ok(crate::mic::calculate_data_mic_1_0(self, key.as_bytes(), keys.f_cnt_msb))
393      }
394      Payload::JoinRequest(_) => {
395        let key = keys
396          .app_key
397          .ok_or(crate::Error::MissingKey("app_key required for Join Request MIC"))?;
398        Ok(crate::mic::calculate_join_request_mic(self, key.as_bytes()))
399      }
400      Payload::JoinAccept(_) => {
401        let key = keys
402          .app_key
403          .ok_or(crate::Error::MissingKey("app_key required for Join Accept MIC"))?;
404        let mhdr_and_body = &self.phy_payload[..self.phy_payload.len() - 4];
405        Ok(crate::mic::calculate_join_accept_mic_1_0(mhdr_and_body, key.as_bytes()))
406      }
407      Payload::RejoinRequest(_) | Payload::Proprietary(_) => Err(crate::Error::UnsupportedForVersion(
408        "Rejoin Request and Proprietary frames are 1.1-only; use calculate_mic_v1_1",
409      )),
410    }
411  }
412
413  /// Calculate the MIC under `LoRaWAN` 1.1 without overwriting `self.mic`.
414  ///
415  /// Dispatches by `MType` and direction:
416  /// - Data uplink: dual-MIC with `FNwkSIntKey` + `SNwkSIntKey`.
417  /// - Data downlink: single MIC with `SNwkSIntKey`.
418  /// - Join Request: `NwkKey`.
419  /// - Join Accept: `JSIntKey` + `JoinEUI` + `DevNonce` + `JoinReqType`.
420  /// - Rejoin Type 1: `JSIntKey`.
421  /// - Rejoin Type 0/2: `SNwkSIntKey`.
422  ///
423  /// # Errors
424  /// [`crate::Error::MissingKey`] if a required key (or context field) for
425  /// the message type is not in `keys`.
426  pub fn calculate_mic_v1_1(&self, keys: &crate::mic::V1_1MicKeys<'_>) -> crate::Result<[u8; 4]> {
427    match &self.payload {
428      Payload::Data(d) => match d.direction {
429        Direction::Uplink => {
430          let f_key = keys.f_nwk_s_int_key.ok_or(crate::Error::MissingKey(
431            "f_nwk_s_int_key required for Data uplink 1.1 MIC",
432          ))?;
433          let s_key = keys.s_nwk_s_int_key.ok_or(crate::Error::MissingKey(
434            "s_nwk_s_int_key required for Data uplink 1.1 MIC",
435          ))?;
436          let conf = keys.conf_fcnt_down_tx_dr_tx_ch.unwrap_or([0, 0, 0, 0]);
437          Ok(crate::mic::calculate_data_mic_1_1_uplink(
438            self,
439            f_key.as_bytes(),
440            s_key.as_bytes(),
441            keys.f_cnt_msb,
442            conf,
443          ))
444        }
445        Direction::Downlink => {
446          let s_key = keys.s_nwk_s_int_key.ok_or(crate::Error::MissingKey(
447            "s_nwk_s_int_key required for Data downlink 1.1 MIC",
448          ))?;
449          let conf = keys.conf_fcnt_down_tx_dr_tx_ch.unwrap_or([0, 0, 0, 0]);
450          Ok(crate::mic::calculate_data_mic_1_1_downlink(
451            self,
452            s_key.as_bytes(),
453            keys.f_cnt_msb,
454            conf,
455          ))
456        }
457      },
458      Payload::JoinRequest(_) => {
459        let key = keys
460          .nwk_key
461          .ok_or(crate::Error::MissingKey("nwk_key required for Join Request 1.1 MIC"))?;
462        Ok(crate::mic::calculate_join_request_mic(self, key.as_bytes()))
463      }
464      Payload::JoinAccept(_) => {
465        let js_key = keys
466          .js_int_key
467          .ok_or(crate::Error::MissingKey("js_int_key required for Join Accept 1.1 MIC"))?;
468        let join_eui = keys
469          .join_eui
470          .ok_or(crate::Error::MissingKey("join_eui required for Join Accept 1.1 MIC"))?;
471        let dev_nonce = keys
472          .dev_nonce
473          .ok_or(crate::Error::MissingKey("dev_nonce required for Join Accept 1.1 MIC"))?;
474        let join_req_type = keys.join_req_type.unwrap_or(0xFF);
475        let mhdr_and_body = &self.phy_payload[..self.phy_payload.len() - 4];
476        Ok(crate::mic::calculate_join_accept_mic_1_1(
477          mhdr_and_body,
478          js_key.as_bytes(),
479          join_req_type,
480          &join_eui,
481          &dev_nonce,
482        ))
483      }
484      Payload::RejoinRequest(rj) => {
485        let key = match rj {
486          RejoinRequest::Type1 { .. } => keys
487            .js_int_key
488            .ok_or(crate::Error::MissingKey("js_int_key required for Rejoin Type 1 MIC"))?
489            .as_bytes(),
490          _ => keys
491            .s_nwk_s_int_key
492            .ok_or(crate::Error::MissingKey(
493              "s_nwk_s_int_key required for Rejoin Type 0/2 MIC",
494            ))?
495            .as_bytes(),
496        };
497        Ok(crate::mic::calculate_rejoin_mic(self, key))
498      }
499      Payload::Proprietary(_) => Err(crate::Error::MissingKey("Proprietary has no defined MIC")),
500    }
501  }
502
503  /// Recompute and overwrite the MIC under `LoRaWAN` 1.0.
504  ///
505  /// Also rewrites `phy_payload` so its trailing 4 bytes match the new MIC.
506  /// Use after mutating fields on a parsed packet, or after building an
507  /// unsigned packet you want to sign in place.
508  ///
509  /// # Errors
510  /// [`crate::Error::MissingKey`] if a required key for the message type is
511  /// not in `keys`.
512  pub fn recalculate_mic_v1_0(&mut self, keys: &crate::mic::V1_0MicKeys<'_>) -> crate::Result<()> {
513    let mic = self.calculate_mic_v1_0(keys)?;
514    self.mic = mic;
515    self.phy_payload = self.to_wire();
516    Ok(())
517  }
518
519  /// Recompute and overwrite the MIC under `LoRaWAN` 1.1.
520  ///
521  /// Also rewrites `phy_payload` so its trailing 4 bytes match the new MIC.
522  ///
523  /// # Errors
524  /// [`crate::Error::MissingKey`] if a required key for the message type is
525  /// not in `keys`.
526  pub fn recalculate_mic_v1_1(&mut self, keys: &crate::mic::V1_1MicKeys<'_>) -> crate::Result<()> {
527    let mic = self.calculate_mic_v1_1(keys)?;
528    self.mic = mic;
529    self.phy_payload = self.to_wire();
530    Ok(())
531  }
532}
533
534impl Data {
535  /// Lower 16 bits of `FCnt` as read from the wire (little-endian).
536  ///
537  /// The wire stores only the bottom 16 bits of the actual 32-bit counter.
538  /// For the full counter use [`Data::f_cnt_32`] together with a
539  /// caller-tracked upper half.
540  pub const fn f_cnt(&self) -> u16 {
541    u16::from_le_bytes(self.f_cnt)
542  }
543
544  /// Full 32-bit `FCnt`, combining the wire LSB16 with a caller-tracked MSB16.
545  ///
546  /// `msb` is the upper 16 bits the caller has been tracking. Pass `0` if
547  /// frame counters never wrap in your deployment; otherwise increment
548  /// `msb` each time the wire counter rolls over from `0xFFFF` to `0x0000`.
549  pub const fn f_cnt_32(&self, msb: u16) -> u32 {
550    ((msb as u32) << 16) | (self.f_cnt() as u32)
551  }
552}
553
554impl LoraPacket {
555  /// Parse a complete `PHYPayload` from wire bytes.
556  ///
557  /// This is the primary entry point for inbound traffic. The returned
558  /// [`LoraPacket`] keeps the full wire bytes in
559  /// [`LoraPacket::phy_payload`] so that MIC re-computation is cheap.
560  ///
561  /// Join Accept frames are encrypted on the wire and cannot be parsed
562  /// directly. Use [`JoinAccept::decrypt_from_wire`] first, then either
563  /// pass the result to [`JoinAccept::from_plaintext`] or work with the
564  /// returned plaintext bytes directly.
565  ///
566  /// # Errors
567  /// - [`crate::Error::TooShort`] if the buffer is shorter than the minimum
568  ///   5 bytes (MHDR + MIC), or shorter than the per-variant minimum.
569  /// - [`crate::Error::TooLong`] if the buffer exceeds the `LoRaWAN` PHY
570  ///   maximum of 256 bytes.
571  /// - [`crate::Error::InvalidMType`] if the MHDR encodes an unknown
572  ///   `MType` (reserved for forward compatibility).
573  /// - [`crate::Error::InvalidRejoinType`] if a Rejoin Request type byte is
574  ///   not in {0, 1, 2}.
575  /// - [`crate::Error::Other`] for a Join Accept input
576  ///   (Join Accept needs `decrypt_from_wire`).
577  ///
578  /// # Examples
579  ///
580  /// ```
581  /// use lora_packet::{LoraPacket, MType};
582  ///
583  /// let bytes = hex::decode("40f17dbe4900020001954378762b11ff0d")?;
584  /// let packet = LoraPacket::from_wire(&bytes)?;
585  ///
586  /// assert!(packet.is_data());
587  /// assert_eq!(packet.m_type(), MType::UnconfirmedDataUp);
588  /// assert_eq!(packet.mic, [0x2b, 0x11, 0xff, 0x0d]);
589  /// # Ok::<(), Box<dyn std::error::Error>>(())
590  /// ```
591  pub fn from_wire(bytes: &[u8]) -> crate::Result<Self> {
592    if bytes.len() < 5 {
593      return Err(crate::Error::TooShort {
594        expected: 5,
595        got: bytes.len(),
596      });
597    }
598    // LoRaWAN PHY payload never exceeds 256 bytes in any regional plan.
599    // Reject larger buffers so the 1-byte length field in CMAC B0/B1
600    // cannot silently wrap and produce a wrong-but-deterministic MIC.
601    if bytes.len() > 256 {
602      return Err(crate::Error::TooLong { got: bytes.len() });
603    }
604    let mhdr = Mhdr::new(bytes[0]);
605    let mic_offset = bytes.len() - 4;
606    let mut mic = [0u8; 4];
607    mic.copy_from_slice(&bytes[mic_offset..]);
608    let m_type = mhdr.m_type()?;
609    let body = &bytes[1..mic_offset];
610
611    let payload = match m_type {
612      MType::JoinRequest => Payload::JoinRequest(parse_join_request(body)?),
613      MType::JoinAccept => {
614        return Err(crate::Error::Other(alloc::string::String::from(
615          "JoinAccept parsing requires decrypt; use JoinAccept::decrypt_from_wire",
616        )));
617      }
618      MType::UnconfirmedDataUp | MType::UnconfirmedDataDown | MType::ConfirmedDataUp | MType::ConfirmedDataDown => {
619        Payload::Data(parse_data(m_type, body)?)
620      }
621      MType::RejoinRequest => Payload::RejoinRequest(parse_rejoin_request(body)?),
622      MType::Proprietary => Payload::Proprietary(body.to_vec()),
623    };
624
625    Ok(Self {
626      phy_payload: bytes.to_vec(),
627      mhdr,
628      mic,
629      payload,
630    })
631  }
632
633  /// Serialise back to wire bytes.
634  ///
635  /// Re-encodes the typed fields into the little-endian wire layout and
636  /// appends `self.mic` unchanged. Call
637  /// [`recalculate_mic_v1_0`](Self::recalculate_mic_v1_0) /
638  /// [`recalculate_mic_v1_1`](Self::recalculate_mic_v1_1) first if you have
639  /// mutated fields and need a fresh MIC.
640  ///
641  /// For round-trip parsing, the output of `from_wire(bytes).to_wire()`
642  /// equals `bytes` (subject to MIC bytes being preserved).
643  pub fn to_wire(&self) -> Vec<u8> {
644    let mut out = Vec::with_capacity(self.phy_payload.len().max(13));
645    out.push(self.mhdr.as_byte());
646    match &self.payload {
647      Payload::JoinRequest(jr) => {
648        let mut tmp = *jr.join_eui.as_bytes();
649        tmp.reverse();
650        out.extend_from_slice(&tmp);
651        let mut tmp = *jr.dev_eui.as_bytes();
652        tmp.reverse();
653        out.extend_from_slice(&tmp);
654        let mut tmp = *jr.dev_nonce.as_bytes();
655        tmp.reverse();
656        out.extend_from_slice(&tmp);
657      }
658      Payload::Data(d) => {
659        let mut tmp = *d.dev_addr.as_bytes();
660        tmp.reverse();
661        out.extend_from_slice(&tmp);
662        out.push(d.f_ctrl.as_byte());
663        out.extend_from_slice(&d.f_cnt);
664        out.extend_from_slice(&d.f_opts);
665        if let Some(p) = d.f_port {
666          out.push(p);
667        }
668        if let Some(payload) = &d.frm_payload {
669          out.extend_from_slice(payload);
670        }
671      }
672      Payload::JoinAccept(ja) => {
673        let mut tmp = *ja.join_nonce.as_bytes();
674        tmp.reverse();
675        out.extend_from_slice(&tmp);
676        let mut tmp = *ja.net_id.as_bytes();
677        tmp.reverse();
678        out.extend_from_slice(&tmp);
679        let mut tmp = *ja.dev_addr.as_bytes();
680        tmp.reverse();
681        out.extend_from_slice(&tmp);
682        out.push(ja.dl_settings.as_byte());
683        out.push(ja.rx_delay);
684        if let Some(cf) = ja.cf_list {
685          out.extend_from_slice(&cf);
686        }
687      }
688      Payload::RejoinRequest(rj) => match rj {
689        RejoinRequest::Type0 {
690          net_id,
691          dev_eui,
692          rj_count_0,
693        } => {
694          out.push(0);
695          let mut tmp = *net_id.as_bytes();
696          tmp.reverse();
697          out.extend_from_slice(&tmp);
698          let mut tmp = *dev_eui.as_bytes();
699          tmp.reverse();
700          out.extend_from_slice(&tmp);
701          let mut tmp = *rj_count_0;
702          tmp.reverse();
703          out.extend_from_slice(&tmp);
704        }
705        RejoinRequest::Type1 {
706          join_eui,
707          dev_eui,
708          rj_count_1,
709        } => {
710          out.push(1);
711          let mut tmp = *join_eui.as_bytes();
712          tmp.reverse();
713          out.extend_from_slice(&tmp);
714          let mut tmp = *dev_eui.as_bytes();
715          tmp.reverse();
716          out.extend_from_slice(&tmp);
717          let mut tmp = *rj_count_1;
718          tmp.reverse();
719          out.extend_from_slice(&tmp);
720        }
721        RejoinRequest::Type2 {
722          net_id,
723          dev_eui,
724          rj_count_0,
725        } => {
726          out.push(2);
727          let mut tmp = *net_id.as_bytes();
728          tmp.reverse();
729          out.extend_from_slice(&tmp);
730          let mut tmp = *dev_eui.as_bytes();
731          tmp.reverse();
732          out.extend_from_slice(&tmp);
733          let mut tmp = *rj_count_0;
734          tmp.reverse();
735          out.extend_from_slice(&tmp);
736        }
737      },
738      Payload::Proprietary(b) => out.extend_from_slice(b),
739    }
740    out.extend_from_slice(&self.mic);
741    out
742  }
743}
744
745#[cfg(feature = "hex_base64")]
746impl LoraPacket {
747  /// Parse from a hex-encoded wire frame.
748  ///
749  /// # Errors
750  /// [`crate::Error::Hex`] for invalid hex; otherwise any error from
751  /// [`LoraPacket::from_wire`].
752  pub fn from_hex(s: &str) -> crate::Result<Self> {
753    let bytes = hex::decode(s)?;
754    Self::from_wire(&bytes)
755  }
756
757  /// Parse from a standard base64-encoded wire frame.
758  ///
759  /// # Errors
760  /// [`crate::Error::Base64`] for invalid base64; otherwise any error from
761  /// [`LoraPacket::from_wire`].
762  pub fn from_base64(s: &str) -> crate::Result<Self> {
763    use base64::Engine as _;
764    let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
765    Self::from_wire(&bytes)
766  }
767}
768
769impl JoinAccept {
770  /// Parse an already-decrypted Join Accept (MHDR + body + MIC).
771  ///
772  /// Most callers start with encrypted wire bytes; use
773  /// [`JoinAccept::decrypt_from_wire`] to decrypt first, then pass the
774  /// resulting plaintext bytes here.
775  ///
776  /// Accepts both single-block (17 bytes total) and `CFList` (33 bytes
777  /// total) Join Accept formats.
778  ///
779  /// # Errors
780  /// [`crate::Error::TooShort`] if the total length is below 17 or the body
781  /// length is neither 12 (no `CFList`) nor 28 bytes (with `CFList`).
782  pub fn from_plaintext(bytes: &[u8]) -> crate::Result<Self> {
783    if bytes.len() < 17 {
784      return Err(crate::Error::TooShort {
785        expected: 17,
786        got: bytes.len(),
787      });
788    }
789    let body = &bytes[1..bytes.len() - 4];
790    if body.len() != 12 && body.len() != 28 {
791      return Err(crate::Error::TooShort {
792        expected: 12,
793        got: body.len(),
794      });
795    }
796
797    let mut join_nonce = [0u8; 3];
798    join_nonce.copy_from_slice(&body[0..3]);
799    join_nonce.reverse();
800    let mut net_id = [0u8; 3];
801    net_id.copy_from_slice(&body[3..6]);
802    net_id.reverse();
803    let mut dev_addr = [0u8; 4];
804    dev_addr.copy_from_slice(&body[6..10]);
805    dev_addr.reverse();
806    let dl_settings = DlSettings(body[10]);
807    let rx_delay = body[11];
808
809    let cf_list = if body.len() == 28 {
810      let mut cf = [0u8; 16];
811      cf.copy_from_slice(&body[12..28]);
812      Some(cf)
813    } else {
814      None
815    };
816
817    Ok(Self {
818      join_nonce: AppNonce::new(join_nonce),
819      net_id: NetId::new(net_id),
820      dev_addr: DevAddr::new(dev_addr),
821      dl_settings,
822      rx_delay,
823      cf_list,
824      join_req_type: None,
825    })
826  }
827}
828
829fn parse_join_request(body: &[u8]) -> crate::Result<JoinRequest> {
830  if body.len() != 18 {
831    return Err(crate::Error::TooShort {
832      expected: 18,
833      got: body.len(),
834    });
835  }
836  let mut app_eui = [0u8; 8];
837  app_eui.copy_from_slice(&body[0..8]);
838  app_eui.reverse();
839  let mut dev_eui = [0u8; 8];
840  dev_eui.copy_from_slice(&body[8..16]);
841  dev_eui.reverse();
842  let mut dev_nonce = [0u8; 2];
843  dev_nonce.copy_from_slice(&body[16..18]);
844  dev_nonce.reverse();
845
846  Ok(JoinRequest {
847    join_eui: AppEui::new(app_eui),
848    dev_eui: DevEui::new(dev_eui),
849    dev_nonce: DevNonce::new(dev_nonce),
850  })
851}
852
853fn parse_data(m_type: MType, body: &[u8]) -> crate::Result<Data> {
854  if body.len() < 7 {
855    return Err(crate::Error::TooShort {
856      expected: 7,
857      got: body.len(),
858    });
859  }
860
861  let mut dev_addr = [0u8; 4];
862  dev_addr.copy_from_slice(&body[0..4]);
863  dev_addr.reverse();
864  let f_ctrl = FCtrl(body[4]);
865  let mut f_cnt = [0u8; 2];
866  f_cnt.copy_from_slice(&body[5..7]);
867
868  let f_opts_len = f_ctrl.f_opts_len() as usize;
869  if 7 + f_opts_len > body.len() {
870    return Err(crate::Error::TooShort {
871      expected: 7 + f_opts_len,
872      got: body.len(),
873    });
874  }
875  let f_opts = body[7..7 + f_opts_len].to_vec();
876
877  let remainder_start = 7 + f_opts_len;
878  let (f_port, frm_payload) = if remainder_start >= body.len() {
879    (None, None)
880  } else {
881    let port = body[remainder_start];
882    let payload = if remainder_start + 1 < body.len() {
883      Some(body[remainder_start + 1..].to_vec())
884    } else {
885      Some(Vec::new())
886    };
887    (Some(port), payload)
888  };
889
890  let (direction, confirmed) = match m_type {
891    MType::UnconfirmedDataUp => (Direction::Uplink, false),
892    MType::ConfirmedDataUp => (Direction::Uplink, true),
893    MType::UnconfirmedDataDown => (Direction::Downlink, false),
894    MType::ConfirmedDataDown => (Direction::Downlink, true),
895    _ => unreachable!("parse_data called with non-data MType"),
896  };
897
898  Ok(Data {
899    direction,
900    confirmed,
901    dev_addr: DevAddr::new(dev_addr),
902    f_ctrl,
903    f_cnt,
904    f_opts,
905    f_port,
906    frm_payload,
907  })
908}
909
910/// Fluent builder for assembling a [`LoraPacket`] field by field.
911///
912/// Pick the message variant first with [`data`](Self::data),
913/// [`join_request`](Self::join_request), [`join_accept`](Self::join_accept),
914/// or [`rejoin_request`](Self::rejoin_request). Then set the per-variant
915/// fields. Finalise with one of:
916///
917/// - [`build_unsigned`](Self::build_unsigned): emit a [`LoraPacket`] with
918///   `mic = [0; 4]`. Sign later by calling
919///   [`recalculate_mic_v1_0`](LoraPacket::recalculate_mic_v1_0) or
920///   [`recalculate_mic_v1_1`](LoraPacket::recalculate_mic_v1_1).
921/// - [`sign_and_encrypt`](Self::sign_and_encrypt): encrypt `FRMPayload` and
922///   compute the 1.0 Data MIC.
923/// - [`sign_join_request`](Self::sign_join_request) /
924///   [`sign_join_request_v1_1`](Self::sign_join_request_v1_1): compute the
925///   Join Request MIC.
926/// - [`sign_join_accept`](Self::sign_join_accept): compute the Join Accept
927///   MIC and encrypt the on-air form.
928///
929/// Every field is optional; required-field validation happens in
930/// `build_unsigned` based on the selected variant.
931#[derive(Debug, Default, Clone)]
932pub struct LoraPacketBuilder {
933  m_type: Option<MType>,
934  major: u8,
935  direction: Option<Direction>,
936  confirmed: bool,
937  dev_addr: Option<DevAddr>,
938  f_ctrl: Option<FCtrl>,
939  f_cnt: Option<u16>,
940  f_opts: Vec<u8>,
941  f_port: Option<u8>,
942  payload: Option<Vec<u8>>,
943  join_eui: Option<AppEui>,
944  dev_eui: Option<DevEui>,
945  dev_nonce: Option<DevNonce>,
946  join_nonce: Option<AppNonce>,
947  net_id: Option<NetId>,
948  dl_settings: Option<DlSettings>,
949  rx_delay: Option<u8>,
950  cf_list: Option<[u8; 16]>,
951  join_req_type: Option<u8>,
952  rejoin_type: Option<u8>,
953  rj_count_0: Option<[u8; 2]>,
954  rj_count_1: Option<[u8; 2]>,
955}
956
957impl LoraPacket {
958  /// Begin building a packet field by field.
959  ///
960  /// See [`LoraPacketBuilder`] for the full surface and finalisation
961  /// methods.
962  ///
963  /// # Examples
964  ///
965  /// Build a Data uplink, encrypt, and sign in one expression:
966  ///
967  /// ```
968  /// use lora_packet::{LoraPacket, Direction, DevAddr, AppSKey, NwkSKey};
969  ///
970  /// let app_s_key = AppSKey::new([0u8; 16]);
971  /// let nwk_s_key = NwkSKey::new([0u8; 16]);
972  /// let packet = LoraPacket::builder()
973  ///   .data(Direction::Uplink, false)
974  ///   .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
975  ///   .f_cnt(2)
976  ///   .f_port(1)
977  ///   .payload(b"hi")
978  ///   .sign_and_encrypt(&app_s_key, &nwk_s_key)?;
979  /// assert!(packet.is_data());
980  /// # Ok::<(), lora_packet::Error>(())
981  /// ```
982  ///
983  /// Build and sign a Join Request:
984  ///
985  /// ```
986  /// use lora_packet::{LoraPacket, AppKey, AppEui, DevEui, DevNonce};
987  ///
988  /// let app_key = AppKey::new([0u8; 16]);
989  /// let packet = LoraPacket::builder()
990  ///   .join_request()
991  ///   .join_eui(AppEui::new([0u8; 8]))
992  ///   .dev_eui(DevEui::new([0u8; 8]))
993  ///   .dev_nonce(DevNonce::new([0u8; 2]))
994  ///   .sign_join_request(&app_key)?;
995  /// assert!(packet.is_join_request());
996  /// # Ok::<(), lora_packet::Error>(())
997  /// ```
998  pub fn builder() -> LoraPacketBuilder {
999    LoraPacketBuilder::default()
1000  }
1001}
1002
1003impl LoraPacketBuilder {
1004  /// Set message type and direction for a Data message.
1005  #[must_use]
1006  pub const fn data(mut self, direction: Direction, confirmed: bool) -> Self {
1007    self.direction = Some(direction);
1008    self.confirmed = confirmed;
1009    self.m_type = Some(match (confirmed, direction) {
1010      (false, Direction::Uplink) => MType::UnconfirmedDataUp,
1011      (false, Direction::Downlink) => MType::UnconfirmedDataDown,
1012      (true, Direction::Uplink) => MType::ConfirmedDataUp,
1013      (true, Direction::Downlink) => MType::ConfirmedDataDown,
1014    });
1015    self
1016  }
1017
1018  /// Begin a Join Request.
1019  #[must_use]
1020  pub const fn join_request(mut self) -> Self {
1021    self.m_type = Some(MType::JoinRequest);
1022    self
1023  }
1024
1025  /// Begin a Join Accept.
1026  #[must_use]
1027  pub const fn join_accept(mut self) -> Self {
1028    self.m_type = Some(MType::JoinAccept);
1029    self
1030  }
1031
1032  /// Begin a Rejoin Request with the given type (0, 1, or 2).
1033  #[must_use]
1034  pub const fn rejoin_request(mut self, rejoin_type: u8) -> Self {
1035    self.m_type = Some(MType::RejoinRequest);
1036    self.rejoin_type = Some(rejoin_type);
1037    self
1038  }
1039
1040  /// Set `DevAddr` (Data and Join Accept).
1041  #[must_use]
1042  pub const fn dev_addr(mut self, addr: DevAddr) -> Self {
1043    self.dev_addr = Some(addr);
1044    self
1045  }
1046
1047  /// Set `FCtrl` byte (Data).
1048  #[must_use]
1049  pub const fn f_ctrl(mut self, c: FCtrl) -> Self {
1050    self.f_ctrl = Some(c);
1051    self
1052  }
1053
1054  /// Set `FCnt` (Data).
1055  #[must_use]
1056  pub const fn f_cnt(mut self, n: u16) -> Self {
1057    self.f_cnt = Some(n);
1058    self
1059  }
1060
1061  /// Set `FOpts` MAC commands (Data).
1062  #[must_use]
1063  pub fn f_opts(mut self, opts: &[u8]) -> Self {
1064    self.f_opts = opts.to_vec();
1065    self
1066  }
1067
1068  /// Set `FPort` (Data).
1069  #[must_use]
1070  pub const fn f_port(mut self, p: u8) -> Self {
1071    self.f_port = Some(p);
1072    self
1073  }
1074
1075  /// Set `FRMPayload` plaintext (Data).
1076  #[must_use]
1077  pub fn payload(mut self, p: &[u8]) -> Self {
1078    self.payload = Some(p.to_vec());
1079    self
1080  }
1081
1082  /// Set Join EUI (Join Request / Rejoin Type 1).
1083  #[must_use]
1084  pub const fn join_eui(mut self, e: AppEui) -> Self {
1085    self.join_eui = Some(e);
1086    self
1087  }
1088
1089  /// Set Device EUI (Join Request / Rejoin).
1090  #[must_use]
1091  pub const fn dev_eui(mut self, e: DevEui) -> Self {
1092    self.dev_eui = Some(e);
1093    self
1094  }
1095
1096  /// Set `DevNonce` (Join Request).
1097  #[must_use]
1098  pub const fn dev_nonce(mut self, n: DevNonce) -> Self {
1099    self.dev_nonce = Some(n);
1100    self
1101  }
1102
1103  /// Set Join Nonce / `AppNonce` (Join Accept).
1104  #[must_use]
1105  pub const fn join_nonce(mut self, n: AppNonce) -> Self {
1106    self.join_nonce = Some(n);
1107    self
1108  }
1109
1110  /// Set `NetID` (Join Accept / Rejoin Type 0/2).
1111  #[must_use]
1112  pub const fn net_id(mut self, id: NetId) -> Self {
1113    self.net_id = Some(id);
1114    self
1115  }
1116
1117  /// Set `DLSettings` (Join Accept).
1118  #[must_use]
1119  pub const fn dl_settings(mut self, s: DlSettings) -> Self {
1120    self.dl_settings = Some(s);
1121    self
1122  }
1123
1124  /// Set `RxDelay` (Join Accept).
1125  #[must_use]
1126  pub const fn rx_delay(mut self, r: u8) -> Self {
1127    self.rx_delay = Some(r);
1128    self
1129  }
1130
1131  /// Set `CFList` (Join Accept).
1132  #[must_use]
1133  pub const fn cf_list(mut self, c: [u8; 16]) -> Self {
1134    self.cf_list = Some(c);
1135    self
1136  }
1137
1138  /// Set `JoinReqType` (`LoRaWAN` 1.1 Join Accept MIC context).
1139  #[must_use]
1140  pub const fn join_req_type(mut self, t: u8) -> Self {
1141    self.join_req_type = Some(t);
1142    self
1143  }
1144
1145  /// Set `RJcount0` (Rejoin Request Type 0 and Type 2).
1146  ///
1147  /// Stored little-endian on the wire. Defaults to 0 when not set.
1148  #[must_use]
1149  pub const fn rj_count_0(mut self, count: u16) -> Self {
1150    self.rj_count_0 = Some(count.to_le_bytes());
1151    self
1152  }
1153
1154  /// Set `RJcount1` (Rejoin Request Type 1).
1155  ///
1156  /// Stored little-endian on the wire. Defaults to 0 when not set.
1157  #[must_use]
1158  pub const fn rj_count_1(mut self, count: u16) -> Self {
1159    self.rj_count_1 = Some(count.to_le_bytes());
1160    self
1161  }
1162
1163  /// Build a Join Accept, compute the MIC, and produce the encrypted wire bytes.
1164  ///
1165  /// Returns `(plaintext_packet, encrypted_wire)`. The plaintext packet has
1166  /// MIC populated and `phy_payload` set to the plaintext form. The wire
1167  /// bytes are what you send over the air; the device will decrypt them back
1168  /// to the plaintext form.
1169  ///
1170  /// # Errors
1171  /// `Error::MissingField` if required Join Accept fields are missing.
1172  pub fn sign_join_accept(self, app_key: &crate::types::AppKey) -> crate::Result<(LoraPacket, alloc::vec::Vec<u8>)> {
1173    let mut packet = self.build_unsigned()?;
1174    let keys = crate::mic::V1_0MicKeys {
1175      app_key: Some(app_key),
1176      ..Default::default()
1177    };
1178    packet.recalculate_mic_v1_0(&keys)?;
1179    let encrypted_wire = JoinAccept::encrypt_for_wire(&packet.phy_payload, app_key)?;
1180    Ok((packet, encrypted_wire))
1181  }
1182
1183  /// Build a Join Request and compute its MIC using `LoRaWAN` 1.0 `AppKey`.
1184  ///
1185  /// For `LoRaWAN` 1.1, use [`sign_join_request_v1_1`](Self::sign_join_request_v1_1)
1186  /// which takes a `NwkKey` directly.
1187  ///
1188  /// # Errors
1189  /// `Error::MissingField` if required fields are missing.
1190  pub fn sign_join_request(self, app_key: &crate::types::AppKey) -> crate::Result<LoraPacket> {
1191    let mut packet = self.build_unsigned()?;
1192    let keys = crate::mic::V1_0MicKeys {
1193      app_key: Some(app_key),
1194      ..Default::default()
1195    };
1196    packet.recalculate_mic_v1_0(&keys)?;
1197    Ok(packet)
1198  }
1199
1200  /// Build a Join Request and compute its MIC using `LoRaWAN` 1.1 `NwkKey`.
1201  ///
1202  /// The CMAC algorithm is identical to 1.0; only the key changes.
1203  ///
1204  /// # Errors
1205  /// `Error::MissingField` if required fields are missing.
1206  pub fn sign_join_request_v1_1(self, nwk_key: &crate::types::NwkKey) -> crate::Result<LoraPacket> {
1207    let mut packet = self.build_unsigned()?;
1208    let keys = crate::mic::V1_1MicKeys {
1209      nwk_key: Some(nwk_key),
1210      ..Default::default()
1211    };
1212    packet.recalculate_mic_v1_1(&keys)?;
1213    Ok(packet)
1214  }
1215
1216  /// Build a Data packet, encrypt `FRMPayload`, and compute the
1217  /// `LoRaWAN` 1.0 MIC in one shot.
1218  ///
1219  /// The plaintext payload provided via [`payload`](Self::payload) is
1220  /// encrypted with `AppSKey` (when `FPort > 0`) or `NwkSKey` (when
1221  /// `FPort == 0`). The MIC is then calculated under `NwkSKey` with the
1222  /// 1.0 algorithm.
1223  ///
1224  /// For 1.1 Data frames, build with [`build_unsigned`](Self::build_unsigned),
1225  /// encrypt manually with [`Data::encrypt_payload`], and sign with
1226  /// [`LoraPacket::recalculate_mic_v1_1`].
1227  ///
1228  /// # Errors
1229  /// - [`crate::Error::MissingField`] if required Data fields are missing.
1230  /// - [`crate::Error::PayloadTooLarge`] if `FRMPayload` exceeds 4080 bytes.
1231  /// - Any error from [`build_unsigned`](Self::build_unsigned) or
1232  ///   [`LoraPacket::recalculate_mic_v1_0`].
1233  ///
1234  /// # Examples
1235  ///
1236  /// ```
1237  /// use lora_packet::{LoraPacket, Direction, DevAddr, AppSKey, NwkSKey};
1238  ///
1239  /// let app_s_key = AppSKey::from_slice(&hex::decode("ec925802ae430ca77fd3dd73cb2cc588")?)?;
1240  /// let nwk_s_key = NwkSKey::from_slice(&hex::decode("44024241ed4ce9a68c6a8bc055233fd3")?)?;
1241  ///
1242  /// let packet = LoraPacket::builder()
1243  ///   .data(Direction::Uplink, false)
1244  ///   .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
1245  ///   .f_cnt(2)
1246  ///   .f_port(1)
1247  ///   .payload(b"test")
1248  ///   .sign_and_encrypt(&app_s_key, &nwk_s_key)?;
1249  ///
1250  /// assert_eq!(packet.to_wire(), hex::decode("40f17dbe4900020001954378762b11ff0d")?);
1251  /// # Ok::<(), Box<dyn std::error::Error>>(())
1252  /// ```
1253  pub fn sign_and_encrypt(
1254    self,
1255    app_s_key: &crate::types::AppSKey,
1256    nwk_s_key: &crate::types::NwkSKey,
1257  ) -> crate::Result<LoraPacket> {
1258    let mut packet = self.build_unsigned()?;
1259    // Encrypt FRMPayload if data variant has one
1260    if let Payload::Data(d) = &mut packet.payload
1261      && let Some(plaintext) = d.frm_payload.clone()
1262    {
1263      let encrypted = d.encrypt_payload(&plaintext, app_s_key, nwk_s_key, 0)?;
1264      d.frm_payload = Some(encrypted);
1265    }
1266    // Refresh phy_payload with encrypted contents (no MIC yet)
1267    packet.phy_payload = packet.to_wire();
1268    let keys = crate::mic::V1_0MicKeys {
1269      nwk_s_key: Some(nwk_s_key),
1270      ..Default::default()
1271    };
1272    packet.recalculate_mic_v1_0(&keys)?;
1273    Ok(packet)
1274  }
1275
1276  /// Finalise the builder into a [`LoraPacket`] with MIC set to zero.
1277  ///
1278  /// Useful when you want to sign and emit in separate steps (e.g. for test
1279  /// vectors or when the signing keys arrive asynchronously). Call a
1280  /// `sign_*` method on the builder, or call
1281  /// [`recalculate_mic_v1_0`](LoraPacket::recalculate_mic_v1_0) /
1282  /// [`recalculate_mic_v1_1`](LoraPacket::recalculate_mic_v1_1) on the
1283  /// resulting packet, to fill in the MIC.
1284  ///
1285  /// # Errors
1286  /// - [`crate::Error::MissingField`] when a required field for the chosen
1287  ///   `MType` is missing. The field name is in the error.
1288  /// - [`crate::Error::FOptsTooLong`] when the `FOpts` vector exceeds the
1289  ///   15-byte wire limit.
1290  /// - [`crate::Error::InvalidRejoinType`] when the rejoin type is not in
1291  ///   {0, 1, 2}.
1292  pub fn build_unsigned(self) -> crate::Result<LoraPacket> {
1293    let m_type = self.m_type.ok_or(crate::Error::MissingField("m_type"))?;
1294    let mhdr = Mhdr::from_parts(m_type, self.major);
1295
1296    let payload = match m_type {
1297      MType::JoinRequest => Payload::JoinRequest(JoinRequest {
1298        join_eui: self.join_eui.ok_or(crate::Error::MissingField("join_eui"))?,
1299        dev_eui: self.dev_eui.ok_or(crate::Error::MissingField("dev_eui"))?,
1300        dev_nonce: self.dev_nonce.ok_or(crate::Error::MissingField("dev_nonce"))?,
1301      }),
1302      MType::JoinAccept => Payload::JoinAccept(JoinAccept {
1303        join_nonce: self.join_nonce.ok_or(crate::Error::MissingField("join_nonce"))?,
1304        net_id: self.net_id.ok_or(crate::Error::MissingField("net_id"))?,
1305        dev_addr: self.dev_addr.ok_or(crate::Error::MissingField("dev_addr"))?,
1306        dl_settings: self.dl_settings.ok_or(crate::Error::MissingField("dl_settings"))?,
1307        rx_delay: self.rx_delay.unwrap_or(0),
1308        cf_list: self.cf_list,
1309        join_req_type: self.join_req_type,
1310      }),
1311      MType::UnconfirmedDataUp | MType::UnconfirmedDataDown | MType::ConfirmedDataUp | MType::ConfirmedDataDown => {
1312        let direction = self.direction.ok_or(crate::Error::MissingField("direction"))?;
1313        let f_opts_len = u8::try_from(self.f_opts.len()).map_err(|_| crate::Error::FOptsTooLong(self.f_opts.len()))?;
1314        if f_opts_len > 15 {
1315          return Err(crate::Error::FOptsTooLong(self.f_opts.len()));
1316        }
1317        // FCtrl.FOptsLen (low nibble) must match the actual FOpts length.
1318        // If the caller supplied a custom FCtrl, keep its upper nibble (ADR,
1319        // ADRACKReq, ACK, FPending) but force the low nibble to f_opts_len.
1320        let f_opts_len_nibble = f_opts_len & 0x0F;
1321        let f_ctrl = self
1322          .f_ctrl
1323          .map_or(FCtrl(f_opts_len_nibble), |fc| FCtrl((fc.0 & 0xF0) | f_opts_len_nibble));
1324        Payload::Data(Data {
1325          direction,
1326          confirmed: self.confirmed,
1327          dev_addr: self.dev_addr.ok_or(crate::Error::MissingField("dev_addr"))?,
1328          f_ctrl,
1329          f_cnt: self.f_cnt.unwrap_or(0).to_le_bytes(),
1330          f_opts: self.f_opts,
1331          f_port: self.f_port,
1332          frm_payload: self.payload,
1333        })
1334      }
1335      MType::RejoinRequest => {
1336        let dev_eui = self.dev_eui.ok_or(crate::Error::MissingField("dev_eui"))?;
1337        Payload::RejoinRequest(match self.rejoin_type.unwrap_or(0) {
1338          0 => RejoinRequest::Type0 {
1339            net_id: self.net_id.ok_or(crate::Error::MissingField("net_id"))?,
1340            dev_eui,
1341            rj_count_0: self.rj_count_0.unwrap_or([0, 0]),
1342          },
1343          1 => RejoinRequest::Type1 {
1344            join_eui: self.join_eui.ok_or(crate::Error::MissingField("join_eui"))?,
1345            dev_eui,
1346            rj_count_1: self.rj_count_1.unwrap_or([0, 0]),
1347          },
1348          2 => RejoinRequest::Type2 {
1349            net_id: self.net_id.ok_or(crate::Error::MissingField("net_id"))?,
1350            dev_eui,
1351            rj_count_0: self.rj_count_0.unwrap_or([0, 0]),
1352          },
1353          other => return Err(crate::Error::InvalidRejoinType(other)),
1354        })
1355      }
1356      MType::Proprietary => Payload::Proprietary(self.payload.unwrap_or_default()),
1357    };
1358
1359    let mut pkt = LoraPacket {
1360      phy_payload: Vec::new(),
1361      mhdr,
1362      mic: [0u8; 4],
1363      payload,
1364    };
1365    pkt.phy_payload = pkt.to_wire();
1366    Ok(pkt)
1367  }
1368}
1369
1370fn parse_rejoin_request(body: &[u8]) -> crate::Result<RejoinRequest> {
1371  if body.is_empty() {
1372    return Err(crate::Error::TooShort { expected: 1, got: 0 });
1373  }
1374  let rejoin_type = body[0];
1375  match rejoin_type {
1376    0 | 2 => {
1377      if body.len() != 14 {
1378        return Err(crate::Error::TooShort {
1379          expected: 14,
1380          got: body.len(),
1381        });
1382      }
1383      let mut net_id = [0u8; 3];
1384      net_id.copy_from_slice(&body[1..4]);
1385      net_id.reverse();
1386      let mut dev_eui = [0u8; 8];
1387      dev_eui.copy_from_slice(&body[4..12]);
1388      dev_eui.reverse();
1389      let mut rj_count_0 = [0u8; 2];
1390      rj_count_0.copy_from_slice(&body[12..14]);
1391      rj_count_0.reverse();
1392      let dev_eui = DevEui::new(dev_eui);
1393      let net_id = NetId::new(net_id);
1394      if rejoin_type == 0 {
1395        Ok(RejoinRequest::Type0 {
1396          net_id,
1397          dev_eui,
1398          rj_count_0,
1399        })
1400      } else {
1401        Ok(RejoinRequest::Type2 {
1402          net_id,
1403          dev_eui,
1404          rj_count_0,
1405        })
1406      }
1407    }
1408    1 => {
1409      if body.len() != 19 {
1410        return Err(crate::Error::TooShort {
1411          expected: 19,
1412          got: body.len(),
1413        });
1414      }
1415      let mut join_eui = [0u8; 8];
1416      join_eui.copy_from_slice(&body[1..9]);
1417      join_eui.reverse();
1418      let mut dev_eui = [0u8; 8];
1419      dev_eui.copy_from_slice(&body[9..17]);
1420      dev_eui.reverse();
1421      let mut rj_count_1 = [0u8; 2];
1422      rj_count_1.copy_from_slice(&body[17..19]);
1423      rj_count_1.reverse();
1424      Ok(RejoinRequest::Type1 {
1425        join_eui: AppEui::new(join_eui),
1426        dev_eui: DevEui::new(dev_eui),
1427        rj_count_1,
1428      })
1429    }
1430    other => Err(crate::Error::InvalidRejoinType(other)),
1431  }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436  use super::*;
1437
1438  #[test]
1439  fn lora_packet_constructs_with_join_request_payload() {
1440    let p = LoraPacket {
1441      phy_payload: alloc::vec![0x00],
1442      mhdr: Mhdr::from_parts(MType::JoinRequest, 0),
1443      mic: [0u8; 4],
1444      payload: Payload::JoinRequest(JoinRequest {
1445        join_eui: AppEui::new([0u8; 8]),
1446        dev_eui: DevEui::new([0u8; 8]),
1447        dev_nonce: DevNonce::new([0u8; 2]),
1448      }),
1449    };
1450    assert!(matches!(p.payload, Payload::JoinRequest(_)));
1451  }
1452
1453  fn sample_data_packet(confirmed: bool, direction: Direction) -> LoraPacket {
1454    let m_type = match (confirmed, direction) {
1455      (false, Direction::Uplink) => MType::UnconfirmedDataUp,
1456      (false, Direction::Downlink) => MType::UnconfirmedDataDown,
1457      (true, Direction::Uplink) => MType::ConfirmedDataUp,
1458      (true, Direction::Downlink) => MType::ConfirmedDataDown,
1459    };
1460    LoraPacket {
1461      phy_payload: alloc::vec![],
1462      mhdr: Mhdr::from_parts(m_type, 0),
1463      mic: [0u8; 4],
1464      payload: Payload::Data(Data {
1465        direction,
1466        confirmed,
1467        dev_addr: DevAddr::new([0u8; 4]),
1468        f_ctrl: FCtrl(0),
1469        f_cnt: [0, 0],
1470        f_opts: alloc::vec![],
1471        f_port: None,
1472        frm_payload: None,
1473      }),
1474    }
1475  }
1476
1477  #[test]
1478  fn accessor_is_data() {
1479    let p = sample_data_packet(false, Direction::Uplink);
1480    assert!(p.is_data());
1481    assert!(!p.is_confirmed());
1482    assert!(p.as_data().is_some());
1483  }
1484
1485  #[test]
1486  fn accessor_is_confirmed() {
1487    let p = sample_data_packet(true, Direction::Downlink);
1488    assert!(p.is_data());
1489    assert!(p.is_confirmed());
1490  }
1491
1492  #[test]
1493  fn accessor_is_join_request() {
1494    let p = LoraPacket {
1495      phy_payload: alloc::vec![],
1496      mhdr: Mhdr::from_parts(MType::JoinRequest, 0),
1497      mic: [0u8; 4],
1498      payload: Payload::JoinRequest(JoinRequest {
1499        join_eui: AppEui::new([0u8; 8]),
1500        dev_eui: DevEui::new([0u8; 8]),
1501        dev_nonce: DevNonce::new([0u8; 2]),
1502      }),
1503    };
1504    assert!(p.is_join_request());
1505    assert!(p.as_join_request().is_some());
1506    assert!(p.as_data().is_none());
1507  }
1508
1509  #[test]
1510  fn direction_join_request_is_uplink() {
1511    let p = LoraPacket {
1512      phy_payload: alloc::vec![],
1513      mhdr: Mhdr::from_parts(MType::JoinRequest, 0),
1514      mic: [0u8; 4],
1515      payload: Payload::JoinRequest(JoinRequest {
1516        join_eui: AppEui::new([0u8; 8]),
1517        dev_eui: DevEui::new([0u8; 8]),
1518        dev_nonce: DevNonce::new([0u8; 2]),
1519      }),
1520    };
1521    assert_eq!(p.direction(), Some(Direction::Uplink));
1522  }
1523
1524  #[test]
1525  fn direction_join_accept_is_downlink() {
1526    let p = LoraPacket {
1527      phy_payload: alloc::vec![],
1528      mhdr: Mhdr::from_parts(MType::JoinAccept, 0),
1529      mic: [0u8; 4],
1530      payload: Payload::JoinAccept(JoinAccept {
1531        join_nonce: AppNonce::new([0u8; 3]),
1532        net_id: NetId::new([0u8; 3]),
1533        dev_addr: DevAddr::new([0u8; 4]),
1534        dl_settings: DlSettings(0),
1535        rx_delay: 0,
1536        cf_list: None,
1537        join_req_type: None,
1538      }),
1539    };
1540    assert_eq!(p.direction(), Some(Direction::Downlink));
1541  }
1542
1543  #[test]
1544  fn direction_data_matches_inner_field() {
1545    assert_eq!(
1546      sample_data_packet(false, Direction::Uplink).direction(),
1547      Some(Direction::Uplink)
1548    );
1549    assert_eq!(
1550      sample_data_packet(true, Direction::Downlink).direction(),
1551      Some(Direction::Downlink)
1552    );
1553  }
1554
1555  #[test]
1556  fn direction_rejoin_request_is_uplink() {
1557    let p = LoraPacket {
1558      phy_payload: alloc::vec![],
1559      mhdr: Mhdr::from_parts(MType::RejoinRequest, 0),
1560      mic: [0u8; 4],
1561      payload: Payload::RejoinRequest(RejoinRequest::Type0 {
1562        net_id: NetId::new([0u8; 3]),
1563        dev_eui: DevEui::new([0u8; 8]),
1564        rj_count_0: [0u8; 2],
1565      }),
1566    };
1567    assert_eq!(p.direction(), Some(Direction::Uplink));
1568  }
1569
1570  #[test]
1571  fn direction_proprietary_is_none() {
1572    let p = LoraPacket {
1573      phy_payload: alloc::vec![],
1574      mhdr: Mhdr::from_parts(MType::Proprietary, 0),
1575      mic: [0u8; 4],
1576      payload: Payload::Proprietary(alloc::vec![1, 2, 3]),
1577    };
1578    assert_eq!(p.direction(), None);
1579  }
1580
1581  #[test]
1582  fn data_f_cnt_little_endian() {
1583    let d = Data {
1584      direction: Direction::Uplink,
1585      confirmed: false,
1586      dev_addr: DevAddr::new([0u8; 4]),
1587      f_ctrl: FCtrl(0),
1588      f_cnt: [0x02, 0x00],
1589      f_opts: alloc::vec![],
1590      f_port: None,
1591      frm_payload: None,
1592    };
1593    assert_eq!(d.f_cnt(), 2);
1594    assert_eq!(d.f_cnt_32(0), 2);
1595    assert_eq!(d.f_cnt_32(1), 0x0001_0002);
1596  }
1597
1598  #[test]
1599  fn from_wire_rejects_empty() {
1600    let err = LoraPacket::from_wire(&[]).unwrap_err();
1601    assert!(matches!(err, crate::Error::TooShort { .. }));
1602  }
1603
1604  #[test]
1605  fn from_wire_rejects_too_short() {
1606    let err = LoraPacket::from_wire(&[1, 2, 3, 4]).unwrap_err();
1607    assert!(matches!(err, crate::Error::TooShort { .. }));
1608  }
1609
1610  /// Mirror of `__tests__/parse_test.ts`: "parses a Join Request"
1611  #[test]
1612  fn parse_join_request_known_vector() {
1613    let bytes = hex_to_vec("0039363463336913aa05693574323831338ef1c1d5ec6c");
1614    let p = LoraPacket::from_wire(&bytes).unwrap();
1615    assert_eq!(p.mhdr.as_byte(), 0x00);
1616    assert_eq!(p.mic, [0xc1, 0xd5, 0xec, 0x6c]);
1617    let jr = p.as_join_request().expect("expected JoinRequest");
1618    assert_eq!(
1619      jr.join_eui.as_bytes(),
1620      &[0xaa, 0x13, 0x69, 0x33, 0x63, 0x34, 0x36, 0x39]
1621    );
1622    assert_eq!(jr.dev_eui.as_bytes(), &[0x33, 0x31, 0x38, 0x32, 0x74, 0x35, 0x69, 0x05]);
1623    assert_eq!(jr.dev_nonce.as_bytes(), &[0xf1, 0x8e]);
1624  }
1625
1626  fn hex_to_vec(s: &str) -> Vec<u8> {
1627    (0..s.len())
1628      .step_by(2)
1629      .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex"))
1630      .collect()
1631  }
1632
1633  #[test]
1634  fn parse_join_accept_plaintext_minimum() {
1635    let plaintext = hex_to_vec("20010203040506070809100001deadbeef");
1636    let ja = JoinAccept::from_plaintext(&plaintext).unwrap();
1637    assert_eq!(ja.join_nonce.as_bytes(), &[0x03, 0x02, 0x01]);
1638    assert_eq!(ja.net_id.as_bytes(), &[0x06, 0x05, 0x04]);
1639    assert_eq!(ja.dev_addr.as_bytes(), &[0x10, 0x09, 0x08, 0x07]);
1640    assert_eq!(ja.dl_settings.as_byte(), 0x00);
1641    assert_eq!(ja.rx_delay, 0x01);
1642    assert!(ja.cf_list.is_none());
1643    assert!(ja.join_req_type.is_none());
1644  }
1645
1646  #[test]
1647  fn parse_join_accept_plaintext_with_cflist() {
1648    let plaintext = hex_to_vec(concat!(
1649      "20",
1650      "010203040506070809100001",
1651      "112233445566778899aabbccddeeff00",
1652      "deadbeef"
1653    ));
1654    let ja = JoinAccept::from_plaintext(&plaintext).unwrap();
1655    assert_eq!(
1656      ja.cf_list.unwrap(),
1657      [
1658        0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
1659      ]
1660    );
1661  }
1662
1663  /// Mirror of `__tests__/parse_test.ts`: "parses an unconfirmed data up"
1664  #[test]
1665  fn parse_data_up_known_vector() {
1666    let bytes = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
1667    let p = LoraPacket::from_wire(&bytes).unwrap();
1668    assert_eq!(p.mhdr.as_byte(), 0x40);
1669    assert_eq!(p.mic, [0x2b, 0x11, 0xff, 0x0d]);
1670    let d = p.as_data().expect("expected Data");
1671    assert_eq!(d.direction, Direction::Uplink);
1672    assert!(!d.confirmed);
1673    assert_eq!(d.dev_addr.as_bytes(), &[0x49, 0xbe, 0x7d, 0xf1]);
1674    assert_eq!(d.f_ctrl.as_byte(), 0x00);
1675    assert_eq!(d.f_cnt(), 2);
1676    assert!(d.f_opts.is_empty());
1677    assert_eq!(d.f_port, Some(0x01));
1678    assert_eq!(d.frm_payload.as_deref(), Some(&[0x95, 0x43, 0x78, 0x76][..]));
1679  }
1680
1681  #[test]
1682  fn parse_rejoin_type_0() {
1683    let bytes = hex_to_vec("c0000102030405060708090a0b0c0ddeadbeef");
1684    let p = LoraPacket::from_wire(&bytes).unwrap();
1685    let rj = p.as_rejoin_request().expect("rejoin");
1686    match rj {
1687      RejoinRequest::Type0 {
1688        net_id,
1689        dev_eui,
1690        rj_count_0,
1691      } => {
1692        assert_eq!(net_id.as_bytes(), &[0x03, 0x02, 0x01]);
1693        assert_eq!(dev_eui.as_bytes(), &[0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04]);
1694        assert_eq!(rj_count_0, &[0x0d, 0x0c]);
1695      }
1696      _ => panic!("expected Type0"),
1697    }
1698  }
1699
1700  #[test]
1701  fn parse_rejoin_type_1() {
1702    let bytes = hex_to_vec("c001aaaaaaaaaaaaaaaa0405060708090a0b0c0ddeadbeef");
1703    let p = LoraPacket::from_wire(&bytes).unwrap();
1704    match p.as_rejoin_request().unwrap() {
1705      RejoinRequest::Type1 {
1706        join_eui,
1707        dev_eui,
1708        rj_count_1,
1709      } => {
1710        assert_eq!(join_eui.as_bytes(), &[0xaa; 8]);
1711        assert_eq!(dev_eui.as_bytes(), &[0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04]);
1712        assert_eq!(rj_count_1, &[0x0d, 0x0c]);
1713      }
1714      _ => panic!("expected Type1"),
1715    }
1716  }
1717
1718  #[test]
1719  fn parse_rejoin_type_2() {
1720    let bytes = hex_to_vec("c0020102030405060708090a0b0c0ddeadbeef");
1721    let p = LoraPacket::from_wire(&bytes).unwrap();
1722    assert!(matches!(p.as_rejoin_request().unwrap(), RejoinRequest::Type2 { .. }));
1723  }
1724
1725  #[test]
1726  fn parse_rejoin_invalid_type() {
1727    let bytes = hex_to_vec("c0030102030405060708090a0b0c0ddeadbeef");
1728    let err = LoraPacket::from_wire(&bytes).unwrap_err();
1729    assert!(matches!(err, crate::Error::InvalidRejoinType(3)));
1730  }
1731
1732  #[test]
1733  fn parse_proprietary_keeps_body() {
1734    let bytes = hex_to_vec("e0deadbeefcafe11223344");
1735    let p = LoraPacket::from_wire(&bytes).unwrap();
1736    match &p.payload {
1737      Payload::Proprietary(body) => assert_eq!(body, &[0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]),
1738      _ => panic!("expected Proprietary"),
1739    }
1740    assert_eq!(p.mic, [0x11, 0x22, 0x33, 0x44]);
1741  }
1742
1743  #[test]
1744  fn builder_constructs() {
1745    let _b = LoraPacket::builder().data(Direction::Uplink, false);
1746  }
1747
1748  #[test]
1749  fn builder_chains_fields() {
1750    let b = LoraPacket::builder()
1751      .data(Direction::Downlink, false)
1752      .dev_addr(DevAddr::new([1, 2, 3, 4]))
1753      .f_cnt(7)
1754      .f_port(1)
1755      .payload(b"hi");
1756    assert_eq!(b.dev_addr.unwrap().as_bytes(), &[1, 2, 3, 4]);
1757    assert_eq!(b.f_cnt.unwrap(), 7);
1758    assert_eq!(b.f_port.unwrap(), 1);
1759    assert_eq!(b.payload.as_deref().unwrap(), b"hi");
1760  }
1761
1762  #[test]
1763  fn build_unsigned_data_round_trip() {
1764    let pkt = LoraPacket::builder()
1765      .data(Direction::Uplink, false)
1766      .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
1767      .f_ctrl(FCtrl(0))
1768      .f_cnt(2)
1769      .f_port(1)
1770      .payload(&[0x95, 0x43, 0x78, 0x76])
1771      .build_unsigned()
1772      .unwrap();
1773
1774    let wire = pkt.to_wire();
1775    assert_eq!(&wire[..1], &[0x40]);
1776    assert_eq!(&wire[1..5], &[0xf1, 0x7d, 0xbe, 0x49]);
1777    assert_eq!(wire[5], 0x00);
1778    assert_eq!(&wire[6..8], &[0x02, 0x00]);
1779    assert_eq!(wire[8], 0x01);
1780    assert_eq!(&wire[9..13], &[0x95, 0x43, 0x78, 0x76]);
1781    assert_eq!(&wire[wire.len() - 4..], &[0, 0, 0, 0]);
1782  }
1783
1784  #[test]
1785  fn round_trip_data_up() {
1786    let wire = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
1787    let p = LoraPacket::from_wire(&wire).unwrap();
1788    let emitted = p.to_wire();
1789    assert_eq!(emitted, wire);
1790  }
1791
1792  #[test]
1793  fn round_trip_join_request() {
1794    let wire = hex_to_vec("0039363463336913aa05693574323831338ef1c1d5ec6c");
1795    let p = LoraPacket::from_wire(&wire).unwrap();
1796    assert_eq!(p.to_wire(), wire);
1797  }
1798
1799  #[test]
1800  fn round_trip_rejoin_type_0() {
1801    let wire = hex_to_vec("c0000102030405060708090a0b0c0ddeadbeef");
1802    let p = LoraPacket::from_wire(&wire).unwrap();
1803    assert_eq!(p.to_wire(), wire);
1804  }
1805
1806  #[test]
1807  fn verify_mic_v1_0_real_vector() {
1808    use crate::mic::V1_0MicKeys;
1809    use crate::types::NwkSKey;
1810    let bytes = hex_to_vec("40F17DBE4900020001954378762B11FF0D");
1811    let packet = LoraPacket::from_wire(&bytes).unwrap();
1812    let nwk_s_key = NwkSKey::from_slice(&hex_to_vec("44024241ed4ce9a68c6a8bc055233fd3")).unwrap();
1813    let keys = V1_0MicKeys {
1814      nwk_s_key: Some(&nwk_s_key),
1815      ..Default::default()
1816    };
1817    assert!(packet.verify_mic_v1_0(&keys).unwrap());
1818  }
1819
1820  #[test]
1821  fn recalculate_mic_v1_0_updates_mic_and_phypayload() {
1822    use crate::mic::V1_0MicKeys;
1823    use crate::types::NwkSKey;
1824    let bytes = hex_to_vec("40f17dbe490002000195437876eeeeeeee");
1825    let mut packet = LoraPacket::from_wire(&bytes).unwrap();
1826    assert_eq!(packet.mic, [0xee, 0xee, 0xee, 0xee]);
1827    let nwk_s_key = NwkSKey::from_slice(&hex_to_vec("44024241ed4ce9a68c6a8bc055233fd3")).unwrap();
1828    let keys = V1_0MicKeys {
1829      nwk_s_key: Some(&nwk_s_key),
1830      ..Default::default()
1831    };
1832    packet.recalculate_mic_v1_0(&keys).unwrap();
1833    assert_eq!(packet.mic, [0x2b, 0x11, 0xff, 0x0d]);
1834    assert_eq!(
1835      &packet.phy_payload[packet.phy_payload.len() - 4..],
1836      &[0x2b, 0x11, 0xff, 0x0d]
1837    );
1838  }
1839
1840  #[test]
1841  fn sign_join_accept_zero_key_vector() {
1842    use crate::types::{AppKey, AppNonce, NetId};
1843
1844    let app_key = AppKey::new([0u8; 16]);
1845    let (packet, encrypted_wire) = LoraPacket::builder()
1846      .join_accept()
1847      .join_nonce(AppNonce::new([0, 0, 0]))
1848      .net_id(NetId::new([0, 0, 0]))
1849      .dev_addr(DevAddr::new([0, 0, 0, 0]))
1850      .dl_settings(DlSettings(0))
1851      .rx_delay(0)
1852      .sign_join_accept(&app_key)
1853      .unwrap();
1854
1855    // Plaintext MIC should be f86f0a91
1856    assert_eq!(packet.mic, [0xf8, 0x6f, 0x0a, 0x91]);
1857    // Encrypted wire should match the TS join_accept_encrypt vector
1858    let expected_encrypted = hex_to_vec("20e3de108795f776b8037610ef7869b5b3");
1859    assert_eq!(encrypted_wire, expected_encrypted);
1860  }
1861
1862  #[test]
1863  fn sign_join_request_produces_verifiable_mic() {
1864    use crate::mic::V1_0MicKeys;
1865    use crate::types::AppKey;
1866
1867    let app_key = AppKey::new([0u8; 16]);
1868    let packet = LoraPacket::builder()
1869      .join_request()
1870      .join_eui(AppEui::new([0u8; 8]))
1871      .dev_eui(DevEui::new([0u8; 8]))
1872      .dev_nonce(DevNonce::new([0u8; 2]))
1873      .sign_join_request(&app_key)
1874      .unwrap();
1875
1876    let keys = V1_0MicKeys {
1877      app_key: Some(&app_key),
1878      ..Default::default()
1879    };
1880    assert!(packet.verify_mic_v1_0(&keys).unwrap());
1881  }
1882
1883  #[test]
1884  fn sign_join_request_v1_1_works() {
1885    use crate::mic::V1_1MicKeys;
1886    use crate::types::NwkKey;
1887    let nwk_key = NwkKey::new([0u8; 16]);
1888    let packet = LoraPacket::builder()
1889      .join_request()
1890      .join_eui(AppEui::new([0; 8]))
1891      .dev_eui(DevEui::new([0; 8]))
1892      .dev_nonce(DevNonce::new([0; 2]))
1893      .sign_join_request_v1_1(&nwk_key)
1894      .unwrap();
1895    let keys = V1_1MicKeys {
1896      nwk_key: Some(&nwk_key),
1897      ..Default::default()
1898    };
1899    assert!(packet.verify_mic_v1_1(&keys).unwrap());
1900  }
1901
1902  #[test]
1903  fn build_unsigned_rejects_fopts_too_long() {
1904    let too_many = alloc::vec![0u8; 16];
1905    let result = LoraPacket::builder()
1906      .data(Direction::Uplink, false)
1907      .dev_addr(DevAddr::new([0; 4]))
1908      .f_opts(&too_many)
1909      .build_unsigned();
1910    assert!(matches!(result, Err(crate::Error::FOptsTooLong(16))));
1911  }
1912
1913  #[test]
1914  fn from_wire_rejects_oversized_buffer() {
1915    // 257 bytes exceeds the LoRaWAN PHY maximum of 256; reject before
1916    // parsing so we cannot produce a wrong-but-deterministic MIC.
1917    let huge = alloc::vec![0u8; 257];
1918    let err = LoraPacket::from_wire(&huge).unwrap_err();
1919    assert!(matches!(err, crate::Error::TooLong { got: 257 }));
1920  }
1921
1922  #[test]
1923  fn from_wire_accepts_max_size_buffer() {
1924    // Boundary: exactly 256 bytes must still be accepted (or rejected on
1925    // semantic grounds, but not on the length cap). We pick a Proprietary
1926    // MType so the parser does not try to interpret the body.
1927    let mut bytes = alloc::vec![0u8; 256];
1928    bytes[0] = 0b1110_0000; // Proprietary MType
1929    let result = LoraPacket::from_wire(&bytes);
1930    assert!(result.is_ok(), "256-byte buffer should parse, got {result:?}");
1931  }
1932
1933  #[test]
1934  fn builder_rj_count_0_persists_through_rejoin_type_0() {
1935    use crate::types::NetId;
1936    let packet = LoraPacket::builder()
1937      .rejoin_request(0)
1938      .net_id(NetId::new([0, 0, 0]))
1939      .dev_eui(DevEui::new([0; 8]))
1940      .rj_count_0(0x1234)
1941      .build_unsigned()
1942      .unwrap();
1943    match &packet.payload {
1944      Payload::RejoinRequest(RejoinRequest::Type0 { rj_count_0, .. }) => {
1945        assert_eq!(rj_count_0, &0x1234u16.to_le_bytes());
1946      }
1947      other => panic!("expected Rejoin Type 0, got {other:?}"),
1948    }
1949  }
1950
1951  #[test]
1952  fn builder_rj_count_1_persists_through_rejoin_type_1() {
1953    let packet = LoraPacket::builder()
1954      .rejoin_request(1)
1955      .join_eui(AppEui::new([0; 8]))
1956      .dev_eui(DevEui::new([0; 8]))
1957      .rj_count_1(0xBEEF)
1958      .build_unsigned()
1959      .unwrap();
1960    match &packet.payload {
1961      Payload::RejoinRequest(RejoinRequest::Type1 { rj_count_1, .. }) => {
1962        assert_eq!(rj_count_1, &0xBEEFu16.to_le_bytes());
1963      }
1964      other => panic!("expected Rejoin Type 1, got {other:?}"),
1965    }
1966  }
1967
1968  #[test]
1969  fn builder_rj_count_0_persists_through_rejoin_type_2() {
1970    use crate::types::NetId;
1971    let packet = LoraPacket::builder()
1972      .rejoin_request(2)
1973      .net_id(NetId::new([0, 0, 0]))
1974      .dev_eui(DevEui::new([0; 8]))
1975      .rj_count_0(0x00DB)
1976      .build_unsigned()
1977      .unwrap();
1978    match &packet.payload {
1979      Payload::RejoinRequest(RejoinRequest::Type2 { rj_count_0, .. }) => {
1980        assert_eq!(rj_count_0, &0x00DBu16.to_le_bytes());
1981      }
1982      other => panic!("expected Rejoin Type 2, got {other:?}"),
1983    }
1984  }
1985
1986  #[test]
1987  fn builder_rejoin_defaults_to_zero_counters() {
1988    use crate::types::NetId;
1989    let packet = LoraPacket::builder()
1990      .rejoin_request(0)
1991      .net_id(NetId::new([0, 0, 0]))
1992      .dev_eui(DevEui::new([0; 8]))
1993      .build_unsigned()
1994      .unwrap();
1995    match &packet.payload {
1996      Payload::RejoinRequest(RejoinRequest::Type0 { rj_count_0, .. }) => {
1997        assert_eq!(rj_count_0, &[0, 0]);
1998      }
1999      other => panic!("expected Rejoin Type 0, got {other:?}"),
2000    }
2001  }
2002
2003  #[test]
2004  fn builder_overrides_f_ctrl_low_nibble_with_actual_fopts_len() {
2005    // Caller passes a FCtrl whose low nibble (0) disagrees with the actual
2006    // f_opts vector length (3). The builder must preserve the upper nibble
2007    // (0xA = ADR + ACK bits) and rewrite the low nibble to 3.
2008    let packet = LoraPacket::builder()
2009      .data(Direction::Uplink, false)
2010      .dev_addr(DevAddr::new([1, 2, 3, 4]))
2011      .f_ctrl(FCtrl(0xA0))
2012      .f_opts(&[0x01, 0x02, 0x03])
2013      .build_unsigned()
2014      .unwrap();
2015    let data = packet.as_data().unwrap();
2016    assert_eq!(data.f_ctrl.as_byte(), 0xA3, "upper nibble preserved, low nibble = 3");
2017    assert_eq!(data.f_opts.len(), 3);
2018  }
2019
2020  #[test]
2021  fn sign_and_encrypt_round_trip() {
2022    use crate::mic::V1_0MicKeys;
2023    use crate::types::{AppSKey, NwkSKey};
2024
2025    let app_s_key = AppSKey::from_slice(&hex_to_vec("ec925802ae430ca77fd3dd73cb2cc588")).unwrap();
2026    let nwk_s_key = NwkSKey::from_slice(&hex_to_vec("44024241ed4ce9a68c6a8bc055233fd3")).unwrap();
2027
2028    let packet = LoraPacket::builder()
2029      .data(Direction::Uplink, false)
2030      .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
2031      .f_ctrl(FCtrl(0))
2032      .f_cnt(2)
2033      .f_port(1)
2034      .payload(b"test")
2035      .sign_and_encrypt(&app_s_key, &nwk_s_key)
2036      .unwrap();
2037
2038    // The encrypted payload should be the known ciphertext
2039    let d = packet.as_data().unwrap();
2040    assert_eq!(d.frm_payload.as_deref(), Some(&[0x95, 0x43, 0x78, 0x76][..]));
2041
2042    // MIC should match the known value
2043    assert_eq!(packet.mic, [0x2b, 0x11, 0xff, 0x0d]);
2044
2045    // PHY payload should be the canonical wire frame
2046    let expected_wire = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
2047    assert_eq!(packet.phy_payload, expected_wire);
2048
2049    // verify_mic_v1_0 succeeds
2050    let keys = V1_0MicKeys {
2051      nwk_s_key: Some(&nwk_s_key),
2052      ..Default::default()
2053    };
2054    assert!(packet.verify_mic_v1_0(&keys).unwrap());
2055  }
2056}
2057
2058#[cfg(test)]
2059mod prop_tests {
2060  use super::*;
2061  use proptest::prelude::*;
2062
2063  proptest! {
2064    #[test]
2065    fn from_wire_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..=1000)) {
2066      // It must return Result, never panic.
2067      let _ = LoraPacket::from_wire(&bytes);
2068    }
2069  }
2070}