Skip to main content

filevault/
unlock.rs

1//! The CoreStorage / FileVault key hierarchy (RustCrypto — never hand-rolled).
2//!
3//! ```text
4//! passphrase_key = PBKDF2-HMAC-SHA256(password, salt, iterations, 16)
5//! KEK            = AES-KW-unwrap(wrapped_kek, passphrase_key)   (RFC 3394, 24->16)
6//! VMK            = AES-KW-unwrap(wrapped_vmk, KEK)              (24->16)
7//! tweak_key      = SHA256(VMK || familyUUID_bytes)[0..16]
8//! ```
9//!
10//! Every value here is checked against the dfvfs `fvdetest` ground truth in
11//! `docs/RESEARCH.md` (VMK `d0d9c323…`, tweak_key `53a17ba3…`).
12
13use aes_kw::KekAes128;
14use hmac::Hmac;
15use sha2::{Digest, Sha256};
16
17use crate::context::EncryptionContext;
18use crate::error::FileVaultError;
19
20/// The volume master key and its XTS tweak key, derived from a password.
21#[derive(Debug, Clone)]
22pub struct VolumeKeys {
23    /// AES-XTS key1 for LV sector decryption.
24    pub vmk: [u8; 16],
25    /// AES-XTS key2 (tweak key) for LV sector decryption.
26    pub tweak_key: [u8; 16],
27}
28
29/// Derive the PBKDF2 passphrase key (16 bytes) from the password and context.
30#[must_use]
31pub fn passphrase_key(password: &str, salt: &[u8; 16], iterations: u32) -> [u8; 16] {
32    let mut out = [0u8; 16];
33    // pbkdf2 with an explicit round count; a zero count would be nonsensical but
34    // is bounded here to at least 1 so the KDF always runs.
35    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
42/// Unwrap a 24-byte RFC 3394 wrapped key with `kek`, returning the 16-byte key.
43///
44/// # Errors
45/// [`FileVaultError::KeyUnwrap`] if the integrity check fails (wrong key /
46/// corrupt input) — the "wrong password" signal at the KEK step.
47fn 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
59/// Run the full key hierarchy for a password against an encryption context and
60/// family UUID, returning the LV keys.
61///
62/// # Errors
63/// [`FileVaultError::KeyUnwrap`] if the password is wrong (KEK unwrap fails) or
64/// the VMK unwrap fails.
65pub 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    // Ground truth: RESEARCH.md Tier-2 table (dfvfs fvdetest, password fvde-TEST).
98    #[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        // familyUUID 1F01CA34-5F6C-4123-AC0C-B0A256889DB2 in canonical byte order.
142        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}