use crate::crypto::Cipher;
use crate::error::LorawanError;
use crate::frame::{
Direction, PhyPayload, MAX_FRAME, MAX_PAYLOAD, MTYPE_CONFIRMED_DOWN, MTYPE_CONFIRMED_UP,
MTYPE_MASK, MTYPE_UNCONFIRMED_DOWN, MTYPE_UNCONFIRMED_UP,
};
const FHDR_LEN: usize = 8;
const MIN_FRAME: usize = FHDR_LEN + 4;
const FCTRL_ADR: u8 = 0x80;
const FCTRL_ACK: u8 = 0x20;
const FCTRL_FPENDING: u8 = 0x10;
const FCTRL_FOPTS_LEN: u8 = 0x0F;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Session {
dev_addr: u32,
nwk_skey: [u8; 16],
app_skey: [u8; 16],
}
impl Session {
pub fn new(dev_addr: u32, nwk_skey: [u8; 16], app_skey: [u8; 16]) -> Self {
Session {
dev_addr,
nwk_skey,
app_skey,
}
}
pub fn dev_addr(&self) -> u32 {
self.dev_addr
}
pub fn encode_uplink(&self, uplink: &Uplink) -> Result<PhyPayload, LorawanError> {
let mtype = if uplink.confirmed {
MTYPE_CONFIRMED_UP
} else {
MTYPE_UNCONFIRMED_UP
};
let mut fctrl = 0;
if uplink.adr {
fctrl |= FCTRL_ADR;
}
if uplink.ack {
fctrl |= FCTRL_ACK;
}
self.encode(
Direction::Uplink,
mtype,
fctrl,
uplink.fcnt,
uplink.fport,
uplink.fopts,
uplink.payload,
)
}
pub fn encode_downlink(&self, downlink: &Downlink) -> Result<PhyPayload, LorawanError> {
let mtype = if downlink.confirmed {
MTYPE_CONFIRMED_DOWN
} else {
MTYPE_UNCONFIRMED_DOWN
};
let mut fctrl = 0;
if downlink.adr {
fctrl |= FCTRL_ADR;
}
if downlink.ack {
fctrl |= FCTRL_ACK;
}
if downlink.fpending {
fctrl |= FCTRL_FPENDING;
}
self.encode(
Direction::Downlink,
mtype,
fctrl,
downlink.fcnt,
downlink.fport,
downlink.fopts,
downlink.payload,
)
}
#[allow(clippy::too_many_arguments)]
fn encode(
&self,
direction: Direction,
mtype: u8,
fctrl: u8,
fcnt: u32,
fport: u8,
fopts: &[u8],
payload: &[u8],
) -> Result<PhyPayload, LorawanError> {
if fopts.len() > usize::from(FCTRL_FOPTS_LEN) {
return Err(LorawanError::PayloadTooLong);
}
let len = MIN_FRAME + fopts.len() + 1 + payload.len();
if len > MAX_FRAME {
return Err(LorawanError::PayloadTooLong);
}
let mut buf = [0u8; MAX_FRAME];
buf[0] = mtype;
buf[1..5].copy_from_slice(&self.dev_addr.to_le_bytes());
buf[5] = fctrl | (fopts.len() as u8);
buf[6..8].copy_from_slice(&(fcnt as u16).to_le_bytes());
let mut at = FHDR_LEN;
buf[at..at + fopts.len()].copy_from_slice(fopts);
at += fopts.len();
buf[at] = fport;
at += 1;
let key = self.payload_key(fport);
crypt_payload(
key,
self.dev_addr,
direction,
fcnt,
payload,
&mut buf[at..at + payload.len()],
);
at += payload.len();
let mic = self.mic(direction, fcnt, &buf[..at]);
buf[at..at + 4].copy_from_slice(&mic);
at += 4;
PhyPayload::new(&buf[..at])
}
pub fn decode(&self, bytes: &[u8], fcnt: u32) -> Result<RxData, LorawanError> {
if bytes.len() < MIN_FRAME {
return Err(LorawanError::FrameTooShort);
}
let mtype = bytes[0] & MTYPE_MASK;
let (direction, confirmed) = match mtype {
MTYPE_UNCONFIRMED_UP => (Direction::Uplink, false),
MTYPE_CONFIRMED_UP => (Direction::Uplink, true),
MTYPE_UNCONFIRMED_DOWN => (Direction::Downlink, false),
MTYPE_CONFIRMED_DOWN => (Direction::Downlink, true),
other => return Err(LorawanError::UnsupportedMType(other)),
};
let dev_addr = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
let fctrl = bytes[5];
let fopts_len = usize::from(fctrl & FCTRL_FOPTS_LEN);
let fcnt_low = u16::from_le_bytes([bytes[6], bytes[7]]);
if fcnt as u16 != fcnt_low {
return Err(LorawanError::FcntMismatch);
}
let mic_start = bytes.len() - 4;
let body_start = FHDR_LEN + fopts_len;
if mic_start < body_start {
return Err(LorawanError::FrameTooShort);
}
let expected = self.mic(direction, fcnt, &bytes[..mic_start]);
if bytes[mic_start..] != expected[..] {
return Err(LorawanError::MicMismatch);
}
let mut fopts = [0u8; FCTRL_FOPTS_LEN as usize];
fopts[..fopts_len].copy_from_slice(&bytes[FHDR_LEN..FHDR_LEN + fopts_len]);
let mut payload = [0u8; MAX_PAYLOAD];
let (fport, payload_len) = if mic_start > body_start {
let fport = bytes[body_start];
let encrypted = &bytes[body_start + 1..mic_start];
let key = self.payload_key(fport);
crypt_payload(
key,
dev_addr,
direction,
fcnt,
encrypted,
&mut payload[..encrypted.len()],
);
(Some(fport), encrypted.len())
} else {
(None, 0)
};
Ok(RxData {
direction,
dev_addr,
fcnt_low,
confirmed,
adr: fctrl & FCTRL_ADR != 0,
ack: fctrl & FCTRL_ACK != 0,
fpending: fctrl & FCTRL_FPENDING != 0,
fport,
fopts,
fopts_len,
payload,
payload_len,
})
}
fn payload_key(&self, fport: u8) -> &[u8; 16] {
if fport == 0 {
&self.nwk_skey
} else {
&self.app_skey
}
}
fn mic(&self, direction: Direction, fcnt: u32, msg: &[u8]) -> [u8; 4] {
let mut block = [0u8; 16 + MAX_FRAME];
block[0] = 0x49;
block[5] = direction.bit();
block[6..10].copy_from_slice(&self.dev_addr.to_le_bytes());
block[10..14].copy_from_slice(&fcnt.to_le_bytes());
block[15] = msg.len() as u8;
block[16..16 + msg.len()].copy_from_slice(msg);
let tag = Cipher::new(&self.nwk_skey).cmac(&block[..16 + msg.len()]);
[tag[0], tag[1], tag[2], tag[3]]
}
}
fn crypt_payload(
key: &[u8; 16],
dev_addr: u32,
direction: Direction,
fcnt: u32,
input: &[u8],
output: &mut [u8],
) {
let cipher = Cipher::new(key);
let blocks = input.len().div_ceil(16);
for i in 0..blocks {
let mut a = [0u8; 16];
a[0] = 0x01;
a[5] = direction.bit();
a[6..10].copy_from_slice(&dev_addr.to_le_bytes());
a[10..14].copy_from_slice(&fcnt.to_le_bytes());
a[15] = (i + 1) as u8;
let stream = cipher.encrypt_block(&a);
let start = i * 16;
let end = (start + 16).min(input.len());
for j in start..end {
output[j] = input[j] ^ stream[j - start];
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Uplink<'a> {
fcnt: u32,
fport: u8,
payload: &'a [u8],
confirmed: bool,
adr: bool,
ack: bool,
fopts: &'a [u8],
}
impl<'a> Uplink<'a> {
pub fn new(fcnt: u32, fport: u8, payload: &'a [u8]) -> Self {
Uplink {
fcnt,
fport,
payload,
confirmed: false,
adr: false,
ack: false,
fopts: &[],
}
}
pub fn confirmed(mut self) -> Self {
self.confirmed = true;
self
}
pub fn with_adr(mut self) -> Self {
self.adr = true;
self
}
pub fn with_ack(mut self) -> Self {
self.ack = true;
self
}
pub fn with_fopts(mut self, fopts: &'a [u8]) -> Self {
self.fopts = fopts;
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct Downlink<'a> {
fcnt: u32,
fport: u8,
payload: &'a [u8],
confirmed: bool,
adr: bool,
ack: bool,
fpending: bool,
fopts: &'a [u8],
}
impl<'a> Downlink<'a> {
pub fn new(fcnt: u32, fport: u8, payload: &'a [u8]) -> Self {
Downlink {
fcnt,
fport,
payload,
confirmed: false,
adr: false,
ack: false,
fpending: false,
fopts: &[],
}
}
pub fn confirmed(mut self) -> Self {
self.confirmed = true;
self
}
pub fn with_adr(mut self) -> Self {
self.adr = true;
self
}
pub fn with_ack(mut self) -> Self {
self.ack = true;
self
}
pub fn with_fpending(mut self) -> Self {
self.fpending = true;
self
}
pub fn with_fopts(mut self, fopts: &'a [u8]) -> Self {
self.fopts = fopts;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RxData {
direction: Direction,
dev_addr: u32,
fcnt_low: u16,
confirmed: bool,
adr: bool,
ack: bool,
fpending: bool,
fport: Option<u8>,
fopts: [u8; FCTRL_FOPTS_LEN as usize],
fopts_len: usize,
payload: [u8; MAX_PAYLOAD],
payload_len: usize,
}
impl RxData {
pub fn direction(&self) -> Direction {
self.direction
}
pub fn dev_addr(&self) -> u32 {
self.dev_addr
}
pub fn fcnt(&self) -> u16 {
self.fcnt_low
}
pub fn confirmed(&self) -> bool {
self.confirmed
}
pub fn adr(&self) -> bool {
self.adr
}
pub fn ack(&self) -> bool {
self.ack
}
pub fn fpending(&self) -> bool {
self.fpending
}
pub fn fport(&self) -> Option<u8> {
self.fport
}
pub fn fopts(&self) -> &[u8] {
&self.fopts[..self.fopts_len]
}
pub fn payload(&self) -> &[u8] {
&self.payload[..self.payload_len]
}
}
#[cfg(test)]
mod tests {
use super::*;
const NWK_SKEY: [u8; 16] = [0x01; 16];
const APP_SKEY: [u8; 16] = [0x02; 16];
const DEV_ADDR: u32 = 0x2601_1BDA;
fn session() -> Session {
Session::new(DEV_ADDR, NWK_SKEY, APP_SKEY)
}
#[test]
fn an_uplink_round_trips() {
let session = session();
let frame = session
.encode_uplink(&Uplink::new(10, 1, b"temperature"))
.unwrap();
let rx = session.decode(frame.as_bytes(), 10).unwrap();
assert_eq!(rx.direction(), Direction::Uplink);
assert_eq!(rx.dev_addr(), DEV_ADDR);
assert_eq!(rx.fcnt(), 10);
assert_eq!(rx.fport(), Some(1));
assert_eq!(rx.payload(), b"temperature");
assert!(!rx.confirmed());
}
#[test]
fn the_payload_is_encrypted_on_the_wire() {
let session = session();
let frame = session
.encode_uplink(&Uplink::new(1, 1, b"secret"))
.unwrap();
assert!(frame
.as_bytes()
.windows(b"secret".len())
.all(|window| window != b"secret"));
}
#[test]
fn the_header_is_laid_out_as_the_spec_requires() {
let session = session();
let frame = session
.encode_uplink(&Uplink::new(0x0102, 1, b"x"))
.unwrap();
let bytes = frame.as_bytes();
assert_eq!(bytes[0], MTYPE_UNCONFIRMED_UP);
assert_eq!(&bytes[1..5], &DEV_ADDR.to_le_bytes());
assert_eq!(&bytes[6..8], &0x0102u16.to_le_bytes());
}
#[test]
fn a_confirmed_downlink_round_trips_with_its_flags() {
let session = session();
let frame = session
.encode_downlink(&Downlink::new(5, 2, b"cmd").confirmed().with_fpending())
.unwrap();
let rx = session.decode(frame.as_bytes(), 5).unwrap();
assert_eq!(rx.direction(), Direction::Downlink);
assert!(rx.confirmed());
assert!(rx.fpending());
assert_eq!(rx.payload(), b"cmd");
}
#[test]
fn frame_options_round_trip() {
let session = session();
let frame = session
.encode_uplink(&Uplink::new(3, 1, b"d").with_fopts(&[0x02, 0x03]))
.unwrap();
let rx = session.decode(frame.as_bytes(), 3).unwrap();
assert_eq!(rx.fopts(), &[0x02, 0x03]);
assert_eq!(rx.payload(), b"d");
}
#[test]
fn an_empty_payload_round_trips() {
let session = session();
let frame = session.encode_uplink(&Uplink::new(1, 1, b"")).unwrap();
let rx = session.decode(frame.as_bytes(), 1).unwrap();
assert_eq!(rx.payload(), b"");
assert_eq!(rx.fport(), Some(1));
}
#[test]
fn a_tampered_payload_fails_the_mic() {
let session = session();
let frame = session.encode_uplink(&Uplink::new(1, 1, b"data")).unwrap();
let mut bytes = frame.as_bytes().to_vec();
let last = bytes.len() - 5; bytes[last] ^= 0xff;
assert_eq!(session.decode(&bytes, 1), Err(LorawanError::MicMismatch));
}
#[test]
fn the_wrong_counter_is_rejected() {
let session = session();
let frame = session.encode_uplink(&Uplink::new(7, 1, b"data")).unwrap();
assert_eq!(
session.decode(frame.as_bytes(), 8),
Err(LorawanError::FcntMismatch)
);
}
#[test]
fn a_join_frame_is_not_decoded_here() {
let session = session();
let bytes = [0u8; MIN_FRAME];
assert_eq!(
session.decode(&bytes, 0),
Err(LorawanError::UnsupportedMType(0x00))
);
}
#[test]
fn a_short_frame_is_rejected() {
let session = session();
assert_eq!(
session.decode(&[0x40, 0x00, 0x00], 0),
Err(LorawanError::FrameTooShort)
);
}
#[test]
fn port_zero_uses_the_network_key() {
let session = Session::new(DEV_ADDR, NWK_SKEY, APP_SKEY);
let frame = session.encode_uplink(&Uplink::new(1, 0, b"mac")).unwrap();
let other = Session::new(DEV_ADDR, NWK_SKEY, [0x33; 16]);
let rx = other.decode(frame.as_bytes(), 1).unwrap();
assert_eq!(rx.payload(), b"mac");
}
#[test]
fn the_largest_payload_round_trips() {
let session = session();
let payload = [0xAB; MAX_PAYLOAD];
let frame = session.encode_uplink(&Uplink::new(1, 1, &payload)).unwrap();
assert_eq!(frame.as_bytes().len(), crate::MAX_FRAME);
let rx = session.decode(frame.as_bytes(), 1).unwrap();
assert_eq!(rx.payload(), &payload[..]);
}
#[test]
fn the_full_frame_counter_is_bound_into_the_mic() {
let session = session();
let frame = session
.encode_uplink(&Uplink::new(0x0001_0001, 1, b"x"))
.unwrap();
assert_eq!(
session.decode(frame.as_bytes(), 0x0000_0001),
Err(LorawanError::MicMismatch)
);
let rx = session.decode(frame.as_bytes(), 0x0001_0001).unwrap();
assert_eq!(rx.fcnt(), 0x0001);
}
#[test]
fn another_sessions_keys_cannot_read_a_frame() {
let session = session();
let frame = session
.encode_uplink(&Uplink::new(1, 1, b"secret"))
.unwrap();
let stranger = Session::new(DEV_ADDR, [0xAA; 16], [0xBB; 16]);
assert_eq!(
stranger.decode(frame.as_bytes(), 1),
Err(LorawanError::MicMismatch)
);
}
}