Skip to main content

zerodds_security_crypto/
plugin.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! AES-GCM-128 / AES-GCM-256 CryptographicPlugin-Implementation.
5//!
6//! zerodds-lint: allow no_dyn_in_safe
7//! (`Arc<dyn SharedSecretProvider>` wird gehalten, damit der
8//! Plugin mit beliebigen Authentication-Plugins arbeitet. Der
9//! Provider ist reines Lookup-Interface ohne Safety-Implikation.)
10
11use 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
28/// Session-ID: 4-byte-Präfix einer 12-byte GCM-Nonce. Pro
29/// `CryptoHandle` zufaellig gezogen; schuetzt vor Nonce-Collision
30/// zwischen verschiedenen Keys.
31type SessionId = [u8; 4];
32
33/// Ein Crypto-Slot — DDS-Security 1.2 §10.5.2 KeyMaterial-AES-GCM-GMAC
34/// Tab.73 Layout (C3.7-b):
35///
36/// ```text
37/// transformation_kind     (1 byte, Suite::transform_kind_id)
38/// master_salt             (32 byte, fuer Spec-konforme session_key-
39///                          Derivation; Spec §10.5.2 Tab.74)
40/// sender_key_id           (4 byte, transformation_key_id)
41/// master_sender_key       (16 oder 32 byte, suite.key_len())
42/// session_id              (4 byte, rotiert pro Session)
43/// ```
44///
45/// Encrypt/Decrypt nutzen `derive_session_key(master_key, master_salt,
46/// session_id)` als Per-Submessage-AES-Key + `compute_aad(transform_kind,
47/// key_id, session_id, &[])` als AES-GCM AAD. Damit ist der Hot-Path
48/// spec-byte-kompatibel zu Cyclone DDS / FastDDS.
49struct KeyMaterial {
50    /// AEAD-Suite (128 vs. 256).
51    suite: Suite,
52    /// `transformation_key_id` (Spec §10.5.2 Tab.73, 4 byte). Eindeutig
53    /// pro Slot.
54    transformation_key_id: [u8; 4],
55    /// Master-Sender-Key. Laenge gemaess `suite.key_len()` (16 oder 32).
56    master_key: Vec<u8>,
57    /// `master_salt` (Spec §10.5.2 Tab.73 + Tab.74) — 32 byte; fliesst
58    /// in `derive_session_key` ein.
59    master_salt: [u8; 32],
60    /// Session-ID — konstant pro Slot, rotiert beim Key-Refresh.
61    session_id: SessionId,
62    /// Nonce-Counter — monoton pro Encrypt. Bei Ueberlauf (u64::MAX)
63    /// wird Encrypt abgelehnt .
64    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    /// Spec-konformes Token-Layout (DDS-Security 1.2 §10.5.2 Tab.73,
105    /// C3.7-b):
106    ///
107    /// ```text
108    /// [transformation_kind(1) | session_id(4) | sender_key_id(4) |
109    ///  master_salt(32) | master_sender_key(N)]
110    /// ```
111    ///
112    /// Total: `1 + 4 + 4 + 32 + suite.key_len()` byte.
113    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        // Spec-konform: AES128_GCM=0x02, AES256_GCM=0x04, HmacSha256
121        // im AES128_GMAC-Slot=0x01 (siehe Suite::transform_kind_id).
122        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    /// Liefert das Spec-konforme `transformation_kind` als 4-byte BE-
163    /// Array (Spec §10.5 Tab.79). Wird in der AAD verwendet.
164    fn transformation_kind_bytes(&self) -> [u8; 4] {
165        self.suite.transform_kind()
166    }
167
168    /// Spec §10.5.2 Tab.74 + §8.1 Tab.78 — leitet den Per-Submessage
169    /// `session_key` und die AAD ab. Verwendet die C3.7-Helper aus
170    /// `zerodds_security_crypto::session_key`.
171    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        // Truncate auf Suite-Key-Laenge (16 byte AES-128, 32 byte AES-256
178        // oder HMAC-SHA256-Auth).
179        full[..self.suite.key_len()].to_vec()
180    }
181
182    /// Baut die AAD fuer diese Submessage. `extension` ist der RTPS-
183    /// Header beim `rtps_protection_kind != NONE` — fuer Submessage-
184    /// Protection ist `extension` leer.
185    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    /// Leitet Master-Key + Session-ID deterministisch aus einem
195    /// `SharedSecret` (32 Byte aus X25519-DH-Handshake) via HKDF-SHA256
196    /// ab. beide Kommunikations-Partner berechnen denselben
197    /// Master-Key, ohne Token-Exchange.
198    ///
199    /// Die `session_id` wird ebenfalls deterministisch aus dem Secret
200    /// abgeleitet (als zweite HKDF-Info), damit beide Seiten
201    /// kompatible Nonces produzieren. Falls dasselbe Secret fuer
202    /// mehrere Slots genutzt wird, muss der Caller die Slots manuell
203    /// separat rotieren.
204    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        // Master-Key (Spec §10.5.2 Tab.73).
235        let master_key = expand(b"dds.sec.crypto.master_key", suite.key_len())?;
236
237        // Master-Salt (Spec §10.5.2 Tab.73 + Tab.74) — 32 byte fuer
238        // HMAC-SHA256-basierte session_key-Derivation.
239        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        // Sender-Key-Id (4 byte) — eindeutig pro Slot, fliesst in AAD.
244        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        // Session-Id (4 byte) — Nonce-Praefix + AAD-Bestandteil.
249        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
278/// Manuelle Laenge-Struktur fuer `ring::hkdf::Prk::expand`. Das
279/// `KeyType`-Trait der ring-Crate akzeptiert nur Types die
280/// `len()` + `HKDF-Algo` liefern.
281struct 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
298/// AES-GCM Crypto-Plugin. Keys werden in einem internen Slab
299/// gehalten; Lookup per `CryptoHandle`. Welche Suite lokal erzeugte
300/// Keys haben, bestimmt `local_suite` — Remote-Keys kommen mit ihrer
301/// eigenen Suite-ID via Token.
302pub struct AesGcmCryptoPlugin {
303    rng: SystemRandom,
304    next_handle: AtomicU64,
305    /// Suite fuer lokal erzeugte Keys.
306    local_suite: Suite,
307    // RwLock fuer `slots` — read-heavy (jedes Encrypt/Decrypt liest);
308    // Register passiert selten beim Setup.
309    slots: RwLock<BTreeMap<CryptoHandle, KeyMaterial>>,
310    /// Optionaler SharedSecretProvider . Wenn gesetzt,
311    /// zieht `register_matched_remote_participant` den Master-Key
312    /// deterministisch aus dem DH-Shared-Secret statt einen Random-
313    /// Key zu generieren. Backward-Compat: `None` = v1.4-Pfad mit
314    /// Random-Key und Token-Exchange.
315    secret_provider: Option<Arc<dyn SharedSecretProvider>>,
316    // Verknuepft ein lokales/Remote-Participant-Paar mit dem
317    // **fuer diesen Link** benutzten Remote-Slot — nach
318    // `set_remote_participant_crypto_tokens`.
319    // (local, remote_identity) → remote_slot_handle
320    #[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    /// Konstruktor mit Default-Suite `AES-GCM-128`.
332    #[must_use]
333    pub fn new() -> Self {
334        Self::with_suite(Suite::Aes128Gcm)
335    }
336
337    /// Konstruktor mit expliziter Suite (`Aes128Gcm` oder `Aes256Gcm`).
338    #[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    /// Baut einen Plugin-Slot mit echtem DH-abgeleiteten Per-Peer-Key
351    /// . Der `SharedSecretProvider` liefert die rohen Bytes
352    /// aus einem abgeschlossenen Authentication-Handshake; wir leiten
353    /// via HKDF-SHA256 einen deterministischen Master-Key ab, den beide
354    /// Seiten ohne Token-Exchange berechnen koennen.
355    ///
356    /// Mit Provider: `register_matched_remote_participant` nutzt den
357    /// uebergebenen `SharedSecretHandle`, um den Per-Peer-Key aus dem
358    /// PKI-Handshake zu ziehen. Ohne Provider bleibt das v1.4-Verhalten
359    /// (Random-Key + Token-Exchange).
360    #[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    /// Lokale Suite (fuer Tests / Metrics).
373    #[must_use]
374    pub fn local_suite(&self) -> Suite {
375        self.local_suite
376    }
377
378    /// Zaehl der verbleibenden sicheren Encrypts auf einem Slot.
379    /// Wrapping-Point: `Suite::max_encrypts()` (2^48 per Default).
380    /// Caller sollte bei `0` einen Key-Refresh anstossen .
381    ///
382    /// # Errors
383    /// `BadArgument` wenn der Handle nicht existiert.
384    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    /// Erzwingt einen neuen Master-Key im Slot. Nach `rotate_key` ist
395    /// der alte Key futsch — Gegenseite muss per
396    /// [`CryptographicPlugin::create_local_participant_crypto_tokens`]
397    /// einen neuen Token ziehen und via
398    /// [`CryptographicPlugin::set_remote_participant_crypto_tokens`]
399    /// synchronisieren.
400    ///
401    /// # Errors
402    /// `BadArgument` wenn der Handle nicht existiert; `CryptoFailed`
403    /// wenn die RNG versagt.
404    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        // wenn ein SharedSecretProvider konfiguriert ist,
457        // ziehen wir den Per-Peer-Key deterministisch aus dem DH-
458        // abgeleiteten SharedSecret. Das spart den Token-Exchange
459        // (beide Seiten rechnen den gleichen Master-Key lokal aus)
460        // und macht jeden Per-Peer-Link kryptographisch eigenstaendig.
461        // Wenn Provider konfiguriert UND er das Secret kennt → HKDF-
462        // Ableitung. Liefert der Provider `None` (Handle unbekannt),
463        // fallen wir auf den v1.4-Random-Pfad zurueck — so koennen
464        // Mixed-Setups (DH-MAC-Keys parallel zu Token-Exchange-Cipher-
465        // Keys) im gleichen Plugin-Objekt existieren.
466        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        // v1.4-Pfad: Placeholder-Slot; wird mit
473        // `set_remote_participant_crypto_tokens` auf den echten
474        // Remote-Key gesetzt.
475        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        // Serialisiert den lokalen Master-Key. Der Token wird ueber die
495        // built-in DCPSParticipantVolatileMessageSecure-Topic ausgetauscht,
496        // die selber bereits mit Session-Keys aus dem SharedSecret-
497        // Handshake verschluesselt ist (Spec §9.5.3.5) — eine zusaetzliche
498        // Wrap-Schicht ueber dem Token waere also Doppel-Encrypt.
499        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        // Spec §10.5.2 Tab.74: Per-Submessage session_key aus master_key
541        // + master_salt + session_id; Spec §8.1 Tab.78: AAD aus
542        // transformation_kind + key_id + session_id + extension. Caller
543        // liefert die Spec-konforme Extension (RTPS-Header bei §8.5.1.9.7,
544        // Submessage-Header bei §8.5.1.9.2, leer bei PSK).
545        let session_key = mat.derive_session_key_bytes();
546        let aad_bytes = mat.aad(aad_extension);
547
548        if !mat.suite.is_aead() {
549            // HMAC-Auth-only-Pfad (ZeroDDS-Erweiterung). HMAC-Input ist
550            // `aad || nonce || plaintext` — domain-separated.
551            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        // Erst plaintext encrypten (AES-GCM haengt den 16-byte Tag
567        // direkt an den Vec an). Danach nonce davor setzen.
568        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        // Wire-Format: [nonce(12) | ciphertext + tag(16)].
575        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        // Spec §10.5.2 Tab.74 + §8.1 Tab.78 — symmetrisch zu encrypt:
596        // session_key + AAD aus master_salt + key_id + session_id +
597        // extension (Caller-supplied, muss byte-identisch zur Sender-
598        // AAD sein, sonst Tag-Mismatch).
599        let session_key = mat.derive_session_key_bytes();
600        let aad_bytes = mat.aad(aad_extension);
601
602        if !mat.suite.is_aead() {
603            // HMAC-Auth-only-Pfad: `[nonce(12) | plaintext | hmac(32)]`.
604            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        // Pro Remote einen truncated-HMAC-SHA256 ueber den Ciphertext
662        // bilden. Der HMAC-Key ist der pro-Receiver-Slot-master_key.
663        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            // Spec §10.5.2 Tab.74: Receiver-Specific-Key via HMAC-SHA256
673            // (master_recv_key, master_salt || "SessionReceiverKey" ||
674            // session_id) — wir mappen master_key auf den Receiver-Slot.
675            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            // 16-byte Truncation (Spec §7.3.6.3 `receiver_mac octet[16]`).
683            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            // Single-MAC-Path: kein Multi-MAC im Datagram → normaler
706            // Decrypt-Pfad mit derselben AAD-Extension.
707            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        // Verify: HMAC(own_receiver_key, ciphertext) gegen our_mac.
721        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        // Spec §10.5.2 Tab.74 — Receiver-Side: gleiche Derivation wie
729        // im Sender (oben in encrypt_submessage_multi).
730        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        // MAC stimmt → normaler Decrypt-Pfad mit Sender-Key (gleiche
746        // AAD-Extension wie der Sender verwendet hat).
747        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        // Wire-Format laenge = plain + 12 nonce + 16 tag
812        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        // Flip ein byte im ciphertext-Bereich.
831        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        // Nonce-Counter ist unterschiedlich → wire-bytes unterschiedlich.
847        assert_ne!(ct1, ct2);
848    }
849
850    #[test]
851    fn cross_plugin_interop_via_tokens() {
852        // Alice verschluesselt, schickt Token an Bob, Bob dekodiert.
853        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        // Alice serialisiert ihren Master-Key als Token.
864        let token = alice
865            .create_local_participant_crypto_tokens(alice_local, CryptoHandle(0))
866            .unwrap();
867
868        // Bob akzeptiert den Token und speichert unter einem neuen Handle.
869        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        // Alice verschluesselt ein Payload.
880        let plain = b"cross-plugin-test";
881        let ct = alice
882            .encrypt_submessage(alice_local, &[], plain, &[])
883            .unwrap();
884
885        // Bob entschluesselt mit dem uebertragenen Key.
886        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    // -------------------------------------------------------------
905    // AES-GCM-256 Suite
906    // -------------------------------------------------------------
907
908    #[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        // Alice = 256, Bob = 128. Alice serialisiert ihren 256er Key
958        // via Token. Bob nimmt den Token entgegen — das Token enthaelt
959        // den Spec-konformen Suite-Tag 0x04 (AES256_GCM, §10.5 Tab.79),
960        // also nutzt Bob fuer DIESEN Slot AES-256, obwohl sein
961        // local_suite auf 128 steht.
962        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        // Token mit suite-id 0xFF (ungueltig).
1005        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    // -------------------------------------------------------------
1015    //  — HMAC-SHA256 Auth-only + Key-Refresh
1016    // -------------------------------------------------------------
1017
1018    #[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        // Wire-Format: nonce(12) + plaintext + hmac(32). Plaintext
1027        // muss **unverschluesselt** vorhanden sein.
1028        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        // Flip byte in plaintext (nach nonce, vor tag).
1048        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        // Encrypt + Token ziehen; dann rotieren; dann neuen Token
1089        // ziehen — beide Tokens muessen verschieden sein.
1090        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    // =======================================================================
1116    // SharedSecretProvider-Integration (PKI ↔ Crypto)
1117    // =======================================================================
1118
1119    use alloc::collections::BTreeMap as BTreeMap2;
1120    use alloc::sync::Arc as ArcA;
1121    use std::sync::RwLock as StdRwLock;
1122
1123    /// Minimaler In-Memory-Provider fuer Tests; in der Produktion
1124    /// kommt `PkiAuthenticationPlugin` zum Einsatz (implementiert
1125    /// `SharedSecretProvider` in `zerodds-security-pki`).
1126    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        // Simulates: Alice & Bob kennen den gleichen 32-byte-DH-
1150        // Shared-Secret (aus X25519). Beide Seiten registrieren den
1151        // Slot via register_matched_remote_participant mit demselben
1152        // SharedSecretHandle — beide HKDF-leiten exakt denselben
1153        // Master-Key ab und koennen deshalb direkt kommunizieren ohne
1154        // Token-Exchange.
1155        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        // Encrypt/Decrypt kreuzen: Alice encryptet, Bob entschluesselt.
1193        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        // Zwei unterschiedliche DH-Shared-Secrets (Alice↔Bob vs
1206        // Alice↔Charlie) muessen zu zwei unterschiedlichen Master-Keys
1207        // fuehren — sonst waere das ein fataler Key-Reuse.
1208        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        // Wenn der Provider das Handle nicht kennt, nimmt der Plugin
1239        // einen Random-Key (v1.4-Pfad). Damit koexistieren DH-MAC-
1240        // Keys und Token-Exchange-Cipher-Keys im gleichen Plugin.
1241        let provider = ArcA::new(MemProvider::new()); // leer
1242        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        // Slot existiert und ist nutzbar (Random-Key).
1253        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        // Kein Provider → v1.4-Pfad: Random-Key bei jedem Register.
1261        let mut p = AesGcmCryptoPlugin::new(); // ohne Provider
1262        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        // Gleicher Secret → gleicher Master-Key bit-identisch.
1283        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}