use crate::crypto::{Kek, SecretKey};
use hkdf::Hkdf;
use sha2::Sha256;
const INFO_DEVICE: &[u8] = b"omy/v1/device-key";
pub const SECRET_LEN: usize = 32;
const ID_PREFIX: &str = "vault-";
#[must_use]
pub fn slot_id(vault_salt: &[u8; 16]) -> String {
let mut s = String::with_capacity(ID_PREFIX.len().saturating_add(32));
s.push_str(ID_PREFIX);
for b in vault_salt {
s.push_str(&format!("{b:02x}"));
}
s
}
#[must_use]
pub fn kek_from_secret(secret: &[u8; SECRET_LEN], vault_salt: &[u8; 16]) -> Kek {
let hk = Hkdf::<Sha256>::new(Some(vault_salt), secret);
let mut out = [0u8; 32];
hk.expand(INFO_DEVICE, &mut out)
.unwrap_or_else(|_| unreachable!("HKDF expand 32 bytes cannot fail"));
Kek::from_key(SecretKey::from_bytes(out))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slot_id_is_stable_and_vault_scoped() {
let a = [7u8; 16];
assert_eq!(slot_id(&a), slot_id(&a), "同一个 salt 必须得到同一个 id");
let mut b = a;
b[0] = 8;
assert_ne!(slot_id(&a), slot_id(&b), "不同 salt 必须得到不同 id");
assert!(
slot_id(&a).strip_prefix(ID_PREFIX).is_some_and(|h| h
.chars()
.all(|c| c.is_ascii_hexdigit())),
"id 里出现了非十六进制字符"
);
}
#[test]
fn same_secret_different_vault_gives_different_kek() {
let secret = [0x5Au8; SECRET_LEN];
let k1 = kek_from_secret(&secret, &[1u8; 16]);
let k2 = kek_from_secret(&secret, &[2u8; 16]);
assert_ne!(
k1.fingerprint(&[0u8; 16]),
k2.fingerprint(&[0u8; 16]),
"不同 vault 派生出了相同的 KEK"
);
}
#[test]
fn derivation_is_deterministic() {
let secret = [0x33u8; SECRET_LEN];
let salt = [9u8; 16];
assert_eq!(
kek_from_secret(&secret, &salt).fingerprint(&salt),
kek_from_secret(&secret, &salt).fingerprint(&salt),
);
}
#[test]
fn different_secret_gives_different_kek() {
let salt = [9u8; 16];
let k1 = kek_from_secret(&[1u8; SECRET_LEN], &salt);
let k2 = kek_from_secret(&[2u8; SECRET_LEN], &salt);
assert_ne!(k1.fingerprint(&salt), k2.fingerprint(&salt));
}
}