use zeroize::Zeroize;
use crate::entropy::EntropySnapshot;
use crate::error::Result;
use crate::kk_mix;
pub fn derive_symbol_key(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
symbol_index: u64,
output_len: usize,
) -> Result<Vec<u8>> {
let mut info = Vec::with_capacity(10 + 8 + 16);
info.extend_from_slice(b"KK-sym-v1\0");
info.extend_from_slice(&symbol_index.to_le_bytes());
info.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
let output = kk_mix::kk_kdf(shared_secret, &snapshot.bytes, &info, output_len);
info.zeroize();
Ok(output)
}
pub fn derive_commitment_key(shared_secret: &[u8], snapshot: &EntropySnapshot) -> Result<Vec<u8>> {
let key = kk_mix::kk_kdf(shared_secret, &snapshot.bytes, b"KK-commit-v1", 32);
Ok(key)
}
pub fn derive_symbol_key_batch(
shared_secret: &[u8],
snapshot: &EntropySnapshot,
base_index: u64,
output_len: usize,
) -> Result<[Vec<u8>; 8]> {
let infos_raw: [Vec<u8>; 8] = core::array::from_fn(|i| {
let mut info = Vec::with_capacity(10 + 8 + 8);
info.extend_from_slice(b"KK-sym-v1\0");
info.extend_from_slice(&(base_index + i as u64).to_le_bytes());
info.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
info
});
let infos: [&[u8]; 8] = core::array::from_fn(|i| infos_raw[i].as_slice());
let output = kk_mix::kk_kdf_batch_8(shared_secret, &snapshot.bytes, infos, output_len);
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entropy;
#[test]
fn same_index_different_entropy_different_key() {
let secret = b"shared-secret";
let snap1 = entropy::gather().unwrap();
let snap2 = entropy::gather().unwrap();
let k1 = derive_symbol_key(secret, &snap1, 0, 32).unwrap();
let k2 = derive_symbol_key(secret, &snap2, 0, 32).unwrap();
assert_ne!(
k1, k2,
"Different entropic moments must yield different keys"
);
}
#[test]
fn different_index_same_entropy_different_key() {
let secret = b"shared-secret";
let snap = entropy::gather().unwrap();
let k0 = derive_symbol_key(secret, &snap, 0, 32).unwrap();
let k1 = derive_symbol_key(secret, &snap, 1, 32).unwrap();
assert_ne!(
k0, k1,
"Different symbol positions must yield different keys"
);
}
#[test]
fn deterministic_with_same_inputs() {
let secret = b"shared-secret";
let snap = entropy::gather().unwrap();
let k1 = derive_symbol_key(secret, &snap, 42, 16).unwrap();
let k2 = derive_symbol_key(secret, &snap, 42, 16).unwrap();
assert_eq!(
k1, k2,
"Same inputs must produce same key (deterministic derivation)"
);
}
#[test]
fn batch_matches_individual_derive() {
let secret = b"batch-derive-secret";
let snap = entropy::gather().unwrap();
let output_len = 4096;
let batch = derive_symbol_key_batch(secret, &snap, 0, output_len).unwrap();
for i in 0..8u64 {
let scalar = derive_symbol_key(secret, &snap, i, output_len).unwrap();
assert_eq!(
batch[i as usize], scalar,
"Batch derive lane {i} diverged from scalar derive_symbol_key"
);
}
}
}