use joy_crypt::kdf::{derive_hkdf_sha256, Salt};
pub fn derive_delegation_seed(
identity_seed: &[u8; 32],
salt: &Salt,
project_id: &str,
ai_member_id: &str,
) -> [u8; 32] {
let mut info = Vec::with_capacity(16 + project_id.len() + 1 + ai_member_id.len());
info.extend_from_slice(b"joy-delegation:");
info.extend_from_slice(project_id.as_bytes());
info.push(b':');
info.extend_from_slice(ai_member_id.as_bytes());
derive_hkdf_sha256(identity_seed, salt.as_bytes(), &info)
}
#[cfg(test)]
mod tests {
use super::*;
use joy_crypt::kdf::generate_salt;
const FIXED_SEED: [u8; 32] = [7u8; 32];
#[test]
fn delegation_seed_is_deterministic() {
let salt = generate_salt();
let s1 = derive_delegation_seed(&FIXED_SEED, &salt, "JOY", "ai:claude@joy");
let s2 = derive_delegation_seed(&FIXED_SEED, &salt, "JOY", "ai:claude@joy");
assert_eq!(s1, s2);
}
#[test]
fn delegation_seed_changes_with_salt() {
let s1 = derive_delegation_seed(&FIXED_SEED, &generate_salt(), "JOY", "ai:claude@joy");
let s2 = derive_delegation_seed(&FIXED_SEED, &generate_salt(), "JOY", "ai:claude@joy");
assert_ne!(s1, s2);
}
#[test]
fn delegation_seed_is_domain_separated_by_project() {
let salt = generate_salt();
let s1 = derive_delegation_seed(&FIXED_SEED, &salt, "JOY", "ai:claude@joy");
let s2 = derive_delegation_seed(&FIXED_SEED, &salt, "OTHER", "ai:claude@joy");
assert_ne!(s1, s2);
}
#[test]
fn delegation_seed_is_domain_separated_by_member() {
let salt = generate_salt();
let s1 = derive_delegation_seed(&FIXED_SEED, &salt, "JOY", "ai:claude@joy");
let s2 = derive_delegation_seed(&FIXED_SEED, &salt, "JOY", "ai:qwen@joy");
assert_ne!(s1, s2);
}
#[test]
fn delegation_seed_changes_with_identity_key() {
let salt = generate_salt();
let seed_a = FIXED_SEED;
let seed_b: [u8; 32] = [8u8; 32];
let s1 = derive_delegation_seed(&seed_a, &salt, "JOY", "ai:claude@joy");
let s2 = derive_delegation_seed(&seed_b, &salt, "JOY", "ai:claude@joy");
assert_ne!(s1, s2);
}
}