use hkdf::Hkdf;
use sha2::Sha256;
use super::{KdfError, Key};
pub fn derive_agent_key(master: &Key, agent_id: u32) -> Result<Key, KdfError> {
let hk = Hkdf::<Sha256>::new(None, master.as_bytes());
let mut info = [0u8; 19];
info[..15].copy_from_slice(b"varta-agent-v1\0");
info[15..].copy_from_slice(&agent_id.to_le_bytes());
let mut okm = [0u8; 32];
hk.expand(&info, &mut okm).map_err(|_| KdfError)?;
Ok(Key::from_bytes(okm))
}
pub fn derive_iv_prefix(session_salt: &[u8; 16], prefix_index: u32) -> Result<[u8; 8], KdfError> {
let hk = Hkdf::<Sha256>::new(None, session_salt);
let mut info = [0u8; 23];
info[..19].copy_from_slice(b"varta-iv-prefix-v1\0");
info[19..].copy_from_slice(&prefix_index.to_le_bytes());
let mut okm = [0u8; 8];
hk.expand(&info, &mut okm).map_err(|_| KdfError)?;
Ok(okm)
}
pub fn derive_epoch_key(agent_key: &Key, epoch: u64) -> Result<Key, KdfError> {
let hk = Hkdf::<Sha256>::new(None, agent_key.as_bytes());
let mut info = [0u8; 23];
info[..15].copy_from_slice(b"varta-epoch-v1\0");
info[15..].copy_from_slice(&epoch.to_le_bytes());
let mut okm = [0u8; 32];
hk.expand(&info, &mut okm).map_err(|_| KdfError)?;
Ok(Key::from_bytes(okm))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_agent_key_deterministic() {
let master = Key::from_bytes([0x42; 32]);
let k1 = derive_agent_key(&master, 1).expect("kdf must succeed");
let k2 = derive_agent_key(&master, 1).expect("kdf must succeed");
assert_eq!(k1.as_bytes(), k2.as_bytes());
}
#[test]
fn derive_agent_key_different_pids_produce_different_keys() {
let master = Key::from_bytes([0x42; 32]);
let k1 = derive_agent_key(&master, 1).expect("kdf must succeed");
let k2 = derive_agent_key(&master, 2).expect("kdf must succeed");
assert_ne!(k1.as_bytes(), k2.as_bytes());
}
#[test]
fn derive_agent_key_different_masters_produce_different_keys() {
let m1 = Key::from_bytes([0x42; 32]);
let m2 = Key::from_bytes([0x43; 32]);
let k1 = derive_agent_key(&m1, 1).expect("kdf must succeed");
let k2 = derive_agent_key(&m2, 1).expect("kdf must succeed");
assert_ne!(k1.as_bytes(), k2.as_bytes());
}
#[test]
fn derive_epoch_key_deterministic() {
let agent = Key::from_bytes([0xab; 32]);
let e1 = derive_epoch_key(&agent, 0).expect("kdf must succeed");
let e2 = derive_epoch_key(&agent, 0).expect("kdf must succeed");
assert_eq!(e1.as_bytes(), e2.as_bytes());
}
#[test]
fn derive_epoch_key_different_epochs_produce_different_keys() {
let agent = Key::from_bytes([0xab; 32]);
let e1 = derive_epoch_key(&agent, 0).expect("kdf must succeed");
let e2 = derive_epoch_key(&agent, 1).expect("kdf must succeed");
assert_ne!(e1.as_bytes(), e2.as_bytes());
}
#[test]
fn key_hierarchy_is_one_way() {
let master = Key::from_bytes([0x42; 32]);
let agent_key = derive_agent_key(&master, 7).expect("kdf must succeed");
let epoch_key = derive_epoch_key(&agent_key, 0).expect("kdf must succeed");
assert_ne!(epoch_key.as_bytes(), agent_key.as_bytes());
assert_ne!(epoch_key.as_bytes(), master.as_bytes());
assert_ne!(agent_key.as_bytes(), master.as_bytes());
}
#[test]
fn agent_key_and_epoch_key_have_different_domains() {
let master = Key::from_bytes([0x42; 32]);
let agent_key = derive_agent_key(&master, 1000).expect("kdf must succeed");
let fake_epoch = derive_epoch_key(&master, 1000).expect("kdf must succeed");
assert_ne!(agent_key.as_bytes(), fake_epoch.as_bytes());
}
#[test]
fn derive_iv_prefix_deterministic() {
let salt = [0x42u8; 16];
let p1 = derive_iv_prefix(&salt, 0).expect("kdf must succeed");
let p2 = derive_iv_prefix(&salt, 0).expect("kdf must succeed");
assert_eq!(p1, p2);
}
#[test]
fn derive_iv_prefix_distinct_indices() {
let salt = [0x42u8; 16];
let samples = [
derive_iv_prefix(&salt, 0).expect("kdf must succeed"),
derive_iv_prefix(&salt, 1).expect("kdf must succeed"),
derive_iv_prefix(&salt, 2).expect("kdf must succeed"),
derive_iv_prefix(&salt, 3).expect("kdf must succeed"),
derive_iv_prefix(&salt, 4).expect("kdf must succeed"),
derive_iv_prefix(&salt, u32::MAX).expect("kdf must succeed"),
];
for i in 0..samples.len() {
for j in (i + 1)..samples.len() {
assert_ne!(
samples[i], samples[j],
"collision at indices {i} and {j}: {:?}",
samples[i]
);
}
}
}
#[test]
fn derive_iv_prefix_distinct_salts() {
let salt_a = [0x01u8; 16];
let salt_b = [0x02u8; 16];
assert_ne!(
derive_iv_prefix(&salt_a, 0).expect("kdf must succeed"),
derive_iv_prefix(&salt_b, 0).expect("kdf must succeed"),
"different salts must produce different prefixes"
);
}
#[test]
fn derive_iv_prefix_domain_separation() {
let salt = [0x42u8; 16];
let mut padded = [0u8; 32];
padded[..16].copy_from_slice(&salt);
let master = Key::from_bytes(padded);
let iv = derive_iv_prefix(&salt, 0).expect("kdf must succeed");
let agent = derive_agent_key(&master, 0).expect("kdf must succeed");
let epoch = derive_epoch_key(&master, 0).expect("kdf must succeed");
assert_ne!(iv, agent.as_bytes()[..8]);
assert_ne!(iv, epoch.as_bytes()[..8]);
assert_ne!(agent.as_bytes(), epoch.as_bytes());
}
}