use crate::room_state::privacy::SecretVersion;
use ed25519_dalek::VerifyingKey;
const ROOT_CONTEXT: &str = "river-rotate v1 2026-04 room-secret-root";
pub fn derive_room_secret(
signing_key_seed: &[u8; 32],
owner_vk: &VerifyingKey,
version: SecretVersion,
) -> [u8; 32] {
let root = blake3::derive_key(ROOT_CONTEXT, signing_key_seed);
let mut hasher = blake3::Hasher::new_keyed(&root);
hasher.update(owner_vk.as_bytes());
hasher.update(&version.to_le_bytes());
*hasher.finalize().as_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn vk_from_seed(seed: [u8; 32]) -> VerifyingKey {
SigningKey::from_bytes(&seed).verifying_key()
}
#[test]
fn derive_is_deterministic() {
let seed = [7u8; 32];
let vk = vk_from_seed([1u8; 32]);
let a = derive_room_secret(&seed, &vk, 0);
let b = derive_room_secret(&seed, &vk, 0);
assert_eq!(a, b, "identical inputs must produce identical outputs");
}
#[test]
fn derive_separates_versions() {
let seed = [7u8; 32];
let vk = vk_from_seed([1u8; 32]);
let v0 = derive_room_secret(&seed, &vk, 0);
let v1 = derive_room_secret(&seed, &vk, 1);
assert_ne!(v0, v1, "different versions must produce different outputs");
}
#[test]
fn derive_separates_owners() {
let seed = [7u8; 32];
let vk_a = vk_from_seed([1u8; 32]);
let vk_b = vk_from_seed([2u8; 32]);
let a = derive_room_secret(&seed, &vk_a, 0);
let b = derive_room_secret(&seed, &vk_b, 0);
assert_ne!(a, b, "different owners must produce different outputs");
}
#[test]
fn derive_separates_keys() {
let seed_a = [7u8; 32];
let seed_b = [8u8; 32];
let vk = vk_from_seed([1u8; 32]);
let a = derive_room_secret(&seed_a, &vk, 0);
let b = derive_room_secret(&seed_b, &vk, 0);
assert_ne!(
a, b,
"different signing key seeds must produce different outputs"
);
}
#[test]
fn derive_locks_input_ordering() {
let seed = [7u8; 32];
let vk_a = vk_from_seed([1u8; 32]);
let vk_b = vk_from_seed([2u8; 32]);
assert_ne!(
derive_room_secret(&seed, &vk_a, 1),
derive_room_secret(&seed, &vk_b, 0),
);
assert_ne!(
derive_room_secret(&seed, &vk_a, 0),
derive_room_secret(&seed, &vk_b, 1),
);
}
#[test]
fn derive_known_answer_v1_zero_seed_zero_version() {
let seed = [0u8; 32];
let vk = SigningKey::from_bytes(&[1u8; 32]).verifying_key();
let actual = derive_room_secret(&seed, &vk, 0);
let expected: [u8; 32] = [
0xdd, 0x18, 0x9c, 0xce, 0x07, 0x93, 0x74, 0x85, 0x6e, 0xb7, 0xa2, 0x01, 0x61, 0x8e,
0x58, 0x86, 0xa1, 0xe9, 0xe5, 0x59, 0x8b, 0x33, 0x34, 0x08, 0x43, 0x00, 0x2c, 0xbb,
0x90, 0x91, 0xe1, 0xa9,
];
assert_eq!(
actual, expected,
"construction changed; KAT mismatch. Actual = {:02x?}",
actual
);
}
#[test]
fn derive_known_answer_v1_multi_byte_version() {
let seed = [0u8; 32];
let vk = SigningKey::from_bytes(&[1u8; 32]).verifying_key();
let actual = derive_room_secret(&seed, &vk, 0x01020304);
let expected: [u8; 32] = [
0xaa, 0x8f, 0x7d, 0x5a, 0xb5, 0x15, 0x84, 0x66, 0x78, 0x72, 0x28, 0xd6, 0x88, 0x54,
0xf6, 0x5d, 0x39, 0xac, 0xe3, 0x13, 0x07, 0x8f, 0x29, 0xa9, 0xfb, 0xad, 0x88, 0x79,
0x70, 0xd3, 0xfe, 0x67,
];
assert_eq!(
actual, expected,
"construction changed; KAT mismatch. Actual = {:02x?}",
actual
);
}
#[test]
fn derive_known_answer_v1_all_ff_seed() {
let seed = [0xFFu8; 32];
let vk = SigningKey::from_bytes(&[1u8; 32]).verifying_key();
let actual = derive_room_secret(&seed, &vk, 0);
let expected: [u8; 32] = [
0x60, 0xb5, 0x60, 0x0b, 0x12, 0xfc, 0xaa, 0x0c, 0x52, 0xda, 0x76, 0x59, 0x95, 0xf6,
0x9c, 0xb3, 0xeb, 0x54, 0x37, 0xd5, 0x67, 0x53, 0xc0, 0x24, 0x97, 0x67, 0x19, 0xf1,
0xe4, 0x31, 0x7e, 0x87,
];
assert_eq!(
actual, expected,
"construction changed; KAT mismatch. Actual = {:02x?}",
actual
);
}
}