1use alloc::collections::BTreeMap;
12use alloc::sync::Arc;
13use alloc::vec::Vec;
14use core::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Mutex, RwLock};
16
17use zerodds_security::authentication::{IdentityHandle, SharedSecretHandle, SharedSecretProvider};
18use zerodds_security::crypto::{CryptoHandle, CryptographicPlugin, ReceiverMac};
19use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
20
21use ring::aead::{LessSafeKey, Nonce, UnboundKey};
22use ring::hkdf;
23use ring::hmac;
24use ring::rand::{SecureRandom, SystemRandom};
25
26use crate::suite::Suite;
27
28type SessionId = [u8; 4];
32
33struct KeyMaterial {
50 suite: Suite,
52 transformation_key_id: [u8; 4],
55 master_key: Vec<u8>,
57 master_salt: [u8; 32],
60 session_id: SessionId,
62 counter: AtomicU64,
65}
66
67impl KeyMaterial {
68 fn new_random(suite: Suite, rng: &SystemRandom) -> SecurityResult<Self> {
69 let mut mk = alloc::vec![0u8; suite.key_len()];
70 rng.fill(&mut mk).map_err(|_| {
71 SecurityError::new(
72 SecurityErrorKind::CryptoFailed,
73 "rng fill master_key failed",
74 )
75 })?;
76 let mut salt = [0u8; 32];
77 rng.fill(&mut salt).map_err(|_| {
78 SecurityError::new(
79 SecurityErrorKind::CryptoFailed,
80 "rng fill master_salt failed",
81 )
82 })?;
83 let mut sid = [0u8; 4];
84 rng.fill(&mut sid).map_err(|_| {
85 SecurityError::new(
86 SecurityErrorKind::CryptoFailed,
87 "rng fill session_id failed",
88 )
89 })?;
90 let mut key_id = [0u8; 4];
91 rng.fill(&mut key_id).map_err(|_| {
92 SecurityError::new(SecurityErrorKind::CryptoFailed, "rng fill key_id failed")
93 })?;
94 Ok(Self {
95 suite,
96 transformation_key_id: key_id,
97 master_key: mk,
98 master_salt: salt,
99 session_id: sid,
100 counter: AtomicU64::new(0),
101 })
102 }
103
104 fn from_serialized(serialized: &[u8]) -> SecurityResult<Self> {
114 if serialized.len() < 1 + 4 + 4 + 32 {
115 return Err(SecurityError::new(
116 SecurityErrorKind::BadArgument,
117 "crypto: token zu kurz (mindestens 41 byte vor master_key)",
118 ));
119 }
120 let suite = Suite::from_transform_kind_id(serialized[0]).ok_or_else(|| {
123 SecurityError::new(
124 SecurityErrorKind::BadArgument,
125 alloc::format!("crypto: unbekannte suite-id 0x{:02x}", serialized[0]),
126 )
127 })?;
128 let expected = 1 + 4 + 4 + 32 + suite.key_len();
129 if serialized.len() != expected {
130 return Err(SecurityError::new(
131 SecurityErrorKind::BadArgument,
132 alloc::format!("crypto: token fuer {suite:?} muss {expected} byte sein"),
133 ));
134 }
135 let mut sid = [0u8; 4];
136 sid.copy_from_slice(&serialized[1..5]);
137 let mut key_id = [0u8; 4];
138 key_id.copy_from_slice(&serialized[5..9]);
139 let mut salt = [0u8; 32];
140 salt.copy_from_slice(&serialized[9..41]);
141 let mk = serialized[41..].to_vec();
142 Ok(Self {
143 suite,
144 transformation_key_id: key_id,
145 master_key: mk,
146 master_salt: salt,
147 session_id: sid,
148 counter: AtomicU64::new(0),
149 })
150 }
151
152 fn serialize(&self) -> Vec<u8> {
153 let mut out = Vec::with_capacity(1 + 4 + 4 + 32 + self.master_key.len());
154 out.push(self.suite.transform_kind_id());
155 out.extend_from_slice(&self.session_id);
156 out.extend_from_slice(&self.transformation_key_id);
157 out.extend_from_slice(&self.master_salt);
158 out.extend_from_slice(&self.master_key);
159 out
160 }
161
162 fn transformation_kind_bytes(&self) -> [u8; 4] {
165 self.suite.transform_kind()
166 }
167
168 fn derive_session_key_bytes(&self) -> Vec<u8> {
172 let full = crate::session_key::derive_session_key(
173 &self.master_key,
174 &self.master_salt,
175 &self.session_id,
176 );
177 full[..self.suite.key_len()].to_vec()
180 }
181
182 fn aad(&self, extension: &[u8]) -> Vec<u8> {
186 crate::session_key::compute_aad(
187 self.transformation_kind_bytes(),
188 self.transformation_key_id,
189 self.session_id,
190 extension,
191 )
192 }
193
194 fn from_shared_secret(suite: Suite, shared_secret: &[u8]) -> SecurityResult<Self> {
205 if shared_secret.is_empty() {
206 return Err(SecurityError::new(
207 SecurityErrorKind::BadArgument,
208 "crypto: empty shared_secret",
209 ));
210 }
211 let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, b"zerodds.crypto.shared-secret.v1");
212 let prk = salt.extract(shared_secret);
213
214 let expand = |info: &[u8], out_len: usize| -> SecurityResult<Vec<u8>> {
215 let info_arr = [info];
216 let okm = prk
217 .expand(
218 &info_arr,
219 HkdfLen {
220 len: out_len,
221 hmac: hkdf::HKDF_SHA256,
222 },
223 )
224 .map_err(|_| {
225 SecurityError::new(SecurityErrorKind::CryptoFailed, "hkdf expand failed")
226 })?;
227 let mut buf = alloc::vec![0u8; out_len];
228 okm.fill(&mut buf).map_err(|_| {
229 SecurityError::new(SecurityErrorKind::CryptoFailed, "hkdf fill failed")
230 })?;
231 Ok(buf)
232 };
233
234 let master_key = expand(b"dds.sec.crypto.master_key", suite.key_len())?;
236
237 let master_salt_vec = expand(b"dds.sec.crypto.master_salt", 32)?;
240 let mut master_salt = [0u8; 32];
241 master_salt.copy_from_slice(&master_salt_vec);
242
243 let key_id_vec = expand(b"dds.sec.crypto.sender_key_id", 4)?;
245 let mut transformation_key_id = [0u8; 4];
246 transformation_key_id.copy_from_slice(&key_id_vec);
247
248 let sid_vec = expand(b"dds.sec.crypto.session_id", 4)?;
250 let mut session_id = [0u8; 4];
251 session_id.copy_from_slice(&sid_vec);
252
253 Ok(Self {
254 suite,
255 transformation_key_id,
256 master_key,
257 master_salt,
258 session_id,
259 counter: AtomicU64::new(0),
260 })
261 }
262
263 fn next_nonce(&self) -> SecurityResult<[u8; 12]> {
264 let c = self.counter.fetch_add(1, Ordering::Relaxed);
265 if c == u64::MAX {
266 return Err(SecurityError::new(
267 SecurityErrorKind::CryptoFailed,
268 "crypto: nonce-counter exhausted — key-refresh required ",
269 ));
270 }
271 let mut n = [0u8; 12];
272 n[..4].copy_from_slice(&self.session_id);
273 n[4..].copy_from_slice(&c.to_be_bytes());
274 Ok(n)
275 }
276}
277
278struct HkdfLen {
282 len: usize,
283 hmac: hkdf::Algorithm,
284}
285
286impl hkdf::KeyType for HkdfLen {
287 fn len(&self) -> usize {
288 self.len
289 }
290}
291
292impl From<HkdfLen> for hkdf::Algorithm {
293 fn from(v: HkdfLen) -> Self {
294 v.hmac
295 }
296}
297
298pub struct AesGcmCryptoPlugin {
303 rng: SystemRandom,
304 next_handle: AtomicU64,
305 local_suite: Suite,
307 slots: RwLock<BTreeMap<CryptoHandle, KeyMaterial>>,
310 secret_provider: Option<Arc<dyn SharedSecretProvider>>,
316 #[allow(clippy::type_complexity)]
321 remote_map: Mutex<BTreeMap<(CryptoHandle, IdentityHandle), CryptoHandle>>,
322}
323
324impl Default for AesGcmCryptoPlugin {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330impl AesGcmCryptoPlugin {
331 #[must_use]
333 pub fn new() -> Self {
334 Self::with_suite(Suite::Aes128Gcm)
335 }
336
337 #[must_use]
339 pub fn with_suite(suite: Suite) -> Self {
340 Self {
341 rng: SystemRandom::new(),
342 next_handle: AtomicU64::new(0),
343 local_suite: suite,
344 slots: RwLock::new(BTreeMap::new()),
345 remote_map: Mutex::new(BTreeMap::new()),
346 secret_provider: None,
347 }
348 }
349
350 #[must_use]
361 pub fn with_secret_provider(suite: Suite, provider: Arc<dyn SharedSecretProvider>) -> Self {
362 Self {
363 rng: SystemRandom::new(),
364 next_handle: AtomicU64::new(0),
365 local_suite: suite,
366 slots: RwLock::new(BTreeMap::new()),
367 remote_map: Mutex::new(BTreeMap::new()),
368 secret_provider: Some(provider),
369 }
370 }
371
372 #[must_use]
374 pub fn local_suite(&self) -> Suite {
375 self.local_suite
376 }
377
378 pub fn encrypts_remaining(&self, handle: CryptoHandle) -> SecurityResult<u64> {
385 let slots = self.slots.read().map_err(|_| poisoned())?;
386 let mat = slots.get(&handle).ok_or_else(|| {
387 SecurityError::new(SecurityErrorKind::BadArgument, "crypto: unknown handle")
388 })?;
389 let used = mat.counter.load(Ordering::Relaxed);
390 let max = mat.suite.max_encrypts();
391 Ok(max.saturating_sub(used))
392 }
393
394 pub fn rotate_key(&mut self, handle: CryptoHandle) -> SecurityResult<()> {
405 let fresh = KeyMaterial::new_random(self.local_suite, &self.rng)?;
406 let mut slots = self.slots.write().map_err(|_| poisoned())?;
407 let slot = slots.get_mut(&handle).ok_or_else(|| {
408 SecurityError::new(
409 SecurityErrorKind::BadArgument,
410 "crypto: rotate_key unknown handle",
411 )
412 })?;
413 slot.master_key = fresh.master_key;
414 slot.session_id = fresh.session_id;
415 slot.counter.store(0, Ordering::Relaxed);
416 Ok(())
417 }
418
419 fn next_id(&self) -> u64 {
420 self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
421 }
422
423 fn insert(&self, mat: KeyMaterial) -> SecurityResult<CryptoHandle> {
424 let handle = CryptoHandle(self.next_id());
425 self.slots
426 .write()
427 .map_err(|_| poisoned())?
428 .insert(handle, mat);
429 Ok(handle)
430 }
431}
432
433fn poisoned() -> SecurityError {
434 SecurityError::new(
435 SecurityErrorKind::Internal,
436 "crypto: internal rwlock poisoned",
437 )
438}
439
440impl CryptographicPlugin for AesGcmCryptoPlugin {
441 fn register_local_participant(
442 &mut self,
443 _identity: IdentityHandle,
444 _properties: &[(&str, &str)],
445 ) -> SecurityResult<CryptoHandle> {
446 let mat = KeyMaterial::new_random(self.local_suite, &self.rng)?;
447 self.insert(mat)
448 }
449
450 fn register_matched_remote_participant(
451 &mut self,
452 _local: CryptoHandle,
453 _remote_identity: IdentityHandle,
454 shared_secret: SharedSecretHandle,
455 ) -> SecurityResult<CryptoHandle> {
456 if let Some(provider) = &self.secret_provider {
467 if let Some(secret) = provider.get_shared_secret(shared_secret) {
468 let mat = KeyMaterial::from_shared_secret(self.local_suite, &secret)?;
469 return self.insert(mat);
470 }
471 }
472 let mat = KeyMaterial::new_random(self.local_suite, &self.rng)?;
476 self.insert(mat)
477 }
478
479 fn register_local_endpoint(
480 &mut self,
481 _participant: CryptoHandle,
482 _is_writer: bool,
483 _properties: &[(&str, &str)],
484 ) -> SecurityResult<CryptoHandle> {
485 let mat = KeyMaterial::new_random(self.local_suite, &self.rng)?;
486 self.insert(mat)
487 }
488
489 fn create_local_participant_crypto_tokens(
490 &mut self,
491 local: CryptoHandle,
492 _remote: CryptoHandle,
493 ) -> SecurityResult<Vec<u8>> {
494 let slots = self.slots.read().map_err(|_| poisoned())?;
500 let mat = slots.get(&local).ok_or_else(|| {
501 SecurityError::new(
502 SecurityErrorKind::BadArgument,
503 "crypto: unknown local handle",
504 )
505 })?;
506 Ok(mat.serialize())
507 }
508
509 fn set_remote_participant_crypto_tokens(
510 &mut self,
511 _local: CryptoHandle,
512 remote: CryptoHandle,
513 tokens: &[u8],
514 ) -> SecurityResult<()> {
515 let mat = KeyMaterial::from_serialized(tokens)?;
516 self.slots
517 .write()
518 .map_err(|_| poisoned())?
519 .insert(remote, mat);
520 Ok(())
521 }
522
523 fn encrypt_submessage(
524 &self,
525 local: CryptoHandle,
526 _remote_list: &[CryptoHandle],
527 plaintext: &[u8],
528 aad_extension: &[u8],
529 ) -> SecurityResult<Vec<u8>> {
530 #[cfg(feature = "metrics")]
531 let _op = crate::metrics::CryptoOp::start("encrypt");
532 let slots = self.slots.read().map_err(|_| poisoned())?;
533 let mat = slots.get(&local).ok_or_else(|| {
534 SecurityError::new(
535 SecurityErrorKind::BadArgument,
536 "crypto: unknown local handle",
537 )
538 })?;
539 let nonce = mat.next_nonce()?;
540 let session_key = mat.derive_session_key_bytes();
546 let aad_bytes = mat.aad(aad_extension);
547
548 if !mat.suite.is_aead() {
549 let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &session_key);
552 let mut ctx = hmac::Context::with_key(&hmac_key);
553 ctx.update(&aad_bytes);
554 ctx.update(&nonce);
555 ctx.update(plaintext);
556 let tag = ctx.sign();
557 let mut out = Vec::with_capacity(12 + plaintext.len() + 32);
558 out.extend_from_slice(&nonce);
559 out.extend_from_slice(plaintext);
560 out.extend_from_slice(tag.as_ref());
561 return Ok(out);
562 }
563
564 let key = key_from_bytes(mat.suite, &session_key)?;
565
566 let mut payload: Vec<u8> = plaintext.to_vec();
569 let nonce_obj = Nonce::assume_unique_for_key(nonce);
570 key.seal_in_place_append_tag(nonce_obj, ring::aead::Aad::from(&aad_bytes), &mut payload)
571 .map_err(|_| {
572 SecurityError::new(SecurityErrorKind::CryptoFailed, "crypto: seal failed")
573 })?;
574 let mut out = Vec::with_capacity(12 + payload.len());
576 out.extend_from_slice(&nonce);
577 out.extend(payload);
578 Ok(out)
579 }
580
581 fn decrypt_submessage(
582 &self,
583 local: CryptoHandle,
584 _remote: CryptoHandle,
585 ciphertext: &[u8],
586 aad_extension: &[u8],
587 ) -> SecurityResult<Vec<u8>> {
588 #[cfg(feature = "metrics")]
589 let _op = crate::metrics::CryptoOp::start("decrypt");
590 let slots = self.slots.read().map_err(|_| poisoned())?;
591 let mat = slots.get(&local).ok_or_else(|| {
592 SecurityError::new(SecurityErrorKind::BadArgument, "crypto: unknown handle")
593 })?;
594
595 let session_key = mat.derive_session_key_bytes();
600 let aad_bytes = mat.aad(aad_extension);
601
602 if !mat.suite.is_aead() {
603 if ciphertext.len() < 12 + 32 {
605 return Err(SecurityError::new(
606 SecurityErrorKind::BadArgument,
607 "crypto: hmac-buffer zu kurz fuer nonce+tag",
608 ));
609 }
610 let (nonce_bytes, rest) = ciphertext.split_at(12);
611 let (plain, tag) = rest.split_at(rest.len() - 32);
612 let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &session_key);
613 let mut signed_input =
614 Vec::with_capacity(aad_bytes.len() + nonce_bytes.len() + plain.len());
615 signed_input.extend_from_slice(&aad_bytes);
616 signed_input.extend_from_slice(nonce_bytes);
617 signed_input.extend_from_slice(plain);
618 hmac::verify(&hmac_key, &signed_input, tag).map_err(|_| {
619 SecurityError::new(
620 SecurityErrorKind::CryptoFailed,
621 "crypto: hmac verify failed (tag mismatch)",
622 )
623 })?;
624 return Ok(plain.to_vec());
625 }
626
627 if ciphertext.len() < 12 + 16 {
628 return Err(SecurityError::new(
629 SecurityErrorKind::BadArgument,
630 "crypto: ciphertext zu kurz fuer nonce+tag",
631 ));
632 }
633 let (nonce_bytes, ct) = ciphertext.split_at(12);
634 let key = key_from_bytes(mat.suite, &session_key)?;
635
636 let mut n = [0u8; 12];
637 n.copy_from_slice(nonce_bytes);
638 let mut buf = ct.to_vec();
639 let nonce_obj = Nonce::assume_unique_for_key(n);
640 let plain = key
641 .open_in_place(nonce_obj, ring::aead::Aad::from(&aad_bytes), &mut buf)
642 .map_err(|_| {
643 SecurityError::new(
644 SecurityErrorKind::CryptoFailed,
645 "crypto: open/verify failed (tag mismatch?)",
646 )
647 })?;
648 Ok(plain.to_vec())
649 }
650
651 fn encrypt_submessage_multi(
652 &self,
653 local: CryptoHandle,
654 receivers: &[(CryptoHandle, u32)],
655 plaintext: &[u8],
656 aad_extension: &[u8],
657 ) -> SecurityResult<(Vec<u8>, Vec<ReceiverMac>)> {
658 let handles: Vec<CryptoHandle> = receivers.iter().map(|(h, _)| *h).collect();
659 let ciphertext = self.encrypt_submessage(local, &handles, plaintext, aad_extension)?;
660
661 let slots = self.slots.read().map_err(|_| poisoned())?;
664 let mut macs = Vec::with_capacity(receivers.len());
665 for (remote, key_id) in receivers {
666 let mat = slots.get(remote).ok_or_else(|| {
667 SecurityError::new(
668 SecurityErrorKind::BadArgument,
669 "crypto: unknown remote handle for receiver-specific mac",
670 )
671 })?;
672 let receiver_session_key = crate::session_key::derive_session_hmac_key(
676 &mat.master_key,
677 &mat.master_salt,
678 &mat.session_id,
679 );
680 let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &receiver_session_key);
681 let tag = hmac::sign(&hmac_key, &ciphertext);
682 let mut mac16 = [0u8; 16];
684 mac16.copy_from_slice(&tag.as_ref()[..16]);
685 macs.push(ReceiverMac {
686 key_id: *key_id,
687 mac: mac16,
688 });
689 }
690 Ok((ciphertext, macs))
691 }
692
693 #[allow(clippy::too_many_arguments)]
694 fn decrypt_submessage_with_receiver_mac(
695 &self,
696 local: CryptoHandle,
697 remote: CryptoHandle,
698 own_key_id: u32,
699 own_mac_key_handle: CryptoHandle,
700 ciphertext: &[u8],
701 macs: &[ReceiverMac],
702 aad_extension: &[u8],
703 ) -> SecurityResult<Vec<u8>> {
704 if macs.is_empty() {
705 return self.decrypt_submessage(local, remote, ciphertext, aad_extension);
708 }
709
710 let our_mac = macs
711 .iter()
712 .find(|m| m.key_id == own_key_id)
713 .ok_or_else(|| {
714 SecurityError::new(
715 SecurityErrorKind::CryptoFailed,
716 "crypto: no receiver-specific MAC matches own key_id",
717 )
718 })?;
719
720 let slots = self.slots.read().map_err(|_| poisoned())?;
722 let mat = slots.get(&own_mac_key_handle).ok_or_else(|| {
723 SecurityError::new(
724 SecurityErrorKind::BadArgument,
725 "crypto: unknown own_mac_key_handle",
726 )
727 })?;
728 let receiver_session_key = crate::session_key::derive_session_hmac_key(
731 &mat.master_key,
732 &mat.master_salt,
733 &mat.session_id,
734 );
735 let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &receiver_session_key);
736 let full_tag = hmac::sign(&hmac_key, ciphertext);
737 if full_tag.as_ref()[..16] != our_mac.mac {
738 return Err(SecurityError::new(
739 SecurityErrorKind::CryptoFailed,
740 "crypto: receiver-specific mac mismatch",
741 ));
742 }
743 drop(slots);
744
745 self.decrypt_submessage(local, remote, ciphertext, aad_extension)
748 }
749
750 fn plugin_class_id(&self) -> &str {
751 "DDS:Crypto:AES-GCM-GMAC:1.2"
752 }
753}
754
755fn key_from_bytes(suite: Suite, k: &[u8]) -> SecurityResult<LessSafeKey> {
756 if k.len() != suite.key_len() {
757 return Err(SecurityError::new(
758 SecurityErrorKind::CryptoFailed,
759 alloc::format!(
760 "crypto: key_from_bytes expected {} bytes, got {}",
761 suite.key_len(),
762 k.len()
763 ),
764 ));
765 }
766 let algo = suite.algorithm().ok_or_else(|| {
767 SecurityError::new(
768 SecurityErrorKind::CryptoFailed,
769 "crypto: key_from_bytes fuer non-AEAD-Suite aufgerufen",
770 )
771 })?;
772 let unbound = UnboundKey::new(algo, k).map_err(|_| {
773 SecurityError::new(
774 SecurityErrorKind::CryptoFailed,
775 "crypto: UnboundKey creation",
776 )
777 })?;
778 Ok(LessSafeKey::new(unbound))
779}
780
781#[allow(dead_code)]
782fn suppress_unused_remote_map_warning(
783 p: &AesGcmCryptoPlugin,
784) -> &Mutex<BTreeMap<(CryptoHandle, IdentityHandle), CryptoHandle>> {
785 &p.remote_map
786}
787
788#[cfg(test)]
789#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
790mod tests {
791 use super::*;
792
793 #[test]
794 fn plugin_class_id_matches_spec() {
795 let p = AesGcmCryptoPlugin::new();
796 assert_eq!(p.plugin_class_id(), "DDS:Crypto:AES-GCM-GMAC:1.2");
797 }
798
799 #[test]
800 fn encrypt_decrypt_roundtrip() {
801 let mut p = AesGcmCryptoPlugin::new();
802 let local = p
803 .register_local_participant(IdentityHandle(1), &[])
804 .unwrap();
805 let remote = p
806 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
807 .unwrap();
808
809 let plain = b"hello zerodds secure world";
810 let ct = p.encrypt_submessage(local, &[remote], plain, &[]).unwrap();
811 assert_eq!(ct.len(), plain.len() + 12 + 16);
813
814 let back = p.decrypt_submessage(local, remote, &ct, &[]).unwrap();
815 assert_eq!(back, plain);
816 }
817
818 #[test]
819 fn decrypt_rejects_tampered_ciphertext() {
820 let mut p = AesGcmCryptoPlugin::new();
821 let local = p
822 .register_local_participant(IdentityHandle(1), &[])
823 .unwrap();
824 let remote = p
825 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
826 .unwrap();
827
828 let plain = b"AAAAAAAAAAAAAAAA";
829 let mut ct = p.encrypt_submessage(local, &[remote], plain, &[]).unwrap();
830 ct[14] ^= 0x01;
832 let err = p.decrypt_submessage(local, remote, &ct, &[]).unwrap_err();
833 assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
834 }
835
836 #[test]
837 fn two_encrypts_produce_different_ciphertexts() {
838 let mut p = AesGcmCryptoPlugin::new();
839 let local = p
840 .register_local_participant(IdentityHandle(1), &[])
841 .unwrap();
842
843 let plain = b"same plaintext";
844 let ct1 = p.encrypt_submessage(local, &[], plain, &[]).unwrap();
845 let ct2 = p.encrypt_submessage(local, &[], plain, &[]).unwrap();
846 assert_ne!(ct1, ct2);
848 }
849
850 #[test]
851 fn cross_plugin_interop_via_tokens() {
852 let mut alice = AesGcmCryptoPlugin::new();
854 let mut bob = AesGcmCryptoPlugin::new();
855
856 let alice_local = alice
857 .register_local_participant(IdentityHandle(1), &[])
858 .unwrap();
859 let bob_local = bob
860 .register_local_participant(IdentityHandle(2), &[])
861 .unwrap();
862
863 let token = alice
865 .create_local_participant_crypto_tokens(alice_local, CryptoHandle(0))
866 .unwrap();
867
868 let alice_seen_by_bob = bob
870 .register_matched_remote_participant(
871 bob_local,
872 IdentityHandle(1),
873 SharedSecretHandle(1),
874 )
875 .unwrap();
876 bob.set_remote_participant_crypto_tokens(bob_local, alice_seen_by_bob, &token)
877 .unwrap();
878
879 let plain = b"cross-plugin-test";
881 let ct = alice
882 .encrypt_submessage(alice_local, &[], plain, &[])
883 .unwrap();
884
885 let back = bob
887 .decrypt_submessage(alice_seen_by_bob, CryptoHandle(0), &ct, &[])
888 .unwrap();
889 assert_eq!(back, plain);
890 }
891
892 #[test]
893 fn decrypt_rejects_too_short_input() {
894 let mut p = AesGcmCryptoPlugin::new();
895 let local = p
896 .register_local_participant(IdentityHandle(1), &[])
897 .unwrap();
898 let err = p
899 .decrypt_submessage(local, CryptoHandle(0), b"short", &[])
900 .unwrap_err();
901 assert_eq!(err.kind, SecurityErrorKind::BadArgument);
902 }
903
904 #[test]
909 fn default_plugin_uses_aes128() {
910 let p = AesGcmCryptoPlugin::new();
911 assert_eq!(p.local_suite(), Suite::Aes128Gcm);
912 }
913
914 #[test]
915 fn aes256_plugin_reports_aes256_suite() {
916 let p = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
917 assert_eq!(p.local_suite(), Suite::Aes256Gcm);
918 }
919
920 #[test]
921 fn aes256_encrypt_decrypt_roundtrip() {
922 let mut p = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
923 let local = p
924 .register_local_participant(IdentityHandle(1), &[])
925 .unwrap();
926 let remote = p
927 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
928 .unwrap();
929
930 let plain = b"aes-256 payload with forward secrecy";
931 let ct = p.encrypt_submessage(local, &[remote], plain, &[]).unwrap();
932 assert_eq!(ct.len(), plain.len() + 12 + 16);
933 let back = p.decrypt_submessage(local, remote, &ct, &[]).unwrap();
934 assert_eq!(back, plain);
935 }
936
937 #[test]
938 fn aes256_tampered_ciphertext_fails_verify() {
939 let mut p = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
940 let local = p
941 .register_local_participant(IdentityHandle(1), &[])
942 .unwrap();
943 let remote = p
944 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
945 .unwrap();
946
947 let mut ct = p
948 .encrypt_submessage(local, &[remote], b"0123456789abcdef0123", &[])
949 .unwrap();
950 ct[14] ^= 0x01;
951 let err = p.decrypt_submessage(local, remote, &ct, &[]).unwrap_err();
952 assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
953 }
954
955 #[test]
956 fn tokens_carry_suite_tag_so_cross_suite_interop_works() {
957 let mut alice = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
963 let mut bob = AesGcmCryptoPlugin::with_suite(Suite::Aes128Gcm);
964
965 let a_local = alice
966 .register_local_participant(IdentityHandle(1), &[])
967 .unwrap();
968 let b_local = bob
969 .register_local_participant(IdentityHandle(2), &[])
970 .unwrap();
971
972 let token = alice
973 .create_local_participant_crypto_tokens(a_local, CryptoHandle(0))
974 .unwrap();
975 assert_eq!(
976 token[0],
977 crate::suite::transform_kind::AES256_GCM,
978 "suite-tag must be Spec AES256_GCM (0x04)"
979 );
980
981 let alice_slot_in_bob = bob
982 .register_matched_remote_participant(b_local, IdentityHandle(1), SharedSecretHandle(1))
983 .unwrap();
984 bob.set_remote_participant_crypto_tokens(b_local, alice_slot_in_bob, &token)
985 .unwrap();
986
987 let plain = b"cross-suite interop ok";
988 let ct = alice.encrypt_submessage(a_local, &[], plain, &[]).unwrap();
989 let back = bob
990 .decrypt_submessage(alice_slot_in_bob, CryptoHandle(0), &ct, &[])
991 .unwrap();
992 assert_eq!(back, plain);
993 }
994
995 #[test]
996 fn rejects_token_with_unknown_suite_id() {
997 let mut p = AesGcmCryptoPlugin::new();
998 let local = p
999 .register_local_participant(IdentityHandle(1), &[])
1000 .unwrap();
1001 let remote = p
1002 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
1003 .unwrap();
1004 let bogus = [
1006 0xFFu8, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1007 ];
1008 let err = p
1009 .set_remote_participant_crypto_tokens(local, remote, &bogus)
1010 .unwrap_err();
1011 assert_eq!(err.kind, SecurityErrorKind::BadArgument);
1012 }
1013
1014 #[test]
1019 fn hmac_only_suite_roundtrip_without_encryption() {
1020 let mut p = AesGcmCryptoPlugin::with_suite(Suite::HmacSha256);
1021 let local = p
1022 .register_local_participant(IdentityHandle(1), &[])
1023 .unwrap();
1024 let plain = b"payload kept in plaintext, only signed";
1025 let signed = p.encrypt_submessage(local, &[], plain, &[]).unwrap();
1026 assert!(
1029 signed.windows(plain.len()).any(|w| w == plain),
1030 "HMAC-Suite sollte plaintext NICHT verschluesseln"
1031 );
1032 let back = p
1033 .decrypt_submessage(local, CryptoHandle(0), &signed, &[])
1034 .unwrap();
1035 assert_eq!(back, plain);
1036 }
1037
1038 #[test]
1039 fn hmac_tampered_payload_fails_verify() {
1040 let mut p = AesGcmCryptoPlugin::with_suite(Suite::HmacSha256);
1041 let local = p
1042 .register_local_participant(IdentityHandle(1), &[])
1043 .unwrap();
1044 let mut signed = p
1045 .encrypt_submessage(local, &[], b"original message", &[])
1046 .unwrap();
1047 signed[15] ^= 0x01;
1049 let err = p
1050 .decrypt_submessage(local, CryptoHandle(0), &signed, &[])
1051 .unwrap_err();
1052 assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
1053 }
1054
1055 #[test]
1056 fn hmac_tampered_tag_fails_verify() {
1057 let mut p = AesGcmCryptoPlugin::with_suite(Suite::HmacSha256);
1058 let local = p
1059 .register_local_participant(IdentityHandle(1), &[])
1060 .unwrap();
1061 let mut signed = p.encrypt_submessage(local, &[], b"x", &[]).unwrap();
1062 let last = signed.len() - 1;
1063 signed[last] ^= 0x01;
1064 let err = p
1065 .decrypt_submessage(local, CryptoHandle(0), &signed, &[])
1066 .unwrap_err();
1067 assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
1068 }
1069
1070 #[test]
1071 fn encrypts_remaining_decrements_per_call() {
1072 let mut p = AesGcmCryptoPlugin::new();
1073 let local = p
1074 .register_local_participant(IdentityHandle(1), &[])
1075 .unwrap();
1076 let before = p.encrypts_remaining(local).unwrap();
1077 let _ = p.encrypt_submessage(local, &[], b"x", &[]).unwrap();
1078 let after = p.encrypts_remaining(local).unwrap();
1079 assert_eq!(before - after, 1);
1080 }
1081
1082 #[test]
1083 fn rotate_key_resets_counter_and_changes_key() {
1084 let mut p = AesGcmCryptoPlugin::new();
1085 let local = p
1086 .register_local_participant(IdentityHandle(1), &[])
1087 .unwrap();
1088 let _ = p.encrypt_submessage(local, &[], b"x", &[]).unwrap();
1091 let token_before = p
1092 .create_local_participant_crypto_tokens(local, CryptoHandle(0))
1093 .unwrap();
1094
1095 p.rotate_key(local).unwrap();
1096 assert_eq!(
1097 p.encrypts_remaining(local).unwrap(),
1098 Suite::Aes128Gcm.max_encrypts(),
1099 "counter muss nach rotate bei 0 starten"
1100 );
1101
1102 let token_after = p
1103 .create_local_participant_crypto_tokens(local, CryptoHandle(0))
1104 .unwrap();
1105 assert_ne!(token_before, token_after, "master-key muss neu sein");
1106 }
1107
1108 #[test]
1109 fn rotate_key_rejects_unknown_handle() {
1110 let mut p = AesGcmCryptoPlugin::new();
1111 let err = p.rotate_key(CryptoHandle(9999)).unwrap_err();
1112 assert_eq!(err.kind, SecurityErrorKind::BadArgument);
1113 }
1114
1115 use alloc::collections::BTreeMap as BTreeMap2;
1120 use alloc::sync::Arc as ArcA;
1121 use std::sync::RwLock as StdRwLock;
1122
1123 struct MemProvider {
1127 inner: StdRwLock<BTreeMap2<SharedSecretHandle, Vec<u8>>>,
1128 }
1129
1130 impl MemProvider {
1131 fn new() -> Self {
1132 Self {
1133 inner: StdRwLock::new(BTreeMap2::new()),
1134 }
1135 }
1136 fn insert(&self, handle: SharedSecretHandle, bytes: Vec<u8>) {
1137 self.inner.write().unwrap().insert(handle, bytes);
1138 }
1139 }
1140
1141 impl SharedSecretProvider for MemProvider {
1142 fn get_shared_secret(&self, handle: SharedSecretHandle) -> Option<Vec<u8>> {
1143 self.inner.read().ok()?.get(&handle).cloned()
1144 }
1145 }
1146
1147 #[test]
1148 fn with_secret_provider_derives_same_master_key_for_both_sides() {
1149 let shared = alloc::vec![0xA5u8; 32];
1156 let provider_a = ArcA::new(MemProvider::new());
1157 let provider_b = ArcA::new(MemProvider::new());
1158 provider_a.insert(SharedSecretHandle(1), shared.clone());
1159 provider_b.insert(SharedSecretHandle(1), shared.clone());
1160
1161 let mut alice = AesGcmCryptoPlugin::with_secret_provider(
1162 Suite::Aes128Gcm,
1163 ArcA::clone(&provider_a) as ArcA<dyn SharedSecretProvider>,
1164 );
1165 let mut bob = AesGcmCryptoPlugin::with_secret_provider(
1166 Suite::Aes128Gcm,
1167 ArcA::clone(&provider_b) as ArcA<dyn SharedSecretProvider>,
1168 );
1169
1170 let alice_local = alice
1171 .register_local_participant(IdentityHandle(1), &[])
1172 .unwrap();
1173 let bob_local = bob
1174 .register_local_participant(IdentityHandle(1), &[])
1175 .unwrap();
1176
1177 let alice_to_bob = alice
1178 .register_matched_remote_participant(
1179 alice_local,
1180 IdentityHandle(2),
1181 SharedSecretHandle(1),
1182 )
1183 .unwrap();
1184 let bob_to_alice = bob
1185 .register_matched_remote_participant(
1186 bob_local,
1187 IdentityHandle(1),
1188 SharedSecretHandle(1),
1189 )
1190 .unwrap();
1191
1192 let plain = b"x25519-handshake-derived-key";
1194 let wire = alice
1195 .encrypt_submessage(alice_to_bob, &[], plain, &[])
1196 .unwrap();
1197 let back = bob
1198 .decrypt_submessage(bob_to_alice, bob_to_alice, &wire, &[])
1199 .unwrap();
1200 assert_eq!(back, plain);
1201 }
1202
1203 #[test]
1204 fn with_secret_provider_different_secrets_yield_distinct_keys() {
1205 let provider = ArcA::new(MemProvider::new());
1209 provider.insert(SharedSecretHandle(1), alloc::vec![0x11u8; 32]);
1210 provider.insert(SharedSecretHandle(2), alloc::vec![0x22u8; 32]);
1211 let mut p = AesGcmCryptoPlugin::with_secret_provider(
1212 Suite::Aes128Gcm,
1213 ArcA::clone(&provider) as ArcA<dyn SharedSecretProvider>,
1214 );
1215 let local = p
1216 .register_local_participant(IdentityHandle(1), &[])
1217 .unwrap();
1218 let bob = p
1219 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
1220 .unwrap();
1221 let charlie = p
1222 .register_matched_remote_participant(local, IdentityHandle(3), SharedSecretHandle(2))
1223 .unwrap();
1224 let tok_bob = p
1225 .create_local_participant_crypto_tokens(bob, CryptoHandle(0))
1226 .unwrap();
1227 let tok_charlie = p
1228 .create_local_participant_crypto_tokens(charlie, CryptoHandle(0))
1229 .unwrap();
1230 assert_ne!(
1231 tok_bob, tok_charlie,
1232 "DH-Shared-Secrets muessen zu verschiedenen Per-Peer-Keys fuehren"
1233 );
1234 }
1235
1236 #[test]
1237 fn with_secret_provider_unknown_handle_falls_back_to_random() {
1238 let provider = ArcA::new(MemProvider::new()); let mut p = AesGcmCryptoPlugin::with_secret_provider(
1243 Suite::Aes128Gcm,
1244 ArcA::clone(&provider) as ArcA<dyn SharedSecretProvider>,
1245 );
1246 let local = p
1247 .register_local_participant(IdentityHandle(1), &[])
1248 .unwrap();
1249 let h = p
1250 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(42))
1251 .expect("unknown handle → random slot, kein Error");
1252 let _tok = p
1254 .create_local_participant_crypto_tokens(h, CryptoHandle(0))
1255 .unwrap();
1256 }
1257
1258 #[test]
1259 fn without_provider_backward_compat_random_key_preserved() {
1260 let mut p = AesGcmCryptoPlugin::new(); let local = p
1263 .register_local_participant(IdentityHandle(1), &[])
1264 .unwrap();
1265 let a = p
1266 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
1267 .unwrap();
1268 let b = p
1269 .register_matched_remote_participant(local, IdentityHandle(3), SharedSecretHandle(2))
1270 .unwrap();
1271 let tok_a = p
1272 .create_local_participant_crypto_tokens(a, CryptoHandle(0))
1273 .unwrap();
1274 let tok_b = p
1275 .create_local_participant_crypto_tokens(b, CryptoHandle(0))
1276 .unwrap();
1277 assert_ne!(tok_a, tok_b, "Random-Keys → zwei verschiedene Tokens");
1278 }
1279
1280 #[test]
1281 fn hkdf_derivation_is_deterministic() {
1282 let secret = alloc::vec![0xCDu8; 32];
1284 let m1 = KeyMaterial::from_shared_secret(Suite::Aes128Gcm, &secret).unwrap();
1285 let m2 = KeyMaterial::from_shared_secret(Suite::Aes128Gcm, &secret).unwrap();
1286 assert_eq!(m1.master_key, m2.master_key);
1287 assert_eq!(m1.session_id, m2.session_id);
1288 }
1289
1290 #[test]
1291 fn hkdf_rejects_empty_secret() {
1292 let res = KeyMaterial::from_shared_secret(Suite::Aes128Gcm, &[]);
1293 match res {
1294 Err(e) => assert_eq!(e.kind, SecurityErrorKind::BadArgument),
1295 Ok(_) => panic!("expected BadArgument, got Ok"),
1296 }
1297 }
1298}