Skip to main content

pamoja_lorawan/
network.rs

1//! The network side of over-the-air activation.
2//!
3//! [`Device`](crate::Device) is the end device half of the join exchange: it broadcasts a
4//! join-request and accepts the reply. This module is the other half, so a deployment can
5//! run its own network rather than joining someone else's: verify the request a device
6//! sent, then grant it an address and the session keys both sides derive independently.
7
8use crate::cflist::CFLIST_LEN;
9use crate::crypto::Cipher;
10use crate::error::LorawanError;
11use crate::frame::{PhyPayload, MTYPE_JOIN_ACCEPT, MTYPE_JOIN_REQUEST, MTYPE_MASK};
12use crate::join::{copy_reversed, derive_key, JOIN_REQUEST_LEN};
13use crate::session::Session;
14
15/// A join-request a device broadcast, with its integrity already verified.
16///
17/// [`parse`](JoinRequest::parse) checks the request against the application root key
18/// before reporting anything, so the identifiers it hands back are the ones the key holder
19/// actually sent rather than whatever arrived on the air.
20///
21/// # Examples
22///
23/// ```
24/// use pamoja_lorawan::{Device, JoinRequest};
25///
26/// const APP_KEY: [u8; 16] = [0xAB; 16];
27/// let device = Device::new([0x11; 8], [0x22; 8], APP_KEY);
28/// let on_air = device.join_request(0x1234);
29///
30/// // The network recognizes the device and the nonce it must not accept twice.
31/// let request = JoinRequest::parse(on_air.as_bytes(), &APP_KEY)?;
32/// assert_eq!(request.dev_eui(), [0x11; 8]);
33/// assert_eq!(request.dev_nonce(), 0x1234);
34/// # Ok::<(), pamoja_lorawan::LorawanError>(())
35/// ```
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct JoinRequest {
38    dev_eui: [u8; 8],
39    app_eui: [u8; 8],
40    dev_nonce: u16,
41}
42
43impl JoinRequest {
44    /// Verifies a join-request and reads the identifiers out of it.
45    ///
46    /// # Arguments
47    ///
48    /// * `bytes` - the raw join-request as it came off the radio.
49    /// * `app_key` - the application root key the device shares with this network.
50    ///
51    /// # Returns
52    ///
53    /// The verified request.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`LorawanError::FrameTooShort`] if the frame is empty,
58    /// [`LorawanError::UnsupportedMType`] if it is not a join-request,
59    /// [`LorawanError::MalformedFrame`] if it is not the fixed 23 bytes a join-request is,
60    /// or [`LorawanError::MicMismatch`] if its MIC does not verify, which means it was not
61    /// sent by a holder of `app_key`.
62    pub fn parse(bytes: &[u8], app_key: &[u8; 16]) -> Result<JoinRequest, LorawanError> {
63        if bytes.is_empty() {
64            return Err(LorawanError::FrameTooShort);
65        }
66        if bytes[0] & MTYPE_MASK != MTYPE_JOIN_REQUEST {
67            return Err(LorawanError::UnsupportedMType(bytes[0] & MTYPE_MASK));
68        }
69        if bytes.len() != JOIN_REQUEST_LEN {
70            return Err(LorawanError::MalformedFrame);
71        }
72
73        let tag = Cipher::new(app_key).cmac(&bytes[..19]);
74        if bytes[19..23] != tag[..4] {
75            return Err(LorawanError::MicMismatch);
76        }
77
78        // The identifiers travel little-endian, so they reverse back to how they read.
79        let mut app_eui = [0u8; 8];
80        let mut dev_eui = [0u8; 8];
81        copy_reversed(&mut app_eui, &bytes[1..9]);
82        copy_reversed(&mut dev_eui, &bytes[9..17]);
83
84        Ok(JoinRequest {
85            dev_eui,
86            app_eui,
87            dev_nonce: u16::from_le_bytes([bytes[17], bytes[18]]),
88        })
89    }
90
91    /// Returns the device identifier, most-significant byte first.
92    ///
93    /// # Returns
94    ///
95    /// The DevEUI, as it is written rather than as it was transmitted.
96    pub fn dev_eui(&self) -> [u8; 8] {
97        self.dev_eui
98    }
99
100    /// Returns the application identifier, most-significant byte first.
101    ///
102    /// # Returns
103    ///
104    /// The AppEUI, as it is written rather than as it was transmitted.
105    pub fn app_eui(&self) -> [u8; 8] {
106        self.app_eui
107    }
108
109    /// Returns the nonce this request carried.
110    ///
111    /// A network must remember the nonces a device has already used and refuse a repeat,
112    /// since replaying one would re-derive the same session keys.
113    ///
114    /// # Returns
115    ///
116    /// The DevNonce.
117    pub fn dev_nonce(&self) -> u16 {
118        self.dev_nonce
119    }
120}
121
122/// What a network grants a device that joined: an address, and the settings to answer on.
123///
124/// Build one, then [`accept`](JoinGrant::accept) it into the frame to transmit and take
125/// the matching [`session`](JoinGrant::session) to secure traffic with. Both sides derive
126/// the same keys from the same nonces, so the session here and the one the device computes
127/// agree without either sending a key.
128///
129/// # Examples
130///
131/// ```
132/// use pamoja_lorawan::{Device, JoinGrant, JoinRequest, Uplink};
133///
134/// const APP_KEY: [u8; 16] = [0xAB; 16];
135/// let device = Device::new([0x11; 8], [0x22; 8], APP_KEY);
136/// let request = JoinRequest::parse(device.join_request(0x1234).as_bytes(), &APP_KEY)?;
137///
138/// // The network assigns an address and replies.
139/// let grant = JoinGrant::new(0x0003_0201, 0x0006_0504, 0x2601_1BDA);
140/// let reply = grant.accept(&APP_KEY, request.dev_nonce());
141///
142/// // The device activates, and the two sessions agree.
143/// let activated = device.accept_join(reply.as_bytes(), 0x1234)?;
144/// assert_eq!(activated.dev_addr(), 0x2601_1BDA);
145///
146/// let uplink = activated.session().encode_uplink(&Uplink::new(1, 1, b"joined"))?;
147/// let heard = grant.session(&APP_KEY, 0x1234).decode(uplink.as_bytes(), 1)?;
148/// assert_eq!(heard.payload(), b"joined");
149/// # Ok::<(), pamoja_lorawan::LorawanError>(())
150/// ```
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct JoinGrant {
153    app_nonce: u32,
154    net_id: u32,
155    dev_addr: u32,
156    dl_settings: u8,
157    rx_delay: u8,
158    cflist: Option<[u8; CFLIST_LEN]>,
159}
160
161impl JoinGrant {
162    /// Creates a grant with no channel list and the default downlink settings.
163    ///
164    /// # Arguments
165    ///
166    /// * `app_nonce` - a nonce this network must not reuse for the device, since the
167    ///   session keys are derived from it; only the low 24 bits are carried.
168    /// * `net_id` - the network identifier; only the low 24 bits are carried.
169    /// * `dev_addr` - the address to assign the device.
170    ///
171    /// # Returns
172    ///
173    /// The grant.
174    pub fn new(app_nonce: u32, net_id: u32, dev_addr: u32) -> Self {
175        JoinGrant {
176            app_nonce,
177            net_id,
178            dev_addr,
179            dl_settings: 0,
180            rx_delay: 0,
181            cflist: None,
182        }
183    }
184
185    /// Sets the downlink settings byte, which selects the downlink data rates.
186    ///
187    /// # Arguments
188    ///
189    /// * `dl_settings` - the DLSettings byte.
190    ///
191    /// # Returns
192    ///
193    /// The updated grant, for chaining.
194    pub fn with_dl_settings(mut self, dl_settings: u8) -> Self {
195        self.dl_settings = dl_settings;
196        self
197    }
198
199    /// Sets the delay, in seconds, before the first receive window.
200    ///
201    /// # Arguments
202    ///
203    /// * `rx_delay` - the RxDelay value.
204    ///
205    /// # Returns
206    ///
207    /// The updated grant, for chaining.
208    pub fn with_rx_delay(mut self, rx_delay: u8) -> Self {
209        self.rx_delay = rx_delay;
210        self
211    }
212
213    /// Attaches the optional channel list, which tells the device where else to transmit.
214    ///
215    /// # Arguments
216    ///
217    /// * `cflist` - the 16-byte CFList, whose meaning is regional; a
218    ///   [`CfList`](crate::CfList) builds either form and hands its bytes over with
219    ///   [`to_bytes`](crate::CfList::to_bytes).
220    ///
221    /// # Returns
222    ///
223    /// The updated grant, for chaining. The accept it builds is 33 bytes rather than 17.
224    pub fn with_cflist(mut self, cflist: [u8; CFLIST_LEN]) -> Self {
225        self.cflist = Some(cflist);
226        self
227    }
228
229    /// Returns the address this grant assigns.
230    ///
231    /// # Returns
232    ///
233    /// The device address.
234    pub fn dev_addr(&self) -> u32 {
235        self.dev_addr
236    }
237
238    /// Returns the network identifier this grant carries.
239    ///
240    /// # Returns
241    ///
242    /// The NetID, in its low 24 bits.
243    pub fn net_id(&self) -> u32 {
244        self.net_id
245    }
246
247    /// Builds the signed join-accept to transmit.
248    ///
249    /// # Arguments
250    ///
251    /// * `app_key` - the application root key the device shares with this network.
252    /// * `dev_nonce` - the nonce the matching [`JoinRequest`] carried.
253    ///
254    /// # Returns
255    ///
256    /// The join-accept frame, encrypted and with its MIC in place.
257    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        // The clear body, then the MIC over the MHDR and everything before it.
263        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        // The network encrypts with AES decryption, so the device decrypts by encrypting.
281        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    /// Derives the session this grant activates, the same one the device computes.
292    ///
293    /// # Arguments
294    ///
295    /// * `app_key` - the application root key the device shares with this network.
296    /// * `dev_nonce` - the nonce the matching [`JoinRequest`] carried.
297    ///
298    /// # Returns
299    ///
300    /// The [`Session`] to secure this device's traffic with.
301    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    /// Returns the length of the encrypted body, which a channel list doubles.
311    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        // Neither side sent a key, yet each can read what the other secures.
405        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    // A real EU868 join-accept captured off the air, with the plaintext fields and the
453    // session keys an independent implementation derived from it. Anchoring to a third
454    // party's numbers is what stops this crate and its bindings from agreeing with each
455    // other on an answer that is wrong.
456    //
457    // Published at https://github.com/anthonykirby/lora-packet/issues/10
458    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        // DLSettings 0x03 is no RX1 offset and DR3 in the second window, and RxDelay 1 is
482        // one second.
483        assert_eq!(accepted.rx1_dr_offset(), 0);
484        assert_eq!(accepted.rx2_data_rate(), 3);
485        assert_eq!(accepted.receive_delay_us(), 1_000_000);
486
487        // The channel list is the five channels between 867.1 and 867.9 MHz.
488        assert_eq!(
489            accepted.cflist().and_then(|list| list.frequencies_hz()),
490            Some([
491                867_100_000,
492                867_300_000,
493                867_500_000,
494                867_700_000,
495                867_900_000
496            ])
497        );
498
499        // The session keys are the real check: they fold in the AppNonce, NetID, and
500        // DevNonce, so matching an independent derivation pins the whole construction.
501        assert_eq!(
502            accepted.session(),
503            Session::new(DEV_ADDR, key(NWK_SKEY), key(APP_SKEY))
504        );
505    }
506
507    #[test]
508    fn this_network_rebuilds_that_join_accept_byte_for_byte() {
509        let grant = JoinGrant::new(APP_NONCE, NET_ID, DEV_ADDR)
510            .with_dl_settings(DL_SETTINGS)
511            .with_rx_delay(RX_DELAY)
512            .with_cflist(hex(CFLIST).try_into().expect("a 16-byte channel list"));
513
514        assert_eq!(
515            grant.accept(&key(APP_KEY), DEV_NONCE).as_bytes(),
516            &hex(FRAME)[..],
517            "the frame this network signs is the one that was captured"
518        );
519        assert_eq!(
520            grant.session(&key(APP_KEY), DEV_NONCE),
521            Session::new(DEV_ADDR, key(NWK_SKEY), key(APP_SKEY))
522        );
523    }
524
525    fn hex(text: &str) -> Vec<u8> {
526        (0..text.len())
527            .step_by(2)
528            .map(|index| u8::from_str_radix(&text[index..index + 2], 16).expect("hex"))
529            .collect()
530    }
531
532    fn key(text: &str) -> [u8; 16] {
533        hex(text).try_into().expect("a 16-byte key")
534    }
535}