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::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
14/// The number of bytes a channel list adds to a join-accept.
15const CFLIST_LEN: usize = 16;
16
17/// A join-request a device broadcast, with its integrity already verified.
18///
19/// [`parse`](JoinRequest::parse) checks the request against the application root key
20/// before reporting anything, so the identifiers it hands back are the ones the key holder
21/// actually sent rather than whatever arrived on the air.
22///
23/// # Examples
24///
25/// ```
26/// use pamoja_lorawan::{Device, JoinRequest};
27///
28/// const APP_KEY: [u8; 16] = [0xAB; 16];
29/// let device = Device::new([0x11; 8], [0x22; 8], APP_KEY);
30/// let on_air = device.join_request(0x1234);
31///
32/// // The network recognises the device and the nonce it must not accept twice.
33/// let request = JoinRequest::parse(on_air.as_bytes(), &APP_KEY)?;
34/// assert_eq!(request.dev_eui(), [0x11; 8]);
35/// assert_eq!(request.dev_nonce(), 0x1234);
36/// # Ok::<(), pamoja_lorawan::LorawanError>(())
37/// ```
38#[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    /// Verifies a join-request and reads the identifiers out of it.
47    ///
48    /// # Arguments
49    ///
50    /// * `bytes` - the raw join-request as it came off the radio.
51    /// * `app_key` - the application root key the device shares with this network.
52    ///
53    /// # Returns
54    ///
55    /// The verified request.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`LorawanError::FrameTooShort`] if the frame is empty,
60    /// [`LorawanError::UnsupportedMType`] if it is not a join-request,
61    /// [`LorawanError::MalformedFrame`] if it is not the fixed 23 bytes a join-request is,
62    /// or [`LorawanError::MicMismatch`] if its MIC does not verify, which means it was not
63    /// sent by a holder of `app_key`.
64    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        // The identifiers travel little-endian, so they reverse back to how they read.
81        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    /// Returns the device identifier, most-significant byte first.
94    ///
95    /// # Returns
96    ///
97    /// The DevEUI, as it is written rather than as it was transmitted.
98    pub fn dev_eui(&self) -> [u8; 8] {
99        self.dev_eui
100    }
101
102    /// Returns the application identifier, most-significant byte first.
103    ///
104    /// # Returns
105    ///
106    /// The AppEUI, as it is written rather than as it was transmitted.
107    pub fn app_eui(&self) -> [u8; 8] {
108        self.app_eui
109    }
110
111    /// Returns the nonce this request carried.
112    ///
113    /// A network must remember the nonces a device has already used and refuse a repeat,
114    /// since replaying one would re-derive the same session keys.
115    ///
116    /// # Returns
117    ///
118    /// The DevNonce.
119    pub fn dev_nonce(&self) -> u16 {
120        self.dev_nonce
121    }
122}
123
124/// What a network grants a device that joined: an address, and the settings to answer on.
125///
126/// Build one, then [`accept`](JoinGrant::accept) it into the frame to transmit and take
127/// the matching [`session`](JoinGrant::session) to secure traffic with. Both sides derive
128/// the same keys from the same nonces, so the session here and the one the device computes
129/// agree without either sending a key.
130///
131/// # Examples
132///
133/// ```
134/// use pamoja_lorawan::{Device, JoinGrant, JoinRequest, Uplink};
135///
136/// const APP_KEY: [u8; 16] = [0xAB; 16];
137/// let device = Device::new([0x11; 8], [0x22; 8], APP_KEY);
138/// let request = JoinRequest::parse(device.join_request(0x1234).as_bytes(), &APP_KEY)?;
139///
140/// // The network assigns an address and replies.
141/// let grant = JoinGrant::new(0x0003_0201, 0x0006_0504, 0x2601_1BDA);
142/// let reply = grant.accept(&APP_KEY, request.dev_nonce());
143///
144/// // The device activates, and the two sessions agree.
145/// let activated = device.accept_join(reply.as_bytes(), 0x1234)?;
146/// assert_eq!(activated.dev_addr(), 0x2601_1BDA);
147///
148/// let uplink = activated.session().encode_uplink(&Uplink::new(1, 1, b"joined"))?;
149/// let heard = grant.session(&APP_KEY, 0x1234).decode(uplink.as_bytes(), 1)?;
150/// assert_eq!(heard.payload(), b"joined");
151/// # Ok::<(), pamoja_lorawan::LorawanError>(())
152/// ```
153#[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    /// Creates a grant with no channel list and the default downlink settings.
165    ///
166    /// # Arguments
167    ///
168    /// * `app_nonce` - a nonce this network must not reuse for the device, since the
169    ///   session keys are derived from it; only the low 24 bits are carried.
170    /// * `net_id` - the network identifier; only the low 24 bits are carried.
171    /// * `dev_addr` - the address to assign the device.
172    ///
173    /// # Returns
174    ///
175    /// The grant.
176    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    /// Sets the downlink settings byte, which selects the downlink data rates.
188    ///
189    /// # Arguments
190    ///
191    /// * `dl_settings` - the DLSettings byte.
192    ///
193    /// # Returns
194    ///
195    /// The updated grant, for chaining.
196    pub fn with_dl_settings(mut self, dl_settings: u8) -> Self {
197        self.dl_settings = dl_settings;
198        self
199    }
200
201    /// Sets the delay, in seconds, before the first receive window.
202    ///
203    /// # Arguments
204    ///
205    /// * `rx_delay` - the RxDelay value.
206    ///
207    /// # Returns
208    ///
209    /// The updated grant, for chaining.
210    pub fn with_rx_delay(mut self, rx_delay: u8) -> Self {
211        self.rx_delay = rx_delay;
212        self
213    }
214
215    /// Attaches the optional channel list, which tells the device where else to transmit.
216    ///
217    /// # Arguments
218    ///
219    /// * `cflist` - the 16-byte CFList, whose meaning is regional.
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        // The session keys are the real check: they fold in the AppNonce, NetID, and
482        // DevNonce, so matching an independent derivation pins the whole construction.
483        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}