lora-packet 1.0.0

LoRaWAN 1.0/1.1 packet decoder and encoder with parse, build, MIC, and key derivation.
Documentation

lora-packet

LoRaWAN 1.0 / 1.1 packet decoder and encoder for Rust.

crates.io docs.rs

Parse and build PHYPayload frames, AES-ECB FRMPayload + FOpts crypt, AES-CMAC MIC, and OTAA/JS/WOR key derivation. Works on std and no_std + alloc.

Why this crate

  • Strong newtypes for every key and identifier. AppSKey, NwkSKey, DevAddr, and FNwkSIntKey are distinct types. The compiler stops you mixing them up.
  • Constant-time MIC comparison. Verification uses subtle::ConstantTimeEq so timing side-channels do not leak.
  • Keys auto-zeroize on drop. Every key newtype derives ZeroizeOnDrop; their Debug impls are redacted.
  • no_std + alloc ready. Switch the std feature off and the same API works on embedded targets with a global allocator.
  • No unsafe. #![deny(unsafe_code)] is enforced at the crate root.

Quickstart

use lora_packet::{LoraPacket, AppSKey, NwkSKey, V1_0MicKeys};

fn main() -> Result<(), Box<dyn std::error::Error>> {
  let bytes = hex::decode("40f17dbe4900020001954378762b11ff0d")?;
  let packet = LoraPacket::from_wire(&bytes)?;

  let nwk_s_key = NwkSKey::from_slice(&hex::decode("44024241ed4ce9a68c6a8bc055233fd3")?)?;
  let app_s_key = AppSKey::from_slice(&hex::decode("ec925802ae430ca77fd3dd73cb2cc588")?)?;

  let keys = V1_0MicKeys { nwk_s_key: Some(&nwk_s_key), ..Default::default() };
  if packet.verify_mic_v1_0(&keys)? {
    if let Some(data) = packet.as_data() {
      let plaintext = data.decrypt_payload(&app_s_key, &nwk_s_key, 0)?;
      println!("payload: {plaintext:?}");
    }
  }
  Ok(())
}

Build a downlink

use lora_packet::{LoraPacket, Direction, DevAddr, AppSKey, NwkSKey};

fn main() -> Result<(), lora_packet::Error> {
  let app_s_key = AppSKey::new([0u8; 16]);
  let nwk_s_key = NwkSKey::new([0u8; 16]);

  let packet = LoraPacket::builder()
    .data(Direction::Downlink, false)
    .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
    .f_cnt(2)
    .f_port(1)
    .payload(b"hello")
    .sign_and_encrypt(&app_s_key, &nwk_s_key)?;

  let wire: Vec<u8> = packet.to_wire();
  let _ = wire;
  Ok(())
}

OTAA session keys

use lora_packet::{SessionKeys10, SessionKeys11, AppKey, NwkKey, NetId, AppNonce, DevNonce, AppEui};

let app_key = AppKey::new([0u8; 16]);
let nwk_key = NwkKey::new([0u8; 16]);
let net_id = NetId::new([0, 0, 0]);
let app_nonce = AppNonce::new([0, 0, 0]);
let dev_nonce = DevNonce::new([0, 0]);
let join_eui = AppEui::new([0u8; 8]);

let keys_10 = SessionKeys10::derive(&app_key, &net_id, &app_nonce, &dev_nonce);
let keys_11 = SessionKeys11::derive(&app_key, &nwk_key, &join_eui, &app_nonce, &dev_nonce);

Rejoin Request

use lora_packet::{LoraPacket, Payload, RejoinRequest};

fn main() -> Result<(), Box<dyn std::error::Error>> {
  let wire = hex::decode("c0000102030405060708090a0b0c0ddeadbeef")?;
  let packet = LoraPacket::from_wire(&wire)?;

  if let Payload::RejoinRequest(rj) = &packet.payload {
    match rj {
      RejoinRequest::Type0 { net_id, dev_eui, rj_count_0 } => { let _ = (net_id, dev_eui, rj_count_0); }
      RejoinRequest::Type1 { join_eui, dev_eui, rj_count_1 } => { let _ = (join_eui, dev_eui, rj_count_1); }
      RejoinRequest::Type2 { net_id, dev_eui, rj_count_0 } => { let _ = (net_id, dev_eui, rj_count_0); }
    }
  }
  Ok(())
}

Features

Feature Default Effect
std yes Enables std::error::Error impls via thiserror/std
serde no Derives Serialize and Deserialize on packet types and keys
hex_base64 no Adds from_hex and from_base64 constructors on keys, ids, packets

Embedded users: cargo add lora-packet --no-default-features.

LoRaWAN reference

License

MIT