use aes_kw::KekAes128;
use hmac::Hmac;
use sha2::{Digest, Sha256};
use crate::context::EncryptionContext;
use crate::error::FileVaultError;
#[derive(Debug, Clone)]
pub struct VolumeKeys {
pub vmk: [u8; 16],
pub tweak_key: [u8; 16],
}
#[must_use]
pub fn passphrase_key(password: &str, salt: &[u8; 16], iterations: u32) -> [u8; 16] {
let mut out = [0u8; 16];
let rounds = iterations.max(1);
pbkdf2::pbkdf2::<Hmac<Sha256>>(password.as_bytes(), salt, rounds, &mut out)
.map_err(|_| ())
.ok();
out
}
fn aes_kw_unwrap(
kek_bytes: &[u8; 16],
wrapped: &[u8; 24],
what: &'static str,
) -> Result<[u8; 16], FileVaultError> {
let kek = KekAes128::from(*kek_bytes);
let mut out = [0u8; 16];
kek.unwrap(wrapped, &mut out)
.map_err(|_| FileVaultError::KeyUnwrap { what })?;
Ok(out)
}
pub fn derive_volume_keys(
password: &str,
context: &EncryptionContext,
family_uuid_bytes: &[u8; 16],
) -> Result<VolumeKeys, FileVaultError> {
let pk = passphrase_key(password, &context.salt, context.iterations);
let kek = aes_kw_unwrap(&pk, &context.wrapped_kek, "KEK")?;
let vmk = aes_kw_unwrap(&kek, &context.wrapped_vmk, "VMK")?;
let mut hasher = Sha256::new();
hasher.update(vmk);
hasher.update(family_uuid_bytes);
let digest = hasher.finalize();
let mut tweak_key = [0u8; 16];
tweak_key.copy_from_slice(&digest[..16]);
Ok(VolumeKeys { vmk, tweak_key })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::EncryptionContext;
fn hexn<const N: usize>(s: &str) -> [u8; N] {
let mut out = [0u8; N];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap();
}
out
}
#[test]
fn pbkdf2_matches_ground_truth() {
let salt: [u8; 16] = hexn("9bfcf480e4d9ad0eddd9ac6f47b85955");
let pk = passphrase_key("fvde-TEST", &salt, 90506);
assert_eq!(pk, hexn::<16>("0ec2849349f914e8bdbc189ac09c8bc7"));
}
#[test]
fn aes_kw_unwrap_kek_matches_ground_truth() {
let pk: [u8; 16] = hexn("0ec2849349f914e8bdbc189ac09c8bc7");
let wrapped: [u8; 24] = hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1");
let kek = aes_kw_unwrap(&pk, &wrapped, "KEK").unwrap();
assert_eq!(kek, hexn::<16>("a2543f0b8a6fc5cf2eaf7e76c95ef49c"));
}
#[test]
fn aes_kw_unwrap_vmk_matches_ground_truth() {
let kek: [u8; 16] = hexn("a2543f0b8a6fc5cf2eaf7e76c95ef49c");
let wrapped: [u8; 24] = hexn("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1");
let vmk = aes_kw_unwrap(&kek, &wrapped, "VMK").unwrap();
assert_eq!(vmk, hexn::<16>("d0d9c323197c62401c6e6b48f1c0f9d7"));
}
#[test]
fn wrong_key_fails_unwrap_loudly() {
let bad: [u8; 16] = [0u8; 16];
let wrapped: [u8; 24] = hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1");
assert!(matches!(
aes_kw_unwrap(&bad, &wrapped, "KEK"),
Err(FileVaultError::KeyUnwrap { what: "KEK" })
));
}
#[test]
fn full_hierarchy_derives_vmk_and_tweak_key() {
let ctx = EncryptionContext {
salt: hexn("9bfcf480e4d9ad0eddd9ac6f47b85955"),
iterations: 90506,
wrapped_kek: hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1"),
wrapped_vmk: hexn("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1"),
protectors: Vec::new(),
conversion_status: None,
};
let family: [u8; 16] = hexn("1f01ca345f6c4123ac0cb0a256889db2");
let keys = derive_volume_keys("fvde-TEST", &ctx, &family).unwrap();
assert_eq!(keys.vmk, hexn::<16>("d0d9c323197c62401c6e6b48f1c0f9d7"));
assert_eq!(
keys.tweak_key,
hexn::<16>("53a17ba3213ec213bedcc34fe4e239af")
);
}
#[test]
fn wrong_password_surfaces_key_unwrap_error() {
let ctx = EncryptionContext {
salt: hexn("9bfcf480e4d9ad0eddd9ac6f47b85955"),
iterations: 90506,
wrapped_kek: hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1"),
wrapped_vmk: hexn("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1"),
protectors: Vec::new(),
conversion_status: None,
};
let family: [u8; 16] = hexn("1f01ca345f6c4123ac0cb0a256889db2");
assert!(matches!(
derive_volume_keys("wrong-password", &ctx, &family),
Err(FileVaultError::KeyUnwrap { .. })
));
}
}