1use aes_kw::KekAes128;
14use hmac::Hmac;
15use sha2::{Digest, Sha256};
16
17use crate::context::EncryptionContext;
18use crate::error::FileVaultError;
19
20#[derive(Debug, Clone)]
22pub struct VolumeKeys {
23 pub vmk: [u8; 16],
25 pub tweak_key: [u8; 16],
27}
28
29#[must_use]
31pub fn passphrase_key(password: &str, salt: &[u8; 16], iterations: u32) -> [u8; 16] {
32 let mut out = [0u8; 16];
33 let rounds = iterations.max(1);
36 pbkdf2::pbkdf2::<Hmac<Sha256>>(password.as_bytes(), salt, rounds, &mut out)
37 .map_err(|_| ())
38 .ok();
39 out
40}
41
42fn aes_kw_unwrap(
48 kek_bytes: &[u8; 16],
49 wrapped: &[u8; 24],
50 what: &'static str,
51) -> Result<[u8; 16], FileVaultError> {
52 let kek = KekAes128::from(*kek_bytes);
53 let mut out = [0u8; 16];
54 kek.unwrap(wrapped, &mut out)
55 .map_err(|_| FileVaultError::KeyUnwrap { what })?;
56 Ok(out)
57}
58
59pub fn derive_volume_keys(
66 password: &str,
67 context: &EncryptionContext,
68 family_uuid_bytes: &[u8; 16],
69) -> Result<VolumeKeys, FileVaultError> {
70 let pk = passphrase_key(password, &context.salt, context.iterations);
71 let kek = aes_kw_unwrap(&pk, &context.wrapped_kek, "KEK")?;
72 let vmk = aes_kw_unwrap(&kek, &context.wrapped_vmk, "VMK")?;
73
74 let mut hasher = Sha256::new();
75 hasher.update(vmk);
76 hasher.update(family_uuid_bytes);
77 let digest = hasher.finalize();
78 let mut tweak_key = [0u8; 16];
79 tweak_key.copy_from_slice(&digest[..16]);
80
81 Ok(VolumeKeys { vmk, tweak_key })
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::context::EncryptionContext;
88
89 fn hexn<const N: usize>(s: &str) -> [u8; N] {
90 let mut out = [0u8; N];
91 for (i, byte) in out.iter_mut().enumerate() {
92 *byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap();
93 }
94 out
95 }
96
97 #[test]
99 fn pbkdf2_matches_ground_truth() {
100 let salt: [u8; 16] = hexn("9bfcf480e4d9ad0eddd9ac6f47b85955");
101 let pk = passphrase_key("fvde-TEST", &salt, 90506);
102 assert_eq!(pk, hexn::<16>("0ec2849349f914e8bdbc189ac09c8bc7"));
103 }
104
105 #[test]
106 fn aes_kw_unwrap_kek_matches_ground_truth() {
107 let pk: [u8; 16] = hexn("0ec2849349f914e8bdbc189ac09c8bc7");
108 let wrapped: [u8; 24] = hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1");
109 let kek = aes_kw_unwrap(&pk, &wrapped, "KEK").unwrap();
110 assert_eq!(kek, hexn::<16>("a2543f0b8a6fc5cf2eaf7e76c95ef49c"));
111 }
112
113 #[test]
114 fn aes_kw_unwrap_vmk_matches_ground_truth() {
115 let kek: [u8; 16] = hexn("a2543f0b8a6fc5cf2eaf7e76c95ef49c");
116 let wrapped: [u8; 24] = hexn("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1");
117 let vmk = aes_kw_unwrap(&kek, &wrapped, "VMK").unwrap();
118 assert_eq!(vmk, hexn::<16>("d0d9c323197c62401c6e6b48f1c0f9d7"));
119 }
120
121 #[test]
122 fn wrong_key_fails_unwrap_loudly() {
123 let bad: [u8; 16] = [0u8; 16];
124 let wrapped: [u8; 24] = hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1");
125 assert!(matches!(
126 aes_kw_unwrap(&bad, &wrapped, "KEK"),
127 Err(FileVaultError::KeyUnwrap { what: "KEK" })
128 ));
129 }
130
131 #[test]
132 fn full_hierarchy_derives_vmk_and_tweak_key() {
133 let ctx = EncryptionContext {
134 salt: hexn("9bfcf480e4d9ad0eddd9ac6f47b85955"),
135 iterations: 90506,
136 wrapped_kek: hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1"),
137 wrapped_vmk: hexn("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1"),
138 protectors: Vec::new(),
139 conversion_status: None,
140 };
141 let family: [u8; 16] = hexn("1f01ca345f6c4123ac0cb0a256889db2");
143 let keys = derive_volume_keys("fvde-TEST", &ctx, &family).unwrap();
144 assert_eq!(keys.vmk, hexn::<16>("d0d9c323197c62401c6e6b48f1c0f9d7"));
145 assert_eq!(
146 keys.tweak_key,
147 hexn::<16>("53a17ba3213ec213bedcc34fe4e239af")
148 );
149 }
150
151 #[test]
152 fn wrong_password_surfaces_key_unwrap_error() {
153 let ctx = EncryptionContext {
154 salt: hexn("9bfcf480e4d9ad0eddd9ac6f47b85955"),
155 iterations: 90506,
156 wrapped_kek: hexn("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1"),
157 wrapped_vmk: hexn("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1"),
158 protectors: Vec::new(),
159 conversion_status: None,
160 };
161 let family: [u8; 16] = hexn("1f01ca345f6c4123ac0cb0a256889db2");
162 assert!(matches!(
163 derive_volume_keys("wrong-password", &ctx, &family),
164 Err(FileVaultError::KeyUnwrap { .. })
165 ));
166 }
167}