Skip to main content

lora_packet/
lib.rs

1//! # lora-packet
2//!
3//! `LoRaWAN` 1.0 and 1.1 packet codec for Rust. Parses and builds `PHYPayload`
4//! frames, performs AES-ECB `FRMPayload` and `FOpts` crypt, computes AES-CMAC
5//! MICs, and derives OTAA, Join Server, and WOR (relay) keys.
6//!
7//! - Works on `std` and on `no_std + alloc` targets (one feature flag away).
8//! - No `unsafe`; constant-time MIC compares; keys auto-zeroize on drop.
9//! - Strong newtypes for every key and identifier so a `NwkSKey` cannot be
10//!   confused with an `AppSKey` or with raw `[u8; 16]` bytes at compile time.
11//!
12//! See the [`README`](https://github.com/tago-io/lora-packet-rs) for a wider
13//! introduction and the per-module pages for full API references.
14//!
15//! ## Quick start: parse, verify, decrypt
16//!
17//! ```
18//! use lora_packet::{LoraPacket, AppSKey, NwkSKey, V1_0MicKeys};
19//!
20//! let bytes = hex::decode("40f17dbe4900020001954378762b11ff0d")?;
21//! let packet = LoraPacket::from_wire(&bytes)?;
22//!
23//! let nwk_s_key = NwkSKey::from_slice(&hex::decode("44024241ed4ce9a68c6a8bc055233fd3")?)?;
24//! let app_s_key = AppSKey::from_slice(&hex::decode("ec925802ae430ca77fd3dd73cb2cc588")?)?;
25//!
26//! let keys = V1_0MicKeys { nwk_s_key: Some(&nwk_s_key), ..Default::default() };
27//! assert!(packet.verify_mic_v1_0(&keys)?);
28//!
29//! let data = packet.as_data().expect("data frame");
30//! let plaintext = data.decrypt_payload(&app_s_key, &nwk_s_key, 0)?;
31//! assert_eq!(&plaintext, b"test");
32//! # Ok::<(), Box<dyn std::error::Error>>(())
33//! ```
34//!
35//! ## Building a downlink
36//!
37//! [`LoraPacket::builder`] composes a packet field by field. Terminal methods
38//! (`sign_and_encrypt`, `sign_join_request`, `sign_join_accept`) finalise the
39//! MIC and produce wire bytes via [`LoraPacket::to_wire`].
40//!
41//! ```
42//! use lora_packet::{LoraPacket, Direction, DevAddr, AppSKey, NwkSKey};
43//!
44//! let app_s_key = AppSKey::new([0u8; 16]);
45//! let nwk_s_key = NwkSKey::new([0u8; 16]);
46//!
47//! let packet = LoraPacket::builder()
48//!   .data(Direction::Downlink, false)
49//!   .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
50//!   .f_cnt(2)
51//!   .f_port(1)
52//!   .payload(b"hello")
53//!   .sign_and_encrypt(&app_s_key, &nwk_s_key)?;
54//!
55//! let wire: Vec<u8> = packet.to_wire();
56//! assert!(!wire.is_empty());
57//! # Ok::<(), lora_packet::Error>(())
58//! ```
59//!
60//! ## OTAA session key derivation
61//!
62//! ```
63//! use lora_packet::{SessionKeys10, SessionKeys11, AppKey, NwkKey, NetId, AppNonce, DevNonce, AppEui};
64//!
65//! let app_key = AppKey::new([0u8; 16]);
66//! let nwk_key = NwkKey::new([0u8; 16]);
67//!
68//! let v10 = SessionKeys10::derive(
69//!   &app_key,
70//!   &NetId::new([0, 0, 0]),
71//!   &AppNonce::new([0, 0, 0]),
72//!   &DevNonce::new([0, 0]),
73//! );
74//!
75//! let v11 = SessionKeys11::derive(
76//!   &app_key,
77//!   &nwk_key,
78//!   &AppEui::new([0u8; 8]),
79//!   &AppNonce::new([0, 0, 0]),
80//!   &DevNonce::new([0, 0]),
81//! );
82//!
83//! let _ = (v10.app_s_key, v11.s_nwk_s_int_key);
84//! ```
85//!
86//! ## API surface at a glance
87//!
88//! | Entry point                                       | Purpose                                            |
89//! | ------------------------------------------------- | -------------------------------------------------- |
90//! | [`LoraPacket::from_wire`]                         | Parse `PHYPayload` bytes                           |
91//! | [`LoraPacket::builder`]                           | Compose a packet field by field                    |
92//! | [`LoraPacket::verify_mic_v1_0`] / `_v1_1`         | Constant-time MIC verification                     |
93//! | [`LoraPacket::calculate_mic_v1_0`] / `_v1_1`      | MIC computation                                    |
94//! | [`LoraPacket::recalculate_mic_v1_0`] / `_v1_1`    | Overwrite MIC after mutations                      |
95//! | [`Data::decrypt_payload`] / `encrypt_payload`     | `FRMPayload` AES-CTR-style crypt                   |
96//! | [`Data::decrypt_fopts`] / `encrypt_fopts`         | 1.1 `FOpts` MAC-command crypt                      |
97//! | [`JoinAccept::decrypt_from_wire`] / `encrypt_for_wire` | Join Accept on-air decrypt/encrypt            |
98//! | [`SessionKeys10::derive`] / [`SessionKeys11::derive`]  | OTAA session-key derivation                   |
99//! | [`JoinServerKeys::derive`]                        | 1.1 JS key derivation                              |
100//! | [`WorKeys::root`] / [`WorKeys::session`]          | Relay (WOR) key derivation                         |
101//! | [`aes_ecb_encrypt`]                               | Low-level AES-128 ECB primitive                    |
102//!
103//! ## The five message variants
104//!
105//! Every [`LoraPacket`] holds exactly one [`Payload`] variant. Match on it to
106//! pull out type-specific fields:
107//!
108//! ```
109//! use lora_packet::{LoraPacket, Payload, RejoinRequest};
110//!
111//! let wire = hex::decode("c0000102030405060708090a0b0c0ddeadbeef")?;
112//! let packet = LoraPacket::from_wire(&wire)?;
113//!
114//! match &packet.payload {
115//!   Payload::JoinRequest(_) => {}
116//!   Payload::JoinAccept(_) => {}
117//!   Payload::Data(_) => {}
118//!   Payload::RejoinRequest(rj) => match rj {
119//!     RejoinRequest::Type0 { .. } | RejoinRequest::Type2 { .. } => {}
120//!     RejoinRequest::Type1 { .. } => {}
121//!   },
122//!   Payload::Proprietary(_) => {}
123//! }
124//! # Ok::<(), Box<dyn std::error::Error>>(())
125//! ```
126//!
127//! ## Cargo features
128//!
129//! | Feature      | Default | Effect                                                                |
130//! | ------------ | ------- | --------------------------------------------------------------------- |
131//! | `std`        | yes     | Enables `std::error::Error` impls via `thiserror/std`                 |
132//! | `serde`      | no      | Derives `Serialize` / `Deserialize` on packet types and keys          |
133//! | `hex_base64` | no      | Adds `from_hex` / `from_base64` constructors on keys, ids, packets    |
134//!
135//! ## `no_std` support
136//!
137//! ```text
138//! cargo add lora-packet --no-default-features
139//! ```
140//!
141//! The crate uses `alloc::vec::Vec` and `alloc::string::String`; targets must
142//! supply a global allocator. Every public API works identically with or
143//! without the `std` feature; `std` only switches `Error: std::error::Error`
144//! on or off.
145//!
146//! ## Endianness contract
147//!
148//! - **Wire format** is little-endian (per the `LoRaWAN` MAC spec).
149//! - **Struct fields** display in network/big-endian order so that printing a
150//!   `DevAddr([0x49, 0xbe, 0x7d, 0xf1])` matches the value used to identify a
151//!   device on a console.
152//!
153//! [`LoraPacket::from_wire`] and [`LoraPacket::to_wire`] handle the byte
154//! reversal; you only see big-endian fields on the struct.
155//!
156//! ## Reference specifications
157//!
158//! - `LoRaWAN` 1.0.4 Specification (`TS001-1.0.4`).
159//! - `LoRaWAN` 1.1 Specification (`TS001-1.1`).
160//! - `LoRaWAN` Regional Parameters `RP002-1.0.4`.
161//! - `LoRa` Alliance Errata "`FCntDwn` Usage in `FOpts` Encryption"
162//!   (`CR v2 r1`).
163
164#![cfg_attr(not(feature = "std"), no_std)]
165#![deny(unsafe_code)]
166#![deny(missing_docs)]
167#![warn(clippy::pedantic)]
168#![warn(clippy::nursery)]
169#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
170
171extern crate alloc;
172
173pub mod codec;
174pub mod crypto;
175pub mod error;
176pub mod mic;
177pub mod types;
178mod util;
179
180pub use codec::{Data, JoinAccept, JoinRequest, LoraPacket, LoraPacketBuilder, Payload, RejoinRequest};
181pub use crypto::{JoinServerKeys, SessionKeys10, SessionKeys11, WorKeys, WorSessionKeys, aes_ecb_encrypt};
182pub use error::{Error, Result};
183pub use mic::{V1_0MicKeys, V1_1MicKeys};
184pub use types::{
185  AppEui, AppKey, AppNonce, AppSKey, DevAddr, DevEui, DevNonce, Direction, DlSettings, FCtrl, FNwkSIntKey, JSEncKey,
186  JSIntKey, JoinEui, JoinNonce, LorawanVersion, MType, Mhdr, NetId, NwkKey, NwkSEncKey, NwkSKey, RootWorSKey,
187  SNwkSIntKey, WorSEncKey, WorSIntKey,
188};