use rand::RngCore;
use std::time::Duration;
use crate::entropy::EntropySnapshot;
use crate::error::{KkError, Result};
use crate::kdf;
use crate::kk_mix;
use zeroize::Zeroize;
pub const GENESIS_MAC: [u8; 32] = [0u8; 32];
#[derive(Clone, Debug)]
pub struct TemporalCommitment {
pub mac: [u8; 32],
}
impl TemporalCommitment {
pub fn to_bytes(&self) -> Vec<u8> {
self.mac.to_vec()
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 32 {
return Err(KkError::InvalidPacket("commitment too short".into()));
}
let mut mac = [0u8; 32];
mac.copy_from_slice(&data[..32]);
Ok(Self { mac })
}
}
pub fn commit(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
ciphertext: &[u8],
) -> Result<TemporalCommitment> {
let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
let mut message = Vec::with_capacity(32 + 16 + ciphertext.len());
message.extend_from_slice(&snapshot.bytes);
message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
message.extend_from_slice(ciphertext);
let mac_bytes = kk_mix::kk_mac(&commit_key, &message);
commit_key.zeroize();
Ok(TemporalCommitment { mac: mac_bytes })
}
pub fn verify(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
ciphertext: &[u8],
commitment: &TemporalCommitment,
) -> Result<()> {
let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
let mut message = Vec::with_capacity(32 + 16 + ciphertext.len());
message.extend_from_slice(&snapshot.bytes);
message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
message.extend_from_slice(ciphertext);
let verified = kk_mix::kk_mac_verify(&commit_key, &message, &commitment.mac);
commit_key.zeroize();
if verified {
Ok(())
} else {
Err(KkError::CommitmentMismatch)
}
}
pub fn commit_aead(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
ciphertext: &[u8],
aad: &[u8],
) -> Result<TemporalCommitment> {
let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
let aad_len = aad.len() as u64;
let mut message = Vec::with_capacity(32 + 16 + 8 + aad.len() + ciphertext.len());
message.extend_from_slice(&snapshot.bytes);
message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
message.extend_from_slice(&aad_len.to_le_bytes());
message.extend_from_slice(aad);
message.extend_from_slice(ciphertext);
let mac_bytes = kk_mix::kk_mac(&commit_key, &message);
commit_key.zeroize();
Ok(TemporalCommitment { mac: mac_bytes })
}
pub fn commit_aead_batch_8(
shared_secret: &[u8],
snapshots: [&EntropySnapshot; 8],
ciphertexts: [&[u8]; 8],
aads: [&[u8]; 8],
) -> Result<[TemporalCommitment; 8]> {
let mut commit_keys: [Vec<u8>; 8] = core::array::from_fn(|i| {
kdf::derive_commitment_key(shared_secret, snapshots[i])
.expect("commitment key derivation should not fail")
});
let prefixes: [Vec<u8>; 8] = core::array::from_fn(|i| {
let aad_len = aads[i].len() as u64;
let mut prefix = Vec::with_capacity(48 + aads[i].len());
prefix.extend_from_slice(&snapshots[i].bytes);
prefix.extend_from_slice(&snapshots[i].timestamp_nanos.to_le_bytes());
prefix.extend_from_slice(&aad_len.to_le_bytes());
prefix.extend_from_slice(aads[i]);
prefix
});
let key_refs: [&[u8]; 8] = core::array::from_fn(|i| commit_keys[i].as_slice());
let prefix_refs: [&[u8]; 8] = core::array::from_fn(|i| prefixes[i].as_slice());
let macs = kk_mix::kk_mac_batch_8_multipart(key_refs, prefix_refs, ciphertexts);
for k in &mut commit_keys {
k.zeroize();
}
Ok(core::array::from_fn(|i| TemporalCommitment {
mac: macs[i],
}))
}
pub fn verify_aead(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
ciphertext: &[u8],
aad: &[u8],
commitment: &TemporalCommitment,
) -> Result<()> {
let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
let aad_len = aad.len() as u64;
let mut message = Vec::with_capacity(32 + 16 + 8 + aad.len() + ciphertext.len());
message.extend_from_slice(&snapshot.bytes);
message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
message.extend_from_slice(&aad_len.to_le_bytes());
message.extend_from_slice(aad);
message.extend_from_slice(ciphertext);
let verified = kk_mix::kk_mac_verify(&commit_key, &message, &commitment.mac);
commit_key.zeroize();
if verified {
Ok(())
} else {
Err(KkError::CommitmentMismatch)
}
}
#[derive(Clone, Debug)]
pub struct TemporalProof {
pub mac: [u8; 32],
pub nonce: [u8; 32],
pub prev_mac: [u8; 32],
}
impl TemporalProof {
pub const BYTES: usize = 96;
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(Self::BYTES);
out.extend_from_slice(&self.mac);
out.extend_from_slice(&self.nonce);
out.extend_from_slice(&self.prev_mac);
out
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < Self::BYTES {
return Err(KkError::InvalidPacket(format!(
"temporal proof too short: need {}, got {}",
Self::BYTES,
data.len()
)));
}
let mut mac = [0u8; 32];
let mut nonce = [0u8; 32];
let mut prev_mac = [0u8; 32];
mac.copy_from_slice(&data[..32]);
nonce.copy_from_slice(&data[32..64]);
prev_mac.copy_from_slice(&data[64..96]);
Ok(Self {
mac,
nonce,
prev_mac,
})
}
}
pub fn generate_challenge() -> Result<[u8; 32]> {
let mut nonce = [0u8; 32];
rand::rngs::OsRng
.try_fill_bytes(&mut nonce)
.map_err(|e| KkError::EntropyFailure(format!("nonce generation: {e}")))?;
Ok(nonce)
}
pub fn commit_bound(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
ciphertext: &[u8],
verifier_nonce: &[u8; 32],
prev_mac: &[u8; 32],
) -> Result<TemporalProof> {
let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
let mut message = Vec::with_capacity(32 + 32 + 32 + 16 + ciphertext.len());
message.extend_from_slice(verifier_nonce);
message.extend_from_slice(prev_mac);
message.extend_from_slice(&snapshot.bytes);
message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
message.extend_from_slice(ciphertext);
let mac_bytes = kk_mix::kk_mac_with_entropy(&commit_key, &message, &snapshot.bytes);
commit_key.zeroize();
Ok(TemporalProof {
mac: mac_bytes,
nonce: *verifier_nonce,
prev_mac: *prev_mac,
})
}
pub fn verify_bound(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
ciphertext: &[u8],
proof: &TemporalProof,
expected_nonce: &[u8; 32],
max_drift: Duration,
) -> Result<()> {
if proof.nonce != *expected_nonce {
return Err(KkError::StaleNonce);
}
let now_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let drift = now_nanos.abs_diff(snapshot.timestamp_nanos);
if drift > max_drift.as_nanos() {
return Err(KkError::EpochDrift {
claimed_nanos: snapshot.timestamp_nanos,
drift_nanos: drift,
max_nanos: max_drift.as_nanos(),
});
}
let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
let mut message = Vec::with_capacity(32 + 32 + 32 + 16 + ciphertext.len());
message.extend_from_slice(&proof.nonce);
message.extend_from_slice(&proof.prev_mac);
message.extend_from_slice(&snapshot.bytes);
message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
message.extend_from_slice(ciphertext);
let verified =
kk_mix::kk_mac_verify_with_entropy(&commit_key, &message, &proof.mac, &snapshot.bytes);
commit_key.zeroize();
if verified {
Ok(())
} else {
Err(KkError::CommitmentMismatch)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entropy;
#[test]
fn valid_commitment_verifies() {
let secret = b"test-key";
let snap = entropy::gather().unwrap();
let ciphertext = b"some ciphertext bytes";
let commitment = commit(secret, &snap, ciphertext).unwrap();
verify(secret, &snap, ciphertext, &commitment).unwrap();
}
#[test]
fn tampered_ciphertext_fails() {
let secret = b"test-key";
let snap = entropy::gather().unwrap();
let ciphertext = b"original ciphertext";
let commitment = commit(secret, &snap, ciphertext).unwrap();
let tampered = b"tampered ciphertext";
let result = verify(secret, &snap, tampered, &commitment);
assert!(
result.is_err(),
"Tampered ciphertext must fail verification"
);
}
#[test]
fn wrong_key_fails() {
let snap = entropy::gather().unwrap();
let ciphertext = b"test data";
let commitment = commit(b"correct-key", &snap, ciphertext).unwrap();
let result = verify(b"wrong-key", &snap, ciphertext, &commitment);
assert!(
result.is_err(),
"Wrong shared secret must fail verification"
);
}
#[test]
fn bound_proof_verifies() {
let secret = b"test-key";
let snap = entropy::gather().unwrap();
let ciphertext = b"bound ciphertext";
let nonce = generate_challenge().unwrap();
let proof = commit_bound(secret, &snap, ciphertext, &nonce, &GENESIS_MAC).unwrap();
verify_bound(
secret,
&snap,
ciphertext,
&proof,
&nonce,
Duration::from_secs(5),
)
.unwrap();
}
#[test]
fn wrong_nonce_rejected() {
let secret = b"test-key";
let snap = entropy::gather().unwrap();
let ciphertext = b"nonce test";
let real_nonce = generate_challenge().unwrap();
let fake_nonce = generate_challenge().unwrap();
let proof = commit_bound(secret, &snap, ciphertext, &real_nonce, &GENESIS_MAC).unwrap();
let result = verify_bound(
secret,
&snap,
ciphertext,
&proof,
&fake_nonce,
Duration::from_secs(5),
);
assert!(
matches!(result, Err(KkError::StaleNonce)),
"Wrong nonce must be rejected as StaleNonce"
);
}
#[test]
fn tampered_ciphertext_fails_bound() {
let secret = b"test-key";
let snap = entropy::gather().unwrap();
let nonce = generate_challenge().unwrap();
let proof = commit_bound(secret, &snap, b"original", &nonce, &GENESIS_MAC).unwrap();
let result = verify_bound(
secret,
&snap,
b"tampered",
&proof,
&nonce,
Duration::from_secs(5),
);
assert!(
result.is_err(),
"Tampered ciphertext must fail bound verification"
);
}
#[test]
fn epoch_drift_rejected() {
let secret = b"test-key";
let ciphertext = b"epoch test";
let nonce = generate_challenge().unwrap();
let real_snap = entropy::gather().unwrap();
let old_snap = EntropySnapshot {
bytes: real_snap.bytes,
timestamp_nanos: 1_000_000_000_000_000_000, };
let proof = commit_bound(secret, &old_snap, ciphertext, &nonce, &GENESIS_MAC).unwrap();
let result = verify_bound(
secret,
&old_snap,
ciphertext,
&proof,
&nonce,
Duration::from_secs(5),
);
assert!(
matches!(result, Err(KkError::EpochDrift { .. })),
"Ancient timestamp must be rejected as EpochDrift"
);
}
#[test]
fn chain_ordering() {
let secret = b"chain-key";
let nonce1 = generate_challenge().unwrap();
let nonce2 = generate_challenge().unwrap();
let snap1 = entropy::gather().unwrap();
let ct1 = b"message one";
let proof1 = commit_bound(secret, &snap1, ct1, &nonce1, &GENESIS_MAC).unwrap();
verify_bound(
secret,
&snap1,
ct1,
&proof1,
&nonce1,
Duration::from_secs(5),
)
.unwrap();
let snap2 = entropy::gather().unwrap();
let ct2 = b"message two";
let proof2 = commit_bound(secret, &snap2, ct2, &nonce2, &proof1.mac).unwrap();
verify_bound(
secret,
&snap2,
ct2,
&proof2,
&nonce2,
Duration::from_secs(5),
)
.unwrap();
assert_eq!(
proof2.prev_mac, proof1.mac,
"Proof 2 must reference Proof 1's MAC"
);
assert_eq!(
proof1.prev_mac, GENESIS_MAC,
"Proof 1 must reference genesis"
);
}
#[test]
fn proof_serde_roundtrip() {
let secret = b"serde-key";
let snap = entropy::gather().unwrap();
let nonce = generate_challenge().unwrap();
let proof = commit_bound(secret, &snap, b"serde test", &nonce, &GENESIS_MAC).unwrap();
let bytes = proof.to_bytes();
assert_eq!(bytes.len(), TemporalProof::BYTES);
let restored = TemporalProof::from_bytes(&bytes).unwrap();
assert_eq!(restored.mac, proof.mac);
assert_eq!(restored.nonce, proof.nonce);
assert_eq!(restored.prev_mac, proof.prev_mac);
}
#[test]
fn wrong_prev_mac_fails() {
let secret = b"chain-key";
let snap = entropy::gather().unwrap();
let nonce = generate_challenge().unwrap();
let ciphertext = b"chain integrity";
let proof = commit_bound(secret, &snap, ciphertext, &nonce, &GENESIS_MAC).unwrap();
let mut forged = proof.clone();
forged.prev_mac = [0xFF; 32];
let result = verify_bound(
secret,
&snap,
ciphertext,
&forged,
&nonce,
Duration::from_secs(5),
);
assert!(
result.is_err(),
"Forged prev_mac must fail MAC verification"
);
}
}