1use alloc::vec::Vec;
18
19use crate::types::{AppEui, AppNonce, DevAddr, DevEui, DevNonce, Direction, DlSettings, FCtrl, MType, Mhdr, NetId};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct LoraPacket {
55 pub phy_payload: Vec<u8>,
60 pub mhdr: Mhdr,
62 pub mic: [u8; 4],
64 pub payload: Payload,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub enum Payload {
90 JoinRequest(JoinRequest),
92 JoinAccept(JoinAccept),
94 Data(Data),
96 RejoinRequest(RejoinRequest),
98 Proprietary(Vec<u8>),
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct JoinRequest {
111 pub join_eui: AppEui,
113 pub dev_eui: DevEui,
115 pub dev_nonce: DevNonce,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct JoinAccept {
128 pub join_nonce: AppNonce,
130 pub net_id: NetId,
132 pub dev_addr: DevAddr,
134 pub dl_settings: DlSettings,
136 pub rx_delay: u8,
138 pub cf_list: Option<[u8; 16]>,
141 pub join_req_type: Option<u8>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160pub struct Data {
161 pub direction: Direction,
163 pub confirmed: bool,
165 pub dev_addr: DevAddr,
167 pub f_ctrl: FCtrl,
169 pub f_cnt: [u8; 2],
174 pub f_opts: Vec<u8>,
177 pub f_port: Option<u8>,
181 pub frm_payload: Option<Vec<u8>>,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
192#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
193pub enum RejoinRequest {
194 Type0 {
196 net_id: NetId,
198 dev_eui: DevEui,
200 rj_count_0: [u8; 2],
202 },
203 Type1 {
205 join_eui: AppEui,
207 dev_eui: DevEui,
209 rj_count_1: [u8; 2],
211 },
212 Type2 {
214 net_id: NetId,
216 dev_eui: DevEui,
218 rj_count_0: [u8; 2],
220 },
221}
222
223impl LoraPacket {
224 pub fn m_type(&self) -> MType {
233 self.mhdr.m_type().expect("LoraPacket MHDR always has a valid MType")
234 }
235
236 pub const fn is_data(&self) -> bool {
238 matches!(self.payload, Payload::Data(_))
239 }
240
241 pub fn is_confirmed(&self) -> bool {
243 matches!(self.m_type(), MType::ConfirmedDataUp | MType::ConfirmedDataDown)
244 }
245
246 pub const fn is_join_request(&self) -> bool {
248 matches!(self.payload, Payload::JoinRequest(_))
249 }
250
251 pub const fn is_join_accept(&self) -> bool {
253 matches!(self.payload, Payload::JoinAccept(_))
254 }
255
256 pub const fn is_rejoin_request(&self) -> bool {
258 matches!(self.payload, Payload::RejoinRequest(_))
259 }
260
261 pub const fn as_data(&self) -> Option<&Data> {
263 if let Payload::Data(d) = &self.payload {
264 Some(d)
265 } else {
266 None
267 }
268 }
269
270 pub const fn as_data_mut(&mut self) -> Option<&mut Data> {
272 if let Payload::Data(d) = &mut self.payload {
273 Some(d)
274 } else {
275 None
276 }
277 }
278
279 pub const fn as_join_request(&self) -> Option<&JoinRequest> {
281 if let Payload::JoinRequest(j) = &self.payload {
282 Some(j)
283 } else {
284 None
285 }
286 }
287
288 pub const fn as_join_accept(&self) -> Option<&JoinAccept> {
290 if let Payload::JoinAccept(j) = &self.payload {
291 Some(j)
292 } else {
293 None
294 }
295 }
296
297 pub const fn as_rejoin_request(&self) -> Option<&RejoinRequest> {
299 if let Payload::RejoinRequest(r) = &self.payload {
300 Some(r)
301 } else {
302 None
303 }
304 }
305
306 pub const fn direction(&self) -> Option<Direction> {
325 match &self.payload {
326 Payload::JoinRequest(_) | Payload::RejoinRequest(_) => Some(Direction::Uplink),
327 Payload::JoinAccept(_) => Some(Direction::Downlink),
328 Payload::Data(d) => Some(d.direction),
329 Payload::Proprietary(_) => None,
330 }
331 }
332
333 pub fn verify_mic_v1_0(&self, keys: &crate::mic::V1_0MicKeys<'_>) -> crate::Result<bool> {
357 let calculated = self.calculate_mic_v1_0(keys)?;
358 Ok(crate::mic::mic_eq(calculated, self.mic))
359 }
360
361 pub fn verify_mic_v1_1(&self, keys: &crate::mic::V1_1MicKeys<'_>) -> crate::Result<bool> {
371 let calculated = self.calculate_mic_v1_1(keys)?;
372 Ok(crate::mic::mic_eq(calculated, self.mic))
373 }
374
375 pub fn calculate_mic_v1_0(&self, keys: &crate::mic::V1_0MicKeys<'_>) -> crate::Result<[u8; 4]> {
387 match &self.payload {
388 Payload::Data(_) => {
389 let key = keys
390 .nwk_s_key
391 .ok_or(crate::Error::MissingKey("nwk_s_key required for Data MIC"))?;
392 Ok(crate::mic::calculate_data_mic_1_0(self, key.as_bytes(), keys.f_cnt_msb))
393 }
394 Payload::JoinRequest(_) => {
395 let key = keys
396 .app_key
397 .ok_or(crate::Error::MissingKey("app_key required for Join Request MIC"))?;
398 Ok(crate::mic::calculate_join_request_mic(self, key.as_bytes()))
399 }
400 Payload::JoinAccept(_) => {
401 let key = keys
402 .app_key
403 .ok_or(crate::Error::MissingKey("app_key required for Join Accept MIC"))?;
404 let mhdr_and_body = &self.phy_payload[..self.phy_payload.len() - 4];
405 Ok(crate::mic::calculate_join_accept_mic_1_0(mhdr_and_body, key.as_bytes()))
406 }
407 Payload::RejoinRequest(_) | Payload::Proprietary(_) => Err(crate::Error::UnsupportedForVersion(
408 "Rejoin Request and Proprietary frames are 1.1-only; use calculate_mic_v1_1",
409 )),
410 }
411 }
412
413 pub fn calculate_mic_v1_1(&self, keys: &crate::mic::V1_1MicKeys<'_>) -> crate::Result<[u8; 4]> {
427 match &self.payload {
428 Payload::Data(d) => match d.direction {
429 Direction::Uplink => {
430 let f_key = keys.f_nwk_s_int_key.ok_or(crate::Error::MissingKey(
431 "f_nwk_s_int_key required for Data uplink 1.1 MIC",
432 ))?;
433 let s_key = keys.s_nwk_s_int_key.ok_or(crate::Error::MissingKey(
434 "s_nwk_s_int_key required for Data uplink 1.1 MIC",
435 ))?;
436 let conf = keys.conf_fcnt_down_tx_dr_tx_ch.unwrap_or([0, 0, 0, 0]);
437 Ok(crate::mic::calculate_data_mic_1_1_uplink(
438 self,
439 f_key.as_bytes(),
440 s_key.as_bytes(),
441 keys.f_cnt_msb,
442 conf,
443 ))
444 }
445 Direction::Downlink => {
446 let s_key = keys.s_nwk_s_int_key.ok_or(crate::Error::MissingKey(
447 "s_nwk_s_int_key required for Data downlink 1.1 MIC",
448 ))?;
449 let conf = keys.conf_fcnt_down_tx_dr_tx_ch.unwrap_or([0, 0, 0, 0]);
450 Ok(crate::mic::calculate_data_mic_1_1_downlink(
451 self,
452 s_key.as_bytes(),
453 keys.f_cnt_msb,
454 conf,
455 ))
456 }
457 },
458 Payload::JoinRequest(_) => {
459 let key = keys
460 .nwk_key
461 .ok_or(crate::Error::MissingKey("nwk_key required for Join Request 1.1 MIC"))?;
462 Ok(crate::mic::calculate_join_request_mic(self, key.as_bytes()))
463 }
464 Payload::JoinAccept(_) => {
465 let js_key = keys
466 .js_int_key
467 .ok_or(crate::Error::MissingKey("js_int_key required for Join Accept 1.1 MIC"))?;
468 let join_eui = keys
469 .join_eui
470 .ok_or(crate::Error::MissingKey("join_eui required for Join Accept 1.1 MIC"))?;
471 let dev_nonce = keys
472 .dev_nonce
473 .ok_or(crate::Error::MissingKey("dev_nonce required for Join Accept 1.1 MIC"))?;
474 let join_req_type = keys.join_req_type.unwrap_or(0xFF);
475 let mhdr_and_body = &self.phy_payload[..self.phy_payload.len() - 4];
476 Ok(crate::mic::calculate_join_accept_mic_1_1(
477 mhdr_and_body,
478 js_key.as_bytes(),
479 join_req_type,
480 &join_eui,
481 &dev_nonce,
482 ))
483 }
484 Payload::RejoinRequest(rj) => {
485 let key = match rj {
486 RejoinRequest::Type1 { .. } => keys
487 .js_int_key
488 .ok_or(crate::Error::MissingKey("js_int_key required for Rejoin Type 1 MIC"))?
489 .as_bytes(),
490 _ => keys
491 .s_nwk_s_int_key
492 .ok_or(crate::Error::MissingKey(
493 "s_nwk_s_int_key required for Rejoin Type 0/2 MIC",
494 ))?
495 .as_bytes(),
496 };
497 Ok(crate::mic::calculate_rejoin_mic(self, key))
498 }
499 Payload::Proprietary(_) => Err(crate::Error::MissingKey("Proprietary has no defined MIC")),
500 }
501 }
502
503 pub fn recalculate_mic_v1_0(&mut self, keys: &crate::mic::V1_0MicKeys<'_>) -> crate::Result<()> {
513 let mic = self.calculate_mic_v1_0(keys)?;
514 self.mic = mic;
515 self.phy_payload = self.to_wire();
516 Ok(())
517 }
518
519 pub fn recalculate_mic_v1_1(&mut self, keys: &crate::mic::V1_1MicKeys<'_>) -> crate::Result<()> {
527 let mic = self.calculate_mic_v1_1(keys)?;
528 self.mic = mic;
529 self.phy_payload = self.to_wire();
530 Ok(())
531 }
532}
533
534impl Data {
535 pub const fn f_cnt(&self) -> u16 {
541 u16::from_le_bytes(self.f_cnt)
542 }
543
544 pub const fn f_cnt_32(&self, msb: u16) -> u32 {
550 ((msb as u32) << 16) | (self.f_cnt() as u32)
551 }
552}
553
554impl LoraPacket {
555 pub fn from_wire(bytes: &[u8]) -> crate::Result<Self> {
592 if bytes.len() < 5 {
593 return Err(crate::Error::TooShort {
594 expected: 5,
595 got: bytes.len(),
596 });
597 }
598 if bytes.len() > 256 {
602 return Err(crate::Error::TooLong { got: bytes.len() });
603 }
604 let mhdr = Mhdr::new(bytes[0]);
605 let mic_offset = bytes.len() - 4;
606 let mut mic = [0u8; 4];
607 mic.copy_from_slice(&bytes[mic_offset..]);
608 let m_type = mhdr.m_type()?;
609 let body = &bytes[1..mic_offset];
610
611 let payload = match m_type {
612 MType::JoinRequest => Payload::JoinRequest(parse_join_request(body)?),
613 MType::JoinAccept => {
614 return Err(crate::Error::Other(alloc::string::String::from(
615 "JoinAccept parsing requires decrypt; use JoinAccept::decrypt_from_wire",
616 )));
617 }
618 MType::UnconfirmedDataUp | MType::UnconfirmedDataDown | MType::ConfirmedDataUp | MType::ConfirmedDataDown => {
619 Payload::Data(parse_data(m_type, body)?)
620 }
621 MType::RejoinRequest => Payload::RejoinRequest(parse_rejoin_request(body)?),
622 MType::Proprietary => Payload::Proprietary(body.to_vec()),
623 };
624
625 Ok(Self {
626 phy_payload: bytes.to_vec(),
627 mhdr,
628 mic,
629 payload,
630 })
631 }
632
633 pub fn to_wire(&self) -> Vec<u8> {
644 let mut out = Vec::with_capacity(self.phy_payload.len().max(13));
645 out.push(self.mhdr.as_byte());
646 match &self.payload {
647 Payload::JoinRequest(jr) => {
648 let mut tmp = *jr.join_eui.as_bytes();
649 tmp.reverse();
650 out.extend_from_slice(&tmp);
651 let mut tmp = *jr.dev_eui.as_bytes();
652 tmp.reverse();
653 out.extend_from_slice(&tmp);
654 let mut tmp = *jr.dev_nonce.as_bytes();
655 tmp.reverse();
656 out.extend_from_slice(&tmp);
657 }
658 Payload::Data(d) => {
659 let mut tmp = *d.dev_addr.as_bytes();
660 tmp.reverse();
661 out.extend_from_slice(&tmp);
662 out.push(d.f_ctrl.as_byte());
663 out.extend_from_slice(&d.f_cnt);
664 out.extend_from_slice(&d.f_opts);
665 if let Some(p) = d.f_port {
666 out.push(p);
667 }
668 if let Some(payload) = &d.frm_payload {
669 out.extend_from_slice(payload);
670 }
671 }
672 Payload::JoinAccept(ja) => {
673 let mut tmp = *ja.join_nonce.as_bytes();
674 tmp.reverse();
675 out.extend_from_slice(&tmp);
676 let mut tmp = *ja.net_id.as_bytes();
677 tmp.reverse();
678 out.extend_from_slice(&tmp);
679 let mut tmp = *ja.dev_addr.as_bytes();
680 tmp.reverse();
681 out.extend_from_slice(&tmp);
682 out.push(ja.dl_settings.as_byte());
683 out.push(ja.rx_delay);
684 if let Some(cf) = ja.cf_list {
685 out.extend_from_slice(&cf);
686 }
687 }
688 Payload::RejoinRequest(rj) => match rj {
689 RejoinRequest::Type0 {
690 net_id,
691 dev_eui,
692 rj_count_0,
693 } => {
694 out.push(0);
695 let mut tmp = *net_id.as_bytes();
696 tmp.reverse();
697 out.extend_from_slice(&tmp);
698 let mut tmp = *dev_eui.as_bytes();
699 tmp.reverse();
700 out.extend_from_slice(&tmp);
701 let mut tmp = *rj_count_0;
702 tmp.reverse();
703 out.extend_from_slice(&tmp);
704 }
705 RejoinRequest::Type1 {
706 join_eui,
707 dev_eui,
708 rj_count_1,
709 } => {
710 out.push(1);
711 let mut tmp = *join_eui.as_bytes();
712 tmp.reverse();
713 out.extend_from_slice(&tmp);
714 let mut tmp = *dev_eui.as_bytes();
715 tmp.reverse();
716 out.extend_from_slice(&tmp);
717 let mut tmp = *rj_count_1;
718 tmp.reverse();
719 out.extend_from_slice(&tmp);
720 }
721 RejoinRequest::Type2 {
722 net_id,
723 dev_eui,
724 rj_count_0,
725 } => {
726 out.push(2);
727 let mut tmp = *net_id.as_bytes();
728 tmp.reverse();
729 out.extend_from_slice(&tmp);
730 let mut tmp = *dev_eui.as_bytes();
731 tmp.reverse();
732 out.extend_from_slice(&tmp);
733 let mut tmp = *rj_count_0;
734 tmp.reverse();
735 out.extend_from_slice(&tmp);
736 }
737 },
738 Payload::Proprietary(b) => out.extend_from_slice(b),
739 }
740 out.extend_from_slice(&self.mic);
741 out
742 }
743}
744
745#[cfg(feature = "hex_base64")]
746impl LoraPacket {
747 pub fn from_hex(s: &str) -> crate::Result<Self> {
753 let bytes = hex::decode(s)?;
754 Self::from_wire(&bytes)
755 }
756
757 pub fn from_base64(s: &str) -> crate::Result<Self> {
763 use base64::Engine as _;
764 let bytes = base64::engine::general_purpose::STANDARD.decode(s)?;
765 Self::from_wire(&bytes)
766 }
767}
768
769impl JoinAccept {
770 pub fn from_plaintext(bytes: &[u8]) -> crate::Result<Self> {
783 if bytes.len() < 17 {
784 return Err(crate::Error::TooShort {
785 expected: 17,
786 got: bytes.len(),
787 });
788 }
789 let body = &bytes[1..bytes.len() - 4];
790 if body.len() != 12 && body.len() != 28 {
791 return Err(crate::Error::TooShort {
792 expected: 12,
793 got: body.len(),
794 });
795 }
796
797 let mut join_nonce = [0u8; 3];
798 join_nonce.copy_from_slice(&body[0..3]);
799 join_nonce.reverse();
800 let mut net_id = [0u8; 3];
801 net_id.copy_from_slice(&body[3..6]);
802 net_id.reverse();
803 let mut dev_addr = [0u8; 4];
804 dev_addr.copy_from_slice(&body[6..10]);
805 dev_addr.reverse();
806 let dl_settings = DlSettings(body[10]);
807 let rx_delay = body[11];
808
809 let cf_list = if body.len() == 28 {
810 let mut cf = [0u8; 16];
811 cf.copy_from_slice(&body[12..28]);
812 Some(cf)
813 } else {
814 None
815 };
816
817 Ok(Self {
818 join_nonce: AppNonce::new(join_nonce),
819 net_id: NetId::new(net_id),
820 dev_addr: DevAddr::new(dev_addr),
821 dl_settings,
822 rx_delay,
823 cf_list,
824 join_req_type: None,
825 })
826 }
827}
828
829fn parse_join_request(body: &[u8]) -> crate::Result<JoinRequest> {
830 if body.len() != 18 {
831 return Err(crate::Error::TooShort {
832 expected: 18,
833 got: body.len(),
834 });
835 }
836 let mut app_eui = [0u8; 8];
837 app_eui.copy_from_slice(&body[0..8]);
838 app_eui.reverse();
839 let mut dev_eui = [0u8; 8];
840 dev_eui.copy_from_slice(&body[8..16]);
841 dev_eui.reverse();
842 let mut dev_nonce = [0u8; 2];
843 dev_nonce.copy_from_slice(&body[16..18]);
844 dev_nonce.reverse();
845
846 Ok(JoinRequest {
847 join_eui: AppEui::new(app_eui),
848 dev_eui: DevEui::new(dev_eui),
849 dev_nonce: DevNonce::new(dev_nonce),
850 })
851}
852
853fn parse_data(m_type: MType, body: &[u8]) -> crate::Result<Data> {
854 if body.len() < 7 {
855 return Err(crate::Error::TooShort {
856 expected: 7,
857 got: body.len(),
858 });
859 }
860
861 let mut dev_addr = [0u8; 4];
862 dev_addr.copy_from_slice(&body[0..4]);
863 dev_addr.reverse();
864 let f_ctrl = FCtrl(body[4]);
865 let mut f_cnt = [0u8; 2];
866 f_cnt.copy_from_slice(&body[5..7]);
867
868 let f_opts_len = f_ctrl.f_opts_len() as usize;
869 if 7 + f_opts_len > body.len() {
870 return Err(crate::Error::TooShort {
871 expected: 7 + f_opts_len,
872 got: body.len(),
873 });
874 }
875 let f_opts = body[7..7 + f_opts_len].to_vec();
876
877 let remainder_start = 7 + f_opts_len;
878 let (f_port, frm_payload) = if remainder_start >= body.len() {
879 (None, None)
880 } else {
881 let port = body[remainder_start];
882 let payload = if remainder_start + 1 < body.len() {
883 Some(body[remainder_start + 1..].to_vec())
884 } else {
885 Some(Vec::new())
886 };
887 (Some(port), payload)
888 };
889
890 let (direction, confirmed) = match m_type {
891 MType::UnconfirmedDataUp => (Direction::Uplink, false),
892 MType::ConfirmedDataUp => (Direction::Uplink, true),
893 MType::UnconfirmedDataDown => (Direction::Downlink, false),
894 MType::ConfirmedDataDown => (Direction::Downlink, true),
895 _ => unreachable!("parse_data called with non-data MType"),
896 };
897
898 Ok(Data {
899 direction,
900 confirmed,
901 dev_addr: DevAddr::new(dev_addr),
902 f_ctrl,
903 f_cnt,
904 f_opts,
905 f_port,
906 frm_payload,
907 })
908}
909
910#[derive(Debug, Default, Clone)]
932pub struct LoraPacketBuilder {
933 m_type: Option<MType>,
934 major: u8,
935 direction: Option<Direction>,
936 confirmed: bool,
937 dev_addr: Option<DevAddr>,
938 f_ctrl: Option<FCtrl>,
939 f_cnt: Option<u16>,
940 f_opts: Vec<u8>,
941 f_port: Option<u8>,
942 payload: Option<Vec<u8>>,
943 join_eui: Option<AppEui>,
944 dev_eui: Option<DevEui>,
945 dev_nonce: Option<DevNonce>,
946 join_nonce: Option<AppNonce>,
947 net_id: Option<NetId>,
948 dl_settings: Option<DlSettings>,
949 rx_delay: Option<u8>,
950 cf_list: Option<[u8; 16]>,
951 join_req_type: Option<u8>,
952 rejoin_type: Option<u8>,
953 rj_count_0: Option<[u8; 2]>,
954 rj_count_1: Option<[u8; 2]>,
955}
956
957impl LoraPacket {
958 pub fn builder() -> LoraPacketBuilder {
999 LoraPacketBuilder::default()
1000 }
1001}
1002
1003impl LoraPacketBuilder {
1004 #[must_use]
1006 pub const fn data(mut self, direction: Direction, confirmed: bool) -> Self {
1007 self.direction = Some(direction);
1008 self.confirmed = confirmed;
1009 self.m_type = Some(match (confirmed, direction) {
1010 (false, Direction::Uplink) => MType::UnconfirmedDataUp,
1011 (false, Direction::Downlink) => MType::UnconfirmedDataDown,
1012 (true, Direction::Uplink) => MType::ConfirmedDataUp,
1013 (true, Direction::Downlink) => MType::ConfirmedDataDown,
1014 });
1015 self
1016 }
1017
1018 #[must_use]
1020 pub const fn join_request(mut self) -> Self {
1021 self.m_type = Some(MType::JoinRequest);
1022 self
1023 }
1024
1025 #[must_use]
1027 pub const fn join_accept(mut self) -> Self {
1028 self.m_type = Some(MType::JoinAccept);
1029 self
1030 }
1031
1032 #[must_use]
1034 pub const fn rejoin_request(mut self, rejoin_type: u8) -> Self {
1035 self.m_type = Some(MType::RejoinRequest);
1036 self.rejoin_type = Some(rejoin_type);
1037 self
1038 }
1039
1040 #[must_use]
1042 pub const fn dev_addr(mut self, addr: DevAddr) -> Self {
1043 self.dev_addr = Some(addr);
1044 self
1045 }
1046
1047 #[must_use]
1049 pub const fn f_ctrl(mut self, c: FCtrl) -> Self {
1050 self.f_ctrl = Some(c);
1051 self
1052 }
1053
1054 #[must_use]
1056 pub const fn f_cnt(mut self, n: u16) -> Self {
1057 self.f_cnt = Some(n);
1058 self
1059 }
1060
1061 #[must_use]
1063 pub fn f_opts(mut self, opts: &[u8]) -> Self {
1064 self.f_opts = opts.to_vec();
1065 self
1066 }
1067
1068 #[must_use]
1070 pub const fn f_port(mut self, p: u8) -> Self {
1071 self.f_port = Some(p);
1072 self
1073 }
1074
1075 #[must_use]
1077 pub fn payload(mut self, p: &[u8]) -> Self {
1078 self.payload = Some(p.to_vec());
1079 self
1080 }
1081
1082 #[must_use]
1084 pub const fn join_eui(mut self, e: AppEui) -> Self {
1085 self.join_eui = Some(e);
1086 self
1087 }
1088
1089 #[must_use]
1091 pub const fn dev_eui(mut self, e: DevEui) -> Self {
1092 self.dev_eui = Some(e);
1093 self
1094 }
1095
1096 #[must_use]
1098 pub const fn dev_nonce(mut self, n: DevNonce) -> Self {
1099 self.dev_nonce = Some(n);
1100 self
1101 }
1102
1103 #[must_use]
1105 pub const fn join_nonce(mut self, n: AppNonce) -> Self {
1106 self.join_nonce = Some(n);
1107 self
1108 }
1109
1110 #[must_use]
1112 pub const fn net_id(mut self, id: NetId) -> Self {
1113 self.net_id = Some(id);
1114 self
1115 }
1116
1117 #[must_use]
1119 pub const fn dl_settings(mut self, s: DlSettings) -> Self {
1120 self.dl_settings = Some(s);
1121 self
1122 }
1123
1124 #[must_use]
1126 pub const fn rx_delay(mut self, r: u8) -> Self {
1127 self.rx_delay = Some(r);
1128 self
1129 }
1130
1131 #[must_use]
1133 pub const fn cf_list(mut self, c: [u8; 16]) -> Self {
1134 self.cf_list = Some(c);
1135 self
1136 }
1137
1138 #[must_use]
1140 pub const fn join_req_type(mut self, t: u8) -> Self {
1141 self.join_req_type = Some(t);
1142 self
1143 }
1144
1145 #[must_use]
1149 pub const fn rj_count_0(mut self, count: u16) -> Self {
1150 self.rj_count_0 = Some(count.to_le_bytes());
1151 self
1152 }
1153
1154 #[must_use]
1158 pub const fn rj_count_1(mut self, count: u16) -> Self {
1159 self.rj_count_1 = Some(count.to_le_bytes());
1160 self
1161 }
1162
1163 pub fn sign_join_accept(self, app_key: &crate::types::AppKey) -> crate::Result<(LoraPacket, alloc::vec::Vec<u8>)> {
1173 let mut packet = self.build_unsigned()?;
1174 let keys = crate::mic::V1_0MicKeys {
1175 app_key: Some(app_key),
1176 ..Default::default()
1177 };
1178 packet.recalculate_mic_v1_0(&keys)?;
1179 let encrypted_wire = JoinAccept::encrypt_for_wire(&packet.phy_payload, app_key)?;
1180 Ok((packet, encrypted_wire))
1181 }
1182
1183 pub fn sign_join_request(self, app_key: &crate::types::AppKey) -> crate::Result<LoraPacket> {
1191 let mut packet = self.build_unsigned()?;
1192 let keys = crate::mic::V1_0MicKeys {
1193 app_key: Some(app_key),
1194 ..Default::default()
1195 };
1196 packet.recalculate_mic_v1_0(&keys)?;
1197 Ok(packet)
1198 }
1199
1200 pub fn sign_join_request_v1_1(self, nwk_key: &crate::types::NwkKey) -> crate::Result<LoraPacket> {
1207 let mut packet = self.build_unsigned()?;
1208 let keys = crate::mic::V1_1MicKeys {
1209 nwk_key: Some(nwk_key),
1210 ..Default::default()
1211 };
1212 packet.recalculate_mic_v1_1(&keys)?;
1213 Ok(packet)
1214 }
1215
1216 pub fn sign_and_encrypt(
1254 self,
1255 app_s_key: &crate::types::AppSKey,
1256 nwk_s_key: &crate::types::NwkSKey,
1257 ) -> crate::Result<LoraPacket> {
1258 let mut packet = self.build_unsigned()?;
1259 if let Payload::Data(d) = &mut packet.payload
1261 && let Some(plaintext) = d.frm_payload.clone()
1262 {
1263 let encrypted = d.encrypt_payload(&plaintext, app_s_key, nwk_s_key, 0)?;
1264 d.frm_payload = Some(encrypted);
1265 }
1266 packet.phy_payload = packet.to_wire();
1268 let keys = crate::mic::V1_0MicKeys {
1269 nwk_s_key: Some(nwk_s_key),
1270 ..Default::default()
1271 };
1272 packet.recalculate_mic_v1_0(&keys)?;
1273 Ok(packet)
1274 }
1275
1276 pub fn build_unsigned(self) -> crate::Result<LoraPacket> {
1293 let m_type = self.m_type.ok_or(crate::Error::MissingField("m_type"))?;
1294 let mhdr = Mhdr::from_parts(m_type, self.major);
1295
1296 let payload = match m_type {
1297 MType::JoinRequest => Payload::JoinRequest(JoinRequest {
1298 join_eui: self.join_eui.ok_or(crate::Error::MissingField("join_eui"))?,
1299 dev_eui: self.dev_eui.ok_or(crate::Error::MissingField("dev_eui"))?,
1300 dev_nonce: self.dev_nonce.ok_or(crate::Error::MissingField("dev_nonce"))?,
1301 }),
1302 MType::JoinAccept => Payload::JoinAccept(JoinAccept {
1303 join_nonce: self.join_nonce.ok_or(crate::Error::MissingField("join_nonce"))?,
1304 net_id: self.net_id.ok_or(crate::Error::MissingField("net_id"))?,
1305 dev_addr: self.dev_addr.ok_or(crate::Error::MissingField("dev_addr"))?,
1306 dl_settings: self.dl_settings.ok_or(crate::Error::MissingField("dl_settings"))?,
1307 rx_delay: self.rx_delay.unwrap_or(0),
1308 cf_list: self.cf_list,
1309 join_req_type: self.join_req_type,
1310 }),
1311 MType::UnconfirmedDataUp | MType::UnconfirmedDataDown | MType::ConfirmedDataUp | MType::ConfirmedDataDown => {
1312 let direction = self.direction.ok_or(crate::Error::MissingField("direction"))?;
1313 let f_opts_len = u8::try_from(self.f_opts.len()).map_err(|_| crate::Error::FOptsTooLong(self.f_opts.len()))?;
1314 if f_opts_len > 15 {
1315 return Err(crate::Error::FOptsTooLong(self.f_opts.len()));
1316 }
1317 let f_opts_len_nibble = f_opts_len & 0x0F;
1321 let f_ctrl = self
1322 .f_ctrl
1323 .map_or(FCtrl(f_opts_len_nibble), |fc| FCtrl((fc.0 & 0xF0) | f_opts_len_nibble));
1324 Payload::Data(Data {
1325 direction,
1326 confirmed: self.confirmed,
1327 dev_addr: self.dev_addr.ok_or(crate::Error::MissingField("dev_addr"))?,
1328 f_ctrl,
1329 f_cnt: self.f_cnt.unwrap_or(0).to_le_bytes(),
1330 f_opts: self.f_opts,
1331 f_port: self.f_port,
1332 frm_payload: self.payload,
1333 })
1334 }
1335 MType::RejoinRequest => {
1336 let dev_eui = self.dev_eui.ok_or(crate::Error::MissingField("dev_eui"))?;
1337 Payload::RejoinRequest(match self.rejoin_type.unwrap_or(0) {
1338 0 => RejoinRequest::Type0 {
1339 net_id: self.net_id.ok_or(crate::Error::MissingField("net_id"))?,
1340 dev_eui,
1341 rj_count_0: self.rj_count_0.unwrap_or([0, 0]),
1342 },
1343 1 => RejoinRequest::Type1 {
1344 join_eui: self.join_eui.ok_or(crate::Error::MissingField("join_eui"))?,
1345 dev_eui,
1346 rj_count_1: self.rj_count_1.unwrap_or([0, 0]),
1347 },
1348 2 => RejoinRequest::Type2 {
1349 net_id: self.net_id.ok_or(crate::Error::MissingField("net_id"))?,
1350 dev_eui,
1351 rj_count_0: self.rj_count_0.unwrap_or([0, 0]),
1352 },
1353 other => return Err(crate::Error::InvalidRejoinType(other)),
1354 })
1355 }
1356 MType::Proprietary => Payload::Proprietary(self.payload.unwrap_or_default()),
1357 };
1358
1359 let mut pkt = LoraPacket {
1360 phy_payload: Vec::new(),
1361 mhdr,
1362 mic: [0u8; 4],
1363 payload,
1364 };
1365 pkt.phy_payload = pkt.to_wire();
1366 Ok(pkt)
1367 }
1368}
1369
1370fn parse_rejoin_request(body: &[u8]) -> crate::Result<RejoinRequest> {
1371 if body.is_empty() {
1372 return Err(crate::Error::TooShort { expected: 1, got: 0 });
1373 }
1374 let rejoin_type = body[0];
1375 match rejoin_type {
1376 0 | 2 => {
1377 if body.len() != 14 {
1378 return Err(crate::Error::TooShort {
1379 expected: 14,
1380 got: body.len(),
1381 });
1382 }
1383 let mut net_id = [0u8; 3];
1384 net_id.copy_from_slice(&body[1..4]);
1385 net_id.reverse();
1386 let mut dev_eui = [0u8; 8];
1387 dev_eui.copy_from_slice(&body[4..12]);
1388 dev_eui.reverse();
1389 let mut rj_count_0 = [0u8; 2];
1390 rj_count_0.copy_from_slice(&body[12..14]);
1391 rj_count_0.reverse();
1392 let dev_eui = DevEui::new(dev_eui);
1393 let net_id = NetId::new(net_id);
1394 if rejoin_type == 0 {
1395 Ok(RejoinRequest::Type0 {
1396 net_id,
1397 dev_eui,
1398 rj_count_0,
1399 })
1400 } else {
1401 Ok(RejoinRequest::Type2 {
1402 net_id,
1403 dev_eui,
1404 rj_count_0,
1405 })
1406 }
1407 }
1408 1 => {
1409 if body.len() != 19 {
1410 return Err(crate::Error::TooShort {
1411 expected: 19,
1412 got: body.len(),
1413 });
1414 }
1415 let mut join_eui = [0u8; 8];
1416 join_eui.copy_from_slice(&body[1..9]);
1417 join_eui.reverse();
1418 let mut dev_eui = [0u8; 8];
1419 dev_eui.copy_from_slice(&body[9..17]);
1420 dev_eui.reverse();
1421 let mut rj_count_1 = [0u8; 2];
1422 rj_count_1.copy_from_slice(&body[17..19]);
1423 rj_count_1.reverse();
1424 Ok(RejoinRequest::Type1 {
1425 join_eui: AppEui::new(join_eui),
1426 dev_eui: DevEui::new(dev_eui),
1427 rj_count_1,
1428 })
1429 }
1430 other => Err(crate::Error::InvalidRejoinType(other)),
1431 }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436 use super::*;
1437
1438 #[test]
1439 fn lora_packet_constructs_with_join_request_payload() {
1440 let p = LoraPacket {
1441 phy_payload: alloc::vec![0x00],
1442 mhdr: Mhdr::from_parts(MType::JoinRequest, 0),
1443 mic: [0u8; 4],
1444 payload: Payload::JoinRequest(JoinRequest {
1445 join_eui: AppEui::new([0u8; 8]),
1446 dev_eui: DevEui::new([0u8; 8]),
1447 dev_nonce: DevNonce::new([0u8; 2]),
1448 }),
1449 };
1450 assert!(matches!(p.payload, Payload::JoinRequest(_)));
1451 }
1452
1453 fn sample_data_packet(confirmed: bool, direction: Direction) -> LoraPacket {
1454 let m_type = match (confirmed, direction) {
1455 (false, Direction::Uplink) => MType::UnconfirmedDataUp,
1456 (false, Direction::Downlink) => MType::UnconfirmedDataDown,
1457 (true, Direction::Uplink) => MType::ConfirmedDataUp,
1458 (true, Direction::Downlink) => MType::ConfirmedDataDown,
1459 };
1460 LoraPacket {
1461 phy_payload: alloc::vec![],
1462 mhdr: Mhdr::from_parts(m_type, 0),
1463 mic: [0u8; 4],
1464 payload: Payload::Data(Data {
1465 direction,
1466 confirmed,
1467 dev_addr: DevAddr::new([0u8; 4]),
1468 f_ctrl: FCtrl(0),
1469 f_cnt: [0, 0],
1470 f_opts: alloc::vec![],
1471 f_port: None,
1472 frm_payload: None,
1473 }),
1474 }
1475 }
1476
1477 #[test]
1478 fn accessor_is_data() {
1479 let p = sample_data_packet(false, Direction::Uplink);
1480 assert!(p.is_data());
1481 assert!(!p.is_confirmed());
1482 assert!(p.as_data().is_some());
1483 }
1484
1485 #[test]
1486 fn accessor_is_confirmed() {
1487 let p = sample_data_packet(true, Direction::Downlink);
1488 assert!(p.is_data());
1489 assert!(p.is_confirmed());
1490 }
1491
1492 #[test]
1493 fn accessor_is_join_request() {
1494 let p = LoraPacket {
1495 phy_payload: alloc::vec![],
1496 mhdr: Mhdr::from_parts(MType::JoinRequest, 0),
1497 mic: [0u8; 4],
1498 payload: Payload::JoinRequest(JoinRequest {
1499 join_eui: AppEui::new([0u8; 8]),
1500 dev_eui: DevEui::new([0u8; 8]),
1501 dev_nonce: DevNonce::new([0u8; 2]),
1502 }),
1503 };
1504 assert!(p.is_join_request());
1505 assert!(p.as_join_request().is_some());
1506 assert!(p.as_data().is_none());
1507 }
1508
1509 #[test]
1510 fn direction_join_request_is_uplink() {
1511 let p = LoraPacket {
1512 phy_payload: alloc::vec![],
1513 mhdr: Mhdr::from_parts(MType::JoinRequest, 0),
1514 mic: [0u8; 4],
1515 payload: Payload::JoinRequest(JoinRequest {
1516 join_eui: AppEui::new([0u8; 8]),
1517 dev_eui: DevEui::new([0u8; 8]),
1518 dev_nonce: DevNonce::new([0u8; 2]),
1519 }),
1520 };
1521 assert_eq!(p.direction(), Some(Direction::Uplink));
1522 }
1523
1524 #[test]
1525 fn direction_join_accept_is_downlink() {
1526 let p = LoraPacket {
1527 phy_payload: alloc::vec![],
1528 mhdr: Mhdr::from_parts(MType::JoinAccept, 0),
1529 mic: [0u8; 4],
1530 payload: Payload::JoinAccept(JoinAccept {
1531 join_nonce: AppNonce::new([0u8; 3]),
1532 net_id: NetId::new([0u8; 3]),
1533 dev_addr: DevAddr::new([0u8; 4]),
1534 dl_settings: DlSettings(0),
1535 rx_delay: 0,
1536 cf_list: None,
1537 join_req_type: None,
1538 }),
1539 };
1540 assert_eq!(p.direction(), Some(Direction::Downlink));
1541 }
1542
1543 #[test]
1544 fn direction_data_matches_inner_field() {
1545 assert_eq!(
1546 sample_data_packet(false, Direction::Uplink).direction(),
1547 Some(Direction::Uplink)
1548 );
1549 assert_eq!(
1550 sample_data_packet(true, Direction::Downlink).direction(),
1551 Some(Direction::Downlink)
1552 );
1553 }
1554
1555 #[test]
1556 fn direction_rejoin_request_is_uplink() {
1557 let p = LoraPacket {
1558 phy_payload: alloc::vec![],
1559 mhdr: Mhdr::from_parts(MType::RejoinRequest, 0),
1560 mic: [0u8; 4],
1561 payload: Payload::RejoinRequest(RejoinRequest::Type0 {
1562 net_id: NetId::new([0u8; 3]),
1563 dev_eui: DevEui::new([0u8; 8]),
1564 rj_count_0: [0u8; 2],
1565 }),
1566 };
1567 assert_eq!(p.direction(), Some(Direction::Uplink));
1568 }
1569
1570 #[test]
1571 fn direction_proprietary_is_none() {
1572 let p = LoraPacket {
1573 phy_payload: alloc::vec![],
1574 mhdr: Mhdr::from_parts(MType::Proprietary, 0),
1575 mic: [0u8; 4],
1576 payload: Payload::Proprietary(alloc::vec![1, 2, 3]),
1577 };
1578 assert_eq!(p.direction(), None);
1579 }
1580
1581 #[test]
1582 fn data_f_cnt_little_endian() {
1583 let d = Data {
1584 direction: Direction::Uplink,
1585 confirmed: false,
1586 dev_addr: DevAddr::new([0u8; 4]),
1587 f_ctrl: FCtrl(0),
1588 f_cnt: [0x02, 0x00],
1589 f_opts: alloc::vec![],
1590 f_port: None,
1591 frm_payload: None,
1592 };
1593 assert_eq!(d.f_cnt(), 2);
1594 assert_eq!(d.f_cnt_32(0), 2);
1595 assert_eq!(d.f_cnt_32(1), 0x0001_0002);
1596 }
1597
1598 #[test]
1599 fn from_wire_rejects_empty() {
1600 let err = LoraPacket::from_wire(&[]).unwrap_err();
1601 assert!(matches!(err, crate::Error::TooShort { .. }));
1602 }
1603
1604 #[test]
1605 fn from_wire_rejects_too_short() {
1606 let err = LoraPacket::from_wire(&[1, 2, 3, 4]).unwrap_err();
1607 assert!(matches!(err, crate::Error::TooShort { .. }));
1608 }
1609
1610 #[test]
1612 fn parse_join_request_known_vector() {
1613 let bytes = hex_to_vec("0039363463336913aa05693574323831338ef1c1d5ec6c");
1614 let p = LoraPacket::from_wire(&bytes).unwrap();
1615 assert_eq!(p.mhdr.as_byte(), 0x00);
1616 assert_eq!(p.mic, [0xc1, 0xd5, 0xec, 0x6c]);
1617 let jr = p.as_join_request().expect("expected JoinRequest");
1618 assert_eq!(
1619 jr.join_eui.as_bytes(),
1620 &[0xaa, 0x13, 0x69, 0x33, 0x63, 0x34, 0x36, 0x39]
1621 );
1622 assert_eq!(jr.dev_eui.as_bytes(), &[0x33, 0x31, 0x38, 0x32, 0x74, 0x35, 0x69, 0x05]);
1623 assert_eq!(jr.dev_nonce.as_bytes(), &[0xf1, 0x8e]);
1624 }
1625
1626 fn hex_to_vec(s: &str) -> Vec<u8> {
1627 (0..s.len())
1628 .step_by(2)
1629 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex"))
1630 .collect()
1631 }
1632
1633 #[test]
1634 fn parse_join_accept_plaintext_minimum() {
1635 let plaintext = hex_to_vec("20010203040506070809100001deadbeef");
1636 let ja = JoinAccept::from_plaintext(&plaintext).unwrap();
1637 assert_eq!(ja.join_nonce.as_bytes(), &[0x03, 0x02, 0x01]);
1638 assert_eq!(ja.net_id.as_bytes(), &[0x06, 0x05, 0x04]);
1639 assert_eq!(ja.dev_addr.as_bytes(), &[0x10, 0x09, 0x08, 0x07]);
1640 assert_eq!(ja.dl_settings.as_byte(), 0x00);
1641 assert_eq!(ja.rx_delay, 0x01);
1642 assert!(ja.cf_list.is_none());
1643 assert!(ja.join_req_type.is_none());
1644 }
1645
1646 #[test]
1647 fn parse_join_accept_plaintext_with_cflist() {
1648 let plaintext = hex_to_vec(concat!(
1649 "20",
1650 "010203040506070809100001",
1651 "112233445566778899aabbccddeeff00",
1652 "deadbeef"
1653 ));
1654 let ja = JoinAccept::from_plaintext(&plaintext).unwrap();
1655 assert_eq!(
1656 ja.cf_list.unwrap(),
1657 [
1658 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
1659 ]
1660 );
1661 }
1662
1663 #[test]
1665 fn parse_data_up_known_vector() {
1666 let bytes = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
1667 let p = LoraPacket::from_wire(&bytes).unwrap();
1668 assert_eq!(p.mhdr.as_byte(), 0x40);
1669 assert_eq!(p.mic, [0x2b, 0x11, 0xff, 0x0d]);
1670 let d = p.as_data().expect("expected Data");
1671 assert_eq!(d.direction, Direction::Uplink);
1672 assert!(!d.confirmed);
1673 assert_eq!(d.dev_addr.as_bytes(), &[0x49, 0xbe, 0x7d, 0xf1]);
1674 assert_eq!(d.f_ctrl.as_byte(), 0x00);
1675 assert_eq!(d.f_cnt(), 2);
1676 assert!(d.f_opts.is_empty());
1677 assert_eq!(d.f_port, Some(0x01));
1678 assert_eq!(d.frm_payload.as_deref(), Some(&[0x95, 0x43, 0x78, 0x76][..]));
1679 }
1680
1681 #[test]
1682 fn parse_rejoin_type_0() {
1683 let bytes = hex_to_vec("c0000102030405060708090a0b0c0ddeadbeef");
1684 let p = LoraPacket::from_wire(&bytes).unwrap();
1685 let rj = p.as_rejoin_request().expect("rejoin");
1686 match rj {
1687 RejoinRequest::Type0 {
1688 net_id,
1689 dev_eui,
1690 rj_count_0,
1691 } => {
1692 assert_eq!(net_id.as_bytes(), &[0x03, 0x02, 0x01]);
1693 assert_eq!(dev_eui.as_bytes(), &[0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04]);
1694 assert_eq!(rj_count_0, &[0x0d, 0x0c]);
1695 }
1696 _ => panic!("expected Type0"),
1697 }
1698 }
1699
1700 #[test]
1701 fn parse_rejoin_type_1() {
1702 let bytes = hex_to_vec("c001aaaaaaaaaaaaaaaa0405060708090a0b0c0ddeadbeef");
1703 let p = LoraPacket::from_wire(&bytes).unwrap();
1704 match p.as_rejoin_request().unwrap() {
1705 RejoinRequest::Type1 {
1706 join_eui,
1707 dev_eui,
1708 rj_count_1,
1709 } => {
1710 assert_eq!(join_eui.as_bytes(), &[0xaa; 8]);
1711 assert_eq!(dev_eui.as_bytes(), &[0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04]);
1712 assert_eq!(rj_count_1, &[0x0d, 0x0c]);
1713 }
1714 _ => panic!("expected Type1"),
1715 }
1716 }
1717
1718 #[test]
1719 fn parse_rejoin_type_2() {
1720 let bytes = hex_to_vec("c0020102030405060708090a0b0c0ddeadbeef");
1721 let p = LoraPacket::from_wire(&bytes).unwrap();
1722 assert!(matches!(p.as_rejoin_request().unwrap(), RejoinRequest::Type2 { .. }));
1723 }
1724
1725 #[test]
1726 fn parse_rejoin_invalid_type() {
1727 let bytes = hex_to_vec("c0030102030405060708090a0b0c0ddeadbeef");
1728 let err = LoraPacket::from_wire(&bytes).unwrap_err();
1729 assert!(matches!(err, crate::Error::InvalidRejoinType(3)));
1730 }
1731
1732 #[test]
1733 fn parse_proprietary_keeps_body() {
1734 let bytes = hex_to_vec("e0deadbeefcafe11223344");
1735 let p = LoraPacket::from_wire(&bytes).unwrap();
1736 match &p.payload {
1737 Payload::Proprietary(body) => assert_eq!(body, &[0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]),
1738 _ => panic!("expected Proprietary"),
1739 }
1740 assert_eq!(p.mic, [0x11, 0x22, 0x33, 0x44]);
1741 }
1742
1743 #[test]
1744 fn builder_constructs() {
1745 let _b = LoraPacket::builder().data(Direction::Uplink, false);
1746 }
1747
1748 #[test]
1749 fn builder_chains_fields() {
1750 let b = LoraPacket::builder()
1751 .data(Direction::Downlink, false)
1752 .dev_addr(DevAddr::new([1, 2, 3, 4]))
1753 .f_cnt(7)
1754 .f_port(1)
1755 .payload(b"hi");
1756 assert_eq!(b.dev_addr.unwrap().as_bytes(), &[1, 2, 3, 4]);
1757 assert_eq!(b.f_cnt.unwrap(), 7);
1758 assert_eq!(b.f_port.unwrap(), 1);
1759 assert_eq!(b.payload.as_deref().unwrap(), b"hi");
1760 }
1761
1762 #[test]
1763 fn build_unsigned_data_round_trip() {
1764 let pkt = LoraPacket::builder()
1765 .data(Direction::Uplink, false)
1766 .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
1767 .f_ctrl(FCtrl(0))
1768 .f_cnt(2)
1769 .f_port(1)
1770 .payload(&[0x95, 0x43, 0x78, 0x76])
1771 .build_unsigned()
1772 .unwrap();
1773
1774 let wire = pkt.to_wire();
1775 assert_eq!(&wire[..1], &[0x40]);
1776 assert_eq!(&wire[1..5], &[0xf1, 0x7d, 0xbe, 0x49]);
1777 assert_eq!(wire[5], 0x00);
1778 assert_eq!(&wire[6..8], &[0x02, 0x00]);
1779 assert_eq!(wire[8], 0x01);
1780 assert_eq!(&wire[9..13], &[0x95, 0x43, 0x78, 0x76]);
1781 assert_eq!(&wire[wire.len() - 4..], &[0, 0, 0, 0]);
1782 }
1783
1784 #[test]
1785 fn round_trip_data_up() {
1786 let wire = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
1787 let p = LoraPacket::from_wire(&wire).unwrap();
1788 let emitted = p.to_wire();
1789 assert_eq!(emitted, wire);
1790 }
1791
1792 #[test]
1793 fn round_trip_join_request() {
1794 let wire = hex_to_vec("0039363463336913aa05693574323831338ef1c1d5ec6c");
1795 let p = LoraPacket::from_wire(&wire).unwrap();
1796 assert_eq!(p.to_wire(), wire);
1797 }
1798
1799 #[test]
1800 fn round_trip_rejoin_type_0() {
1801 let wire = hex_to_vec("c0000102030405060708090a0b0c0ddeadbeef");
1802 let p = LoraPacket::from_wire(&wire).unwrap();
1803 assert_eq!(p.to_wire(), wire);
1804 }
1805
1806 #[test]
1807 fn verify_mic_v1_0_real_vector() {
1808 use crate::mic::V1_0MicKeys;
1809 use crate::types::NwkSKey;
1810 let bytes = hex_to_vec("40F17DBE4900020001954378762B11FF0D");
1811 let packet = LoraPacket::from_wire(&bytes).unwrap();
1812 let nwk_s_key = NwkSKey::from_slice(&hex_to_vec("44024241ed4ce9a68c6a8bc055233fd3")).unwrap();
1813 let keys = V1_0MicKeys {
1814 nwk_s_key: Some(&nwk_s_key),
1815 ..Default::default()
1816 };
1817 assert!(packet.verify_mic_v1_0(&keys).unwrap());
1818 }
1819
1820 #[test]
1821 fn recalculate_mic_v1_0_updates_mic_and_phypayload() {
1822 use crate::mic::V1_0MicKeys;
1823 use crate::types::NwkSKey;
1824 let bytes = hex_to_vec("40f17dbe490002000195437876eeeeeeee");
1825 let mut packet = LoraPacket::from_wire(&bytes).unwrap();
1826 assert_eq!(packet.mic, [0xee, 0xee, 0xee, 0xee]);
1827 let nwk_s_key = NwkSKey::from_slice(&hex_to_vec("44024241ed4ce9a68c6a8bc055233fd3")).unwrap();
1828 let keys = V1_0MicKeys {
1829 nwk_s_key: Some(&nwk_s_key),
1830 ..Default::default()
1831 };
1832 packet.recalculate_mic_v1_0(&keys).unwrap();
1833 assert_eq!(packet.mic, [0x2b, 0x11, 0xff, 0x0d]);
1834 assert_eq!(
1835 &packet.phy_payload[packet.phy_payload.len() - 4..],
1836 &[0x2b, 0x11, 0xff, 0x0d]
1837 );
1838 }
1839
1840 #[test]
1841 fn sign_join_accept_zero_key_vector() {
1842 use crate::types::{AppKey, AppNonce, NetId};
1843
1844 let app_key = AppKey::new([0u8; 16]);
1845 let (packet, encrypted_wire) = LoraPacket::builder()
1846 .join_accept()
1847 .join_nonce(AppNonce::new([0, 0, 0]))
1848 .net_id(NetId::new([0, 0, 0]))
1849 .dev_addr(DevAddr::new([0, 0, 0, 0]))
1850 .dl_settings(DlSettings(0))
1851 .rx_delay(0)
1852 .sign_join_accept(&app_key)
1853 .unwrap();
1854
1855 assert_eq!(packet.mic, [0xf8, 0x6f, 0x0a, 0x91]);
1857 let expected_encrypted = hex_to_vec("20e3de108795f776b8037610ef7869b5b3");
1859 assert_eq!(encrypted_wire, expected_encrypted);
1860 }
1861
1862 #[test]
1863 fn sign_join_request_produces_verifiable_mic() {
1864 use crate::mic::V1_0MicKeys;
1865 use crate::types::AppKey;
1866
1867 let app_key = AppKey::new([0u8; 16]);
1868 let packet = LoraPacket::builder()
1869 .join_request()
1870 .join_eui(AppEui::new([0u8; 8]))
1871 .dev_eui(DevEui::new([0u8; 8]))
1872 .dev_nonce(DevNonce::new([0u8; 2]))
1873 .sign_join_request(&app_key)
1874 .unwrap();
1875
1876 let keys = V1_0MicKeys {
1877 app_key: Some(&app_key),
1878 ..Default::default()
1879 };
1880 assert!(packet.verify_mic_v1_0(&keys).unwrap());
1881 }
1882
1883 #[test]
1884 fn sign_join_request_v1_1_works() {
1885 use crate::mic::V1_1MicKeys;
1886 use crate::types::NwkKey;
1887 let nwk_key = NwkKey::new([0u8; 16]);
1888 let packet = LoraPacket::builder()
1889 .join_request()
1890 .join_eui(AppEui::new([0; 8]))
1891 .dev_eui(DevEui::new([0; 8]))
1892 .dev_nonce(DevNonce::new([0; 2]))
1893 .sign_join_request_v1_1(&nwk_key)
1894 .unwrap();
1895 let keys = V1_1MicKeys {
1896 nwk_key: Some(&nwk_key),
1897 ..Default::default()
1898 };
1899 assert!(packet.verify_mic_v1_1(&keys).unwrap());
1900 }
1901
1902 #[test]
1903 fn build_unsigned_rejects_fopts_too_long() {
1904 let too_many = alloc::vec![0u8; 16];
1905 let result = LoraPacket::builder()
1906 .data(Direction::Uplink, false)
1907 .dev_addr(DevAddr::new([0; 4]))
1908 .f_opts(&too_many)
1909 .build_unsigned();
1910 assert!(matches!(result, Err(crate::Error::FOptsTooLong(16))));
1911 }
1912
1913 #[test]
1914 fn from_wire_rejects_oversized_buffer() {
1915 let huge = alloc::vec![0u8; 257];
1918 let err = LoraPacket::from_wire(&huge).unwrap_err();
1919 assert!(matches!(err, crate::Error::TooLong { got: 257 }));
1920 }
1921
1922 #[test]
1923 fn from_wire_accepts_max_size_buffer() {
1924 let mut bytes = alloc::vec![0u8; 256];
1928 bytes[0] = 0b1110_0000; let result = LoraPacket::from_wire(&bytes);
1930 assert!(result.is_ok(), "256-byte buffer should parse, got {result:?}");
1931 }
1932
1933 #[test]
1934 fn builder_rj_count_0_persists_through_rejoin_type_0() {
1935 use crate::types::NetId;
1936 let packet = LoraPacket::builder()
1937 .rejoin_request(0)
1938 .net_id(NetId::new([0, 0, 0]))
1939 .dev_eui(DevEui::new([0; 8]))
1940 .rj_count_0(0x1234)
1941 .build_unsigned()
1942 .unwrap();
1943 match &packet.payload {
1944 Payload::RejoinRequest(RejoinRequest::Type0 { rj_count_0, .. }) => {
1945 assert_eq!(rj_count_0, &0x1234u16.to_le_bytes());
1946 }
1947 other => panic!("expected Rejoin Type 0, got {other:?}"),
1948 }
1949 }
1950
1951 #[test]
1952 fn builder_rj_count_1_persists_through_rejoin_type_1() {
1953 let packet = LoraPacket::builder()
1954 .rejoin_request(1)
1955 .join_eui(AppEui::new([0; 8]))
1956 .dev_eui(DevEui::new([0; 8]))
1957 .rj_count_1(0xBEEF)
1958 .build_unsigned()
1959 .unwrap();
1960 match &packet.payload {
1961 Payload::RejoinRequest(RejoinRequest::Type1 { rj_count_1, .. }) => {
1962 assert_eq!(rj_count_1, &0xBEEFu16.to_le_bytes());
1963 }
1964 other => panic!("expected Rejoin Type 1, got {other:?}"),
1965 }
1966 }
1967
1968 #[test]
1969 fn builder_rj_count_0_persists_through_rejoin_type_2() {
1970 use crate::types::NetId;
1971 let packet = LoraPacket::builder()
1972 .rejoin_request(2)
1973 .net_id(NetId::new([0, 0, 0]))
1974 .dev_eui(DevEui::new([0; 8]))
1975 .rj_count_0(0x00DB)
1976 .build_unsigned()
1977 .unwrap();
1978 match &packet.payload {
1979 Payload::RejoinRequest(RejoinRequest::Type2 { rj_count_0, .. }) => {
1980 assert_eq!(rj_count_0, &0x00DBu16.to_le_bytes());
1981 }
1982 other => panic!("expected Rejoin Type 2, got {other:?}"),
1983 }
1984 }
1985
1986 #[test]
1987 fn builder_rejoin_defaults_to_zero_counters() {
1988 use crate::types::NetId;
1989 let packet = LoraPacket::builder()
1990 .rejoin_request(0)
1991 .net_id(NetId::new([0, 0, 0]))
1992 .dev_eui(DevEui::new([0; 8]))
1993 .build_unsigned()
1994 .unwrap();
1995 match &packet.payload {
1996 Payload::RejoinRequest(RejoinRequest::Type0 { rj_count_0, .. }) => {
1997 assert_eq!(rj_count_0, &[0, 0]);
1998 }
1999 other => panic!("expected Rejoin Type 0, got {other:?}"),
2000 }
2001 }
2002
2003 #[test]
2004 fn builder_overrides_f_ctrl_low_nibble_with_actual_fopts_len() {
2005 let packet = LoraPacket::builder()
2009 .data(Direction::Uplink, false)
2010 .dev_addr(DevAddr::new([1, 2, 3, 4]))
2011 .f_ctrl(FCtrl(0xA0))
2012 .f_opts(&[0x01, 0x02, 0x03])
2013 .build_unsigned()
2014 .unwrap();
2015 let data = packet.as_data().unwrap();
2016 assert_eq!(data.f_ctrl.as_byte(), 0xA3, "upper nibble preserved, low nibble = 3");
2017 assert_eq!(data.f_opts.len(), 3);
2018 }
2019
2020 #[test]
2021 fn sign_and_encrypt_round_trip() {
2022 use crate::mic::V1_0MicKeys;
2023 use crate::types::{AppSKey, NwkSKey};
2024
2025 let app_s_key = AppSKey::from_slice(&hex_to_vec("ec925802ae430ca77fd3dd73cb2cc588")).unwrap();
2026 let nwk_s_key = NwkSKey::from_slice(&hex_to_vec("44024241ed4ce9a68c6a8bc055233fd3")).unwrap();
2027
2028 let packet = LoraPacket::builder()
2029 .data(Direction::Uplink, false)
2030 .dev_addr(DevAddr::new([0x49, 0xbe, 0x7d, 0xf1]))
2031 .f_ctrl(FCtrl(0))
2032 .f_cnt(2)
2033 .f_port(1)
2034 .payload(b"test")
2035 .sign_and_encrypt(&app_s_key, &nwk_s_key)
2036 .unwrap();
2037
2038 let d = packet.as_data().unwrap();
2040 assert_eq!(d.frm_payload.as_deref(), Some(&[0x95, 0x43, 0x78, 0x76][..]));
2041
2042 assert_eq!(packet.mic, [0x2b, 0x11, 0xff, 0x0d]);
2044
2045 let expected_wire = hex_to_vec("40f17dbe4900020001954378762b11ff0d");
2047 assert_eq!(packet.phy_payload, expected_wire);
2048
2049 let keys = V1_0MicKeys {
2051 nwk_s_key: Some(&nwk_s_key),
2052 ..Default::default()
2053 };
2054 assert!(packet.verify_mic_v1_0(&keys).unwrap());
2055 }
2056}
2057
2058#[cfg(test)]
2059mod prop_tests {
2060 use super::*;
2061 use proptest::prelude::*;
2062
2063 proptest! {
2064 #[test]
2065 fn from_wire_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..=1000)) {
2066 let _ = LoraPacket::from_wire(&bytes);
2068 }
2069 }
2070}