pamoja_ffi/lorawan.rs
1//! The C ABI for LoRaWAN 1.0.x MAC framing.
2//!
3//! These functions wrap [`pamoja_lorawan`] for callers that reach the SDK through
4//! the flat C boundary: the secured frame a long-range node puts on the air, and
5//! the over-the-air activation that hands it its session keys.
6//!
7//! A session and a device hold key material, so they cross as opaque handles and
8//! the keys never leave the library once set. An encoded frame comes back as a
9//! [`PamojaBuffer`], and a decoded one as a handle carrying its recovered payload.
10//! The header flags a sender chooses are only scalars, so they cross by value as
11//! [`PamojaLorawanFlags`].
12
13use std::ptr;
14
15use pamoja_lorawan::{
16 Device, Direction, Downlink, FrameHeader, JoinAccept, JoinGrant, JoinRequest, LorawanError,
17 MessageType, PhyPayload, RxData, Session, Uplink,
18};
19
20use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
21
22/// The largest LoRaWAN frame, in bytes, this build accepts.
23pub const PAMOJA_LORAWAN_FRAME_MAX: usize = 256;
24
25/// The largest application payload, in bytes, a single frame can carry.
26pub const PAMOJA_LORAWAN_PAYLOAD_MAX: usize = 243;
27
28/// The length of a LoRaWAN key, in bytes.
29pub const PAMOJA_LORAWAN_KEY_LEN: usize = 16;
30
31/// The length of a LoRaWAN EUI, in bytes.
32pub const PAMOJA_LORAWAN_EUI_LEN: usize = 8;
33
34/// The direction a frame traveled, which its MIC and encryption both fold in.
35#[repr(C)]
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PamojaLorawanDirection {
38 /// From an end device up to the network.
39 Uplink = 0,
40 /// From the network down to an end device.
41 Downlink = 1,
42}
43
44/// The header flags a sender sets on a data frame.
45///
46/// Each is `1` for on and `0` for off. `fpending` applies to a downlink only and
47/// is ignored when encoding an uplink, and `adr_ack_req` applies to an uplink only
48/// and is ignored when encoding a downlink.
49#[repr(C)]
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PamojaLorawanFlags {
52 /// Ask the far end to acknowledge this frame.
53 pub confirmed: u8,
54 /// Mark the frame as taking part in adaptive data rate.
55 pub adr: u8,
56 /// Acknowledge the last confirmed frame from the far end.
57 pub ack: u8,
58 /// Tell the device more downlink data is waiting.
59 pub fpending: u8,
60 /// Ask the network to answer, because a device running adaptive data rate has gone
61 /// too long without hearing it.
62 pub adr_ack_req: u8,
63}
64
65/// An opaque handle to an activated LoRaWAN session.
66///
67/// Holds a device address and the two session keys, and never hands them back.
68/// Release it with [`pamoja_lorawan_session_free`].
69pub struct PamojaLorawanSession {
70 pub(crate) session: Session,
71}
72
73/// An opaque handle to the root credentials of a device.
74///
75/// Holds the EUIs and the application key that over-the-air activation is built
76/// on. Release it with [`pamoja_lorawan_device_free`].
77pub struct PamojaLorawanDevice {
78 pub(crate) device: Device,
79}
80
81/// An opaque handle to an accepted join.
82///
83/// Read the network settings off it, then take the session it grants with
84/// [`pamoja_lorawan_join_accept_session`]. Release it with
85/// [`pamoja_lorawan_join_accept_free`].
86pub struct PamojaLorawanJoinAccept {
87 accept: JoinAccept,
88}
89
90/// An opaque handle to a decoded data frame.
91///
92/// What a successful [`pamoja_lorawan_session_decode`] produces: the header fields
93/// and the decrypted payload. Release it with [`pamoja_lorawan_rx_free`].
94pub struct PamojaLorawanRx {
95 rx: RxData,
96}
97
98/// Creates a session from a device address and its two session keys.
99///
100/// # Arguments
101///
102/// * `dev_addr` - the device address the network assigned.
103/// * `nwk_skey` - the 16-byte network session key, which authenticates frames.
104/// * `nwk_skey_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
105/// * `app_skey` - the 16-byte application session key, which encrypts payloads.
106/// * `app_skey_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
107/// * `out_session` - receives the new session.
108///
109/// # Returns
110///
111/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller
112/// must release with [`pamoja_lorawan_session_free`], or
113/// [`PamojaStatus::InvalidArgument`] if either key is the wrong length.
114///
115/// # Safety
116///
117/// Each key pointer must point to at least its stated length in readable bytes,
118/// and `out_session` must point to a writable `*mut PamojaLorawanSession`.
119#[no_mangle]
120pub unsafe extern "C" fn pamoja_lorawan_session_new(
121 dev_addr: u32,
122 nwk_skey: *const u8,
123 nwk_skey_len: usize,
124 app_skey: *const u8,
125 app_skey_len: usize,
126 out_session: *mut *mut PamojaLorawanSession,
127) -> PamojaStatus {
128 if out_session.is_null() {
129 set_last_error("out_session must not be null".to_owned());
130 return PamojaStatus::InvalidArgument;
131 }
132 let slot = &mut *out_session;
133 *slot = ptr::null_mut();
134
135 let nwk_skey = match key(nwk_skey, nwk_skey_len, "the network session key") {
136 Ok(key) => key,
137 Err(status) => return status,
138 };
139 let app_skey = match key(app_skey, app_skey_len, "the application session key") {
140 Ok(key) => key,
141 Err(status) => return status,
142 };
143
144 *slot = Box::into_raw(Box::new(PamojaLorawanSession {
145 session: Session::new(dev_addr, nwk_skey, app_skey),
146 }));
147 PamojaStatus::Ok
148}
149
150/// Returns the device address a session is bound to.
151///
152/// # Returns
153///
154/// The device address, or 0 if `session` is null.
155///
156/// # Safety
157///
158/// `session` must be a live handle from a call that produced one, or null.
159#[no_mangle]
160pub unsafe extern "C" fn pamoja_lorawan_session_dev_addr(
161 session: *const PamojaLorawanSession,
162) -> u32 {
163 if session.is_null() {
164 return 0;
165 }
166 (*session).session.dev_addr()
167}
168
169/// Encodes an uplink data frame, encrypting the payload and appending the MIC.
170///
171/// # Arguments
172///
173/// * `session` - the activated session to send from.
174/// * `fcnt` - the frame counter for this uplink.
175/// * `fport` - the port; `0` for MAC commands, otherwise an application port.
176/// * `payload` - the application payload to carry.
177/// * `payload_len` - its length.
178/// * `fopts` - the frame options to carry in the header, at most 15 bytes.
179/// * `fopts_len` - their length.
180/// * `flags` - the header flags to set; `fpending` is ignored on an uplink.
181/// * `out_frame` - receives the encoded frame.
182///
183/// # Returns
184///
185/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
186/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
187/// [`PamojaStatus::InvalidArgument`] if the payload and options do not fit one
188/// frame.
189///
190/// # Safety
191///
192/// `payload` and `fopts` must each point to at least their stated lengths in
193/// readable bytes when those lengths are non-zero, and `out_frame` must point to a
194/// writable `*mut PamojaBuffer`.
195#[no_mangle]
196#[allow(clippy::too_many_arguments)]
197pub unsafe extern "C" fn pamoja_lorawan_session_encode_uplink(
198 session: *const PamojaLorawanSession,
199 fcnt: u32,
200 fport: u8,
201 payload: *const u8,
202 payload_len: usize,
203 fopts: *const u8,
204 fopts_len: usize,
205 flags: PamojaLorawanFlags,
206 out_frame: *mut *mut PamojaBuffer,
207) -> PamojaStatus {
208 encode(
209 session,
210 payload,
211 payload_len,
212 fopts,
213 fopts_len,
214 out_frame,
215 |session, payload, fopts| {
216 let mut uplink = Uplink::new(fcnt, fport, payload).with_fopts(fopts);
217 if flags.confirmed != 0 {
218 uplink = uplink.confirmed();
219 }
220 if flags.adr != 0 {
221 uplink = uplink.with_adr();
222 }
223 if flags.ack != 0 {
224 uplink = uplink.with_ack();
225 }
226 if flags.adr_ack_req != 0 {
227 uplink = uplink.with_adr_ack_req();
228 }
229 session.encode_uplink(&uplink)
230 },
231 )
232}
233
234/// Encodes a downlink data frame, encrypting the payload and appending the MIC.
235///
236/// # Arguments
237///
238/// * `session` - the session the frame is addressed to.
239/// * `fcnt` - the frame counter for this downlink.
240/// * `fport` - the port; `0` for MAC commands, otherwise an application port.
241/// * `payload` - the application payload to carry.
242/// * `payload_len` - its length.
243/// * `fopts` - the frame options to carry in the header, at most 15 bytes.
244/// * `fopts_len` - their length.
245/// * `flags` - the header flags to set.
246/// * `out_frame` - receives the encoded frame.
247///
248/// # Returns
249///
250/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
251/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
252/// [`PamojaStatus::InvalidArgument`] if the payload and options do not fit one
253/// frame.
254///
255/// # Safety
256///
257/// `payload` and `fopts` must each point to at least their stated lengths in
258/// readable bytes when those lengths are non-zero, and `out_frame` must point to a
259/// writable `*mut PamojaBuffer`.
260#[no_mangle]
261#[allow(clippy::too_many_arguments)]
262pub unsafe extern "C" fn pamoja_lorawan_session_encode_downlink(
263 session: *const PamojaLorawanSession,
264 fcnt: u32,
265 fport: u8,
266 payload: *const u8,
267 payload_len: usize,
268 fopts: *const u8,
269 fopts_len: usize,
270 flags: PamojaLorawanFlags,
271 out_frame: *mut *mut PamojaBuffer,
272) -> PamojaStatus {
273 encode(
274 session,
275 payload,
276 payload_len,
277 fopts,
278 fopts_len,
279 out_frame,
280 |session, payload, fopts| {
281 let mut downlink = Downlink::new(fcnt, fport, payload).with_fopts(fopts);
282 if flags.confirmed != 0 {
283 downlink = downlink.confirmed();
284 }
285 if flags.adr != 0 {
286 downlink = downlink.with_adr();
287 }
288 if flags.ack != 0 {
289 downlink = downlink.with_ack();
290 }
291 if flags.fpending != 0 {
292 downlink = downlink.with_fpending();
293 }
294 session.encode_downlink(&downlink)
295 },
296 )
297}
298
299/// Decodes a received data frame: verifies the MIC, then decrypts the payload.
300///
301/// # Arguments
302///
303/// * `session` - the session the frame belongs to.
304/// * `bytes` - the frame exactly as it came off the radio.
305/// * `bytes_len` - its length.
306/// * `fcnt` - the full 32-bit frame counter expected for this frame; its low 16
307/// bits must match the counter the frame carries.
308/// * `out_rx` - receives the decoded frame.
309///
310/// # Returns
311///
312/// [`PamojaStatus::Ok`] on success, with `*out_rx` set to a handle the caller must
313/// release with [`pamoja_lorawan_rx_free`], [`PamojaStatus::Auth`] if the MIC does
314/// not verify or the counter does not match, or [`PamojaStatus::Codec`] if the
315/// frame is truncated or is not a data frame.
316///
317/// # Safety
318///
319/// `bytes` must point to at least `bytes_len` readable bytes when that length is
320/// non-zero, and `out_rx` must point to a writable `*mut PamojaLorawanRx`.
321#[no_mangle]
322pub unsafe extern "C" fn pamoja_lorawan_session_decode(
323 session: *const PamojaLorawanSession,
324 bytes: *const u8,
325 bytes_len: usize,
326 fcnt: u32,
327 out_rx: *mut *mut PamojaLorawanRx,
328) -> PamojaStatus {
329 if out_rx.is_null() {
330 set_last_error("out_rx must not be null".to_owned());
331 return PamojaStatus::InvalidArgument;
332 }
333 let slot = &mut *out_rx;
334 *slot = ptr::null_mut();
335
336 if session.is_null() {
337 set_last_error("session must not be null".to_owned());
338 return PamojaStatus::InvalidArgument;
339 }
340 let bytes = match read_bytes(bytes, bytes_len) {
341 Ok(bytes) => bytes,
342 Err(status) => return status,
343 };
344 match (*session).session.decode(&bytes, fcnt) {
345 Ok(rx) => {
346 *slot = Box::into_raw(Box::new(PamojaLorawanRx { rx }));
347 PamojaStatus::Ok
348 }
349 Err(error) => failed(error),
350 }
351}
352
353/// Releases a session handle.
354///
355/// Passing null is a no-op.
356///
357/// # Safety
358///
359/// `session` must be a handle from a call that produced one and that has not
360/// already been freed, or null. After this call it must not be used again.
361#[no_mangle]
362pub unsafe extern "C" fn pamoja_lorawan_session_free(session: *mut PamojaLorawanSession) {
363 if !session.is_null() {
364 drop(Box::from_raw(session));
365 }
366}
367
368/// Returns the direction a decoded frame traveled.
369///
370/// # Returns
371///
372/// The direction, or [`PamojaLorawanDirection::Uplink`] if `rx` is null.
373///
374/// # Safety
375///
376/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
377#[no_mangle]
378pub unsafe extern "C" fn pamoja_lorawan_rx_direction(
379 rx: *const PamojaLorawanRx,
380) -> PamojaLorawanDirection {
381 if rx.is_null() {
382 return PamojaLorawanDirection::Uplink;
383 }
384 match (*rx).rx.direction() {
385 Direction::Uplink => PamojaLorawanDirection::Uplink,
386 Direction::Downlink => PamojaLorawanDirection::Downlink,
387 }
388}
389
390/// Returns the device address a decoded frame carries.
391///
392/// # Returns
393///
394/// The device address, or 0 if `rx` is null.
395///
396/// # Safety
397///
398/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
399#[no_mangle]
400pub unsafe extern "C" fn pamoja_lorawan_rx_dev_addr(rx: *const PamojaLorawanRx) -> u32 {
401 if rx.is_null() {
402 return 0;
403 }
404 (*rx).rx.dev_addr()
405}
406
407/// Returns the low 16 bits of the frame counter a decoded frame carries.
408///
409/// # Returns
410///
411/// The counter, or 0 if `rx` is null.
412///
413/// # Safety
414///
415/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
416#[no_mangle]
417pub unsafe extern "C" fn pamoja_lorawan_rx_fcnt(rx: *const PamojaLorawanRx) -> u16 {
418 if rx.is_null() {
419 return 0;
420 }
421 (*rx).rx.fcnt()
422}
423
424/// Reports whether a decoded frame asks to be acknowledged.
425///
426/// # Returns
427///
428/// `true` when the frame is confirmed, or `false` if `rx` is null.
429///
430/// # Safety
431///
432/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
433#[no_mangle]
434pub unsafe extern "C" fn pamoja_lorawan_rx_confirmed(rx: *const PamojaLorawanRx) -> bool {
435 !rx.is_null() && (*rx).rx.confirmed()
436}
437
438/// Reports whether a decoded frame takes part in adaptive data rate.
439///
440/// # Returns
441///
442/// `true` when the ADR bit is set, or `false` if `rx` is null.
443///
444/// # Safety
445///
446/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
447#[no_mangle]
448pub unsafe extern "C" fn pamoja_lorawan_rx_adr(rx: *const PamojaLorawanRx) -> bool {
449 !rx.is_null() && (*rx).rx.adr()
450}
451
452/// Reports whether a decoded uplink asks the network to answer.
453///
454/// A device running adaptive data rate sets this once it has gone too long without a
455/// downlink. The bit is reserved on a downlink, which reads `false`.
456///
457/// # Returns
458///
459/// `true` when the ADRACKReq bit of an uplink is set, or `false` if `rx` is null.
460///
461/// # Safety
462///
463/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
464#[no_mangle]
465pub unsafe extern "C" fn pamoja_lorawan_rx_adr_ack_req(rx: *const PamojaLorawanRx) -> bool {
466 !rx.is_null() && (*rx).rx.adr_ack_req()
467}
468
469/// Reports whether a decoded uplink came from a device running Class B.
470///
471/// # Returns
472///
473/// `true` when the ClassB bit of an uplink is set, or `false` for a downlink or if `rx`
474/// is null.
475///
476/// # Safety
477///
478/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
479#[no_mangle]
480pub unsafe extern "C" fn pamoja_lorawan_rx_class_b(rx: *const PamojaLorawanRx) -> bool {
481 !rx.is_null() && (*rx).rx.class_b()
482}
483
484/// Reports whether a decoded frame acknowledges the last confirmed one.
485///
486/// # Returns
487///
488/// `true` when the ACK bit is set, or `false` if `rx` is null.
489///
490/// # Safety
491///
492/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
493#[no_mangle]
494pub unsafe extern "C" fn pamoja_lorawan_rx_ack(rx: *const PamojaLorawanRx) -> bool {
495 !rx.is_null() && (*rx).rx.ack()
496}
497
498/// Reports whether the network has more downlink data waiting.
499///
500/// # Returns
501///
502/// `true` when the frame-pending bit is set, or `false` if `rx` is null.
503///
504/// # Safety
505///
506/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
507#[no_mangle]
508pub unsafe extern "C" fn pamoja_lorawan_rx_fpending(rx: *const PamojaLorawanRx) -> bool {
509 !rx.is_null() && (*rx).rx.fpending()
510}
511
512/// Returns the port a decoded frame was sent on.
513///
514/// # Arguments
515///
516/// * `rx` - the decoded frame.
517/// * `out_fport` - receives the port.
518///
519/// # Returns
520///
521/// `true` when the frame carries a port, with `*out_fport` written, or `false`
522/// for a frame that carries only frame options and so has none.
523///
524/// # Safety
525///
526/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null,
527/// and `out_fport` must point to a writable `uint8_t`.
528#[no_mangle]
529pub unsafe extern "C" fn pamoja_lorawan_rx_fport(
530 rx: *const PamojaLorawanRx,
531 out_fport: *mut u8,
532) -> bool {
533 if rx.is_null() || out_fport.is_null() {
534 return false;
535 }
536 match (*rx).rx.fport() {
537 Some(fport) => {
538 *out_fport = fport;
539 true
540 }
541 None => false,
542 }
543}
544
545/// Returns a pointer to the frame options a decoded frame carries.
546///
547/// Use [`pamoja_lorawan_rx_fopts_len`] for the length. The pointer is valid until
548/// the frame is freed.
549///
550/// # Returns
551///
552/// A pointer to the options, or null if `rx` is null or there are none.
553///
554/// # Safety
555///
556/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
557#[no_mangle]
558pub unsafe extern "C" fn pamoja_lorawan_rx_fopts(rx: *const PamojaLorawanRx) -> *const u8 {
559 if rx.is_null() {
560 return ptr::null();
561 }
562 let fopts = (*rx).rx.fopts();
563 if fopts.is_empty() {
564 ptr::null()
565 } else {
566 fopts.as_ptr()
567 }
568}
569
570/// Returns the length in bytes of the frame options a decoded frame carries.
571///
572/// # Returns
573///
574/// The length, or 0 if `rx` is null.
575///
576/// # Safety
577///
578/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
579#[no_mangle]
580pub unsafe extern "C" fn pamoja_lorawan_rx_fopts_len(rx: *const PamojaLorawanRx) -> usize {
581 if rx.is_null() {
582 return 0;
583 }
584 (*rx).rx.fopts().len()
585}
586
587/// Returns a pointer to the decrypted payload of a decoded frame.
588///
589/// Use [`pamoja_lorawan_rx_payload_len`] for the length. The pointer is valid
590/// until the frame is freed.
591///
592/// # Returns
593///
594/// A pointer to the payload, or null if `rx` is null or the payload is empty.
595///
596/// # Safety
597///
598/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
599#[no_mangle]
600pub unsafe extern "C" fn pamoja_lorawan_rx_payload(rx: *const PamojaLorawanRx) -> *const u8 {
601 if rx.is_null() {
602 return ptr::null();
603 }
604 let payload = (*rx).rx.payload();
605 if payload.is_empty() {
606 ptr::null()
607 } else {
608 payload.as_ptr()
609 }
610}
611
612/// Returns the length in bytes of the decrypted payload of a decoded frame.
613///
614/// # Returns
615///
616/// The payload length, or 0 if `rx` is null.
617///
618/// # Safety
619///
620/// `rx` must be a live handle from [`pamoja_lorawan_session_decode`], or null.
621#[no_mangle]
622pub unsafe extern "C" fn pamoja_lorawan_rx_payload_len(rx: *const PamojaLorawanRx) -> usize {
623 if rx.is_null() {
624 return 0;
625 }
626 (*rx).rx.payload().len()
627}
628
629/// Releases a decoded frame handle.
630///
631/// Passing null is a no-op.
632///
633/// # Safety
634///
635/// `rx` must be a handle from [`pamoja_lorawan_session_decode`] that has not
636/// already been freed, or null. After this call it must not be used again.
637#[no_mangle]
638pub unsafe extern "C" fn pamoja_lorawan_rx_free(rx: *mut PamojaLorawanRx) {
639 if !rx.is_null() {
640 drop(Box::from_raw(rx));
641 }
642}
643
644/// Creates a device from the root credentials over-the-air activation uses.
645///
646/// # Arguments
647///
648/// * `dev_eui` - the 8-byte device EUI.
649/// * `dev_eui_len` - its length, which must be [`PAMOJA_LORAWAN_EUI_LEN`].
650/// * `app_eui` - the 8-byte application (join) EUI.
651/// * `app_eui_len` - its length, which must be [`PAMOJA_LORAWAN_EUI_LEN`].
652/// * `app_key` - the 16-byte application key the join exchange is secured with.
653/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
654/// * `out_device` - receives the new device.
655///
656/// # Returns
657///
658/// [`PamojaStatus::Ok`] on success, with `*out_device` set to a handle the caller
659/// must release with [`pamoja_lorawan_device_free`], or
660/// [`PamojaStatus::InvalidArgument`] if any credential is the wrong length.
661///
662/// # Safety
663///
664/// Each pointer must point to at least its stated length in readable bytes, and
665/// `out_device` must point to a writable `*mut PamojaLorawanDevice`.
666#[no_mangle]
667pub unsafe extern "C" fn pamoja_lorawan_device_new(
668 dev_eui: *const u8,
669 dev_eui_len: usize,
670 app_eui: *const u8,
671 app_eui_len: usize,
672 app_key: *const u8,
673 app_key_len: usize,
674 out_device: *mut *mut PamojaLorawanDevice,
675) -> PamojaStatus {
676 if out_device.is_null() {
677 set_last_error("out_device must not be null".to_owned());
678 return PamojaStatus::InvalidArgument;
679 }
680 let slot = &mut *out_device;
681 *slot = ptr::null_mut();
682
683 let dev_eui = match eui(dev_eui, dev_eui_len, "the device EUI") {
684 Ok(eui) => eui,
685 Err(status) => return status,
686 };
687 let app_eui = match eui(app_eui, app_eui_len, "the application EUI") {
688 Ok(eui) => eui,
689 Err(status) => return status,
690 };
691 let app_key = match key(app_key, app_key_len, "the application key") {
692 Ok(key) => key,
693 Err(status) => return status,
694 };
695
696 *slot = Box::into_raw(Box::new(PamojaLorawanDevice {
697 device: Device::new(dev_eui, app_eui, app_key),
698 }));
699 PamojaStatus::Ok
700}
701
702/// Builds the join request a device broadcasts to activate.
703///
704/// # Arguments
705///
706/// * `device` - the device to activate.
707/// * `dev_nonce` - a nonce that must never repeat for this device, since the
708/// network rejects a replayed one.
709/// * `out_frame` - receives the encoded join request.
710///
711/// # Returns
712///
713/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
714/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
715/// [`PamojaStatus::InvalidArgument`] if `device` is null.
716///
717/// # Safety
718///
719/// `device` must be a live handle from [`pamoja_lorawan_device_new`], or null, and
720/// `out_frame` must point to a writable `*mut PamojaBuffer`.
721#[no_mangle]
722pub unsafe extern "C" fn pamoja_lorawan_device_join_request(
723 device: *const PamojaLorawanDevice,
724 dev_nonce: u16,
725 out_frame: *mut *mut PamojaBuffer,
726) -> PamojaStatus {
727 if out_frame.is_null() {
728 set_last_error("out_frame must not be null".to_owned());
729 return PamojaStatus::InvalidArgument;
730 }
731 let slot = &mut *out_frame;
732 *slot = ptr::null_mut();
733
734 if device.is_null() {
735 set_last_error("device must not be null".to_owned());
736 return PamojaStatus::InvalidArgument;
737 }
738 let request = (*device).device.join_request(dev_nonce);
739 *slot = PamojaBuffer::into_raw(request.as_bytes().to_vec());
740 PamojaStatus::Ok
741}
742
743/// Turns the join accept a network sent into the settings it grants.
744///
745/// # Arguments
746///
747/// * `device` - the device that sent the join request.
748/// * `bytes` - the join accept exactly as it arrived.
749/// * `bytes_len` - its length.
750/// * `dev_nonce` - the nonce the matching join request carried.
751/// * `out_accept` - receives the accepted join.
752///
753/// # Returns
754///
755/// [`PamojaStatus::Ok`] on success, with `*out_accept` set to a handle the caller
756/// must release with [`pamoja_lorawan_join_accept_free`],
757/// [`PamojaStatus::Auth`] if the MIC does not verify, or
758/// [`PamojaStatus::Codec`] if the frame is truncated or is not a join accept.
759///
760/// # Safety
761///
762/// `device` must be a live handle from [`pamoja_lorawan_device_new`], or null,
763/// `bytes` must point to at least `bytes_len` readable bytes when that length is
764/// non-zero, and `out_accept` must point to a writable
765/// `*mut PamojaLorawanJoinAccept`.
766#[no_mangle]
767pub unsafe extern "C" fn pamoja_lorawan_device_accept_join(
768 device: *const PamojaLorawanDevice,
769 bytes: *const u8,
770 bytes_len: usize,
771 dev_nonce: u16,
772 out_accept: *mut *mut PamojaLorawanJoinAccept,
773) -> PamojaStatus {
774 if out_accept.is_null() {
775 set_last_error("out_accept must not be null".to_owned());
776 return PamojaStatus::InvalidArgument;
777 }
778 let slot = &mut *out_accept;
779 *slot = ptr::null_mut();
780
781 if device.is_null() {
782 set_last_error("device must not be null".to_owned());
783 return PamojaStatus::InvalidArgument;
784 }
785 let bytes = match read_bytes(bytes, bytes_len) {
786 Ok(bytes) => bytes,
787 Err(status) => return status,
788 };
789 match (*device).device.accept_join(&bytes, dev_nonce) {
790 Ok(accept) => {
791 *slot = Box::into_raw(Box::new(PamojaLorawanJoinAccept { accept }));
792 PamojaStatus::Ok
793 }
794 Err(error) => failed(error),
795 }
796}
797
798/// Copies the device EUI a device was created with.
799///
800/// # Arguments
801///
802/// * `device` - the device.
803/// * `out_dev_eui` - receives [`PAMOJA_LORAWAN_EUI_LEN`] bytes, most-significant byte
804/// first.
805///
806/// # Returns
807///
808/// `true` when the identifier was written, or `false` if either pointer is null.
809///
810/// # Safety
811///
812/// `device` must be a live handle from [`pamoja_lorawan_device_new`], or null, and
813/// `out_dev_eui` must point to at least [`PAMOJA_LORAWAN_EUI_LEN`] writable bytes.
814#[no_mangle]
815pub unsafe extern "C" fn pamoja_lorawan_device_dev_eui(
816 device: *const PamojaLorawanDevice,
817 out_dev_eui: *mut u8,
818) -> bool {
819 if device.is_null() || out_dev_eui.is_null() {
820 return false;
821 }
822 let eui = (*device).device.dev_eui();
823 ptr::copy_nonoverlapping(eui.as_ptr(), out_dev_eui, PAMOJA_LORAWAN_EUI_LEN);
824 true
825}
826
827/// Releases a device handle.
828///
829/// Passing null is a no-op.
830///
831/// # Safety
832///
833/// `device` must be a handle from [`pamoja_lorawan_device_new`] that has not
834/// already been freed, or null. After this call it must not be used again.
835#[no_mangle]
836pub unsafe extern "C" fn pamoja_lorawan_device_free(device: *mut PamojaLorawanDevice) {
837 if !device.is_null() {
838 drop(Box::from_raw(device));
839 }
840}
841
842/// Returns the device address a join grants.
843///
844/// # Returns
845///
846/// The device address, or 0 if `accept` is null.
847///
848/// # Safety
849///
850/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
851/// null.
852#[no_mangle]
853pub unsafe extern "C" fn pamoja_lorawan_join_accept_dev_addr(
854 accept: *const PamojaLorawanJoinAccept,
855) -> u32 {
856 if accept.is_null() {
857 return 0;
858 }
859 (*accept).accept.dev_addr()
860}
861
862/// Returns the identifier of the network that accepted a join.
863///
864/// # Returns
865///
866/// The network identifier, or 0 if `accept` is null.
867///
868/// # Safety
869///
870/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
871/// null.
872#[no_mangle]
873pub unsafe extern "C" fn pamoja_lorawan_join_accept_net_id(
874 accept: *const PamojaLorawanJoinAccept,
875) -> u32 {
876 if accept.is_null() {
877 return 0;
878 }
879 (*accept).accept.net_id()
880}
881
882/// Returns the downlink settings byte a join grants.
883///
884/// # Returns
885///
886/// The settings byte, which carries the second receive window data rate and the
887/// first window offset, or 0 if `accept` is null.
888///
889/// # Safety
890///
891/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
892/// null.
893#[no_mangle]
894pub unsafe extern "C" fn pamoja_lorawan_join_accept_dl_settings(
895 accept: *const PamojaLorawanJoinAccept,
896) -> u8 {
897 if accept.is_null() {
898 return 0;
899 }
900 (*accept).accept.dl_settings()
901}
902
903/// Returns the delay before the first receive window, in seconds.
904///
905/// # Returns
906///
907/// The delay, or 0 if `accept` is null.
908///
909/// # Safety
910///
911/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
912/// null.
913#[no_mangle]
914pub unsafe extern "C" fn pamoja_lorawan_join_accept_rx_delay(
915 accept: *const PamojaLorawanJoinAccept,
916) -> u8 {
917 if accept.is_null() {
918 return 0;
919 }
920 (*accept).accept.rx_delay()
921}
922
923/// Returns how far below the uplink's data rate the first receive window listens.
924///
925/// # Returns
926///
927/// The RX1DROffset field of the downlink settings byte, or 0 if `accept` is null.
928///
929/// # Safety
930///
931/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
932/// null.
933#[no_mangle]
934pub unsafe extern "C" fn pamoja_lorawan_join_accept_rx1_dr_offset(
935 accept: *const PamojaLorawanJoinAccept,
936) -> u8 {
937 if accept.is_null() {
938 return 0;
939 }
940 (*accept).accept.rx1_dr_offset()
941}
942
943/// Returns the data rate the second receive window listens at.
944///
945/// # Returns
946///
947/// The RX2DataRate field of the downlink settings byte, or 0 if `accept` is null.
948///
949/// # Safety
950///
951/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
952/// null.
953#[no_mangle]
954pub unsafe extern "C" fn pamoja_lorawan_join_accept_rx2_data_rate(
955 accept: *const PamojaLorawanJoinAccept,
956) -> u8 {
957 if accept.is_null() {
958 return 0;
959 }
960 (*accept).accept.rx2_data_rate()
961}
962
963/// Returns the delay from the end of an uplink to the first receive window.
964///
965/// # Returns
966///
967/// The delay in microseconds, where an RxDelay of 0 means one second as the
968/// specification says, or 0 if `accept` is null.
969///
970/// # Safety
971///
972/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
973/// null.
974#[no_mangle]
975pub unsafe extern "C" fn pamoja_lorawan_join_accept_receive_delay_us(
976 accept: *const PamojaLorawanJoinAccept,
977) -> u32 {
978 if accept.is_null() {
979 return 0;
980 }
981 (*accept).accept.receive_delay_us()
982}
983
984/// Copies the channel list a join accept carried.
985///
986/// # Arguments
987///
988/// * `accept` - the accepted join.
989/// * `out_cflist` - receives the 16 CFList bytes exactly as they arrived.
990///
991/// # Returns
992///
993/// `true` when the accept carried a channel list and it was written, or `false` when it
994/// carried none or either pointer is null.
995///
996/// # Safety
997///
998/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or null,
999/// and `out_cflist` must point to at least 16 writable bytes.
1000#[no_mangle]
1001pub unsafe extern "C" fn pamoja_lorawan_join_accept_cflist(
1002 accept: *const PamojaLorawanJoinAccept,
1003 out_cflist: *mut u8,
1004) -> bool {
1005 if accept.is_null() || out_cflist.is_null() {
1006 return false;
1007 }
1008 match (*accept).accept.cflist() {
1009 Some(list) => {
1010 let bytes = list.to_bytes();
1011 ptr::copy_nonoverlapping(bytes.as_ptr(), out_cflist, bytes.len());
1012 true
1013 }
1014 None => false,
1015 }
1016}
1017
1018/// Takes the activated session a join grants.
1019///
1020/// # Arguments
1021///
1022/// * `accept` - the accepted join.
1023/// * `out_session` - receives the session.
1024///
1025/// # Returns
1026///
1027/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller
1028/// must release with [`pamoja_lorawan_session_free`], or
1029/// [`PamojaStatus::InvalidArgument`] if `accept` is null.
1030///
1031/// # Safety
1032///
1033/// `accept` must be a live handle from [`pamoja_lorawan_device_accept_join`], or
1034/// null, and `out_session` must point to a writable `*mut PamojaLorawanSession`.
1035#[no_mangle]
1036pub unsafe extern "C" fn pamoja_lorawan_join_accept_session(
1037 accept: *const PamojaLorawanJoinAccept,
1038 out_session: *mut *mut PamojaLorawanSession,
1039) -> PamojaStatus {
1040 if out_session.is_null() {
1041 set_last_error("out_session must not be null".to_owned());
1042 return PamojaStatus::InvalidArgument;
1043 }
1044 let slot = &mut *out_session;
1045 *slot = ptr::null_mut();
1046
1047 if accept.is_null() {
1048 set_last_error("accept must not be null".to_owned());
1049 return PamojaStatus::InvalidArgument;
1050 }
1051 *slot = Box::into_raw(Box::new(PamojaLorawanSession {
1052 session: (*accept).accept.session(),
1053 }));
1054 PamojaStatus::Ok
1055}
1056
1057/// Releases an accepted join handle.
1058///
1059/// Passing null is a no-op.
1060///
1061/// # Safety
1062///
1063/// `accept` must be a handle from [`pamoja_lorawan_device_accept_join`] that has
1064/// not already been freed, or null. After this call it must not be used again.
1065#[no_mangle]
1066pub unsafe extern "C" fn pamoja_lorawan_join_accept_free(accept: *mut PamojaLorawanJoinAccept) {
1067 if !accept.is_null() {
1068 drop(Box::from_raw(accept));
1069 }
1070}
1071
1072/// Encodes a data frame with whichever builder the caller asked for.
1073///
1074/// # Safety
1075///
1076/// `payload` and `fopts` must each point to at least their stated lengths in
1077/// readable bytes when those lengths are non-zero, and `out_frame` must point to a
1078/// writable `*mut PamojaBuffer`.
1079unsafe fn encode(
1080 session: *const PamojaLorawanSession,
1081 payload: *const u8,
1082 payload_len: usize,
1083 fopts: *const u8,
1084 fopts_len: usize,
1085 out_frame: *mut *mut PamojaBuffer,
1086 build: impl FnOnce(&Session, &[u8], &[u8]) -> Result<PhyPayload, LorawanError>,
1087) -> PamojaStatus {
1088 if out_frame.is_null() {
1089 set_last_error("out_frame must not be null".to_owned());
1090 return PamojaStatus::InvalidArgument;
1091 }
1092 let slot = &mut *out_frame;
1093 *slot = ptr::null_mut();
1094
1095 if session.is_null() {
1096 set_last_error("session must not be null".to_owned());
1097 return PamojaStatus::InvalidArgument;
1098 }
1099 let payload = match read_bytes(payload, payload_len) {
1100 Ok(bytes) => bytes,
1101 Err(status) => return status,
1102 };
1103 let fopts = match read_bytes(fopts, fopts_len) {
1104 Ok(bytes) => bytes,
1105 Err(status) => return status,
1106 };
1107 match build(&(*session).session, &payload, &fopts) {
1108 Ok(frame) => {
1109 *slot = PamojaBuffer::into_raw(frame.as_bytes().to_vec());
1110 PamojaStatus::Ok
1111 }
1112 Err(error) => failed(error),
1113 }
1114}
1115
1116/// Copies a borrowed 16-byte key.
1117///
1118/// # Safety
1119///
1120/// `bytes` must point to at least `len` readable bytes when that length is
1121/// non-zero.
1122unsafe fn key(bytes: *const u8, len: usize, what: &str) -> Result<[u8; 16], PamojaStatus> {
1123 let bytes = read_bytes(bytes, len)?;
1124 <[u8; PAMOJA_LORAWAN_KEY_LEN]>::try_from(&bytes[..]).map_err(|_| {
1125 set_last_error(format!(
1126 "{what} must be exactly {PAMOJA_LORAWAN_KEY_LEN} bytes"
1127 ));
1128 PamojaStatus::InvalidArgument
1129 })
1130}
1131
1132/// Copies a borrowed 8-byte EUI.
1133///
1134/// # Safety
1135///
1136/// `bytes` must point to at least `len` readable bytes when that length is
1137/// non-zero.
1138unsafe fn eui(bytes: *const u8, len: usize, what: &str) -> Result<[u8; 8], PamojaStatus> {
1139 let bytes = read_bytes(bytes, len)?;
1140 <[u8; PAMOJA_LORAWAN_EUI_LEN]>::try_from(&bytes[..]).map_err(|_| {
1141 set_last_error(format!(
1142 "{what} must be exactly {PAMOJA_LORAWAN_EUI_LEN} bytes"
1143 ));
1144 PamojaStatus::InvalidArgument
1145 })
1146}
1147
1148/// Records a LoRaWAN error and classifies it.
1149///
1150/// # Arguments
1151///
1152/// * `error` - the failure the LoRaWAN crate reported.
1153///
1154/// # Returns
1155///
1156/// [`PamojaStatus::Auth`] when a frame failed its integrity or counter check,
1157/// [`PamojaStatus::InvalidArgument`] when the caller asked for a frame that cannot
1158/// be built, and [`PamojaStatus::Codec`] when a received frame could not be read.
1159pub(crate) fn failed(error: LorawanError) -> PamojaStatus {
1160 set_last_error(error.to_string());
1161 match error {
1162 LorawanError::MicMismatch | LorawanError::FcntMismatch => PamojaStatus::Auth,
1163 LorawanError::PayloadTooLong => PamojaStatus::InvalidArgument,
1164 _ => PamojaStatus::Codec,
1165 }
1166}
1167
1168/// What kind of message a frame is, read from its header.
1169#[repr(C)]
1170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1171pub enum PamojaLorawanMessageType {
1172 /// A device asking to join a network.
1173 JoinRequest = 0,
1174 /// A network admitting a device.
1175 JoinAccept = 1,
1176 /// Data from a device that does not need acknowledging.
1177 UnconfirmedUp = 2,
1178 /// Data from a device that asks to be acknowledged.
1179 ConfirmedUp = 3,
1180 /// Data to a device that does not need acknowledging.
1181 UnconfirmedDown = 4,
1182 /// Data to a device that asks to be acknowledged.
1183 ConfirmedDown = 5,
1184}
1185
1186/// What a frame says about itself before any key is involved.
1187///
1188/// Every field is a scalar, so this crosses the boundary by value. `is_data` is
1189/// `1` when `dev_addr` and `fcnt` are meaningful, which is every message type
1190/// except the two join frames, and `has_fport` is `1` when `fport` is.
1191///
1192/// Nothing here is authenticated, since checking the MIC needs the session key.
1193/// Treat it as a routing hint until [`pamoja_lorawan_session_decode`] has verified
1194/// the frame.
1195#[repr(C)]
1196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1197pub struct PamojaLorawanHeader {
1198 /// The length of the still-encrypted payload, in bytes.
1199 pub payload_len: usize,
1200 /// The device address, meaningful only when `is_data` is `1`.
1201 pub dev_addr: u32,
1202 /// The low 16 bits of the frame counter, meaningful only when `is_data` is `1`.
1203 pub fcnt: u16,
1204 /// What kind of message the frame is.
1205 pub message_type: PamojaLorawanMessageType,
1206 /// The port the frame was sent on, meaningful only when `has_fport` is `1`.
1207 pub fport: u8,
1208 /// `1` for a data frame, `0` for one of the two join frames.
1209 pub is_data: u8,
1210 /// `1` when the frame carries a port rather than only frame options.
1211 pub has_fport: u8,
1212 /// `1` when the frame asks to be acknowledged.
1213 pub confirmed: u8,
1214 /// `1` when the frame takes part in adaptive data rate.
1215 pub adr: u8,
1216 /// `1` when the frame acknowledges the last confirmed one.
1217 pub ack: u8,
1218 /// `1` when the network has more downlink data waiting.
1219 pub fpending: u8,
1220 /// How many bytes of frame options the header carries, from 0 to 15.
1221 pub fopts_len: u8,
1222 /// `1` when an uplink asks the network to answer. Reserved on a downlink, which reads
1223 /// `0`.
1224 pub adr_ack_req: u8,
1225 /// `1` when an uplink comes from a device running Class B.
1226 pub class_b: u8,
1227}
1228
1229/// What a network grants a device that joined.
1230///
1231/// Every field is a scalar, so this crosses the boundary by value. The optional
1232/// channel list is passed alongside it, since it is bytes rather than a scalar.
1233#[repr(C)]
1234#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1235pub struct PamojaLorawanGrant {
1236 /// A nonce this network must not reuse for the device; low 24 bits only.
1237 pub app_nonce: u32,
1238 /// The network identifier; low 24 bits only.
1239 pub net_id: u32,
1240 /// The address to assign the device.
1241 pub dev_addr: u32,
1242 /// The downlink settings byte.
1243 pub dl_settings: u8,
1244 /// The delay before the first receive window, in seconds.
1245 pub rx_delay: u8,
1246}
1247
1248/// An opaque handle to a verified join-request.
1249///
1250/// Read it with the `pamoja_lorawan_join_request_*` calls, then release it with
1251/// [`pamoja_lorawan_join_request_free`].
1252pub struct PamojaLorawanJoinRequest {
1253 request: JoinRequest,
1254}
1255
1256/// Reads a frame far enough to route it, without any key.
1257///
1258/// A receiver holding many sessions uses this to find which one a frame belongs
1259/// to: the device address travels in the clear, so it can be read before the
1260/// session that would verify the frame is even known.
1261///
1262/// # Arguments
1263///
1264/// * `bytes` - the raw frame as it came off the radio.
1265/// * `bytes_len` - its length.
1266/// * `out_header` - receives the header fields.
1267///
1268/// # Returns
1269///
1270/// [`PamojaStatus::Ok`] on success, with `*out_header` filled in, or
1271/// [`PamojaStatus::Codec`] if the frame is truncated, carries a message type this
1272/// build does not read, or declares more frame options than it holds.
1273///
1274/// # Safety
1275///
1276/// `bytes` must point to at least `bytes_len` readable bytes when that length is
1277/// non-zero, and `out_header` must point to a writable [`PamojaLorawanHeader`].
1278#[no_mangle]
1279pub unsafe extern "C" fn pamoja_lorawan_header_parse(
1280 bytes: *const u8,
1281 bytes_len: usize,
1282 out_header: *mut PamojaLorawanHeader,
1283) -> PamojaStatus {
1284 if out_header.is_null() {
1285 set_last_error("out_header must not be null".to_owned());
1286 return PamojaStatus::InvalidArgument;
1287 }
1288 let bytes = match read_bytes(bytes, bytes_len) {
1289 Ok(bytes) => bytes,
1290 Err(status) => return status,
1291 };
1292 let header = match FrameHeader::parse(&bytes) {
1293 Ok(header) => header,
1294 Err(error) => return failed(error),
1295 };
1296
1297 *out_header = PamojaLorawanHeader {
1298 payload_len: header.payload_len(),
1299 dev_addr: header.dev_addr().unwrap_or(0),
1300 fcnt: header.fcnt().unwrap_or(0),
1301 message_type: match header.message_type() {
1302 MessageType::JoinRequest => PamojaLorawanMessageType::JoinRequest,
1303 MessageType::JoinAccept => PamojaLorawanMessageType::JoinAccept,
1304 MessageType::UnconfirmedUp => PamojaLorawanMessageType::UnconfirmedUp,
1305 MessageType::ConfirmedUp => PamojaLorawanMessageType::ConfirmedUp,
1306 MessageType::UnconfirmedDown => PamojaLorawanMessageType::UnconfirmedDown,
1307 MessageType::ConfirmedDown => PamojaLorawanMessageType::ConfirmedDown,
1308 },
1309 fport: header.fport().unwrap_or(0),
1310 is_data: u8::from(header.message_type().is_data()),
1311 has_fport: u8::from(header.fport().is_some()),
1312 confirmed: u8::from(header.confirmed()),
1313 adr: u8::from(header.adr()),
1314 ack: u8::from(header.ack()),
1315 fpending: u8::from(header.fpending()),
1316 fopts_len: header.fopts_len() as u8,
1317 adr_ack_req: u8::from(header.adr_ack_req()),
1318 class_b: u8::from(header.class_b()),
1319 };
1320 PamojaStatus::Ok
1321}
1322
1323/// Verifies a join-request and reads the identifiers out of it.
1324///
1325/// # Arguments
1326///
1327/// * `bytes` - the raw join-request as it came off the radio.
1328/// * `bytes_len` - its length.
1329/// * `app_key` - the 16-byte application root key the device shares.
1330/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
1331/// * `out_request` - receives the verified request.
1332///
1333/// # Returns
1334///
1335/// [`PamojaStatus::Ok`] on success, with `*out_request` set to a handle the caller
1336/// must release with [`pamoja_lorawan_join_request_free`], [`PamojaStatus::Auth`]
1337/// if the MIC does not verify, or [`PamojaStatus::Codec`] if the frame is not a
1338/// well-formed join-request.
1339///
1340/// # Safety
1341///
1342/// `bytes` and `app_key` must each point to at least their stated lengths in
1343/// readable bytes, and `out_request` must point to a writable
1344/// `*mut PamojaLorawanJoinRequest`.
1345#[no_mangle]
1346pub unsafe extern "C" fn pamoja_lorawan_join_request_parse(
1347 bytes: *const u8,
1348 bytes_len: usize,
1349 app_key: *const u8,
1350 app_key_len: usize,
1351 out_request: *mut *mut PamojaLorawanJoinRequest,
1352) -> PamojaStatus {
1353 if out_request.is_null() {
1354 set_last_error("out_request must not be null".to_owned());
1355 return PamojaStatus::InvalidArgument;
1356 }
1357 let slot = &mut *out_request;
1358 *slot = ptr::null_mut();
1359
1360 let bytes = match read_bytes(bytes, bytes_len) {
1361 Ok(bytes) => bytes,
1362 Err(status) => return status,
1363 };
1364 let app_key = match key(app_key, app_key_len, "the application key") {
1365 Ok(key) => key,
1366 Err(status) => return status,
1367 };
1368 match JoinRequest::parse(&bytes, &app_key) {
1369 Ok(request) => {
1370 *slot = Box::into_raw(Box::new(PamojaLorawanJoinRequest { request }));
1371 PamojaStatus::Ok
1372 }
1373 Err(error) => failed(error),
1374 }
1375}
1376
1377/// Copies the device identifier out of a verified join-request.
1378///
1379/// # Arguments
1380///
1381/// * `request` - the verified request.
1382/// * `out_dev_eui` - receives [`PAMOJA_LORAWAN_EUI_LEN`] bytes, most-significant
1383/// byte first.
1384///
1385/// # Returns
1386///
1387/// `true` when the identifier was written, or `false` if either pointer is null.
1388///
1389/// # Safety
1390///
1391/// `request` must be a live handle from [`pamoja_lorawan_join_request_parse`], or
1392/// null, and `out_dev_eui` must point to at least
1393/// [`PAMOJA_LORAWAN_EUI_LEN`] writable bytes.
1394#[no_mangle]
1395pub unsafe extern "C" fn pamoja_lorawan_join_request_dev_eui(
1396 request: *const PamojaLorawanJoinRequest,
1397 out_dev_eui: *mut u8,
1398) -> bool {
1399 if request.is_null() || out_dev_eui.is_null() {
1400 return false;
1401 }
1402 let eui = (*request).request.dev_eui();
1403 ptr::copy_nonoverlapping(eui.as_ptr(), out_dev_eui, PAMOJA_LORAWAN_EUI_LEN);
1404 true
1405}
1406
1407/// Copies the application identifier out of a verified join-request.
1408///
1409/// # Arguments
1410///
1411/// * `request` - the verified request.
1412/// * `out_app_eui` - receives [`PAMOJA_LORAWAN_EUI_LEN`] bytes, most-significant
1413/// byte first.
1414///
1415/// # Returns
1416///
1417/// `true` when the identifier was written, or `false` if either pointer is null.
1418///
1419/// # Safety
1420///
1421/// `request` must be a live handle from [`pamoja_lorawan_join_request_parse`], or
1422/// null, and `out_app_eui` must point to at least
1423/// [`PAMOJA_LORAWAN_EUI_LEN`] writable bytes.
1424#[no_mangle]
1425pub unsafe extern "C" fn pamoja_lorawan_join_request_app_eui(
1426 request: *const PamojaLorawanJoinRequest,
1427 out_app_eui: *mut u8,
1428) -> bool {
1429 if request.is_null() || out_app_eui.is_null() {
1430 return false;
1431 }
1432 let eui = (*request).request.app_eui();
1433 ptr::copy_nonoverlapping(eui.as_ptr(), out_app_eui, PAMOJA_LORAWAN_EUI_LEN);
1434 true
1435}
1436
1437/// Returns the nonce a verified join-request carried.
1438///
1439/// A network must remember the nonces a device has used and refuse a repeat, since
1440/// replaying one would re-derive the same session keys.
1441///
1442/// # Returns
1443///
1444/// The DevNonce, or 0 if `request` is null.
1445///
1446/// # Safety
1447///
1448/// `request` must be a live handle from [`pamoja_lorawan_join_request_parse`], or
1449/// null.
1450#[no_mangle]
1451pub unsafe extern "C" fn pamoja_lorawan_join_request_dev_nonce(
1452 request: *const PamojaLorawanJoinRequest,
1453) -> u16 {
1454 if request.is_null() {
1455 return 0;
1456 }
1457 (*request).request.dev_nonce()
1458}
1459
1460/// Releases a verified join-request handle.
1461///
1462/// Passing null is a no-op.
1463///
1464/// # Safety
1465///
1466/// `request` must be a handle from [`pamoja_lorawan_join_request_parse`] that has
1467/// not already been freed, or null. After this call it must not be used again.
1468#[no_mangle]
1469pub unsafe extern "C" fn pamoja_lorawan_join_request_free(request: *mut PamojaLorawanJoinRequest) {
1470 if !request.is_null() {
1471 drop(Box::from_raw(request));
1472 }
1473}
1474
1475/// Builds the signed join-accept a network sends to admit a device.
1476///
1477/// # Arguments
1478///
1479/// * `grant` - the address and settings to grant.
1480/// * `cflist` - the optional 16-byte channel list, or null for none.
1481/// * `cflist_len` - its length, either 0 or 16.
1482/// * `app_key` - the 16-byte application root key the device shares.
1483/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
1484/// * `dev_nonce` - the nonce the matching join-request carried.
1485/// * `out_frame` - receives the encoded join-accept.
1486///
1487/// # Returns
1488///
1489/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller
1490/// must release with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or
1491/// [`PamojaStatus::InvalidArgument`] if the key or the channel list is the wrong
1492/// length.
1493///
1494/// # Safety
1495///
1496/// `cflist` and `app_key` must each point to at least their stated lengths in
1497/// readable bytes, and `out_frame` must point to a writable `*mut PamojaBuffer`.
1498#[no_mangle]
1499pub unsafe extern "C" fn pamoja_lorawan_grant_accept(
1500 grant: PamojaLorawanGrant,
1501 cflist: *const u8,
1502 cflist_len: usize,
1503 app_key: *const u8,
1504 app_key_len: usize,
1505 dev_nonce: u16,
1506 out_frame: *mut *mut PamojaBuffer,
1507) -> PamojaStatus {
1508 if out_frame.is_null() {
1509 set_last_error("out_frame must not be null".to_owned());
1510 return PamojaStatus::InvalidArgument;
1511 }
1512 let slot = &mut *out_frame;
1513 *slot = ptr::null_mut();
1514
1515 let (grant, app_key) = match granted(grant, cflist, cflist_len, app_key, app_key_len) {
1516 Ok(pair) => pair,
1517 Err(status) => return status,
1518 };
1519 *slot = PamojaBuffer::into_raw(grant.accept(&app_key, dev_nonce).as_bytes().to_vec());
1520 PamojaStatus::Ok
1521}
1522
1523/// Derives the session a grant activates, the same one the device computes.
1524///
1525/// # Arguments
1526///
1527/// * `grant` - the address and settings granted.
1528/// * `cflist` - the optional 16-byte channel list, or null for none.
1529/// * `cflist_len` - its length, either 0 or 16.
1530/// * `app_key` - the 16-byte application root key the device shares.
1531/// * `app_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
1532/// * `dev_nonce` - the nonce the matching join-request carried.
1533/// * `out_session` - receives the session.
1534///
1535/// # Returns
1536///
1537/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller
1538/// must release with [`pamoja_lorawan_session_free`], or
1539/// [`PamojaStatus::InvalidArgument`] if the key or the channel list is the wrong
1540/// length.
1541///
1542/// # Safety
1543///
1544/// `cflist` and `app_key` must each point to at least their stated lengths in
1545/// readable bytes, and `out_session` must point to a writable
1546/// `*mut PamojaLorawanSession`.
1547#[no_mangle]
1548pub unsafe extern "C" fn pamoja_lorawan_grant_session(
1549 grant: PamojaLorawanGrant,
1550 cflist: *const u8,
1551 cflist_len: usize,
1552 app_key: *const u8,
1553 app_key_len: usize,
1554 dev_nonce: u16,
1555 out_session: *mut *mut PamojaLorawanSession,
1556) -> PamojaStatus {
1557 if out_session.is_null() {
1558 set_last_error("out_session must not be null".to_owned());
1559 return PamojaStatus::InvalidArgument;
1560 }
1561 let slot = &mut *out_session;
1562 *slot = ptr::null_mut();
1563
1564 let (grant, app_key) = match granted(grant, cflist, cflist_len, app_key, app_key_len) {
1565 Ok(pair) => pair,
1566 Err(status) => return status,
1567 };
1568 *slot = Box::into_raw(Box::new(PamojaLorawanSession {
1569 session: grant.session(&app_key, dev_nonce),
1570 }));
1571 PamojaStatus::Ok
1572}
1573
1574/// Rebuilds a Rust grant and the key it is used with from what crossed the ABI.
1575///
1576/// # Safety
1577///
1578/// `cflist` and `app_key` must each point to at least their stated lengths in
1579/// readable bytes when those lengths are non-zero.
1580unsafe fn granted(
1581 grant: PamojaLorawanGrant,
1582 cflist: *const u8,
1583 cflist_len: usize,
1584 app_key: *const u8,
1585 app_key_len: usize,
1586) -> Result<(JoinGrant, [u8; 16]), PamojaStatus> {
1587 let app_key = key(app_key, app_key_len, "the application key")?;
1588 let mut built = JoinGrant::new(grant.app_nonce, grant.net_id, grant.dev_addr)
1589 .with_dl_settings(grant.dl_settings)
1590 .with_rx_delay(grant.rx_delay);
1591
1592 let cflist = read_bytes(cflist, cflist_len)?;
1593 if !cflist.is_empty() {
1594 let Ok(cflist) = <[u8; 16]>::try_from(&cflist[..]) else {
1595 set_last_error("the channel list must be exactly 16 bytes".to_owned());
1596 return Err(PamojaStatus::InvalidArgument);
1597 };
1598 built = built.with_cflist(cflist);
1599 }
1600 Ok((built, app_key))
1601}
1602
1603#[cfg(test)]
1604mod tests {
1605 use super::*;
1606
1607 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1608
1609 const NWK_SKEY: [u8; 16] = [0x2B; 16];
1610 const APP_SKEY: [u8; 16] = [0x99; 16];
1611
1612 /// Every flag off, which is a plain unconfirmed frame.
1613 fn quiet() -> PamojaLorawanFlags {
1614 PamojaLorawanFlags {
1615 confirmed: 0,
1616 adr: 0,
1617 ack: 0,
1618 fpending: 0,
1619 adr_ack_req: 0,
1620 }
1621 }
1622
1623 /// Creates a session over the test keys.
1624 ///
1625 /// # Safety
1626 ///
1627 /// The returned handle must be released with [`pamoja_lorawan_session_free`].
1628 unsafe fn session() -> *mut PamojaLorawanSession {
1629 let mut session = ptr::null_mut();
1630 assert_eq!(
1631 pamoja_lorawan_session_new(
1632 0x2601_1BDA,
1633 NWK_SKEY.as_ptr(),
1634 NWK_SKEY.len(),
1635 APP_SKEY.as_ptr(),
1636 APP_SKEY.len(),
1637 &mut session
1638 ),
1639 PamojaStatus::Ok
1640 );
1641 session
1642 }
1643
1644 #[test]
1645 fn the_constants_match_the_lorawan_crate() {
1646 assert_eq!(PAMOJA_LORAWAN_FRAME_MAX, pamoja_lorawan::MAX_FRAME);
1647 assert_eq!(PAMOJA_LORAWAN_PAYLOAD_MAX, pamoja_lorawan::MAX_PAYLOAD);
1648 }
1649
1650 #[test]
1651 fn a_confirmed_uplink_round_trips_through_the_boundary() {
1652 // Safety: every pointer below is valid and every handle is released.
1653 unsafe {
1654 let session = session();
1655 assert_eq!(pamoja_lorawan_session_dev_addr(session), 0x2601_1BDA);
1656
1657 let payload = b"temp=4.8";
1658 let mut frame = ptr::null_mut();
1659 let flags = PamojaLorawanFlags {
1660 confirmed: 1,
1661 adr: 1,
1662 ..quiet()
1663 };
1664 assert_eq!(
1665 pamoja_lorawan_session_encode_uplink(
1666 session,
1667 42,
1668 1,
1669 payload.as_ptr(),
1670 payload.len(),
1671 ptr::null(),
1672 0,
1673 flags,
1674 &mut frame
1675 ),
1676 PamojaStatus::Ok
1677 );
1678 let on_air =
1679 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1680 .to_vec();
1681 pamoja_buffer_free(frame);
1682
1683 let mut rx = ptr::null_mut();
1684 assert_eq!(
1685 pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 42, &mut rx),
1686 PamojaStatus::Ok
1687 );
1688 assert_eq!(
1689 pamoja_lorawan_rx_direction(rx),
1690 PamojaLorawanDirection::Uplink
1691 );
1692 assert!(pamoja_lorawan_rx_confirmed(rx));
1693 assert!(pamoja_lorawan_rx_adr(rx));
1694 assert_eq!(pamoja_lorawan_rx_fcnt(rx), 42);
1695 assert_eq!(pamoja_lorawan_rx_dev_addr(rx), 0x2601_1BDA);
1696
1697 let mut fport = 0u8;
1698 assert!(pamoja_lorawan_rx_fport(rx, &mut fport));
1699 assert_eq!(fport, 1);
1700
1701 let recovered = std::slice::from_raw_parts(
1702 pamoja_lorawan_rx_payload(rx),
1703 pamoja_lorawan_rx_payload_len(rx),
1704 );
1705 assert_eq!(recovered, payload);
1706
1707 pamoja_lorawan_rx_free(rx);
1708 pamoja_lorawan_session_free(session);
1709 }
1710 }
1711
1712 #[test]
1713 fn a_downlink_carries_its_frame_options() {
1714 // Safety: every pointer below is valid and every handle is released.
1715 unsafe {
1716 let session = session();
1717 let fopts = [0x03u8, 0x50, 0x00];
1718 let mut frame = ptr::null_mut();
1719 let flags = PamojaLorawanFlags {
1720 fpending: 1,
1721 ..quiet()
1722 };
1723 assert_eq!(
1724 pamoja_lorawan_session_encode_downlink(
1725 session,
1726 7,
1727 2,
1728 ptr::null(),
1729 0,
1730 fopts.as_ptr(),
1731 fopts.len(),
1732 flags,
1733 &mut frame
1734 ),
1735 PamojaStatus::Ok
1736 );
1737 let on_air =
1738 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1739 .to_vec();
1740 pamoja_buffer_free(frame);
1741
1742 let mut rx = ptr::null_mut();
1743 assert_eq!(
1744 pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 7, &mut rx),
1745 PamojaStatus::Ok
1746 );
1747 assert_eq!(
1748 pamoja_lorawan_rx_direction(rx),
1749 PamojaLorawanDirection::Downlink
1750 );
1751 assert!(pamoja_lorawan_rx_fpending(rx));
1752 let recovered = std::slice::from_raw_parts(
1753 pamoja_lorawan_rx_fopts(rx),
1754 pamoja_lorawan_rx_fopts_len(rx),
1755 );
1756 assert_eq!(recovered, fopts);
1757 assert_eq!(pamoja_lorawan_rx_payload_len(rx), 0);
1758 assert!(pamoja_lorawan_rx_payload(rx).is_null());
1759
1760 pamoja_lorawan_rx_free(rx);
1761 pamoja_lorawan_session_free(session);
1762 }
1763 }
1764
1765 #[test]
1766 fn a_forged_frame_fails_its_integrity_check() {
1767 // Safety: every pointer below is valid and every handle is released.
1768 unsafe {
1769 let session = session();
1770 let payload = b"reading";
1771 let mut frame = ptr::null_mut();
1772 assert_eq!(
1773 pamoja_lorawan_session_encode_uplink(
1774 session,
1775 1,
1776 1,
1777 payload.as_ptr(),
1778 payload.len(),
1779 ptr::null(),
1780 0,
1781 quiet(),
1782 &mut frame
1783 ),
1784 PamojaStatus::Ok
1785 );
1786 let mut on_air =
1787 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1788 .to_vec();
1789 pamoja_buffer_free(frame);
1790 let last = on_air.len() - 1;
1791 on_air[last] ^= 0xFF;
1792
1793 let mut rx = ptr::null_mut();
1794 assert_eq!(
1795 pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 1, &mut rx),
1796 PamojaStatus::Auth
1797 );
1798 assert!(rx.is_null());
1799 pamoja_lorawan_session_free(session);
1800 }
1801 }
1802
1803 #[test]
1804 fn a_counter_that_does_not_match_is_refused() {
1805 // Safety: every pointer below is valid and every handle is released.
1806 unsafe {
1807 let session = session();
1808 let payload = b"reading";
1809 let mut frame = ptr::null_mut();
1810 assert_eq!(
1811 pamoja_lorawan_session_encode_uplink(
1812 session,
1813 1,
1814 1,
1815 payload.as_ptr(),
1816 payload.len(),
1817 ptr::null(),
1818 0,
1819 quiet(),
1820 &mut frame
1821 ),
1822 PamojaStatus::Ok
1823 );
1824 let on_air =
1825 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1826 .to_vec();
1827 pamoja_buffer_free(frame);
1828
1829 let mut rx = ptr::null_mut();
1830 assert_eq!(
1831 pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 2, &mut rx),
1832 PamojaStatus::Auth
1833 );
1834 assert!(rx.is_null());
1835 pamoja_lorawan_session_free(session);
1836 }
1837 }
1838
1839 #[test]
1840 fn a_key_of_the_wrong_length_is_refused() {
1841 let short = [0u8; 8];
1842 let mut session = ptr::null_mut();
1843 // Safety: the buffers and out-pointer are valid.
1844 unsafe {
1845 assert_eq!(
1846 pamoja_lorawan_session_new(
1847 1,
1848 short.as_ptr(),
1849 short.len(),
1850 APP_SKEY.as_ptr(),
1851 APP_SKEY.len(),
1852 &mut session
1853 ),
1854 PamojaStatus::InvalidArgument
1855 );
1856 assert!(session.is_null());
1857 }
1858 }
1859
1860 #[test]
1861 fn a_join_request_crosses_the_boundary_byte_for_byte() {
1862 let dev_eui = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
1863 let app_eui = [0x11u8, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18];
1864 let app_key = [0x2Bu8; 16];
1865 // Safety: every pointer below is valid and every handle is released.
1866 unsafe {
1867 let mut device = ptr::null_mut();
1868 assert_eq!(
1869 pamoja_lorawan_device_new(
1870 dev_eui.as_ptr(),
1871 dev_eui.len(),
1872 app_eui.as_ptr(),
1873 app_eui.len(),
1874 app_key.as_ptr(),
1875 app_key.len(),
1876 &mut device
1877 ),
1878 PamojaStatus::Ok
1879 );
1880
1881 let mut request = ptr::null_mut();
1882 assert_eq!(
1883 pamoja_lorawan_device_join_request(device, 0x0102, &mut request),
1884 PamojaStatus::Ok
1885 );
1886 let bytes =
1887 std::slice::from_raw_parts(pamoja_buffer_data(request), pamoja_buffer_len(request));
1888 let reference = Device::new(dev_eui, app_eui, app_key).join_request(0x0102);
1889 assert_eq!(bytes, reference.as_bytes());
1890 pamoja_buffer_free(request);
1891
1892 // A join accept the network never signed must not activate a session.
1893 let forged = [0x20u8; 17];
1894 let mut accept = ptr::null_mut();
1895 assert_eq!(
1896 pamoja_lorawan_device_accept_join(
1897 device,
1898 forged.as_ptr(),
1899 forged.len(),
1900 0x0102,
1901 &mut accept
1902 ),
1903 PamojaStatus::Auth
1904 );
1905 assert!(accept.is_null());
1906
1907 pamoja_lorawan_device_free(device);
1908 }
1909 }
1910
1911 #[test]
1912 fn null_handles_are_tolerated() {
1913 // Safety: every call below is documented to accept null.
1914 unsafe {
1915 assert_eq!(pamoja_lorawan_session_dev_addr(ptr::null()), 0);
1916 assert_eq!(pamoja_lorawan_rx_fcnt(ptr::null()), 0);
1917 assert!(!pamoja_lorawan_rx_confirmed(ptr::null()));
1918 assert!(pamoja_lorawan_rx_payload(ptr::null()).is_null());
1919 assert_eq!(pamoja_lorawan_join_accept_dev_addr(ptr::null()), 0);
1920 pamoja_lorawan_session_free(ptr::null_mut());
1921 pamoja_lorawan_rx_free(ptr::null_mut());
1922 pamoja_lorawan_device_free(ptr::null_mut());
1923 pamoja_lorawan_join_accept_free(ptr::null_mut());
1924 }
1925 }
1926
1927 #[test]
1928 fn a_header_routes_a_frame_before_any_key_is_known() {
1929 // Safety: every pointer below is valid and every handle is released.
1930 unsafe {
1931 let session = session();
1932 let payload = b"temp=4.8";
1933 let mut frame = ptr::null_mut();
1934 let flags = PamojaLorawanFlags {
1935 confirmed: 1,
1936 adr: 1,
1937 ..quiet()
1938 };
1939 assert_eq!(
1940 pamoja_lorawan_session_encode_uplink(
1941 session,
1942 42,
1943 1,
1944 payload.as_ptr(),
1945 payload.len(),
1946 ptr::null(),
1947 0,
1948 flags,
1949 &mut frame
1950 ),
1951 PamojaStatus::Ok
1952 );
1953 let on_air =
1954 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
1955 .to_vec();
1956 pamoja_buffer_free(frame);
1957
1958 let mut header = std::mem::zeroed::<PamojaLorawanHeader>();
1959 assert_eq!(
1960 pamoja_lorawan_header_parse(on_air.as_ptr(), on_air.len(), &mut header),
1961 PamojaStatus::Ok
1962 );
1963 assert_eq!(header.message_type, PamojaLorawanMessageType::ConfirmedUp);
1964 assert_eq!(header.is_data, 1);
1965 assert_eq!(header.dev_addr, 0x2601_1BDA);
1966 assert_eq!(header.fcnt, 42);
1967 assert_eq!(header.has_fport, 1);
1968 assert_eq!(header.fport, 1);
1969 assert_eq!(header.confirmed, 1);
1970 assert_eq!(header.adr, 1);
1971 assert_eq!(header.payload_len, payload.len());
1972 pamoja_lorawan_session_free(session);
1973 }
1974 }
1975
1976 #[test]
1977 fn a_network_completes_an_activation_across_the_boundary() {
1978 let dev_eui = [0x00u8, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77];
1979 let app_eui = [0x88u8, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
1980 let app_key = [0xABu8; 16];
1981 let grant = PamojaLorawanGrant {
1982 app_nonce: 0x0003_0201,
1983 net_id: 0x0006_0504,
1984 dev_addr: 0x2601_1BDA,
1985 dl_settings: 0x00,
1986 rx_delay: 0x01,
1987 };
1988 // Safety: every pointer below is valid and every handle is released.
1989 unsafe {
1990 let mut device = ptr::null_mut();
1991 assert_eq!(
1992 pamoja_lorawan_device_new(
1993 dev_eui.as_ptr(),
1994 dev_eui.len(),
1995 app_eui.as_ptr(),
1996 app_eui.len(),
1997 app_key.as_ptr(),
1998 app_key.len(),
1999 &mut device
2000 ),
2001 PamojaStatus::Ok
2002 );
2003
2004 // The device asks, and this network reads the request back.
2005 let mut request_frame = ptr::null_mut();
2006 assert_eq!(
2007 pamoja_lorawan_device_join_request(device, 0x1234, &mut request_frame),
2008 PamojaStatus::Ok
2009 );
2010 let on_air = std::slice::from_raw_parts(
2011 pamoja_buffer_data(request_frame),
2012 pamoja_buffer_len(request_frame),
2013 )
2014 .to_vec();
2015 pamoja_buffer_free(request_frame);
2016
2017 let mut request = ptr::null_mut();
2018 assert_eq!(
2019 pamoja_lorawan_join_request_parse(
2020 on_air.as_ptr(),
2021 on_air.len(),
2022 app_key.as_ptr(),
2023 app_key.len(),
2024 &mut request
2025 ),
2026 PamojaStatus::Ok
2027 );
2028 let mut read_eui = [0u8; 8];
2029 assert!(pamoja_lorawan_join_request_dev_eui(
2030 request,
2031 read_eui.as_mut_ptr()
2032 ));
2033 assert_eq!(read_eui, dev_eui);
2034 assert!(pamoja_lorawan_join_request_app_eui(
2035 request,
2036 read_eui.as_mut_ptr()
2037 ));
2038 assert_eq!(read_eui, app_eui);
2039 let dev_nonce = pamoja_lorawan_join_request_dev_nonce(request);
2040 assert_eq!(dev_nonce, 0x1234);
2041 pamoja_lorawan_join_request_free(request);
2042
2043 // This network answers, and the device activates on the reply.
2044 let mut reply = ptr::null_mut();
2045 assert_eq!(
2046 pamoja_lorawan_grant_accept(
2047 grant,
2048 ptr::null(),
2049 0,
2050 app_key.as_ptr(),
2051 app_key.len(),
2052 dev_nonce,
2053 &mut reply
2054 ),
2055 PamojaStatus::Ok
2056 );
2057 let accept_bytes =
2058 std::slice::from_raw_parts(pamoja_buffer_data(reply), pamoja_buffer_len(reply))
2059 .to_vec();
2060 pamoja_buffer_free(reply);
2061
2062 let mut accept = ptr::null_mut();
2063 assert_eq!(
2064 pamoja_lorawan_device_accept_join(
2065 device,
2066 accept_bytes.as_ptr(),
2067 accept_bytes.len(),
2068 dev_nonce,
2069 &mut accept
2070 ),
2071 PamojaStatus::Ok
2072 );
2073 assert_eq!(pamoja_lorawan_join_accept_dev_addr(accept), grant.dev_addr);
2074 assert_eq!(pamoja_lorawan_join_accept_net_id(accept), grant.net_id);
2075 assert_eq!(pamoja_lorawan_join_accept_rx_delay(accept), grant.rx_delay);
2076
2077 // Both sides now hold a session, and each can read what the other secures.
2078 let mut device_session = ptr::null_mut();
2079 assert_eq!(
2080 pamoja_lorawan_join_accept_session(accept, &mut device_session),
2081 PamojaStatus::Ok
2082 );
2083 let mut network_session = ptr::null_mut();
2084 assert_eq!(
2085 pamoja_lorawan_grant_session(
2086 grant,
2087 ptr::null(),
2088 0,
2089 app_key.as_ptr(),
2090 app_key.len(),
2091 dev_nonce,
2092 &mut network_session
2093 ),
2094 PamojaStatus::Ok
2095 );
2096
2097 let payload = b"joined";
2098 let mut uplink = ptr::null_mut();
2099 assert_eq!(
2100 pamoja_lorawan_session_encode_uplink(
2101 device_session,
2102 1,
2103 1,
2104 payload.as_ptr(),
2105 payload.len(),
2106 ptr::null(),
2107 0,
2108 quiet(),
2109 &mut uplink
2110 ),
2111 PamojaStatus::Ok
2112 );
2113 let uplink_bytes =
2114 std::slice::from_raw_parts(pamoja_buffer_data(uplink), pamoja_buffer_len(uplink))
2115 .to_vec();
2116 pamoja_buffer_free(uplink);
2117
2118 let mut rx = ptr::null_mut();
2119 assert_eq!(
2120 pamoja_lorawan_session_decode(
2121 network_session,
2122 uplink_bytes.as_ptr(),
2123 uplink_bytes.len(),
2124 1,
2125 &mut rx
2126 ),
2127 PamojaStatus::Ok,
2128 "the network reads what the device it just admitted sent"
2129 );
2130 let recovered = std::slice::from_raw_parts(
2131 pamoja_lorawan_rx_payload(rx),
2132 pamoja_lorawan_rx_payload_len(rx),
2133 );
2134 assert_eq!(recovered, payload);
2135
2136 pamoja_lorawan_rx_free(rx);
2137 pamoja_lorawan_session_free(network_session);
2138 pamoja_lorawan_session_free(device_session);
2139 pamoja_lorawan_join_accept_free(accept);
2140 pamoja_lorawan_device_free(device);
2141 }
2142 }
2143
2144 #[test]
2145 fn a_request_signed_with_another_key_is_refused_at_the_boundary() {
2146 let app_key = [0xABu8; 16];
2147 let other = [0x00u8; 16];
2148 // Safety: every pointer below is valid and every handle is released.
2149 unsafe {
2150 let mut device = ptr::null_mut();
2151 assert_eq!(
2152 pamoja_lorawan_device_new(
2153 [0x11u8; 8].as_ptr(),
2154 8,
2155 [0x22u8; 8].as_ptr(),
2156 8,
2157 other.as_ptr(),
2158 other.len(),
2159 &mut device
2160 ),
2161 PamojaStatus::Ok
2162 );
2163 let mut frame = ptr::null_mut();
2164 pamoja_lorawan_device_join_request(device, 1, &mut frame);
2165 let on_air =
2166 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
2167 .to_vec();
2168 pamoja_buffer_free(frame);
2169 pamoja_lorawan_device_free(device);
2170
2171 let mut request = ptr::null_mut();
2172 assert_eq!(
2173 pamoja_lorawan_join_request_parse(
2174 on_air.as_ptr(),
2175 on_air.len(),
2176 app_key.as_ptr(),
2177 app_key.len(),
2178 &mut request
2179 ),
2180 PamojaStatus::Auth
2181 );
2182 assert!(request.is_null());
2183 }
2184 }
2185
2186 #[test]
2187 fn the_adaptive_data_rate_bits_and_join_settings_cross_the_boundary() {
2188 // Safety: every pointer below is valid and every handle is released.
2189 unsafe {
2190 let session = session();
2191 let mut frame = ptr::null_mut();
2192 let flags = PamojaLorawanFlags {
2193 adr: 1,
2194 adr_ack_req: 1,
2195 ..quiet()
2196 };
2197 assert_eq!(
2198 pamoja_lorawan_session_encode_uplink(
2199 session,
2200 65,
2201 1,
2202 b"x".as_ptr(),
2203 1,
2204 ptr::null(),
2205 0,
2206 flags,
2207 &mut frame
2208 ),
2209 PamojaStatus::Ok
2210 );
2211 let on_air =
2212 std::slice::from_raw_parts(pamoja_buffer_data(frame), pamoja_buffer_len(frame))
2213 .to_vec();
2214 pamoja_buffer_free(frame);
2215 assert_eq!(on_air[5], 0xC0, "FCtrl carries ADR and ADRACKReq");
2216
2217 let mut header = std::mem::zeroed::<PamojaLorawanHeader>();
2218 assert_eq!(
2219 pamoja_lorawan_header_parse(on_air.as_ptr(), on_air.len(), &mut header),
2220 PamojaStatus::Ok
2221 );
2222 assert_eq!((header.adr, header.adr_ack_req, header.class_b), (1, 1, 0));
2223
2224 let mut rx = ptr::null_mut();
2225 assert_eq!(
2226 pamoja_lorawan_session_decode(session, on_air.as_ptr(), on_air.len(), 65, &mut rx),
2227 PamojaStatus::Ok
2228 );
2229 assert!(pamoja_lorawan_rx_adr_ack_req(rx));
2230 assert!(!pamoja_lorawan_rx_class_b(rx));
2231 pamoja_lorawan_rx_free(rx);
2232 pamoja_lorawan_session_free(session);
2233
2234 // An uplink header with the ClassB bit set, which FrameHeader reads without a
2235 // key: MHDR, DevAddr, FCtrl 0x10, FCnt, FPort, one payload byte and a MIC.
2236 let class_b = [
2237 0x40, 0xDA, 0x1B, 0x01, 0x26, 0x10, 0x01, 0x00, 0x01, 0x00, 1, 2, 3, 4,
2238 ];
2239 assert_eq!(
2240 pamoja_lorawan_header_parse(class_b.as_ptr(), class_b.len(), &mut header),
2241 PamojaStatus::Ok
2242 );
2243 assert_eq!((header.class_b, header.adr_ack_req), (1, 0));
2244
2245 // The EU868 join accept published in lora-packet issue 10.
2246 let app_key = [
2247 0xB6, 0xB5, 0x3F, 0x4A, 0x16, 0x8A, 0x7A, 0x88, 0xBD, 0xF7, 0xEA, 0x13, 0x5C, 0xE9,
2248 0xCF, 0xCA,
2249 ];
2250 let accept = [
2251 0x20, 0x4D, 0xD8, 0x5A, 0xE6, 0x08, 0xB8, 0x7F, 0xC4, 0x88, 0x99, 0x70, 0xB7, 0xD2,
2252 0x04, 0x2C, 0x9E, 0x72, 0x95, 0x9B, 0x00, 0x57, 0xAE, 0xD6, 0x09, 0x4B, 0x16, 0x00,
2253 0x3D, 0xF1, 0x2D, 0xE1, 0x45,
2254 ];
2255 let dev_eui = [0x70, 0xB3, 0xD5, 0x7E, 0xD0, 0x05, 0x12, 0x34];
2256 let mut device = ptr::null_mut();
2257 assert_eq!(
2258 pamoja_lorawan_device_new(
2259 dev_eui.as_ptr(),
2260 8,
2261 [0u8; 8].as_ptr(),
2262 8,
2263 app_key.as_ptr(),
2264 16,
2265 &mut device
2266 ),
2267 PamojaStatus::Ok
2268 );
2269 let mut eui = [0u8; 8];
2270 assert!(pamoja_lorawan_device_dev_eui(device, eui.as_mut_ptr()));
2271 assert_eq!(eui, dev_eui);
2272
2273 let mut joined = ptr::null_mut();
2274 assert_eq!(
2275 pamoja_lorawan_device_accept_join(
2276 device,
2277 accept.as_ptr(),
2278 accept.len(),
2279 0xCC85,
2280 &mut joined
2281 ),
2282 PamojaStatus::Ok
2283 );
2284 assert_eq!(pamoja_lorawan_join_accept_dl_settings(joined), 0x03);
2285 assert_eq!(pamoja_lorawan_join_accept_rx1_dr_offset(joined), 0);
2286 assert_eq!(pamoja_lorawan_join_accept_rx2_data_rate(joined), 3);
2287 assert_eq!(
2288 pamoja_lorawan_join_accept_receive_delay_us(joined),
2289 1_000_000
2290 );
2291 let mut cflist = [0u8; 16];
2292 assert!(pamoja_lorawan_join_accept_cflist(
2293 joined,
2294 cflist.as_mut_ptr()
2295 ));
2296 assert_eq!(
2297 cflist,
2298 [
2299 0x18, 0x4F, 0x84, 0xE8, 0x56, 0x84, 0xB8, 0x5E, 0x84, 0x88, 0x66, 0x84, 0x58,
2300 0x6E, 0x84, 0x00
2301 ]
2302 );
2303 pamoja_lorawan_join_accept_free(joined);
2304 pamoja_lorawan_device_free(device);
2305 }
2306 }
2307}