use crate::derec_message::current_timestamp;
use crate::primitives::pairing::PairingError;
use crate::types::ChannelId;
use derec_cryptography::pairing::PairingContactMessageMaterial;
use derec_proto::{ContactMessage, ContactMode, SenderKind, TransportProtocol};
use rand::{Rng, rng};
#[cfg(not(target_arch = "wasm32"))]
use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::Zeroizing;
pub(crate) fn generate_seed<const N: usize>() -> Zeroizing<[u8; N]> {
let mut entropy = Zeroizing::new([0u8; N]);
let mut rng = rng();
rng.fill_bytes(&mut *entropy);
entropy
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub(crate) fn verify_timestamps(
envelope_timestamp: Option<prost_types::Timestamp>,
timestamp: Option<prost_types::Timestamp>,
) -> Result<(), crate::Error> {
match (envelope_timestamp, timestamp) {
(Some(envelope_ts), Some(inner_ts)) if envelope_ts == inner_ts => Ok(()),
(None, None) => {
#[cfg(feature = "logging")]
tracing::warn!(
"timestamp invariant violated: both envelope and inner timestamp absent"
);
Err(crate::Error::Invariant(
"Envelope and request/response timestamps are both absent; absence cannot be used to satisfy the binding check",
))
}
_ => {
#[cfg(feature = "logging")]
tracing::warn!("timestamp invariant violated");
Err(crate::Error::Invariant(
"Envelope timestamp does not match request/response timestamp",
))
}
}
}
pub(crate) trait ContactMessageExt {
fn validate(&self) -> Result<(), crate::Error>;
fn requires_pre_pair(&self) -> bool;
fn inline_keys(
channel_id: ChannelId,
nonce: u64,
transport_protocol: TransportProtocol,
pk: PairingContactMessageMaterial,
) -> ContactMessage;
fn hashed_keys(
channel_id: ChannelId,
nonce: u64,
transport_protocol: TransportProtocol,
pk: &PairingContactMessageMaterial,
) -> ContactMessage;
fn no_keys(
channel_id: ChannelId,
nonce: u64,
transport_protocol: TransportProtocol,
) -> ContactMessage;
}
impl ContactMessageExt for ContactMessage {
fn validate(&self) -> Result<(), crate::Error> {
let mode = ContactMode::try_from(self.contact_mode).map_err(|_| {
#[cfg(feature = "logging")]
tracing::warn!(
contact_mode = self.contact_mode,
"unknown contact_mode value"
);
PairingError::InvalidContactMessage("unknown contact_mode value")
})?;
let mlkem_present = self
.mlkem_encapsulation_key
.as_ref()
.is_some_and(|v| !v.is_empty());
let ecies_present = self
.ecies_public_key
.as_ref()
.is_some_and(|v| !v.is_empty());
let hash_present = self
.contact_binding_hash
.as_ref()
.is_some_and(|v| !v.is_empty());
match mode {
ContactMode::InlineKeys => {
if !mlkem_present {
return Err(PairingError::InvalidContactMessage(
"inline_keys contact missing mlkem_encapsulation_key",
)
.into());
}
if !ecies_present {
return Err(PairingError::InvalidContactMessage(
"inline_keys contact missing ecies_public_key",
)
.into());
}
if hash_present {
return Err(PairingError::InvalidContactMessage(
"inline_keys contact must not carry contact_binding_hash",
)
.into());
}
}
ContactMode::HashedKeys => {
if mlkem_present || ecies_present {
return Err(PairingError::InvalidContactMessage(
"hashed_keys contact must not carry inline keys",
)
.into());
}
const BINDING_HASH_LEN: usize = 48;
let hash = self.contact_binding_hash.as_ref().ok_or(
PairingError::InvalidContactMessage(
"hashed_keys contact missing contact_binding_hash",
),
)?;
if hash.is_empty() {
return Err(PairingError::InvalidContactMessage(
"hashed_keys contact missing contact_binding_hash",
)
.into());
}
if hash.len() != BINDING_HASH_LEN {
return Err(PairingError::InvalidContactMessage(
"hashed_keys contact_binding_hash is not a SHA-384 digest",
)
.into());
}
}
ContactMode::NoKeys => {
if mlkem_present || ecies_present {
return Err(PairingError::InvalidContactMessage(
"no_keys contact must not carry inline keys",
)
.into());
}
if hash_present {
return Err(PairingError::InvalidContactMessage(
"no_keys contact must not carry contact_binding_hash",
)
.into());
}
}
}
Ok(())
}
fn requires_pre_pair(&self) -> bool {
self.contact_mode == ContactMode::HashedKeys as i32
|| self.contact_mode == ContactMode::NoKeys as i32
}
fn inline_keys(
channel_id: ChannelId,
nonce: u64,
transport_protocol: TransportProtocol,
pk: PairingContactMessageMaterial,
) -> ContactMessage {
ContactMessage {
channel_id: channel_id.into(),
transport_protocol: Some(transport_protocol),
contact_mode: ContactMode::InlineKeys as i32,
mlkem_encapsulation_key: Some(pk.mlkem_encapsulation_key),
ecies_public_key: Some(pk.ecies_public_key),
contact_binding_hash: None,
nonce,
timestamp: Some(current_timestamp()),
}
}
fn hashed_keys(
channel_id: ChannelId,
nonce: u64,
transport_protocol: TransportProtocol,
pk: &PairingContactMessageMaterial,
) -> ContactMessage {
let binding_hash = derec_cryptography::pairing::contact_binding_hash(
&pk.mlkem_encapsulation_key,
&pk.ecies_public_key,
nonce,
channel_id.into(),
);
ContactMessage {
channel_id: channel_id.into(),
transport_protocol: Some(transport_protocol),
contact_mode: ContactMode::HashedKeys as i32,
mlkem_encapsulation_key: None,
ecies_public_key: None,
contact_binding_hash: Some(binding_hash.to_vec()),
nonce,
timestamp: Some(current_timestamp()),
}
}
fn no_keys(
channel_id: ChannelId,
nonce: u64,
transport_protocol: TransportProtocol,
) -> ContactMessage {
ContactMessage {
channel_id: channel_id.into(),
transport_protocol: Some(transport_protocol),
contact_mode: ContactMode::NoKeys as i32,
mlkem_encapsulation_key: None,
ecies_public_key: None,
contact_binding_hash: None,
nonce,
timestamp: Some(current_timestamp()),
}
}
}
pub(crate) trait SenderKindExt {
fn derive_peer(&self) -> SenderKind;
fn is_replica(&self) -> bool;
}
impl SenderKindExt for SenderKind {
fn derive_peer(&self) -> SenderKind {
match self {
SenderKind::Owner => SenderKind::Helper,
SenderKind::Helper => SenderKind::Owner,
SenderKind::ReplicaSource => SenderKind::ReplicaDestination,
SenderKind::ReplicaDestination => SenderKind::ReplicaSource,
}
}
fn is_replica(&self) -> bool {
matches!(
self,
SenderKind::ReplicaSource | SenderKind::ReplicaDestination
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Error;
use derec_proto::{Protocol, TransportProtocol};
use prost_types::Timestamp;
fn ts(seconds: i64) -> Timestamp {
Timestamp { seconds, nanos: 0 }
}
fn well_formed_inline_keys_contact() -> ContactMessage {
ContactMessage {
channel_id: 42,
transport_protocol: Some(TransportProtocol {
uri: "https://relay.example/alice".to_owned(),
protocol: Protocol::Https.into(),
}),
contact_mode: ContactMode::InlineKeys as i32,
mlkem_encapsulation_key: Some(vec![1; 1184]),
ecies_public_key: Some(vec![2; 33]),
contact_binding_hash: None,
nonce: 0xCAFE_BABE,
timestamp: Some(ts(1_700_000_000)),
}
}
fn well_formed_hashed_keys_contact() -> ContactMessage {
ContactMessage {
channel_id: 42,
transport_protocol: Some(TransportProtocol {
uri: "https://relay.example/alice/ephemeral".to_owned(),
protocol: Protocol::Https.into(),
}),
contact_mode: ContactMode::HashedKeys as i32,
mlkem_encapsulation_key: None,
ecies_public_key: None,
contact_binding_hash: Some(vec![0xAB; 48]),
nonce: 0xDEAD_BEEF,
timestamp: Some(ts(1_700_000_000)),
}
}
fn well_formed_no_keys_contact() -> ContactMessage {
ContactMessage {
channel_id: 1234,
transport_protocol: Some(TransportProtocol {
uri: "https://institution.example/pair".to_owned(),
protocol: Protocol::Https.into(),
}),
contact_mode: ContactMode::NoKeys as i32,
mlkem_encapsulation_key: None,
ecies_public_key: None,
contact_binding_hash: None,
nonce: 4321,
timestamp: Some(ts(1_700_000_000)),
}
}
fn assert_invalid_contact(result: Result<(), crate::Error>, expected: &'static str) {
match result {
Err(Error::Pairing(PairingError::InvalidContactMessage(m))) => {
assert_eq!(m, expected, "wrong InvalidContactMessage payload")
}
other => panic!("expected InvalidContactMessage({expected:?}), got {other:?}"),
}
}
#[test]
fn accepts_matching_some_timestamps() {
assert!(verify_timestamps(Some(ts(1_700_000_000)), Some(ts(1_700_000_000))).is_ok());
}
#[test]
fn rejects_mismatched_some_timestamps() {
let res = verify_timestamps(Some(ts(1_700_000_000)), Some(ts(1_700_000_001)));
match res {
Err(crate::Error::Invariant(msg)) => {
assert!(msg.contains("does not match"), "msg: {msg}")
}
other => panic!("expected Invariant mismatch, got {other:?}"),
}
}
#[test]
fn rejects_both_none() {
let res = verify_timestamps(None, None);
match res {
Err(crate::Error::Invariant(msg)) => {
assert!(
msg.contains("absent"),
"expected absence-specific message, got: {msg}"
)
}
other => panic!("expected Invariant for (None, None), got {other:?}"),
}
}
#[test]
fn rejects_envelope_none() {
let res = verify_timestamps(None, Some(ts(1_700_000_000)));
assert!(matches!(res, Err(crate::Error::Invariant(_))));
}
#[test]
fn rejects_inner_none() {
let res = verify_timestamps(Some(ts(1_700_000_000)), None);
assert!(matches!(res, Err(crate::Error::Invariant(_))));
}
#[test]
fn validate_accepts_well_formed_inline_keys() {
well_formed_inline_keys_contact()
.validate()
.expect("well-formed inline_keys must pass");
}
#[test]
fn validate_accepts_well_formed_hashed_keys() {
well_formed_hashed_keys_contact()
.validate()
.expect("well-formed hashed_keys must pass");
}
#[test]
fn validate_accepts_well_formed_no_keys() {
well_formed_no_keys_contact()
.validate()
.expect("well-formed no_keys must pass");
}
#[test]
fn validate_rejects_unknown_contact_mode() {
let mut c = well_formed_inline_keys_contact();
c.contact_mode = 7;
assert_invalid_contact(c.validate(), "unknown contact_mode value");
}
#[test]
fn validate_rejects_inline_missing_mlkem_key() {
let mut c = well_formed_inline_keys_contact();
c.mlkem_encapsulation_key = None;
assert_invalid_contact(
c.validate(),
"inline_keys contact missing mlkem_encapsulation_key",
);
}
#[test]
fn validate_rejects_inline_missing_ecies_key() {
let mut c = well_formed_inline_keys_contact();
c.ecies_public_key = None;
assert_invalid_contact(c.validate(), "inline_keys contact missing ecies_public_key");
}
#[test]
fn validate_rejects_inline_with_binding_hash() {
let mut c = well_formed_inline_keys_contact();
c.contact_binding_hash = Some(vec![0xCD; 48]);
assert_invalid_contact(
c.validate(),
"inline_keys contact must not carry contact_binding_hash",
);
}
#[test]
fn validate_rejects_hashed_with_inline_keys() {
let mut c = well_formed_hashed_keys_contact();
c.mlkem_encapsulation_key = Some(vec![1; 1184]);
assert_invalid_contact(
c.validate(),
"hashed_keys contact must not carry inline keys",
);
}
#[test]
fn validate_rejects_hashed_missing_binding_hash() {
let mut c = well_formed_hashed_keys_contact();
c.contact_binding_hash = None;
assert_invalid_contact(
c.validate(),
"hashed_keys contact missing contact_binding_hash",
);
}
#[test]
fn validate_rejects_hashed_empty_binding_hash() {
let mut c = well_formed_hashed_keys_contact();
c.contact_binding_hash = Some(Vec::new());
assert_invalid_contact(
c.validate(),
"hashed_keys contact missing contact_binding_hash",
);
}
#[test]
fn validate_rejects_hashed_wrong_hash_length() {
let mut c = well_formed_hashed_keys_contact();
c.contact_binding_hash = Some(vec![0xAB; 32]);
assert_invalid_contact(
c.validate(),
"hashed_keys contact_binding_hash is not a SHA-384 digest",
);
}
#[test]
fn validate_rejects_no_keys_with_mlkem_key() {
let mut c = well_formed_no_keys_contact();
c.mlkem_encapsulation_key = Some(vec![1; 1184]);
assert_invalid_contact(c.validate(), "no_keys contact must not carry inline keys");
}
#[test]
fn validate_rejects_no_keys_with_ecies_key() {
let mut c = well_formed_no_keys_contact();
c.ecies_public_key = Some(vec![2; 33]);
assert_invalid_contact(c.validate(), "no_keys contact must not carry inline keys");
}
#[test]
fn validate_rejects_no_keys_with_binding_hash() {
let mut c = well_formed_no_keys_contact();
c.contact_binding_hash = Some(vec![0xAB; 48]);
assert_invalid_contact(
c.validate(),
"no_keys contact must not carry contact_binding_hash",
);
}
}