use crate::crypto::Cipher;
use crate::error::LorawanError;
use crate::frame::{PhyPayload, MTYPE_JOIN_ACCEPT, MTYPE_JOIN_REQUEST, MTYPE_MASK};
use crate::session::Session;
pub(crate) const JOIN_REQUEST_LEN: usize = 1 + 8 + 8 + 2 + 4;
pub struct Device {
dev_eui: [u8; 8],
app_eui: [u8; 8],
app_key: [u8; 16],
}
impl Device {
pub fn new(dev_eui: [u8; 8], app_eui: [u8; 8], app_key: [u8; 16]) -> Self {
Device {
dev_eui,
app_eui,
app_key,
}
}
pub fn join_request(&self, dev_nonce: u16) -> PhyPayload {
let mut buf = [0u8; JOIN_REQUEST_LEN];
buf[0] = MTYPE_JOIN_REQUEST;
copy_reversed(&mut buf[1..9], &self.app_eui);
copy_reversed(&mut buf[9..17], &self.dev_eui);
buf[17..19].copy_from_slice(&dev_nonce.to_le_bytes());
let tag = Cipher::new(&self.app_key).cmac(&buf[..19]);
buf[19..23].copy_from_slice(&tag[..4]);
PhyPayload::new(&buf).expect("a join-request always fits a frame")
}
pub fn accept_join(&self, bytes: &[u8], dev_nonce: u16) -> Result<JoinAccept, LorawanError> {
if bytes.is_empty() {
return Err(LorawanError::FrameTooShort);
}
if bytes[0] & MTYPE_MASK != MTYPE_JOIN_ACCEPT {
return Err(LorawanError::UnsupportedMType(bytes[0] & MTYPE_MASK));
}
let encrypted = &bytes[1..];
if encrypted.len() != 16 && encrypted.len() != 32 {
return Err(LorawanError::MalformedFrame);
}
let cipher = Cipher::new(&self.app_key);
let mut clear = [0u8; 32];
for (i, chunk) in encrypted.chunks(16).enumerate() {
let block: [u8; 16] = chunk.try_into().map_err(|_| LorawanError::MalformedFrame)?;
clear[i * 16..i * 16 + 16].copy_from_slice(&cipher.encrypt_block(&block));
}
let clear = &clear[..encrypted.len()];
let mic_at = clear.len() - 4;
let mut signed = [0u8; 1 + 28];
signed[0] = bytes[0];
signed[1..1 + mic_at].copy_from_slice(&clear[..mic_at]);
let tag = cipher.cmac(&signed[..1 + mic_at]);
if clear[mic_at..] != tag[..4] {
return Err(LorawanError::MicMismatch);
}
let app_nonce = &clear[0..3];
let net_id_bytes = &clear[3..6];
let dev_addr = u32::from_le_bytes([clear[6], clear[7], clear[8], clear[9]]);
let dl_settings = clear[10];
let rx_delay = clear[11];
let net_id = u32::from_le_bytes([net_id_bytes[0], net_id_bytes[1], net_id_bytes[2], 0]);
let nwk_skey = derive_key(&cipher, 0x01, app_nonce, net_id_bytes, dev_nonce);
let app_skey = derive_key(&cipher, 0x02, app_nonce, net_id_bytes, dev_nonce);
Ok(JoinAccept {
session: Session::new(dev_addr, nwk_skey, app_skey),
net_id,
dev_addr,
dl_settings,
rx_delay,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct JoinAccept {
session: Session,
net_id: u32,
dev_addr: u32,
dl_settings: u8,
rx_delay: u8,
}
impl JoinAccept {
pub fn session(&self) -> Session {
self.session
}
pub fn dev_addr(&self) -> u32 {
self.dev_addr
}
pub fn net_id(&self) -> u32 {
self.net_id
}
pub fn dl_settings(&self) -> u8 {
self.dl_settings
}
pub fn rx_delay(&self) -> u8 {
self.rx_delay
}
}
pub(crate) fn copy_reversed(dst: &mut [u8], src: &[u8]) {
for (d, s) in dst.iter_mut().zip(src.iter().rev()) {
*d = *s;
}
}
pub(crate) fn derive_key(
cipher: &Cipher,
kind: u8,
app_nonce: &[u8],
net_id: &[u8],
dev_nonce: u16,
) -> [u8; 16] {
let mut block = [0u8; 16];
block[0] = kind;
block[1..4].copy_from_slice(app_nonce);
block[4..7].copy_from_slice(net_id);
block[7..9].copy_from_slice(&dev_nonce.to_le_bytes());
cipher.encrypt_block(&block)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Uplink;
const APP_KEY: [u8; 16] = [0xAB; 16];
const DEV_EUI: [u8; 8] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
const APP_EUI: [u8; 8] = [0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
const DEV_NONCE: u16 = 0x1234;
fn make_join_accept(
app_key: &[u8; 16],
app_nonce: [u8; 3],
net_id: [u8; 3],
dev_addr: u32,
dl_settings: u8,
rx_delay: u8,
) -> [u8; 17] {
let cipher = Cipher::new(app_key);
let mut clear = [0u8; 16];
clear[0..3].copy_from_slice(&app_nonce);
clear[3..6].copy_from_slice(&net_id);
clear[6..10].copy_from_slice(&dev_addr.to_le_bytes());
clear[10] = dl_settings;
clear[11] = rx_delay;
let mut signed = [0u8; 13];
signed[0] = MTYPE_JOIN_ACCEPT;
signed[1..13].copy_from_slice(&clear[..12]);
let tag = cipher.cmac(&signed);
clear[12..16].copy_from_slice(&tag[..4]);
let mut frame = [0u8; 17];
frame[0] = MTYPE_JOIN_ACCEPT;
frame[1..17].copy_from_slice(&cipher.decrypt_block(&clear));
frame
}
#[test]
fn a_join_request_is_well_formed() {
let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
let request = device.join_request(DEV_NONCE);
let bytes = request.as_bytes();
assert_eq!(bytes.len(), JOIN_REQUEST_LEN);
assert_eq!(bytes[0], MTYPE_JOIN_REQUEST);
assert_eq!(
&bytes[1..9],
&[0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88]
);
assert_eq!(&bytes[17..19], &DEV_NONCE.to_le_bytes());
}
#[test]
fn a_join_activates_a_session_that_secures_data() {
let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
let frame = make_join_accept(
&APP_KEY,
[0x01, 0x02, 0x03],
[0x04, 0x05, 0x06],
0x2601_1BDA,
0x00,
0x01,
);
let accepted = device.accept_join(&frame, DEV_NONCE).unwrap();
assert_eq!(accepted.dev_addr(), 0x2601_1BDA);
assert_eq!(accepted.net_id(), 0x0006_0504);
assert_eq!(accepted.rx_delay(), 0x01);
let session = accepted.session();
let uplink = session
.encode_uplink(&Uplink::new(1, 1, b"joined"))
.unwrap();
let rx = session.decode(uplink.as_bytes(), 1).unwrap();
assert_eq!(rx.payload(), b"joined");
}
#[test]
fn a_tampered_join_accept_fails_the_mic() {
let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
let mut frame = make_join_accept(
&APP_KEY,
[0x01, 0x02, 0x03],
[0x04, 0x05, 0x06],
0x2601_1BDA,
0x00,
0x01,
);
frame[5] ^= 0xff;
assert_eq!(
device.accept_join(&frame, DEV_NONCE),
Err(LorawanError::MicMismatch)
);
}
#[test]
fn the_wrong_root_key_rejects_the_join() {
let device = Device::new(DEV_EUI, APP_EUI, [0x00; 16]);
let frame = make_join_accept(
&APP_KEY,
[0x01, 0x02, 0x03],
[0x04, 0x05, 0x06],
0x2601_1BDA,
0x00,
0x01,
);
assert_eq!(
device.accept_join(&frame, DEV_NONCE),
Err(LorawanError::MicMismatch)
);
}
#[test]
fn a_join_accept_of_the_wrong_length_is_malformed() {
let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
assert_eq!(
device.accept_join(&[MTYPE_JOIN_ACCEPT; 20], DEV_NONCE),
Err(LorawanError::MalformedFrame)
);
}
#[test]
fn a_non_join_frame_is_rejected() {
let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
let mut frame = [0u8; 17];
frame[0] = MTYPE_JOIN_REQUEST; assert_eq!(
device.accept_join(&frame, DEV_NONCE),
Err(LorawanError::UnsupportedMType(0x00))
);
}
fn make_join_accept_with_cflist(app_key: &[u8; 16], cflist: [u8; 16]) -> [u8; 33] {
let cipher = Cipher::new(app_key);
let mut clear = [0u8; 32];
clear[0..3].copy_from_slice(&[0x01, 0x02, 0x03]); clear[3..6].copy_from_slice(&[0x04, 0x05, 0x06]); clear[6..10].copy_from_slice(&0x2601_1BDAu32.to_le_bytes()); clear[10] = 0x00; clear[11] = 0x01; clear[12..28].copy_from_slice(&cflist);
let mut signed = [0u8; 29];
signed[0] = MTYPE_JOIN_ACCEPT;
signed[1..29].copy_from_slice(&clear[..28]);
let tag = cipher.cmac(&signed);
clear[28..32].copy_from_slice(&tag[..4]);
let mut frame = [0u8; 33];
frame[0] = MTYPE_JOIN_ACCEPT;
let first: [u8; 16] = clear[0..16].try_into().unwrap();
let second: [u8; 16] = clear[16..32].try_into().unwrap();
frame[1..17].copy_from_slice(&cipher.decrypt_block(&first));
frame[17..33].copy_from_slice(&cipher.decrypt_block(&second));
frame
}
#[test]
fn a_join_accept_with_a_channel_list_activates() {
let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
let frame = make_join_accept_with_cflist(&APP_KEY, [0x11; 16]);
let accepted = device.accept_join(&frame, DEV_NONCE).unwrap();
assert_eq!(accepted.dev_addr(), 0x2601_1BDA);
let session = accepted.session();
let uplink = session.encode_uplink(&Uplink::new(1, 1, b"cf")).unwrap();
assert_eq!(
session.decode(uplink.as_bytes(), 1).unwrap().payload(),
b"cf"
);
}
}