use alloc::collections::BTreeMap;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, RwLock};
use zerodds_security::authentication::{IdentityHandle, SharedSecretHandle, SharedSecretProvider};
use zerodds_security::crypto::{CryptoHandle, CryptographicPlugin, ReceiverMac};
use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
use ring::aead::{LessSafeKey, Nonce, UnboundKey};
use ring::hkdf;
use ring::hmac;
use ring::rand::{SecureRandom, SystemRandom};
use crate::suite::Suite;
type SessionId = [u8; 4];
struct KeyMaterial {
suite: Suite,
transformation_key_id: [u8; 4],
master_key: Vec<u8>,
master_salt: [u8; 32],
session_id: SessionId,
counter: AtomicU64,
}
impl KeyMaterial {
fn new_random(suite: Suite, rng: &SystemRandom) -> SecurityResult<Self> {
let mut mk = alloc::vec![0u8; suite.key_len()];
rng.fill(&mut mk).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"rng fill master_key failed",
)
})?;
let mut salt = [0u8; 32];
rng.fill(&mut salt).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"rng fill master_salt failed",
)
})?;
let mut sid = [0u8; 4];
rng.fill(&mut sid).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"rng fill session_id failed",
)
})?;
let mut key_id = [0u8; 4];
rng.fill(&mut key_id).map_err(|_| {
SecurityError::new(SecurityErrorKind::CryptoFailed, "rng fill key_id failed")
})?;
Ok(Self {
suite,
transformation_key_id: key_id,
master_key: mk,
master_salt: salt,
session_id: sid,
counter: AtomicU64::new(0),
})
}
fn from_serialized(serialized: &[u8]) -> SecurityResult<Self> {
if serialized.len() < 1 + 4 + 4 + 32 {
return Err(SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: token zu kurz (mindestens 41 byte vor master_key)",
));
}
let suite = Suite::from_transform_kind_id(serialized[0]).ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::BadArgument,
alloc::format!("crypto: unbekannte suite-id 0x{:02x}", serialized[0]),
)
})?;
let expected = 1 + 4 + 4 + 32 + suite.key_len();
if serialized.len() != expected {
return Err(SecurityError::new(
SecurityErrorKind::BadArgument,
alloc::format!("crypto: token fuer {suite:?} muss {expected} byte sein"),
));
}
let mut sid = [0u8; 4];
sid.copy_from_slice(&serialized[1..5]);
let mut key_id = [0u8; 4];
key_id.copy_from_slice(&serialized[5..9]);
let mut salt = [0u8; 32];
salt.copy_from_slice(&serialized[9..41]);
let mk = serialized[41..].to_vec();
Ok(Self {
suite,
transformation_key_id: key_id,
master_key: mk,
master_salt: salt,
session_id: sid,
counter: AtomicU64::new(0),
})
}
fn serialize(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 4 + 4 + 32 + self.master_key.len());
out.push(self.suite.transform_kind_id());
out.extend_from_slice(&self.session_id);
out.extend_from_slice(&self.transformation_key_id);
out.extend_from_slice(&self.master_salt);
out.extend_from_slice(&self.master_key);
out
}
fn transformation_kind_bytes(&self) -> [u8; 4] {
self.suite.transform_kind()
}
fn derive_session_key_bytes(&self) -> Vec<u8> {
let full = crate::session_key::derive_session_key(
&self.master_key,
&self.master_salt,
&self.session_id,
);
full[..self.suite.key_len()].to_vec()
}
fn aad(&self, extension: &[u8]) -> Vec<u8> {
crate::session_key::compute_aad(
self.transformation_kind_bytes(),
self.transformation_key_id,
self.session_id,
extension,
)
}
fn from_shared_secret(suite: Suite, shared_secret: &[u8]) -> SecurityResult<Self> {
if shared_secret.is_empty() {
return Err(SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: empty shared_secret",
));
}
let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, b"zerodds.crypto.shared-secret.v1");
let prk = salt.extract(shared_secret);
let expand = |info: &[u8], out_len: usize| -> SecurityResult<Vec<u8>> {
let info_arr = [info];
let okm = prk
.expand(
&info_arr,
HkdfLen {
len: out_len,
hmac: hkdf::HKDF_SHA256,
},
)
.map_err(|_| {
SecurityError::new(SecurityErrorKind::CryptoFailed, "hkdf expand failed")
})?;
let mut buf = alloc::vec![0u8; out_len];
okm.fill(&mut buf).map_err(|_| {
SecurityError::new(SecurityErrorKind::CryptoFailed, "hkdf fill failed")
})?;
Ok(buf)
};
let master_key = expand(b"dds.sec.crypto.master_key", suite.key_len())?;
let master_salt_vec = expand(b"dds.sec.crypto.master_salt", 32)?;
let mut master_salt = [0u8; 32];
master_salt.copy_from_slice(&master_salt_vec);
let key_id_vec = expand(b"dds.sec.crypto.sender_key_id", 4)?;
let mut transformation_key_id = [0u8; 4];
transformation_key_id.copy_from_slice(&key_id_vec);
let sid_vec = expand(b"dds.sec.crypto.session_id", 4)?;
let mut session_id = [0u8; 4];
session_id.copy_from_slice(&sid_vec);
Ok(Self {
suite,
transformation_key_id,
master_key,
master_salt,
session_id,
counter: AtomicU64::new(0),
})
}
fn next_nonce(&self) -> SecurityResult<[u8; 12]> {
let c = self.counter.fetch_add(1, Ordering::Relaxed);
if c == u64::MAX {
return Err(SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: nonce-counter exhausted — key-refresh required ",
));
}
let mut n = [0u8; 12];
n[..4].copy_from_slice(&self.session_id);
n[4..].copy_from_slice(&c.to_be_bytes());
Ok(n)
}
}
struct HkdfLen {
len: usize,
hmac: hkdf::Algorithm,
}
impl hkdf::KeyType for HkdfLen {
fn len(&self) -> usize {
self.len
}
}
impl From<HkdfLen> for hkdf::Algorithm {
fn from(v: HkdfLen) -> Self {
v.hmac
}
}
pub struct AesGcmCryptoPlugin {
rng: SystemRandom,
next_handle: AtomicU64,
local_suite: Suite,
slots: RwLock<BTreeMap<CryptoHandle, KeyMaterial>>,
secret_provider: Option<Arc<dyn SharedSecretProvider>>,
#[allow(clippy::type_complexity)]
remote_map: Mutex<BTreeMap<(CryptoHandle, IdentityHandle), CryptoHandle>>,
}
impl Default for AesGcmCryptoPlugin {
fn default() -> Self {
Self::new()
}
}
impl AesGcmCryptoPlugin {
#[must_use]
pub fn new() -> Self {
Self::with_suite(Suite::Aes128Gcm)
}
#[must_use]
pub fn with_suite(suite: Suite) -> Self {
Self {
rng: SystemRandom::new(),
next_handle: AtomicU64::new(0),
local_suite: suite,
slots: RwLock::new(BTreeMap::new()),
remote_map: Mutex::new(BTreeMap::new()),
secret_provider: None,
}
}
#[must_use]
pub fn with_secret_provider(suite: Suite, provider: Arc<dyn SharedSecretProvider>) -> Self {
Self {
rng: SystemRandom::new(),
next_handle: AtomicU64::new(0),
local_suite: suite,
slots: RwLock::new(BTreeMap::new()),
remote_map: Mutex::new(BTreeMap::new()),
secret_provider: Some(provider),
}
}
#[must_use]
pub fn local_suite(&self) -> Suite {
self.local_suite
}
pub fn encrypts_remaining(&self, handle: CryptoHandle) -> SecurityResult<u64> {
let slots = self.slots.read().map_err(|_| poisoned())?;
let mat = slots.get(&handle).ok_or_else(|| {
SecurityError::new(SecurityErrorKind::BadArgument, "crypto: unknown handle")
})?;
let used = mat.counter.load(Ordering::Relaxed);
let max = mat.suite.max_encrypts();
Ok(max.saturating_sub(used))
}
pub fn rotate_key(&mut self, handle: CryptoHandle) -> SecurityResult<()> {
let fresh = KeyMaterial::new_random(self.local_suite, &self.rng)?;
let mut slots = self.slots.write().map_err(|_| poisoned())?;
let slot = slots.get_mut(&handle).ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: rotate_key unknown handle",
)
})?;
slot.master_key = fresh.master_key;
slot.session_id = fresh.session_id;
slot.counter.store(0, Ordering::Relaxed);
Ok(())
}
fn next_id(&self) -> u64 {
self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
}
fn insert(&self, mat: KeyMaterial) -> SecurityResult<CryptoHandle> {
let handle = CryptoHandle(self.next_id());
self.slots
.write()
.map_err(|_| poisoned())?
.insert(handle, mat);
Ok(handle)
}
}
fn poisoned() -> SecurityError {
SecurityError::new(
SecurityErrorKind::Internal,
"crypto: internal rwlock poisoned",
)
}
impl CryptographicPlugin for AesGcmCryptoPlugin {
fn register_local_participant(
&mut self,
_identity: IdentityHandle,
_properties: &[(&str, &str)],
) -> SecurityResult<CryptoHandle> {
let mat = KeyMaterial::new_random(self.local_suite, &self.rng)?;
self.insert(mat)
}
fn register_matched_remote_participant(
&mut self,
_local: CryptoHandle,
_remote_identity: IdentityHandle,
shared_secret: SharedSecretHandle,
) -> SecurityResult<CryptoHandle> {
if let Some(provider) = &self.secret_provider {
if let Some(secret) = provider.get_shared_secret(shared_secret) {
let mat = KeyMaterial::from_shared_secret(self.local_suite, &secret)?;
return self.insert(mat);
}
}
let mat = KeyMaterial::new_random(self.local_suite, &self.rng)?;
self.insert(mat)
}
fn register_local_endpoint(
&mut self,
_participant: CryptoHandle,
_is_writer: bool,
_properties: &[(&str, &str)],
) -> SecurityResult<CryptoHandle> {
let mat = KeyMaterial::new_random(self.local_suite, &self.rng)?;
self.insert(mat)
}
fn create_local_participant_crypto_tokens(
&mut self,
local: CryptoHandle,
_remote: CryptoHandle,
) -> SecurityResult<Vec<u8>> {
let slots = self.slots.read().map_err(|_| poisoned())?;
let mat = slots.get(&local).ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: unknown local handle",
)
})?;
Ok(mat.serialize())
}
fn set_remote_participant_crypto_tokens(
&mut self,
_local: CryptoHandle,
remote: CryptoHandle,
tokens: &[u8],
) -> SecurityResult<()> {
let mat = KeyMaterial::from_serialized(tokens)?;
self.slots
.write()
.map_err(|_| poisoned())?
.insert(remote, mat);
Ok(())
}
fn encrypt_submessage(
&self,
local: CryptoHandle,
_remote_list: &[CryptoHandle],
plaintext: &[u8],
aad_extension: &[u8],
) -> SecurityResult<Vec<u8>> {
#[cfg(feature = "metrics")]
let _op = crate::metrics::CryptoOp::start("encrypt");
let slots = self.slots.read().map_err(|_| poisoned())?;
let mat = slots.get(&local).ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: unknown local handle",
)
})?;
let nonce = mat.next_nonce()?;
let session_key = mat.derive_session_key_bytes();
let aad_bytes = mat.aad(aad_extension);
if !mat.suite.is_aead() {
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &session_key);
let mut ctx = hmac::Context::with_key(&hmac_key);
ctx.update(&aad_bytes);
ctx.update(&nonce);
ctx.update(plaintext);
let tag = ctx.sign();
let mut out = Vec::with_capacity(12 + plaintext.len() + 32);
out.extend_from_slice(&nonce);
out.extend_from_slice(plaintext);
out.extend_from_slice(tag.as_ref());
return Ok(out);
}
let key = key_from_bytes(mat.suite, &session_key)?;
let mut payload: Vec<u8> = plaintext.to_vec();
let nonce_obj = Nonce::assume_unique_for_key(nonce);
key.seal_in_place_append_tag(nonce_obj, ring::aead::Aad::from(&aad_bytes), &mut payload)
.map_err(|_| {
SecurityError::new(SecurityErrorKind::CryptoFailed, "crypto: seal failed")
})?;
let mut out = Vec::with_capacity(12 + payload.len());
out.extend_from_slice(&nonce);
out.extend(payload);
Ok(out)
}
fn decrypt_submessage(
&self,
local: CryptoHandle,
_remote: CryptoHandle,
ciphertext: &[u8],
aad_extension: &[u8],
) -> SecurityResult<Vec<u8>> {
#[cfg(feature = "metrics")]
let _op = crate::metrics::CryptoOp::start("decrypt");
let slots = self.slots.read().map_err(|_| poisoned())?;
let mat = slots.get(&local).ok_or_else(|| {
SecurityError::new(SecurityErrorKind::BadArgument, "crypto: unknown handle")
})?;
let session_key = mat.derive_session_key_bytes();
let aad_bytes = mat.aad(aad_extension);
if !mat.suite.is_aead() {
if ciphertext.len() < 12 + 32 {
return Err(SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: hmac-buffer zu kurz fuer nonce+tag",
));
}
let (nonce_bytes, rest) = ciphertext.split_at(12);
let (plain, tag) = rest.split_at(rest.len() - 32);
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &session_key);
let mut signed_input =
Vec::with_capacity(aad_bytes.len() + nonce_bytes.len() + plain.len());
signed_input.extend_from_slice(&aad_bytes);
signed_input.extend_from_slice(nonce_bytes);
signed_input.extend_from_slice(plain);
hmac::verify(&hmac_key, &signed_input, tag).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: hmac verify failed (tag mismatch)",
)
})?;
return Ok(plain.to_vec());
}
if ciphertext.len() < 12 + 16 {
return Err(SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: ciphertext zu kurz fuer nonce+tag",
));
}
let (nonce_bytes, ct) = ciphertext.split_at(12);
let key = key_from_bytes(mat.suite, &session_key)?;
let mut n = [0u8; 12];
n.copy_from_slice(nonce_bytes);
let mut buf = ct.to_vec();
let nonce_obj = Nonce::assume_unique_for_key(n);
let plain = key
.open_in_place(nonce_obj, ring::aead::Aad::from(&aad_bytes), &mut buf)
.map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: open/verify failed (tag mismatch?)",
)
})?;
Ok(plain.to_vec())
}
fn encrypt_submessage_multi(
&self,
local: CryptoHandle,
receivers: &[(CryptoHandle, u32)],
plaintext: &[u8],
aad_extension: &[u8],
) -> SecurityResult<(Vec<u8>, Vec<ReceiverMac>)> {
let handles: Vec<CryptoHandle> = receivers.iter().map(|(h, _)| *h).collect();
let ciphertext = self.encrypt_submessage(local, &handles, plaintext, aad_extension)?;
let slots = self.slots.read().map_err(|_| poisoned())?;
let mut macs = Vec::with_capacity(receivers.len());
for (remote, key_id) in receivers {
let mat = slots.get(remote).ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: unknown remote handle for receiver-specific mac",
)
})?;
let receiver_session_key = crate::session_key::derive_session_hmac_key(
&mat.master_key,
&mat.master_salt,
&mat.session_id,
);
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &receiver_session_key);
let tag = hmac::sign(&hmac_key, &ciphertext);
let mut mac16 = [0u8; 16];
mac16.copy_from_slice(&tag.as_ref()[..16]);
macs.push(ReceiverMac {
key_id: *key_id,
mac: mac16,
});
}
Ok((ciphertext, macs))
}
#[allow(clippy::too_many_arguments)]
fn decrypt_submessage_with_receiver_mac(
&self,
local: CryptoHandle,
remote: CryptoHandle,
own_key_id: u32,
own_mac_key_handle: CryptoHandle,
ciphertext: &[u8],
macs: &[ReceiverMac],
aad_extension: &[u8],
) -> SecurityResult<Vec<u8>> {
if macs.is_empty() {
return self.decrypt_submessage(local, remote, ciphertext, aad_extension);
}
let our_mac = macs
.iter()
.find(|m| m.key_id == own_key_id)
.ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: no receiver-specific MAC matches own key_id",
)
})?;
let slots = self.slots.read().map_err(|_| poisoned())?;
let mat = slots.get(&own_mac_key_handle).ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::BadArgument,
"crypto: unknown own_mac_key_handle",
)
})?;
let receiver_session_key = crate::session_key::derive_session_hmac_key(
&mat.master_key,
&mat.master_salt,
&mat.session_id,
);
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &receiver_session_key);
let full_tag = hmac::sign(&hmac_key, ciphertext);
if full_tag.as_ref()[..16] != our_mac.mac {
return Err(SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: receiver-specific mac mismatch",
));
}
drop(slots);
self.decrypt_submessage(local, remote, ciphertext, aad_extension)
}
fn plugin_class_id(&self) -> &str {
"DDS:Crypto:AES-GCM-GMAC:1.2"
}
}
fn key_from_bytes(suite: Suite, k: &[u8]) -> SecurityResult<LessSafeKey> {
if k.len() != suite.key_len() {
return Err(SecurityError::new(
SecurityErrorKind::CryptoFailed,
alloc::format!(
"crypto: key_from_bytes expected {} bytes, got {}",
suite.key_len(),
k.len()
),
));
}
let algo = suite.algorithm().ok_or_else(|| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: key_from_bytes fuer non-AEAD-Suite aufgerufen",
)
})?;
let unbound = UnboundKey::new(algo, k).map_err(|_| {
SecurityError::new(
SecurityErrorKind::CryptoFailed,
"crypto: UnboundKey creation",
)
})?;
Ok(LessSafeKey::new(unbound))
}
#[allow(dead_code)]
fn suppress_unused_remote_map_warning(
p: &AesGcmCryptoPlugin,
) -> &Mutex<BTreeMap<(CryptoHandle, IdentityHandle), CryptoHandle>> {
&p.remote_map
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn plugin_class_id_matches_spec() {
let p = AesGcmCryptoPlugin::new();
assert_eq!(p.plugin_class_id(), "DDS:Crypto:AES-GCM-GMAC:1.2");
}
#[test]
fn encrypt_decrypt_roundtrip() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let remote = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let plain = b"hello zerodds secure world";
let ct = p.encrypt_submessage(local, &[remote], plain, &[]).unwrap();
assert_eq!(ct.len(), plain.len() + 12 + 16);
let back = p.decrypt_submessage(local, remote, &ct, &[]).unwrap();
assert_eq!(back, plain);
}
#[test]
fn decrypt_rejects_tampered_ciphertext() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let remote = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let plain = b"AAAAAAAAAAAAAAAA";
let mut ct = p.encrypt_submessage(local, &[remote], plain, &[]).unwrap();
ct[14] ^= 0x01;
let err = p.decrypt_submessage(local, remote, &ct, &[]).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
}
#[test]
fn two_encrypts_produce_different_ciphertexts() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let plain = b"same plaintext";
let ct1 = p.encrypt_submessage(local, &[], plain, &[]).unwrap();
let ct2 = p.encrypt_submessage(local, &[], plain, &[]).unwrap();
assert_ne!(ct1, ct2);
}
#[test]
fn cross_plugin_interop_via_tokens() {
let mut alice = AesGcmCryptoPlugin::new();
let mut bob = AesGcmCryptoPlugin::new();
let alice_local = alice
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let bob_local = bob
.register_local_participant(IdentityHandle(2), &[])
.unwrap();
let token = alice
.create_local_participant_crypto_tokens(alice_local, CryptoHandle(0))
.unwrap();
let alice_seen_by_bob = bob
.register_matched_remote_participant(
bob_local,
IdentityHandle(1),
SharedSecretHandle(1),
)
.unwrap();
bob.set_remote_participant_crypto_tokens(bob_local, alice_seen_by_bob, &token)
.unwrap();
let plain = b"cross-plugin-test";
let ct = alice
.encrypt_submessage(alice_local, &[], plain, &[])
.unwrap();
let back = bob
.decrypt_submessage(alice_seen_by_bob, CryptoHandle(0), &ct, &[])
.unwrap();
assert_eq!(back, plain);
}
#[test]
fn decrypt_rejects_too_short_input() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let err = p
.decrypt_submessage(local, CryptoHandle(0), b"short", &[])
.unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::BadArgument);
}
#[test]
fn default_plugin_uses_aes128() {
let p = AesGcmCryptoPlugin::new();
assert_eq!(p.local_suite(), Suite::Aes128Gcm);
}
#[test]
fn aes256_plugin_reports_aes256_suite() {
let p = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
assert_eq!(p.local_suite(), Suite::Aes256Gcm);
}
#[test]
fn aes256_encrypt_decrypt_roundtrip() {
let mut p = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let remote = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let plain = b"aes-256 payload with forward secrecy";
let ct = p.encrypt_submessage(local, &[remote], plain, &[]).unwrap();
assert_eq!(ct.len(), plain.len() + 12 + 16);
let back = p.decrypt_submessage(local, remote, &ct, &[]).unwrap();
assert_eq!(back, plain);
}
#[test]
fn aes256_tampered_ciphertext_fails_verify() {
let mut p = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let remote = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let mut ct = p
.encrypt_submessage(local, &[remote], b"0123456789abcdef0123", &[])
.unwrap();
ct[14] ^= 0x01;
let err = p.decrypt_submessage(local, remote, &ct, &[]).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
}
#[test]
fn tokens_carry_suite_tag_so_cross_suite_interop_works() {
let mut alice = AesGcmCryptoPlugin::with_suite(Suite::Aes256Gcm);
let mut bob = AesGcmCryptoPlugin::with_suite(Suite::Aes128Gcm);
let a_local = alice
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let b_local = bob
.register_local_participant(IdentityHandle(2), &[])
.unwrap();
let token = alice
.create_local_participant_crypto_tokens(a_local, CryptoHandle(0))
.unwrap();
assert_eq!(
token[0],
crate::suite::transform_kind::AES256_GCM,
"suite-tag must be Spec AES256_GCM (0x04)"
);
let alice_slot_in_bob = bob
.register_matched_remote_participant(b_local, IdentityHandle(1), SharedSecretHandle(1))
.unwrap();
bob.set_remote_participant_crypto_tokens(b_local, alice_slot_in_bob, &token)
.unwrap();
let plain = b"cross-suite interop ok";
let ct = alice.encrypt_submessage(a_local, &[], plain, &[]).unwrap();
let back = bob
.decrypt_submessage(alice_slot_in_bob, CryptoHandle(0), &ct, &[])
.unwrap();
assert_eq!(back, plain);
}
#[test]
fn rejects_token_with_unknown_suite_id() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let remote = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let bogus = [
0xFFu8, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let err = p
.set_remote_participant_crypto_tokens(local, remote, &bogus)
.unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::BadArgument);
}
#[test]
fn hmac_only_suite_roundtrip_without_encryption() {
let mut p = AesGcmCryptoPlugin::with_suite(Suite::HmacSha256);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let plain = b"payload kept in plaintext, only signed";
let signed = p.encrypt_submessage(local, &[], plain, &[]).unwrap();
assert!(
signed.windows(plain.len()).any(|w| w == plain),
"HMAC-Suite sollte plaintext NICHT verschluesseln"
);
let back = p
.decrypt_submessage(local, CryptoHandle(0), &signed, &[])
.unwrap();
assert_eq!(back, plain);
}
#[test]
fn hmac_tampered_payload_fails_verify() {
let mut p = AesGcmCryptoPlugin::with_suite(Suite::HmacSha256);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let mut signed = p
.encrypt_submessage(local, &[], b"original message", &[])
.unwrap();
signed[15] ^= 0x01;
let err = p
.decrypt_submessage(local, CryptoHandle(0), &signed, &[])
.unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
}
#[test]
fn hmac_tampered_tag_fails_verify() {
let mut p = AesGcmCryptoPlugin::with_suite(Suite::HmacSha256);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let mut signed = p.encrypt_submessage(local, &[], b"x", &[]).unwrap();
let last = signed.len() - 1;
signed[last] ^= 0x01;
let err = p
.decrypt_submessage(local, CryptoHandle(0), &signed, &[])
.unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
}
#[test]
fn encrypts_remaining_decrements_per_call() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let before = p.encrypts_remaining(local).unwrap();
let _ = p.encrypt_submessage(local, &[], b"x", &[]).unwrap();
let after = p.encrypts_remaining(local).unwrap();
assert_eq!(before - after, 1);
}
#[test]
fn rotate_key_resets_counter_and_changes_key() {
let mut p = AesGcmCryptoPlugin::new();
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let _ = p.encrypt_submessage(local, &[], b"x", &[]).unwrap();
let token_before = p
.create_local_participant_crypto_tokens(local, CryptoHandle(0))
.unwrap();
p.rotate_key(local).unwrap();
assert_eq!(
p.encrypts_remaining(local).unwrap(),
Suite::Aes128Gcm.max_encrypts(),
"counter muss nach rotate bei 0 starten"
);
let token_after = p
.create_local_participant_crypto_tokens(local, CryptoHandle(0))
.unwrap();
assert_ne!(token_before, token_after, "master-key muss neu sein");
}
#[test]
fn rotate_key_rejects_unknown_handle() {
let mut p = AesGcmCryptoPlugin::new();
let err = p.rotate_key(CryptoHandle(9999)).unwrap_err();
assert_eq!(err.kind, SecurityErrorKind::BadArgument);
}
use alloc::collections::BTreeMap as BTreeMap2;
use alloc::sync::Arc as ArcA;
use std::sync::RwLock as StdRwLock;
struct MemProvider {
inner: StdRwLock<BTreeMap2<SharedSecretHandle, Vec<u8>>>,
}
impl MemProvider {
fn new() -> Self {
Self {
inner: StdRwLock::new(BTreeMap2::new()),
}
}
fn insert(&self, handle: SharedSecretHandle, bytes: Vec<u8>) {
self.inner.write().unwrap().insert(handle, bytes);
}
}
impl SharedSecretProvider for MemProvider {
fn get_shared_secret(&self, handle: SharedSecretHandle) -> Option<Vec<u8>> {
self.inner.read().ok()?.get(&handle).cloned()
}
}
#[test]
fn with_secret_provider_derives_same_master_key_for_both_sides() {
let shared = alloc::vec![0xA5u8; 32];
let provider_a = ArcA::new(MemProvider::new());
let provider_b = ArcA::new(MemProvider::new());
provider_a.insert(SharedSecretHandle(1), shared.clone());
provider_b.insert(SharedSecretHandle(1), shared.clone());
let mut alice = AesGcmCryptoPlugin::with_secret_provider(
Suite::Aes128Gcm,
ArcA::clone(&provider_a) as ArcA<dyn SharedSecretProvider>,
);
let mut bob = AesGcmCryptoPlugin::with_secret_provider(
Suite::Aes128Gcm,
ArcA::clone(&provider_b) as ArcA<dyn SharedSecretProvider>,
);
let alice_local = alice
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let bob_local = bob
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let alice_to_bob = alice
.register_matched_remote_participant(
alice_local,
IdentityHandle(2),
SharedSecretHandle(1),
)
.unwrap();
let bob_to_alice = bob
.register_matched_remote_participant(
bob_local,
IdentityHandle(1),
SharedSecretHandle(1),
)
.unwrap();
let plain = b"x25519-handshake-derived-key";
let wire = alice
.encrypt_submessage(alice_to_bob, &[], plain, &[])
.unwrap();
let back = bob
.decrypt_submessage(bob_to_alice, bob_to_alice, &wire, &[])
.unwrap();
assert_eq!(back, plain);
}
#[test]
fn with_secret_provider_different_secrets_yield_distinct_keys() {
let provider = ArcA::new(MemProvider::new());
provider.insert(SharedSecretHandle(1), alloc::vec![0x11u8; 32]);
provider.insert(SharedSecretHandle(2), alloc::vec![0x22u8; 32]);
let mut p = AesGcmCryptoPlugin::with_secret_provider(
Suite::Aes128Gcm,
ArcA::clone(&provider) as ArcA<dyn SharedSecretProvider>,
);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let bob = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let charlie = p
.register_matched_remote_participant(local, IdentityHandle(3), SharedSecretHandle(2))
.unwrap();
let tok_bob = p
.create_local_participant_crypto_tokens(bob, CryptoHandle(0))
.unwrap();
let tok_charlie = p
.create_local_participant_crypto_tokens(charlie, CryptoHandle(0))
.unwrap();
assert_ne!(
tok_bob, tok_charlie,
"DH-Shared-Secrets muessen zu verschiedenen Per-Peer-Keys fuehren"
);
}
#[test]
fn with_secret_provider_unknown_handle_falls_back_to_random() {
let provider = ArcA::new(MemProvider::new()); let mut p = AesGcmCryptoPlugin::with_secret_provider(
Suite::Aes128Gcm,
ArcA::clone(&provider) as ArcA<dyn SharedSecretProvider>,
);
let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let h = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(42))
.expect("unknown handle → random slot, kein Error");
let _tok = p
.create_local_participant_crypto_tokens(h, CryptoHandle(0))
.unwrap();
}
#[test]
fn without_provider_backward_compat_random_key_preserved() {
let mut p = AesGcmCryptoPlugin::new(); let local = p
.register_local_participant(IdentityHandle(1), &[])
.unwrap();
let a = p
.register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
.unwrap();
let b = p
.register_matched_remote_participant(local, IdentityHandle(3), SharedSecretHandle(2))
.unwrap();
let tok_a = p
.create_local_participant_crypto_tokens(a, CryptoHandle(0))
.unwrap();
let tok_b = p
.create_local_participant_crypto_tokens(b, CryptoHandle(0))
.unwrap();
assert_ne!(tok_a, tok_b, "Random-Keys → zwei verschiedene Tokens");
}
#[test]
fn hkdf_derivation_is_deterministic() {
let secret = alloc::vec![0xCDu8; 32];
let m1 = KeyMaterial::from_shared_secret(Suite::Aes128Gcm, &secret).unwrap();
let m2 = KeyMaterial::from_shared_secret(Suite::Aes128Gcm, &secret).unwrap();
assert_eq!(m1.master_key, m2.master_key);
assert_eq!(m1.session_id, m2.session_id);
}
#[test]
fn hkdf_rejects_empty_secret() {
let res = KeyMaterial::from_shared_secret(Suite::Aes128Gcm, &[]);
match res {
Err(e) => assert_eq!(e.kind, SecurityErrorKind::BadArgument),
Ok(_) => panic!("expected BadArgument, got Ok"),
}
}
}