1use crate::cflist::{CfList, CFLIST_LEN};
4use crate::crypto::Cipher;
5use crate::error::LorawanError;
6use crate::frame::{PhyPayload, MTYPE_JOIN_ACCEPT, MTYPE_JOIN_REQUEST, MTYPE_MASK};
7use crate::mac::receive_delay_s;
8use crate::session::Session;
9
10pub(crate) const JOIN_REQUEST_LEN: usize = 1 + 8 + 8 + 2 + 4;
12
13#[derive(Clone)]
24pub struct Device {
25 dev_eui: [u8; 8],
26 app_eui: [u8; 8],
27 app_key: [u8; 16],
28}
29
30impl Device {
31 pub fn new(dev_eui: [u8; 8], app_eui: [u8; 8], app_key: [u8; 16]) -> Self {
43 Device {
44 dev_eui,
45 app_eui,
46 app_key,
47 }
48 }
49
50 pub fn dev_eui(&self) -> [u8; 8] {
56 self.dev_eui
57 }
58
59 pub fn join_request(&self, dev_nonce: u16) -> PhyPayload {
70 let mut buf = [0u8; JOIN_REQUEST_LEN];
71 buf[0] = MTYPE_JOIN_REQUEST;
72 copy_reversed(&mut buf[1..9], &self.app_eui);
73 copy_reversed(&mut buf[9..17], &self.dev_eui);
74 buf[17..19].copy_from_slice(&dev_nonce.to_le_bytes());
75 let tag = Cipher::new(&self.app_key).cmac(&buf[..19]);
76 buf[19..23].copy_from_slice(&tag[..4]);
77 PhyPayload::new(&buf).expect("a join-request always fits a frame")
78 }
79
80 pub fn accept_join(&self, bytes: &[u8], dev_nonce: u16) -> Result<JoinAccept, LorawanError> {
101 if bytes.is_empty() {
102 return Err(LorawanError::FrameTooShort);
103 }
104 if bytes[0] & MTYPE_MASK != MTYPE_JOIN_ACCEPT {
105 return Err(LorawanError::UnsupportedMType(bytes[0] & MTYPE_MASK));
106 }
107 let encrypted = &bytes[1..];
108 if encrypted.len() != 16 && encrypted.len() != 32 {
110 return Err(LorawanError::MalformedFrame);
111 }
112
113 let cipher = Cipher::new(&self.app_key);
114 let mut clear = [0u8; 32];
116 for (i, chunk) in encrypted.chunks(16).enumerate() {
117 let block: [u8; 16] = chunk.try_into().map_err(|_| LorawanError::MalformedFrame)?;
118 clear[i * 16..i * 16 + 16].copy_from_slice(&cipher.encrypt_block(&block));
119 }
120 let clear = &clear[..encrypted.len()];
121
122 let mic_at = clear.len() - 4;
124 let mut signed = [0u8; 1 + 28];
125 signed[0] = bytes[0];
126 signed[1..1 + mic_at].copy_from_slice(&clear[..mic_at]);
127 let tag = cipher.cmac(&signed[..1 + mic_at]);
128 if clear[mic_at..] != tag[..4] {
129 return Err(LorawanError::MicMismatch);
130 }
131
132 let app_nonce = &clear[0..3];
133 let net_id_bytes = &clear[3..6];
134 let dev_addr = u32::from_le_bytes([clear[6], clear[7], clear[8], clear[9]]);
135 let dl_settings = clear[10];
136 let rx_delay = clear[11];
137 let net_id = u32::from_le_bytes([net_id_bytes[0], net_id_bytes[1], net_id_bytes[2], 0]);
138
139 let nwk_skey = derive_key(&cipher, 0x01, app_nonce, net_id_bytes, dev_nonce);
140 let app_skey = derive_key(&cipher, 0x02, app_nonce, net_id_bytes, dev_nonce);
141
142 let cflist = clear
143 .get(12..12 + CFLIST_LEN)
144 .and_then(|bytes| <[u8; CFLIST_LEN]>::try_from(bytes).ok())
145 .map(CfList::from_bytes);
146
147 Ok(JoinAccept {
148 session: Session::new(dev_addr, nwk_skey, app_skey),
149 net_id,
150 dev_addr,
151 dl_settings,
152 rx_delay,
153 cflist,
154 })
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub struct JoinAccept {
162 session: Session,
163 net_id: u32,
164 dev_addr: u32,
165 dl_settings: u8,
166 rx_delay: u8,
167 cflist: Option<CfList>,
168}
169
170impl JoinAccept {
171 pub fn session(&self) -> Session {
177 self.session
178 }
179
180 pub fn dev_addr(&self) -> u32 {
186 self.dev_addr
187 }
188
189 pub fn net_id(&self) -> u32 {
195 self.net_id
196 }
197
198 pub fn dl_settings(&self) -> u8 {
207 self.dl_settings
208 }
209
210 pub fn rx_delay(&self) -> u8 {
218 self.rx_delay
219 }
220
221 pub fn rx1_dr_offset(&self) -> u8 {
232 (self.dl_settings >> 4) & 0x07
233 }
234
235 pub fn rx2_data_rate(&self) -> u8 {
241 self.dl_settings & 0x0F
242 }
243
244 pub fn receive_delay_us(&self) -> u32 {
265 u32::from(receive_delay_s(self.rx_delay & 0x0F)) * 1_000_000
266 }
267
268 pub fn cflist(&self) -> Option<CfList> {
274 self.cflist
275 }
276}
277
278pub(crate) fn copy_reversed(dst: &mut [u8], src: &[u8]) {
281 for (d, s) in dst.iter_mut().zip(src.iter().rev()) {
282 *d = *s;
283 }
284}
285
286pub(crate) fn derive_key(
288 cipher: &Cipher,
289 kind: u8,
290 app_nonce: &[u8],
291 net_id: &[u8],
292 dev_nonce: u16,
293) -> [u8; 16] {
294 let mut block = [0u8; 16];
295 block[0] = kind;
296 block[1..4].copy_from_slice(app_nonce);
297 block[4..7].copy_from_slice(net_id);
298 block[7..9].copy_from_slice(&dev_nonce.to_le_bytes());
299 cipher.encrypt_block(&block)
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::Uplink;
306
307 const APP_KEY: [u8; 16] = [0xAB; 16];
308 const DEV_EUI: [u8; 8] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
309 const APP_EUI: [u8; 8] = [0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
310 const DEV_NONCE: u16 = 0x1234;
311
312 fn make_join_accept(
314 app_key: &[u8; 16],
315 app_nonce: [u8; 3],
316 net_id: [u8; 3],
317 dev_addr: u32,
318 dl_settings: u8,
319 rx_delay: u8,
320 ) -> [u8; 17] {
321 let cipher = Cipher::new(app_key);
322 let mut clear = [0u8; 16];
323 clear[0..3].copy_from_slice(&app_nonce);
324 clear[3..6].copy_from_slice(&net_id);
325 clear[6..10].copy_from_slice(&dev_addr.to_le_bytes());
326 clear[10] = dl_settings;
327 clear[11] = rx_delay;
328 let mut signed = [0u8; 13];
329 signed[0] = MTYPE_JOIN_ACCEPT;
330 signed[1..13].copy_from_slice(&clear[..12]);
331 let tag = cipher.cmac(&signed);
332 clear[12..16].copy_from_slice(&tag[..4]);
333
334 let mut frame = [0u8; 17];
335 frame[0] = MTYPE_JOIN_ACCEPT;
336 frame[1..17].copy_from_slice(&cipher.decrypt_block(&clear));
337 frame
338 }
339
340 #[test]
341 fn a_join_request_is_well_formed() {
342 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
343 let request = device.join_request(DEV_NONCE);
344 let bytes = request.as_bytes();
345 assert_eq!(bytes.len(), JOIN_REQUEST_LEN);
346 assert_eq!(bytes[0], MTYPE_JOIN_REQUEST);
347 assert_eq!(
349 &bytes[1..9],
350 &[0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88]
351 );
352 assert_eq!(&bytes[17..19], &DEV_NONCE.to_le_bytes());
353 }
354
355 #[test]
356 fn a_join_activates_a_session_that_secures_data() {
357 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
358 let frame = make_join_accept(
359 &APP_KEY,
360 [0x01, 0x02, 0x03],
361 [0x04, 0x05, 0x06],
362 0x2601_1BDA,
363 0x00,
364 0x01,
365 );
366
367 let accepted = device.accept_join(&frame, DEV_NONCE).unwrap();
368 assert_eq!(accepted.dev_addr(), 0x2601_1BDA);
369 assert_eq!(accepted.net_id(), 0x0006_0504);
370 assert_eq!(accepted.rx_delay(), 0x01);
371
372 let session = accepted.session();
374 let uplink = session
375 .encode_uplink(&Uplink::new(1, 1, b"joined"))
376 .unwrap();
377 let rx = session.decode(uplink.as_bytes(), 1).unwrap();
378 assert_eq!(rx.payload(), b"joined");
379 }
380
381 #[test]
382 fn a_tampered_join_accept_fails_the_mic() {
383 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
384 let mut frame = make_join_accept(
385 &APP_KEY,
386 [0x01, 0x02, 0x03],
387 [0x04, 0x05, 0x06],
388 0x2601_1BDA,
389 0x00,
390 0x01,
391 );
392 frame[5] ^= 0xff;
393 assert_eq!(
394 device.accept_join(&frame, DEV_NONCE),
395 Err(LorawanError::MicMismatch)
396 );
397 }
398
399 #[test]
400 fn the_wrong_root_key_rejects_the_join() {
401 let device = Device::new(DEV_EUI, APP_EUI, [0x00; 16]);
402 let frame = make_join_accept(
403 &APP_KEY,
404 [0x01, 0x02, 0x03],
405 [0x04, 0x05, 0x06],
406 0x2601_1BDA,
407 0x00,
408 0x01,
409 );
410 assert_eq!(
411 device.accept_join(&frame, DEV_NONCE),
412 Err(LorawanError::MicMismatch)
413 );
414 }
415
416 #[test]
417 fn a_join_accept_of_the_wrong_length_is_malformed() {
418 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
419 assert_eq!(
420 device.accept_join(&[MTYPE_JOIN_ACCEPT; 20], DEV_NONCE),
421 Err(LorawanError::MalformedFrame)
422 );
423 }
424
425 #[test]
426 fn a_non_join_frame_is_rejected() {
427 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
428 let mut frame = [0u8; 17];
429 frame[0] = MTYPE_JOIN_REQUEST; assert_eq!(
431 device.accept_join(&frame, DEV_NONCE),
432 Err(LorawanError::UnsupportedMType(0x00))
433 );
434 }
435
436 fn make_join_accept_with_cflist(app_key: &[u8; 16], cflist: [u8; 16]) -> [u8; 33] {
438 let cipher = Cipher::new(app_key);
439 let mut clear = [0u8; 32];
440 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);
446 let mut signed = [0u8; 29];
447 signed[0] = MTYPE_JOIN_ACCEPT;
448 signed[1..29].copy_from_slice(&clear[..28]);
449 let tag = cipher.cmac(&signed);
450 clear[28..32].copy_from_slice(&tag[..4]);
451
452 let mut frame = [0u8; 33];
453 frame[0] = MTYPE_JOIN_ACCEPT;
454 let first: [u8; 16] = clear[0..16].try_into().unwrap();
455 let second: [u8; 16] = clear[16..32].try_into().unwrap();
456 frame[1..17].copy_from_slice(&cipher.decrypt_block(&first));
457 frame[17..33].copy_from_slice(&cipher.decrypt_block(&second));
458 frame
459 }
460
461 #[test]
462 fn a_join_accept_with_a_channel_list_activates() {
463 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
464 let frame = make_join_accept_with_cflist(&APP_KEY, [0x11; 16]);
465 let accepted = device.accept_join(&frame, DEV_NONCE).unwrap();
466 assert_eq!(accepted.dev_addr(), 0x2601_1BDA);
467 assert_eq!(
468 accepted.cflist(),
469 Some(CfList::from_bytes([0x11; 16])),
470 "the list is handed back as it arrived, whatever its type"
471 );
472 let session = accepted.session();
474 let uplink = session.encode_uplink(&Uplink::new(1, 1, b"cf")).unwrap();
475 assert_eq!(
476 session.decode(uplink.as_bytes(), 1).unwrap().payload(),
477 b"cf"
478 );
479 }
480}