1use std::panic::{catch_unwind, AssertUnwindSafe};
13use std::ptr;
14use std::slice;
15
16use pamoja_session::{
17 hkdf_sha256, hmac_sha256, AgreementKey, AgreementPublicKey, Role, Sealed, Session,
18};
19
20use crate::{read_bytes, set_last_error, PamojaStatus};
21
22pub const PAMOJA_SESSION_KEY_LEN: usize = 32;
24
25pub const PAMOJA_SESSION_TAG_LEN: usize = 16;
27
28#[repr(C)]
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum PamojaSessionRole {
36 Initiator = 0,
38 Responder = 1,
40}
41
42#[repr(C)]
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct PamojaSealed {
49 pub counter: u64,
51 pub tag: [u8; PAMOJA_SESSION_TAG_LEN],
53}
54
55pub struct PamojaAgreementKey {
60 key: AgreementKey,
61}
62
63pub struct PamojaSession {
68 session: Session,
69}
70
71#[no_mangle]
89pub unsafe extern "C" fn pamoja_agreement_key_from_seed(
90 seed: *const u8,
91 seed_len: usize,
92) -> *mut PamojaAgreementKey {
93 let Ok(bytes) = read_bytes(seed, seed_len) else {
94 return ptr::null_mut();
95 };
96 let Ok(seed) = <[u8; PAMOJA_SESSION_KEY_LEN]>::try_from(bytes.as_slice()) else {
97 set_last_error(format!(
98 "seed must be exactly {PAMOJA_SESSION_KEY_LEN} bytes"
99 ));
100 return ptr::null_mut();
101 };
102 Box::into_raw(Box::new(PamojaAgreementKey {
103 key: AgreementKey::from_seed(&seed),
104 }))
105}
106
107#[no_mangle]
124pub unsafe extern "C" fn pamoja_agreement_key_public(
125 key: *const PamojaAgreementKey,
126 out_public_key: *mut u8,
127) -> PamojaStatus {
128 if key.is_null() {
129 set_last_error("key must not be null".to_owned());
130 return PamojaStatus::InvalidArgument;
131 }
132 if out_public_key.is_null() {
133 set_last_error("out_public_key must not be null".to_owned());
134 return PamojaStatus::InvalidArgument;
135 }
136 let public = (*key).key.public().to_bytes();
137 ptr::copy_nonoverlapping(public.as_ptr(), out_public_key, PAMOJA_SESSION_KEY_LEN);
138 PamojaStatus::Ok
139}
140
141#[no_mangle]
150pub unsafe extern "C" fn pamoja_agreement_key_free(key: *mut PamojaAgreementKey) {
151 if !key.is_null() {
152 drop(Box::from_raw(key));
153 }
154}
155
156#[no_mangle]
183pub unsafe extern "C" fn pamoja_session_establish(
184 local: *const PamojaAgreementKey,
185 peer_public_key: *const u8,
186 salt: *const u8,
187 salt_len: usize,
188 role: PamojaSessionRole,
189) -> *mut PamojaSession {
190 if local.is_null() {
191 set_last_error("local must not be null".to_owned());
192 return ptr::null_mut();
193 }
194 let Ok(peer_bytes) = read_bytes(peer_public_key, PAMOJA_SESSION_KEY_LEN) else {
195 return ptr::null_mut();
196 };
197 let Ok(peer_bytes) = <[u8; PAMOJA_SESSION_KEY_LEN]>::try_from(peer_bytes.as_slice()) else {
198 set_last_error(format!(
199 "peer public key must be exactly {PAMOJA_SESSION_KEY_LEN} bytes"
200 ));
201 return ptr::null_mut();
202 };
203 let Ok(salt) = read_bytes(salt, salt_len) else {
204 return ptr::null_mut();
205 };
206
207 let peer = AgreementPublicKey::from_bytes(&peer_bytes);
208 let established = catch_unwind(AssertUnwindSafe(|| {
209 Session::establish(&(*local).key, &peer, &salt, rust_role(role))
210 }));
211 match established {
212 Ok(session) => Box::into_raw(Box::new(PamojaSession { session })),
213 Err(_) => {
214 set_last_error("establishing the session panicked".to_owned());
215 ptr::null_mut()
216 }
217 }
218}
219
220#[no_mangle]
247pub unsafe extern "C" fn pamoja_session_seal(
248 session: *mut PamojaSession,
249 buf: *mut u8,
250 len: usize,
251 aad: *const u8,
252 aad_len: usize,
253 out_sealed: *mut PamojaSealed,
254) -> PamojaStatus {
255 if session.is_null() {
256 set_last_error("session must not be null".to_owned());
257 return PamojaStatus::InvalidArgument;
258 }
259 if out_sealed.is_null() {
260 set_last_error("out_sealed must not be null".to_owned());
261 return PamojaStatus::InvalidArgument;
262 }
263 if len != 0 && buf.is_null() {
264 set_last_error("buf must not be null when its length is non-zero".to_owned());
265 return PamojaStatus::InvalidArgument;
266 }
267 let aad = match read_bytes(aad, aad_len) {
268 Ok(aad) => aad,
269 Err(status) => return status,
270 };
271
272 let message = if len == 0 {
273 &mut [][..]
274 } else {
275 slice::from_raw_parts_mut(buf, len)
276 };
277 let sealed = (*session).session.seal(message, &aad);
278 *out_sealed = PamojaSealed {
279 counter: sealed.counter,
280 tag: sealed.tag,
281 };
282 PamojaStatus::Ok
283}
284
285#[no_mangle]
314pub unsafe extern "C" fn pamoja_session_open(
315 session: *mut PamojaSession,
316 sealed: PamojaSealed,
317 buf: *mut u8,
318 len: usize,
319 aad: *const u8,
320 aad_len: usize,
321) -> PamojaStatus {
322 if session.is_null() {
323 set_last_error("session must not be null".to_owned());
324 return PamojaStatus::InvalidArgument;
325 }
326 if len != 0 && buf.is_null() {
327 set_last_error("buf must not be null when its length is non-zero".to_owned());
328 return PamojaStatus::InvalidArgument;
329 }
330 let aad = match read_bytes(aad, aad_len) {
331 Ok(aad) => aad,
332 Err(status) => return status,
333 };
334
335 let message = if len == 0 {
336 &mut [][..]
337 } else {
338 slice::from_raw_parts_mut(buf, len)
339 };
340 let header = Sealed {
341 counter: sealed.counter,
342 tag: sealed.tag,
343 };
344 match (*session).session.open(&header, message, &aad) {
345 Ok(()) => PamojaStatus::Ok,
346 Err(error) => {
347 set_last_error(error.to_string());
348 PamojaStatus::Auth
349 }
350 }
351}
352
353#[no_mangle]
362pub unsafe extern "C" fn pamoja_session_free(session: *mut PamojaSession) {
363 if !session.is_null() {
364 drop(Box::from_raw(session));
365 }
366}
367
368#[no_mangle]
391pub unsafe extern "C" fn pamoja_session_hmac_sha256(
392 key: *const u8,
393 key_len: usize,
394 message: *const u8,
395 message_len: usize,
396 out_digest: *mut u8,
397) -> PamojaStatus {
398 let key = match read_bytes(key, key_len) {
399 Ok(key) => key,
400 Err(status) => return status,
401 };
402 let message = match read_bytes(message, message_len) {
403 Ok(message) => message,
404 Err(status) => return status,
405 };
406 if out_digest.is_null() {
407 set_last_error("out_digest must not be null".to_owned());
408 return PamojaStatus::InvalidArgument;
409 }
410 let digest = hmac_sha256(&key, &message);
411 ptr::copy_nonoverlapping(digest.as_ptr(), out_digest, PAMOJA_SESSION_KEY_LEN);
412 PamojaStatus::Ok
413}
414
415#[allow(clippy::too_many_arguments)]
438#[no_mangle]
439pub unsafe extern "C" fn pamoja_session_hkdf_sha256(
440 salt: *const u8,
441 salt_len: usize,
442 ikm: *const u8,
443 ikm_len: usize,
444 info: *const u8,
445 info_len: usize,
446 out: *mut u8,
447 out_len: usize,
448) -> PamojaStatus {
449 let salt = match read_bytes(salt, salt_len) {
450 Ok(salt) => salt,
451 Err(status) => return status,
452 };
453 let ikm = match read_bytes(ikm, ikm_len) {
454 Ok(ikm) => ikm,
455 Err(status) => return status,
456 };
457 let info = match read_bytes(info, info_len) {
458 Ok(info) => info,
459 Err(status) => return status,
460 };
461 if out_len == 0 {
462 return PamojaStatus::Ok;
463 }
464 if out.is_null() {
465 set_last_error("out must not be null when its length is non-zero".to_owned());
466 return PamojaStatus::InvalidArgument;
467 }
468
469 let mut derived = vec![0u8; out_len];
470 hkdf_sha256(&salt, &ikm, &info, &mut derived);
471 ptr::copy_nonoverlapping(derived.as_ptr(), out, out_len);
472 PamojaStatus::Ok
473}
474
475fn rust_role(role: PamojaSessionRole) -> Role {
477 match role {
478 PamojaSessionRole::Initiator => Role::Initiator,
479 PamojaSessionRole::Responder => Role::Responder,
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 unsafe fn pair() -> (
489 *mut PamojaAgreementKey,
490 *mut PamojaAgreementKey,
491 *mut PamojaSession,
492 *mut PamojaSession,
493 ) {
494 let device_seed = [1u8; PAMOJA_SESSION_KEY_LEN];
495 let gateway_seed = [2u8; PAMOJA_SESSION_KEY_LEN];
496 let device_key = pamoja_agreement_key_from_seed(device_seed.as_ptr(), device_seed.len());
497 let gateway_key = pamoja_agreement_key_from_seed(gateway_seed.as_ptr(), gateway_seed.len());
498
499 let mut device_public = [0u8; PAMOJA_SESSION_KEY_LEN];
500 let mut gateway_public = [0u8; PAMOJA_SESSION_KEY_LEN];
501 assert_eq!(
502 pamoja_agreement_key_public(device_key, device_public.as_mut_ptr()),
503 PamojaStatus::Ok
504 );
505 assert_eq!(
506 pamoja_agreement_key_public(gateway_key, gateway_public.as_mut_ptr()),
507 PamojaStatus::Ok
508 );
509
510 let salt = [9u8; 16];
511 let device = pamoja_session_establish(
512 device_key,
513 gateway_public.as_ptr(),
514 salt.as_ptr(),
515 salt.len(),
516 PamojaSessionRole::Initiator,
517 );
518 let gateway = pamoja_session_establish(
519 gateway_key,
520 device_public.as_ptr(),
521 salt.as_ptr(),
522 salt.len(),
523 PamojaSessionRole::Responder,
524 );
525 assert!(!device.is_null() && !gateway.is_null());
526 (device_key, gateway_key, device, gateway)
527 }
528
529 #[test]
530 fn a_sealed_message_opens_at_the_peer() {
531 unsafe {
532 let (device_key, gateway_key, device, gateway) = pair();
533
534 let mut message = *b"4.8C";
535 let mut sealed = PamojaSealed {
536 counter: 0,
537 tag: [0; PAMOJA_SESSION_TAG_LEN],
538 };
539 assert_eq!(
540 pamoja_session_seal(
541 device,
542 message.as_mut_ptr(),
543 message.len(),
544 b"fridge-1".as_ptr(),
545 8,
546 &mut sealed,
547 ),
548 PamojaStatus::Ok
549 );
550 assert_ne!(&message, b"4.8C");
551
552 assert_eq!(
553 pamoja_session_open(
554 gateway,
555 sealed,
556 message.as_mut_ptr(),
557 message.len(),
558 b"fridge-1".as_ptr(),
559 8,
560 ),
561 PamojaStatus::Ok
562 );
563 assert_eq!(&message, b"4.8C");
564
565 pamoja_session_free(gateway);
566 pamoja_session_free(device);
567 pamoja_agreement_key_free(gateway_key);
568 pamoja_agreement_key_free(device_key);
569 }
570 }
571
572 #[test]
573 fn a_repeated_counter_is_refused() {
574 unsafe {
575 let (device_key, gateway_key, device, gateway) = pair();
576
577 let mut message = *b"on";
578 let mut sealed = PamojaSealed {
579 counter: 0,
580 tag: [0; PAMOJA_SESSION_TAG_LEN],
581 };
582 pamoja_session_seal(
583 device,
584 message.as_mut_ptr(),
585 message.len(),
586 ptr::null(),
587 0,
588 &mut sealed,
589 );
590 let ciphertext = message;
591
592 assert_eq!(
593 pamoja_session_open(
594 gateway,
595 sealed,
596 message.as_mut_ptr(),
597 message.len(),
598 ptr::null(),
599 0
600 ),
601 PamojaStatus::Ok
602 );
603
604 message = ciphertext;
605 assert_eq!(
606 pamoja_session_open(
607 gateway,
608 sealed,
609 message.as_mut_ptr(),
610 message.len(),
611 ptr::null(),
612 0
613 ),
614 PamojaStatus::Auth
615 );
616
617 pamoja_session_free(gateway);
618 pamoja_session_free(device);
619 pamoja_agreement_key_free(gateway_key);
620 pamoja_agreement_key_free(device_key);
621 }
622 }
623
624 #[test]
625 fn altered_associated_data_fails_authentication() {
626 unsafe {
627 let (device_key, gateway_key, device, gateway) = pair();
628
629 let mut message = *b"open";
630 let mut sealed = PamojaSealed {
631 counter: 0,
632 tag: [0; PAMOJA_SESSION_TAG_LEN],
633 };
634 pamoja_session_seal(
635 device,
636 message.as_mut_ptr(),
637 message.len(),
638 b"door-1".as_ptr(),
639 6,
640 &mut sealed,
641 );
642
643 assert_eq!(
644 pamoja_session_open(
645 gateway,
646 sealed,
647 message.as_mut_ptr(),
648 message.len(),
649 b"door-2".as_ptr(),
650 6,
651 ),
652 PamojaStatus::Auth
653 );
654
655 pamoja_session_free(gateway);
656 pamoja_session_free(device);
657 pamoja_agreement_key_free(gateway_key);
658 pamoja_agreement_key_free(device_key);
659 }
660 }
661
662 #[test]
663 fn the_keyed_hash_matches_the_crate() {
664 unsafe {
665 let mut digest = [0u8; PAMOJA_SESSION_KEY_LEN];
666 assert_eq!(
667 pamoja_session_hmac_sha256(
668 b"key".as_ptr(),
669 3,
670 b"message".as_ptr(),
671 7,
672 digest.as_mut_ptr()
673 ),
674 PamojaStatus::Ok
675 );
676 assert_eq!(digest, hmac_sha256(b"key", b"message"));
677 }
678 }
679
680 #[test]
681 fn expansion_matches_the_crate() {
682 unsafe {
683 let mut derived = [0u8; 40];
684 assert_eq!(
685 pamoja_session_hkdf_sha256(
686 b"salt".as_ptr(),
687 4,
688 b"secret".as_ptr(),
689 6,
690 b"pairing".as_ptr(),
691 7,
692 derived.as_mut_ptr(),
693 derived.len(),
694 ),
695 PamojaStatus::Ok
696 );
697
698 let mut want = [0u8; 40];
699 hkdf_sha256(b"salt", b"secret", b"pairing", &mut want);
700 assert_eq!(derived, want);
701 }
702 }
703
704 #[test]
705 fn a_null_handle_is_refused_rather_than_dereferenced() {
706 unsafe {
707 assert!(pamoja_agreement_key_from_seed(ptr::null(), 0).is_null());
708 assert_eq!(
709 pamoja_agreement_key_public(ptr::null(), ptr::null_mut()),
710 PamojaStatus::InvalidArgument
711 );
712 assert!(pamoja_session_establish(
713 ptr::null(),
714 ptr::null(),
715 ptr::null(),
716 0,
717 PamojaSessionRole::Initiator
718 )
719 .is_null());
720 pamoja_agreement_key_free(ptr::null_mut());
721 pamoja_session_free(ptr::null_mut());
722 }
723 }
724}