Skip to main content

pamoja_lorawan/
session.rs

1//! The activated session and the data frames it secures.
2
3use crate::crypto::Cipher;
4use crate::error::LorawanError;
5use crate::frame::{
6    Direction, PhyPayload, MAX_FRAME, MAX_PAYLOAD, MTYPE_CONFIRMED_DOWN, MTYPE_CONFIRMED_UP,
7    MTYPE_MASK, MTYPE_UNCONFIRMED_DOWN, MTYPE_UNCONFIRMED_UP,
8};
9
10// The fixed header bytes of a data frame: MHDR, DevAddr, FCtrl, and FCnt.
11const FHDR_LEN: usize = 8;
12// The smallest data frame: the fixed header and the MIC, with no port or payload.
13const MIN_FRAME: usize = FHDR_LEN + 4;
14
15// FCtrl flag bits.
16const FCTRL_ADR: u8 = 0x80;
17const FCTRL_ACK: u8 = 0x20;
18const FCTRL_FPENDING: u8 = 0x10;
19const FCTRL_FOPTS_LEN: u8 = 0x0F;
20
21/// An activated LoRaWAN session: a device address and the two session keys.
22///
23/// This is the state a device holds once it is activated, whether by personalization
24/// (the address and keys provisioned directly) or by a join exchange. It secures every
25/// data frame: the network session key authenticates the whole frame through its MIC, and
26/// the application session key encrypts the payload, with the device address and frame
27/// counter folded into both so a frame is bound to its place in the stream.
28///
29/// # Examples
30///
31/// ```
32/// use pamoja_lorawan::{Session, Uplink};
33///
34/// let session = Session::new(0x2601_1BDA, [0x11; 16], [0x22; 16]);
35/// let frame = session.encode_uplink(&Uplink::new(1, 1, b"hello")).unwrap();
36///
37/// // The receiver, holding the same session, recovers the payload.
38/// let rx = session.decode(frame.as_bytes(), 1).unwrap();
39/// assert_eq!(rx.payload(), b"hello");
40/// ```
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct Session {
43    dev_addr: u32,
44    nwk_skey: [u8; 16],
45    app_skey: [u8; 16],
46}
47
48impl Session {
49    /// Creates a session from a device address and its two session keys.
50    ///
51    /// # Arguments
52    ///
53    /// * `dev_addr` - the device address the network assigned.
54    /// * `nwk_skey` - the network session key, which authenticates frames.
55    /// * `app_skey` - the application session key, which encrypts payloads.
56    ///
57    /// # Returns
58    ///
59    /// The session.
60    pub fn new(dev_addr: u32, nwk_skey: [u8; 16], app_skey: [u8; 16]) -> Self {
61        Session {
62            dev_addr,
63            nwk_skey,
64            app_skey,
65        }
66    }
67
68    /// Returns the device address this session is bound to.
69    ///
70    /// # Returns
71    ///
72    /// The device address.
73    pub fn dev_addr(&self) -> u32 {
74        self.dev_addr
75    }
76
77    /// Encodes an uplink data frame, encrypting the payload and appending the MIC.
78    ///
79    /// # Arguments
80    ///
81    /// * `uplink` - the uplink to send.
82    ///
83    /// # Returns
84    ///
85    /// The frame ready for the radio.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`LorawanError::PayloadTooLong`] if the payload and options do not fit a
90    /// single frame.
91    pub fn encode_uplink(&self, uplink: &Uplink) -> Result<PhyPayload, LorawanError> {
92        let mtype = if uplink.confirmed {
93            MTYPE_CONFIRMED_UP
94        } else {
95            MTYPE_UNCONFIRMED_UP
96        };
97        let mut fctrl = 0;
98        if uplink.adr {
99            fctrl |= FCTRL_ADR;
100        }
101        if uplink.ack {
102            fctrl |= FCTRL_ACK;
103        }
104        self.encode(
105            Direction::Uplink,
106            mtype,
107            fctrl,
108            uplink.fcnt,
109            uplink.fport,
110            uplink.fopts,
111            uplink.payload,
112        )
113    }
114
115    /// Encodes a downlink data frame, encrypting the payload and appending the MIC.
116    ///
117    /// # Arguments
118    ///
119    /// * `downlink` - the downlink to send.
120    ///
121    /// # Returns
122    ///
123    /// The frame ready for the radio.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`LorawanError::PayloadTooLong`] if the payload and options do not fit a
128    /// single frame.
129    pub fn encode_downlink(&self, downlink: &Downlink) -> Result<PhyPayload, LorawanError> {
130        let mtype = if downlink.confirmed {
131            MTYPE_CONFIRMED_DOWN
132        } else {
133            MTYPE_UNCONFIRMED_DOWN
134        };
135        let mut fctrl = 0;
136        if downlink.adr {
137            fctrl |= FCTRL_ADR;
138        }
139        if downlink.ack {
140            fctrl |= FCTRL_ACK;
141        }
142        if downlink.fpending {
143            fctrl |= FCTRL_FPENDING;
144        }
145        self.encode(
146            Direction::Downlink,
147            mtype,
148            fctrl,
149            downlink.fcnt,
150            downlink.fport,
151            downlink.fopts,
152            downlink.payload,
153        )
154    }
155
156    #[allow(clippy::too_many_arguments)]
157    fn encode(
158        &self,
159        direction: Direction,
160        mtype: u8,
161        fctrl: u8,
162        fcnt: u32,
163        fport: u8,
164        fopts: &[u8],
165        payload: &[u8],
166    ) -> Result<PhyPayload, LorawanError> {
167        if fopts.len() > usize::from(FCTRL_FOPTS_LEN) {
168            return Err(LorawanError::PayloadTooLong);
169        }
170        let len = MIN_FRAME + fopts.len() + 1 + payload.len();
171        if len > MAX_FRAME {
172            return Err(LorawanError::PayloadTooLong);
173        }
174
175        let mut buf = [0u8; MAX_FRAME];
176        buf[0] = mtype;
177        buf[1..5].copy_from_slice(&self.dev_addr.to_le_bytes());
178        buf[5] = fctrl | (fopts.len() as u8);
179        buf[6..8].copy_from_slice(&(fcnt as u16).to_le_bytes());
180        let mut at = FHDR_LEN;
181        buf[at..at + fopts.len()].copy_from_slice(fopts);
182        at += fopts.len();
183        buf[at] = fport;
184        at += 1;
185
186        let key = self.payload_key(fport);
187        crypt_payload(
188            key,
189            self.dev_addr,
190            direction,
191            fcnt,
192            payload,
193            &mut buf[at..at + payload.len()],
194        );
195        at += payload.len();
196
197        let mic = self.mic(direction, fcnt, &buf[..at]);
198        buf[at..at + 4].copy_from_slice(&mic);
199        at += 4;
200
201        PhyPayload::new(&buf[..at])
202    }
203
204    /// Decodes a received data frame: verifies the MIC, then decrypts the payload.
205    ///
206    /// # Arguments
207    ///
208    /// * `bytes` - the raw frame as it came off the radio.
209    /// * `fcnt` - the full 32-bit frame counter expected for this frame; its low 16 bits
210    ///   must match the counter the frame carries.
211    ///
212    /// # Returns
213    ///
214    /// The decoded frame, with its payload decrypted.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`LorawanError::FrameTooShort`] if the frame is too small,
219    /// [`LorawanError::UnsupportedMType`] if it is not a data frame,
220    /// [`LorawanError::FcntMismatch`] if the counter does not match, or
221    /// [`LorawanError::MicMismatch`] if the MIC does not verify.
222    pub fn decode(&self, bytes: &[u8], fcnt: u32) -> Result<RxData, LorawanError> {
223        if bytes.len() < MIN_FRAME {
224            return Err(LorawanError::FrameTooShort);
225        }
226        let mtype = bytes[0] & MTYPE_MASK;
227        let (direction, confirmed) = match mtype {
228            MTYPE_UNCONFIRMED_UP => (Direction::Uplink, false),
229            MTYPE_CONFIRMED_UP => (Direction::Uplink, true),
230            MTYPE_UNCONFIRMED_DOWN => (Direction::Downlink, false),
231            MTYPE_CONFIRMED_DOWN => (Direction::Downlink, true),
232            other => return Err(LorawanError::UnsupportedMType(other)),
233        };
234
235        let dev_addr = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
236        let fctrl = bytes[5];
237        let fopts_len = usize::from(fctrl & FCTRL_FOPTS_LEN);
238        let fcnt_low = u16::from_le_bytes([bytes[6], bytes[7]]);
239        if fcnt as u16 != fcnt_low {
240            return Err(LorawanError::FcntMismatch);
241        }
242
243        let mic_start = bytes.len() - 4;
244        let body_start = FHDR_LEN + fopts_len;
245        if mic_start < body_start {
246            return Err(LorawanError::FrameTooShort);
247        }
248        let expected = self.mic(direction, fcnt, &bytes[..mic_start]);
249        if bytes[mic_start..] != expected[..] {
250            return Err(LorawanError::MicMismatch);
251        }
252
253        let mut fopts = [0u8; FCTRL_FOPTS_LEN as usize];
254        fopts[..fopts_len].copy_from_slice(&bytes[FHDR_LEN..FHDR_LEN + fopts_len]);
255
256        let mut payload = [0u8; MAX_PAYLOAD];
257        let (fport, payload_len) = if mic_start > body_start {
258            let fport = bytes[body_start];
259            let encrypted = &bytes[body_start + 1..mic_start];
260            let key = self.payload_key(fport);
261            crypt_payload(
262                key,
263                dev_addr,
264                direction,
265                fcnt,
266                encrypted,
267                &mut payload[..encrypted.len()],
268            );
269            (Some(fport), encrypted.len())
270        } else {
271            (None, 0)
272        };
273
274        Ok(RxData {
275            direction,
276            dev_addr,
277            fcnt_low,
278            confirmed,
279            adr: fctrl & FCTRL_ADR != 0,
280            ack: fctrl & FCTRL_ACK != 0,
281            fpending: fctrl & FCTRL_FPENDING != 0,
282            fport,
283            fopts,
284            fopts_len,
285            payload,
286            payload_len,
287        })
288    }
289
290    // The key that encrypts a payload: the network key for port 0 (MAC commands), the
291    // application key for every other port.
292    fn payload_key(&self, fport: u8) -> &[u8; 16] {
293        if fport == 0 {
294            &self.nwk_skey
295        } else {
296            &self.app_skey
297        }
298    }
299
300    // The four-byte MIC over a frame's contents, per the spec's B0 block.
301    fn mic(&self, direction: Direction, fcnt: u32, msg: &[u8]) -> [u8; 4] {
302        let mut block = [0u8; 16 + MAX_FRAME];
303        block[0] = 0x49;
304        block[5] = direction.bit();
305        block[6..10].copy_from_slice(&self.dev_addr.to_le_bytes());
306        block[10..14].copy_from_slice(&fcnt.to_le_bytes());
307        block[15] = msg.len() as u8;
308        block[16..16 + msg.len()].copy_from_slice(msg);
309        let tag = Cipher::new(&self.nwk_skey).cmac(&block[..16 + msg.len()]);
310        [tag[0], tag[1], tag[2], tag[3]]
311    }
312}
313
314// Encrypts (or, being a XOR keystream, decrypts) a payload in place into `output`, per the
315// spec's A_i block construction.
316fn crypt_payload(
317    key: &[u8; 16],
318    dev_addr: u32,
319    direction: Direction,
320    fcnt: u32,
321    input: &[u8],
322    output: &mut [u8],
323) {
324    let cipher = Cipher::new(key);
325    let blocks = input.len().div_ceil(16);
326    for i in 0..blocks {
327        let mut a = [0u8; 16];
328        a[0] = 0x01;
329        a[5] = direction.bit();
330        a[6..10].copy_from_slice(&dev_addr.to_le_bytes());
331        a[10..14].copy_from_slice(&fcnt.to_le_bytes());
332        a[15] = (i + 1) as u8;
333        let stream = cipher.encrypt_block(&a);
334
335        let start = i * 16;
336        let end = (start + 16).min(input.len());
337        for j in start..end {
338            output[j] = input[j] ^ stream[j - start];
339        }
340    }
341}
342
343/// An uplink data frame to encode, built up from the fields a sender sets.
344///
345/// Construct one with [`new`](Uplink::new) and turn on whatever applies; the rest default
346/// off. A higher port carries application data; port `0` carries MAC commands.
347///
348/// # Examples
349///
350/// ```
351/// use pamoja_lorawan::Uplink;
352///
353/// let uplink = Uplink::new(7, 2, b"reading").confirmed().with_adr();
354/// ```
355#[derive(Clone, Copy, Debug)]
356pub struct Uplink<'a> {
357    fcnt: u32,
358    fport: u8,
359    payload: &'a [u8],
360    confirmed: bool,
361    adr: bool,
362    ack: bool,
363    fopts: &'a [u8],
364}
365
366impl<'a> Uplink<'a> {
367    /// Creates an unconfirmed uplink with no options set.
368    ///
369    /// # Arguments
370    ///
371    /// * `fcnt` - the frame counter for this uplink.
372    /// * `fport` - the port; `0` for MAC commands, otherwise an application port.
373    /// * `payload` - the application payload to carry.
374    ///
375    /// # Returns
376    ///
377    /// The uplink.
378    pub fn new(fcnt: u32, fport: u8, payload: &'a [u8]) -> Self {
379        Uplink {
380            fcnt,
381            fport,
382            payload,
383            confirmed: false,
384            adr: false,
385            ack: false,
386            fopts: &[],
387        }
388    }
389
390    /// Marks the uplink as confirmed, asking the network to acknowledge it.
391    ///
392    /// # Returns
393    ///
394    /// The uplink, for chaining.
395    pub fn confirmed(mut self) -> Self {
396        self.confirmed = true;
397        self
398    }
399
400    /// Sets the adaptive-data-rate bit, letting the network manage the data rate.
401    ///
402    /// # Returns
403    ///
404    /// The uplink, for chaining.
405    pub fn with_adr(mut self) -> Self {
406        self.adr = true;
407        self
408    }
409
410    /// Sets the acknowledgement bit, confirming a previously received downlink.
411    ///
412    /// # Returns
413    ///
414    /// The uplink, for chaining.
415    pub fn with_ack(mut self) -> Self {
416        self.ack = true;
417        self
418    }
419
420    /// Carries MAC command options in the frame header.
421    ///
422    /// # Arguments
423    ///
424    /// * `fopts` - the frame options, up to 15 bytes.
425    ///
426    /// # Returns
427    ///
428    /// The uplink, for chaining.
429    pub fn with_fopts(mut self, fopts: &'a [u8]) -> Self {
430        self.fopts = fopts;
431        self
432    }
433}
434
435/// A downlink data frame to encode, built up from the fields a sender sets.
436///
437/// Construct one with [`new`](Downlink::new) and turn on whatever applies; the rest
438/// default off.
439#[derive(Clone, Copy, Debug)]
440pub struct Downlink<'a> {
441    fcnt: u32,
442    fport: u8,
443    payload: &'a [u8],
444    confirmed: bool,
445    adr: bool,
446    ack: bool,
447    fpending: bool,
448    fopts: &'a [u8],
449}
450
451impl<'a> Downlink<'a> {
452    /// Creates an unconfirmed downlink with no options set.
453    ///
454    /// # Arguments
455    ///
456    /// * `fcnt` - the frame counter for this downlink.
457    /// * `fport` - the port; `0` for MAC commands, otherwise an application port.
458    /// * `payload` - the application payload to carry.
459    ///
460    /// # Returns
461    ///
462    /// The downlink.
463    pub fn new(fcnt: u32, fport: u8, payload: &'a [u8]) -> Self {
464        Downlink {
465            fcnt,
466            fport,
467            payload,
468            confirmed: false,
469            adr: false,
470            ack: false,
471            fpending: false,
472            fopts: &[],
473        }
474    }
475
476    /// Marks the downlink as confirmed, asking the device to acknowledge it.
477    ///
478    /// # Returns
479    ///
480    /// The downlink, for chaining.
481    pub fn confirmed(mut self) -> Self {
482        self.confirmed = true;
483        self
484    }
485
486    /// Sets the adaptive-data-rate bit.
487    ///
488    /// # Returns
489    ///
490    /// The downlink, for chaining.
491    pub fn with_adr(mut self) -> Self {
492        self.adr = true;
493        self
494    }
495
496    /// Sets the acknowledgement bit, confirming a previously received uplink.
497    ///
498    /// # Returns
499    ///
500    /// The downlink, for chaining.
501    pub fn with_ack(mut self) -> Self {
502        self.ack = true;
503        self
504    }
505
506    /// Sets the frame-pending bit, signalling more downlinks are waiting.
507    ///
508    /// # Returns
509    ///
510    /// The downlink, for chaining.
511    pub fn with_fpending(mut self) -> Self {
512        self.fpending = true;
513        self
514    }
515
516    /// Carries MAC command options in the frame header.
517    ///
518    /// # Arguments
519    ///
520    /// * `fopts` - the frame options, up to 15 bytes.
521    ///
522    /// # Returns
523    ///
524    /// The downlink, for chaining.
525    pub fn with_fopts(mut self, fopts: &'a [u8]) -> Self {
526        self.fopts = fopts;
527        self
528    }
529}
530
531/// A decoded data frame, with its payload decrypted.
532///
533/// What [`Session::decode`] returns once a frame's MIC has verified: the header fields and
534/// the recovered payload, held in fixed buffers.
535#[derive(Clone, Copy, Debug, PartialEq, Eq)]
536pub struct RxData {
537    direction: Direction,
538    dev_addr: u32,
539    fcnt_low: u16,
540    confirmed: bool,
541    adr: bool,
542    ack: bool,
543    fpending: bool,
544    fport: Option<u8>,
545    fopts: [u8; FCTRL_FOPTS_LEN as usize],
546    fopts_len: usize,
547    payload: [u8; MAX_PAYLOAD],
548    payload_len: usize,
549}
550
551impl RxData {
552    /// Returns the direction the frame travelled.
553    ///
554    /// # Returns
555    ///
556    /// [`Direction::Uplink`] or [`Direction::Downlink`].
557    pub fn direction(&self) -> Direction {
558        self.direction
559    }
560
561    /// Returns the device address the frame carried.
562    ///
563    /// # Returns
564    ///
565    /// The device address.
566    pub fn dev_addr(&self) -> u32 {
567        self.dev_addr
568    }
569
570    /// Returns the low 16 bits of the frame counter the frame carried.
571    ///
572    /// # Returns
573    ///
574    /// The frame counter's low half.
575    pub fn fcnt(&self) -> u16 {
576        self.fcnt_low
577    }
578
579    /// Reports whether the frame is a confirmed frame that expects an acknowledgement.
580    ///
581    /// # Returns
582    ///
583    /// `true` for a confirmed frame.
584    pub fn confirmed(&self) -> bool {
585        self.confirmed
586    }
587
588    /// Reports whether the adaptive-data-rate bit is set.
589    ///
590    /// # Returns
591    ///
592    /// `true` if the bit is set.
593    pub fn adr(&self) -> bool {
594        self.adr
595    }
596
597    /// Reports whether the acknowledgement bit is set.
598    ///
599    /// # Returns
600    ///
601    /// `true` if the bit is set.
602    pub fn ack(&self) -> bool {
603        self.ack
604    }
605
606    /// Reports whether the frame-pending bit is set (downlink only).
607    ///
608    /// # Returns
609    ///
610    /// `true` if the bit is set.
611    pub fn fpending(&self) -> bool {
612        self.fpending
613    }
614
615    /// Returns the port the frame was sent on, if it carried a port and payload.
616    ///
617    /// # Returns
618    ///
619    /// The port, or [`None`] for a frame with no port or payload.
620    pub fn fport(&self) -> Option<u8> {
621        self.fport
622    }
623
624    /// Returns the frame options carried in the header.
625    ///
626    /// # Returns
627    ///
628    /// The frame option bytes, which may be empty.
629    pub fn fopts(&self) -> &[u8] {
630        &self.fopts[..self.fopts_len]
631    }
632
633    /// Returns the decrypted payload.
634    ///
635    /// # Returns
636    ///
637    /// The application payload, which may be empty.
638    pub fn payload(&self) -> &[u8] {
639        &self.payload[..self.payload_len]
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    const NWK_SKEY: [u8; 16] = [0x01; 16];
648    const APP_SKEY: [u8; 16] = [0x02; 16];
649    const DEV_ADDR: u32 = 0x2601_1BDA;
650
651    fn session() -> Session {
652        Session::new(DEV_ADDR, NWK_SKEY, APP_SKEY)
653    }
654
655    #[test]
656    fn an_uplink_round_trips() {
657        let session = session();
658        let frame = session
659            .encode_uplink(&Uplink::new(10, 1, b"temperature"))
660            .unwrap();
661        let rx = session.decode(frame.as_bytes(), 10).unwrap();
662        assert_eq!(rx.direction(), Direction::Uplink);
663        assert_eq!(rx.dev_addr(), DEV_ADDR);
664        assert_eq!(rx.fcnt(), 10);
665        assert_eq!(rx.fport(), Some(1));
666        assert_eq!(rx.payload(), b"temperature");
667        assert!(!rx.confirmed());
668    }
669
670    #[test]
671    fn the_payload_is_encrypted_on_the_wire() {
672        let session = session();
673        let frame = session
674            .encode_uplink(&Uplink::new(1, 1, b"secret"))
675            .unwrap();
676        // The plaintext must not appear in the encoded frame.
677        assert!(frame
678            .as_bytes()
679            .windows(b"secret".len())
680            .all(|window| window != b"secret"));
681    }
682
683    #[test]
684    fn the_header_is_laid_out_as_the_spec_requires() {
685        let session = session();
686        let frame = session
687            .encode_uplink(&Uplink::new(0x0102, 1, b"x"))
688            .unwrap();
689        let bytes = frame.as_bytes();
690        assert_eq!(bytes[0], MTYPE_UNCONFIRMED_UP);
691        // DevAddr little-endian.
692        assert_eq!(&bytes[1..5], &DEV_ADDR.to_le_bytes());
693        // FCnt little-endian, low 16 bits.
694        assert_eq!(&bytes[6..8], &0x0102u16.to_le_bytes());
695    }
696
697    #[test]
698    fn a_confirmed_downlink_round_trips_with_its_flags() {
699        let session = session();
700        let frame = session
701            .encode_downlink(&Downlink::new(5, 2, b"cmd").confirmed().with_fpending())
702            .unwrap();
703        let rx = session.decode(frame.as_bytes(), 5).unwrap();
704        assert_eq!(rx.direction(), Direction::Downlink);
705        assert!(rx.confirmed());
706        assert!(rx.fpending());
707        assert_eq!(rx.payload(), b"cmd");
708    }
709
710    #[test]
711    fn frame_options_round_trip() {
712        let session = session();
713        let frame = session
714            .encode_uplink(&Uplink::new(3, 1, b"d").with_fopts(&[0x02, 0x03]))
715            .unwrap();
716        let rx = session.decode(frame.as_bytes(), 3).unwrap();
717        assert_eq!(rx.fopts(), &[0x02, 0x03]);
718        assert_eq!(rx.payload(), b"d");
719    }
720
721    #[test]
722    fn an_empty_payload_round_trips() {
723        let session = session();
724        let frame = session.encode_uplink(&Uplink::new(1, 1, b"")).unwrap();
725        let rx = session.decode(frame.as_bytes(), 1).unwrap();
726        assert_eq!(rx.payload(), b"");
727        assert_eq!(rx.fport(), Some(1));
728    }
729
730    #[test]
731    fn a_tampered_payload_fails_the_mic() {
732        let session = session();
733        let frame = session.encode_uplink(&Uplink::new(1, 1, b"data")).unwrap();
734        let mut bytes = frame.as_bytes().to_vec();
735        let last = bytes.len() - 5; // a payload byte, before the 4-byte MIC
736        bytes[last] ^= 0xff;
737        assert_eq!(session.decode(&bytes, 1), Err(LorawanError::MicMismatch));
738    }
739
740    #[test]
741    fn the_wrong_counter_is_rejected() {
742        let session = session();
743        let frame = session.encode_uplink(&Uplink::new(7, 1, b"data")).unwrap();
744        assert_eq!(
745            session.decode(frame.as_bytes(), 8),
746            Err(LorawanError::FcntMismatch)
747        );
748    }
749
750    #[test]
751    fn a_join_frame_is_not_decoded_here() {
752        let session = session();
753        // MHDR 0x00 is a join-request, not a data frame.
754        let bytes = [0u8; MIN_FRAME];
755        assert_eq!(
756            session.decode(&bytes, 0),
757            Err(LorawanError::UnsupportedMType(0x00))
758        );
759    }
760
761    #[test]
762    fn a_short_frame_is_rejected() {
763        let session = session();
764        assert_eq!(
765            session.decode(&[0x40, 0x00, 0x00], 0),
766            Err(LorawanError::FrameTooShort)
767        );
768    }
769
770    #[test]
771    fn port_zero_uses_the_network_key() {
772        // A port-0 payload is encrypted with the network key, so decoding it with a
773        // session whose application key differs still recovers it.
774        let session = Session::new(DEV_ADDR, NWK_SKEY, APP_SKEY);
775        let frame = session.encode_uplink(&Uplink::new(1, 0, b"mac")).unwrap();
776        let other = Session::new(DEV_ADDR, NWK_SKEY, [0x33; 16]);
777        let rx = other.decode(frame.as_bytes(), 1).unwrap();
778        assert_eq!(rx.payload(), b"mac");
779    }
780
781    #[test]
782    fn the_largest_payload_round_trips() {
783        let session = session();
784        let payload = [0xAB; MAX_PAYLOAD];
785        let frame = session.encode_uplink(&Uplink::new(1, 1, &payload)).unwrap();
786        assert_eq!(frame.as_bytes().len(), crate::MAX_FRAME);
787        let rx = session.decode(frame.as_bytes(), 1).unwrap();
788        assert_eq!(rx.payload(), &payload[..]);
789    }
790
791    #[test]
792    fn the_full_frame_counter_is_bound_into_the_mic() {
793        let session = session();
794        // Only the low 16 bits of the counter travel on the wire, but the whole 32-bit
795        // value is folded into the MIC.
796        let frame = session
797            .encode_uplink(&Uplink::new(0x0001_0001, 1, b"x"))
798            .unwrap();
799        // The right low bits but the wrong upper bits must still fail the MIC.
800        assert_eq!(
801            session.decode(frame.as_bytes(), 0x0000_0001),
802            Err(LorawanError::MicMismatch)
803        );
804        // The full counter verifies.
805        let rx = session.decode(frame.as_bytes(), 0x0001_0001).unwrap();
806        assert_eq!(rx.fcnt(), 0x0001);
807    }
808
809    #[test]
810    fn another_sessions_keys_cannot_read_a_frame() {
811        let session = session();
812        let frame = session
813            .encode_uplink(&Uplink::new(1, 1, b"secret"))
814            .unwrap();
815        let stranger = Session::new(DEV_ADDR, [0xAA; 16], [0xBB; 16]);
816        assert_eq!(
817            stranger.decode(frame.as_bytes(), 1),
818            Err(LorawanError::MicMismatch)
819        );
820    }
821}