1use crate::crypto::Cipher;
9use crate::error::LorawanError;
10use crate::frame::{PhyPayload, MTYPE_JOIN_ACCEPT, MTYPE_JOIN_REQUEST, MTYPE_MASK};
11use crate::join::{copy_reversed, derive_key, JOIN_REQUEST_LEN};
12use crate::session::Session;
13
14const CFLIST_LEN: usize = 16;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct JoinRequest {
40 dev_eui: [u8; 8],
41 app_eui: [u8; 8],
42 dev_nonce: u16,
43}
44
45impl JoinRequest {
46 pub fn parse(bytes: &[u8], app_key: &[u8; 16]) -> Result<JoinRequest, LorawanError> {
65 if bytes.is_empty() {
66 return Err(LorawanError::FrameTooShort);
67 }
68 if bytes[0] & MTYPE_MASK != MTYPE_JOIN_REQUEST {
69 return Err(LorawanError::UnsupportedMType(bytes[0] & MTYPE_MASK));
70 }
71 if bytes.len() != JOIN_REQUEST_LEN {
72 return Err(LorawanError::MalformedFrame);
73 }
74
75 let tag = Cipher::new(app_key).cmac(&bytes[..19]);
76 if bytes[19..23] != tag[..4] {
77 return Err(LorawanError::MicMismatch);
78 }
79
80 let mut app_eui = [0u8; 8];
82 let mut dev_eui = [0u8; 8];
83 copy_reversed(&mut app_eui, &bytes[1..9]);
84 copy_reversed(&mut dev_eui, &bytes[9..17]);
85
86 Ok(JoinRequest {
87 dev_eui,
88 app_eui,
89 dev_nonce: u16::from_le_bytes([bytes[17], bytes[18]]),
90 })
91 }
92
93 pub fn dev_eui(&self) -> [u8; 8] {
99 self.dev_eui
100 }
101
102 pub fn app_eui(&self) -> [u8; 8] {
108 self.app_eui
109 }
110
111 pub fn dev_nonce(&self) -> u16 {
120 self.dev_nonce
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub struct JoinGrant {
155 app_nonce: u32,
156 net_id: u32,
157 dev_addr: u32,
158 dl_settings: u8,
159 rx_delay: u8,
160 cflist: Option<[u8; CFLIST_LEN]>,
161}
162
163impl JoinGrant {
164 pub fn new(app_nonce: u32, net_id: u32, dev_addr: u32) -> Self {
177 JoinGrant {
178 app_nonce,
179 net_id,
180 dev_addr,
181 dl_settings: 0,
182 rx_delay: 0,
183 cflist: None,
184 }
185 }
186
187 pub fn with_dl_settings(mut self, dl_settings: u8) -> Self {
197 self.dl_settings = dl_settings;
198 self
199 }
200
201 pub fn with_rx_delay(mut self, rx_delay: u8) -> Self {
211 self.rx_delay = rx_delay;
212 self
213 }
214
215 pub fn with_cflist(mut self, cflist: [u8; CFLIST_LEN]) -> Self {
225 self.cflist = Some(cflist);
226 self
227 }
228
229 pub fn dev_addr(&self) -> u32 {
235 self.dev_addr
236 }
237
238 pub fn net_id(&self) -> u32 {
244 self.net_id
245 }
246
247 pub fn accept(&self, app_key: &[u8; 16], dev_nonce: u16) -> PhyPayload {
258 let _ = dev_nonce;
259 let cipher = Cipher::new(app_key);
260 let body = self.body_len();
261
262 let mut clear = [0u8; 32];
264 clear[0..3].copy_from_slice(&self.app_nonce.to_le_bytes()[..3]);
265 clear[3..6].copy_from_slice(&self.net_id.to_le_bytes()[..3]);
266 clear[6..10].copy_from_slice(&self.dev_addr.to_le_bytes());
267 clear[10] = self.dl_settings;
268 clear[11] = self.rx_delay;
269 if let Some(cflist) = self.cflist {
270 clear[12..12 + CFLIST_LEN].copy_from_slice(&cflist);
271 }
272
273 let mic_at = body - 4;
274 let mut signed = [0u8; 1 + 28];
275 signed[0] = MTYPE_JOIN_ACCEPT;
276 signed[1..1 + mic_at].copy_from_slice(&clear[..mic_at]);
277 let tag = cipher.cmac(&signed[..1 + mic_at]);
278 clear[mic_at..body].copy_from_slice(&tag[..4]);
279
280 let mut frame = [0u8; 1 + 32];
282 frame[0] = MTYPE_JOIN_ACCEPT;
283 for (index, chunk) in clear[..body].chunks(16).enumerate() {
284 let block: [u8; 16] = chunk.try_into().expect("the body is whole blocks");
285 frame[1 + index * 16..1 + index * 16 + 16]
286 .copy_from_slice(&cipher.decrypt_block(&block));
287 }
288 PhyPayload::new(&frame[..1 + body]).expect("a join-accept always fits a frame")
289 }
290
291 pub fn session(&self, app_key: &[u8; 16], dev_nonce: u16) -> Session {
302 let cipher = Cipher::new(app_key);
303 let app_nonce = self.app_nonce.to_le_bytes();
304 let net_id = self.net_id.to_le_bytes();
305 let nwk_skey = derive_key(&cipher, 0x01, &app_nonce[..3], &net_id[..3], dev_nonce);
306 let app_skey = derive_key(&cipher, 0x02, &app_nonce[..3], &net_id[..3], dev_nonce);
307 Session::new(self.dev_addr, nwk_skey, app_skey)
308 }
309
310 fn body_len(&self) -> usize {
312 if self.cflist.is_some() {
313 32
314 } else {
315 16
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::{Device, Uplink};
324
325 const APP_KEY: [u8; 16] = [0xAB; 16];
326 const DEV_EUI: [u8; 8] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
327 const APP_EUI: [u8; 8] = [0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
328 const DEV_NONCE: u16 = 0x1234;
329
330 fn grant() -> JoinGrant {
331 JoinGrant::new(0x0003_0201, 0x0006_0504, 0x2601_1BDA)
332 .with_dl_settings(0x00)
333 .with_rx_delay(0x01)
334 }
335
336 #[test]
337 fn a_request_verifies_and_reads_back_the_identifiers() {
338 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
339 let request =
340 JoinRequest::parse(device.join_request(DEV_NONCE).as_bytes(), &APP_KEY).unwrap();
341 assert_eq!(request.dev_eui(), DEV_EUI);
342 assert_eq!(request.app_eui(), APP_EUI);
343 assert_eq!(request.dev_nonce(), DEV_NONCE);
344 }
345
346 #[test]
347 fn a_request_signed_with_another_key_is_refused() {
348 let device = Device::new(DEV_EUI, APP_EUI, [0x00; 16]);
349 assert_eq!(
350 JoinRequest::parse(device.join_request(DEV_NONCE).as_bytes(), &APP_KEY),
351 Err(LorawanError::MicMismatch)
352 );
353 }
354
355 #[test]
356 fn a_tampered_request_fails_its_mic() {
357 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
358 let mut bytes = device.join_request(DEV_NONCE).as_bytes().to_vec();
359 bytes[10] ^= 0xFF;
360 assert_eq!(
361 JoinRequest::parse(&bytes, &APP_KEY),
362 Err(LorawanError::MicMismatch)
363 );
364 }
365
366 #[test]
367 fn a_data_frame_is_not_a_join_request() {
368 assert_eq!(
369 JoinRequest::parse(&[0x40; JOIN_REQUEST_LEN], &APP_KEY),
370 Err(LorawanError::UnsupportedMType(0x40))
371 );
372 }
373
374 #[test]
375 fn a_truncated_request_is_malformed() {
376 assert_eq!(
377 JoinRequest::parse(&[MTYPE_JOIN_REQUEST; 20], &APP_KEY),
378 Err(LorawanError::MalformedFrame)
379 );
380 }
381
382 #[test]
383 fn the_accept_this_network_builds_activates_the_device() {
384 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
385 let grant = grant();
386 let accepted = device
387 .accept_join(grant.accept(&APP_KEY, DEV_NONCE).as_bytes(), DEV_NONCE)
388 .expect("the device accepts what this network signed");
389
390 assert_eq!(accepted.dev_addr(), grant.dev_addr());
391 assert_eq!(accepted.net_id(), grant.net_id());
392 assert_eq!(accepted.dl_settings(), 0x00);
393 assert_eq!(accepted.rx_delay(), 0x01);
394 }
395
396 #[test]
397 fn both_sides_derive_the_same_session() {
398 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
399 let grant = grant();
400 let accepted = device
401 .accept_join(grant.accept(&APP_KEY, DEV_NONCE).as_bytes(), DEV_NONCE)
402 .expect("the device activates");
403
404 let network = grant.session(&APP_KEY, DEV_NONCE);
406 assert_eq!(accepted.session(), network);
407
408 let uplink = accepted
409 .session()
410 .encode_uplink(&Uplink::new(1, 1, b"joined"))
411 .unwrap();
412 assert_eq!(
413 network.decode(uplink.as_bytes(), 1).unwrap().payload(),
414 b"joined"
415 );
416 }
417
418 #[test]
419 fn a_grant_with_a_channel_list_activates_too() {
420 let device = Device::new(DEV_EUI, APP_EUI, APP_KEY);
421 let grant = grant().with_cflist([0x11; CFLIST_LEN]);
422 let reply = grant.accept(&APP_KEY, DEV_NONCE);
423 assert_eq!(
424 reply.as_bytes().len(),
425 33,
426 "a channel list doubles the body"
427 );
428
429 let accepted = device
430 .accept_join(reply.as_bytes(), DEV_NONCE)
431 .expect("the device accepts the longer form");
432 assert_eq!(accepted.dev_addr(), grant.dev_addr());
433 assert_eq!(accepted.session(), grant.session(&APP_KEY, DEV_NONCE));
434 }
435
436 #[test]
437 fn a_different_nonce_derives_a_different_session() {
438 let grant = grant();
439 assert_ne!(
440 grant.session(&APP_KEY, DEV_NONCE),
441 grant.session(&APP_KEY, DEV_NONCE + 1),
442 "replaying a nonce is what the network must refuse, so the keys must differ"
443 );
444 }
445}
446
447#[cfg(test)]
448mod published_vector {
449 use super::*;
450 use crate::Device;
451
452 const FRAME: &str = "204dd85ae608b87fc4889970b7d2042c9e72959b0057aed6094b16003df12de145";
459 const APP_KEY: &str = "b6b53f4a168a7a88bdf7ea135ce9cfca";
460 const DEV_NONCE: u16 = 0xCC85;
461 const APP_NONCE: u32 = 0x00E5_063A;
462 const NET_ID: u32 = 0x0000_0013;
463 const DEV_ADDR: u32 = 0x2601_2E43;
464 const DL_SETTINGS: u8 = 0x03;
465 const RX_DELAY: u8 = 0x01;
466 const CFLIST: &str = "184f84e85684b85e84886684586e8400";
467 const NWK_SKEY: &str = "2c96f7028184bb0be8aa49275290d4fc";
468 const APP_SKEY: &str = "f3a5c8f0232a38c144029c165865802c";
469
470 #[test]
471 fn a_device_activates_from_a_captured_join_accept() {
472 let accepted = Device::new([0; 8], [0; 8], key(APP_KEY))
473 .accept_join(&hex(FRAME), DEV_NONCE)
474 .expect("the captured accept verifies against its own key");
475
476 assert_eq!(accepted.dev_addr(), DEV_ADDR);
477 assert_eq!(accepted.net_id(), NET_ID);
478 assert_eq!(accepted.dl_settings(), DL_SETTINGS);
479 assert_eq!(accepted.rx_delay(), RX_DELAY);
480
481 assert_eq!(
484 accepted.session(),
485 Session::new(DEV_ADDR, key(NWK_SKEY), key(APP_SKEY))
486 );
487 }
488
489 #[test]
490 fn this_network_rebuilds_that_join_accept_byte_for_byte() {
491 let grant = JoinGrant::new(APP_NONCE, NET_ID, DEV_ADDR)
492 .with_dl_settings(DL_SETTINGS)
493 .with_rx_delay(RX_DELAY)
494 .with_cflist(hex(CFLIST).try_into().expect("a 16-byte channel list"));
495
496 assert_eq!(
497 grant.accept(&key(APP_KEY), DEV_NONCE).as_bytes(),
498 &hex(FRAME)[..],
499 "the frame this network signs is the one that was captured"
500 );
501 assert_eq!(
502 grant.session(&key(APP_KEY), DEV_NONCE),
503 Session::new(DEV_ADDR, key(NWK_SKEY), key(APP_SKEY))
504 );
505 }
506
507 fn hex(text: &str) -> Vec<u8> {
508 (0..text.len())
509 .step_by(2)
510 .map(|index| u8::from_str_radix(&text[index..index + 2], 16).expect("hex"))
511 .collect()
512 }
513
514 fn key(text: &str) -> [u8; 16] {
515 hex(text).try_into().expect("a 16-byte key")
516 }
517}