Skip to main content

bitlocker/
metadata.rs

1//! FVE metadata block, header, and entry parsing.
2//!
3//! A BitLocker volume carries three copies of an FVE metadata block. Each block
4//! is a `-FVE-FS-` block header, a 48-byte metadata header (cipher, volume GUID,
5//! creation time), and a recursive array of metadata entries — the key
6//! protectors (VMK), the FVEK, and the volume-header-block descriptor. See
7//! `docs/RESEARCH.md`.
8
9use crate::bytes::{le_u16, le_u32, le_u64, read_guid, slice_owned};
10
11const FVE_SIGNATURE: &[u8; 8] = b"-FVE-FS-";
12
13/// Metadata-entry type: a Volume Master Key protector.
14pub const ENTRY_TYPE_VMK: u16 = 0x0002;
15/// Metadata-entry type: the Full Volume Encryption Key.
16pub const ENTRY_TYPE_FVEK: u16 = 0x0003;
17/// Metadata-entry type / value type: the volume-header block descriptor.
18pub const ENTRY_TYPE_VOLUME_HEADER: u16 = 0x000f;
19
20/// Value type: a key (`method(u32)@0` then the raw key data). Holds the clear
21/// key inside a clear-key VMK protector, and the external key inside a `.BEK`.
22pub const VALUE_TYPE_KEY: u16 = 0x0001;
23/// Value type: an AES-CCM encrypted key.
24pub const VALUE_TYPE_AES_CCM: u16 = 0x0005;
25/// Value type: an external key (the payload of a startup-key `.BEK` file: a
26/// 16-byte key GUID, an 8-byte FILETIME, then nested entries holding the key).
27pub const VALUE_TYPE_EXTERNAL_KEY: u16 = 0x0009;
28/// Value type: a stretch key (salt + nested AES-CCM key).
29pub const VALUE_TYPE_STRETCH: u16 = 0x0003;
30/// Value type: a Volume Master Key protector.
31pub const VALUE_TYPE_VMK: u16 = 0x0008;
32
33/// Key-protection type: password.
34pub const PROTECTION_PASSWORD: u16 = 0x2000;
35/// Key-protection type: recovery password (the 48-digit numeric key).
36pub const PROTECTION_RECOVERY: u16 = 0x0800;
37/// Key-protection type: startup key (an external `.BEK` key on removable media).
38/// The VMK is AES-CCM-wrapped directly by the 256-bit external key — no stretch.
39pub const PROTECTION_STARTUP_KEY: u16 = 0x0200;
40/// Key-protection type: clear key. The VMK is stored unprotected (BitLocker adds
41/// this when protection is *suspended*), so the volume needs no credential.
42pub const PROTECTION_CLEAR: u16 = 0x0000;
43
44/// One FVE metadata entry (`entry_type`, `value_type`, `version`, and the raw
45/// value data that follows the 8-byte entry header).
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct MetadataEntry {
48    /// Entry type (e.g. `ENTRY_TYPE_VMK`).
49    pub entry_type: u16,
50    /// Value type (e.g. `VALUE_TYPE_AES_CCM`).
51    pub value_type: u16,
52    /// Entry version (typically 1).
53    pub version: u16,
54    /// The value data following the 8-byte entry header.
55    pub data: Vec<u8>,
56}
57
58impl MetadataEntry {
59    /// Parse a flat sequence of metadata entries from `data`.
60    ///
61    /// Each entry is `size(u16) | entry_type(u16) | value_type(u16) |
62    /// version(u16) | value_data`. Parsing stops on a size below the 8-byte
63    /// header, an entry that would run past the buffer, or the end of `data` —
64    /// never looping forever on a lying size.
65    #[must_use]
66    pub fn parse_sequence(data: &[u8]) -> Vec<MetadataEntry> {
67        let mut out = Vec::new();
68        let mut pos = 0usize;
69        while pos + 8 <= data.len() {
70            let size = le_u16(data, pos) as usize;
71            if size < 8 || pos + size > data.len() {
72                break;
73            }
74            out.push(MetadataEntry {
75                entry_type: le_u16(data, pos + 2),
76                value_type: le_u16(data, pos + 4),
77                version: le_u16(data, pos + 6),
78                data: slice_owned(data, pos + 8, size - 8),
79            });
80            pos += size;
81        }
82        out
83    }
84
85    /// Parse this entry's value data (from `offset`) as a nested entry sequence.
86    #[must_use]
87    pub fn nested(&self, offset: usize) -> Vec<MetadataEntry> {
88        let start = offset.min(self.data.len());
89        Self::parse_sequence(&self.data[start..])
90    }
91
92    /// Whether this entry is a VMK protector.
93    #[must_use]
94    pub fn is_vmk(&self) -> bool {
95        self.entry_type == ENTRY_TYPE_VMK && self.value_type == VALUE_TYPE_VMK
96    }
97
98    /// The key-protection type of a VMK entry (protector type at value offset 26),
99    /// or `None` for a non-VMK entry.
100    #[must_use]
101    pub fn protection_type(&self) -> Option<u16> {
102        self.is_vmk().then(|| le_u16(&self.data, 26))
103    }
104}
105
106/// A parsed FVE metadata block: cipher, identity, and the entry array, plus the
107/// block-header fields the read path needs (encrypted size and the relocated
108/// volume-header region).
109#[derive(Debug, Clone)]
110pub struct FveMetadata {
111    /// Volume encryption method (metadata header offset 36).
112    pub encryption_method: u16,
113    /// Volume identifier GUID (metadata header offset 16).
114    pub volume_guid: [u8; 16],
115    /// Volume creation time as a Windows FILETIME (metadata header offset 40).
116    pub creation_time: u64,
117    /// The metadata entries.
118    pub entries: Vec<MetadataEntry>,
119    /// Number of still-encrypted bytes from the front (block header, v2 offset 16).
120    /// Zero means "whole volume".
121    pub encrypted_volume_size: u64,
122    /// Byte offset where the original volume header is stored, relocated.
123    pub volume_header_offset: u64,
124    /// Size in bytes of the relocated volume-header region.
125    pub volume_header_size: u64,
126    /// Byte offsets of the three metadata blocks (read back as zeros).
127    pub metadata_offsets: [u64; 3],
128    /// Size of the metadata region (metadata header offset 0).
129    pub metadata_size: u32,
130}
131
132impl FveMetadata {
133    /// Parse an FVE metadata block from bytes beginning at its block header.
134    ///
135    /// Returns `None` when the `-FVE-FS-` block-header signature is absent, so
136    /// the caller can try the next candidate block offset.
137    #[must_use]
138    pub fn parse(block: &[u8], bytes_per_sector: u16) -> Option<FveMetadata> {
139        if block.get(0..8) != Some(FVE_SIGNATURE.as_slice()) {
140            return None;
141        }
142
143        let encrypted_volume_size = le_u64(block, 16);
144        let num_volume_header_sectors = le_u32(block, 28);
145        let metadata_offsets = [le_u64(block, 32), le_u64(block, 40), le_u64(block, 48)];
146        let block_volume_header_offset = le_u64(block, 56);
147
148        // FVE metadata header starts at block offset 64.
149        let mh = 64usize;
150        let metadata_size = le_u32(block, mh);
151        let volume_guid = read_guid(block, mh + 16);
152        let encryption_method = le_u16(block, mh + 36);
153        let creation_time = le_u64(block, mh + 40);
154
155        // Entries follow the 48-byte metadata header, bounded by metadata_size.
156        let entries_start = mh + 48;
157        let entries_end = (mh + metadata_size as usize).min(block.len());
158        let entries = if entries_end > entries_start {
159            MetadataEntry::parse_sequence(&block[entries_start..entries_end])
160        } else {
161            Vec::new()
162        };
163
164        // Resolve the relocated volume-header region: prefer the dedicated
165        // volume-header-block entry (type 0x000f), else the block-header fields.
166        let mut volume_header_offset = block_volume_header_offset;
167        let mut volume_header_size =
168            u64::from(num_volume_header_sectors) * u64::from(bytes_per_sector);
169        if let Some(e) = entries
170            .iter()
171            .find(|e| e.entry_type == ENTRY_TYPE_VOLUME_HEADER)
172        {
173            let bo = le_u64(&e.data, 0);
174            let bs = le_u64(&e.data, 8);
175            if bo != 0 {
176                volume_header_offset = bo;
177            }
178            if bs != 0 {
179                volume_header_size = bs;
180            }
181        }
182
183        Some(FveMetadata {
184            encryption_method,
185            volume_guid,
186            creation_time,
187            entries,
188            encrypted_volume_size,
189            volume_header_offset,
190            volume_header_size,
191            metadata_offsets,
192            metadata_size,
193        })
194    }
195
196    /// Iterate the VMK protector entries.
197    pub fn vmk_entries(&self) -> impl Iterator<Item = &MetadataEntry> {
198        self.entries.iter().filter(|e| e.is_vmk())
199    }
200
201    /// The key-protection types present, in metadata order.
202    #[must_use]
203    pub fn protector_types(&self) -> Vec<u16> {
204        self.vmk_entries()
205            .filter_map(MetadataEntry::protection_type)
206            .collect()
207    }
208
209    /// The top-level FVEK entry (an AES-CCM encrypted key wrapped by the VMK).
210    #[must_use]
211    pub fn fvek_entry(&self) -> Option<&MetadataEntry> {
212        self.entries
213            .iter()
214            .find(|e| e.entry_type == ENTRY_TYPE_FVEK && e.value_type == VALUE_TYPE_AES_CCM)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    fn entry_bytes(entry_type: u16, value_type: u16, version: u16, data: &[u8]) -> Vec<u8> {
223        let size = (8 + data.len()) as u16;
224        let mut v = Vec::new();
225        v.extend_from_slice(&size.to_le_bytes());
226        v.extend_from_slice(&entry_type.to_le_bytes());
227        v.extend_from_slice(&value_type.to_le_bytes());
228        v.extend_from_slice(&version.to_le_bytes());
229        v.extend_from_slice(data);
230        v
231    }
232
233    #[test]
234    fn parse_sequence_splits_entries() {
235        let mut buf = Vec::new();
236        buf.extend(entry_bytes(0x000f, 0x000f, 1, &[1, 2, 3, 4]));
237        buf.extend(entry_bytes(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, 1, &[9; 20]));
238        let entries = MetadataEntry::parse_sequence(&buf);
239        assert_eq!(entries.len(), 2);
240        assert_eq!(entries[0].entry_type, 0x000f);
241        assert_eq!(entries[0].data, vec![1, 2, 3, 4]);
242        assert!(entries[1].is_vmk());
243    }
244
245    #[test]
246    fn parse_sequence_stops_on_lying_size() {
247        // size field claims 8 (empty) then a size of 0 — must not loop forever.
248        let mut buf = entry_bytes(1, 2, 1, &[]);
249        buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); // size=0 -> stop
250        let entries = MetadataEntry::parse_sequence(&buf);
251        assert_eq!(entries.len(), 1);
252    }
253
254    #[test]
255    fn parse_sequence_stops_on_oversize() {
256        // size claims 100 but only 8 bytes present.
257        let mut buf = Vec::new();
258        buf.extend_from_slice(&100u16.to_le_bytes());
259        buf.extend_from_slice(&[0u8; 6]);
260        assert!(MetadataEntry::parse_sequence(&buf).is_empty());
261    }
262
263    fn build_block(entries: &[Vec<u8>]) -> Vec<u8> {
264        let mut entry_bytes = Vec::new();
265        for e in entries {
266            entry_bytes.extend_from_slice(e);
267        }
268        let metadata_size = 48 + entry_bytes.len();
269        let mut block = vec![0u8; 64 + metadata_size];
270        block[0..8].copy_from_slice(FVE_SIGNATURE);
271        block[10..12].copy_from_slice(&2u16.to_le_bytes()); // version 2
272        block[16..24].copy_from_slice(&0x0400_0000u64.to_le_bytes()); // encrypted size
273        block[28..32].copy_from_slice(&16u32.to_le_bytes()); // vol header sectors
274        block[56..64].copy_from_slice(&0x0211_0800u64.to_le_bytes()); // vol header offset
275                                                                      // metadata header @64
276        block[64..68].copy_from_slice(&(metadata_size as u32).to_le_bytes());
277        block[64 + 16..64 + 32].copy_from_slice(&[0xAB; 16]); // volume guid
278        block[64 + 36..64 + 38].copy_from_slice(&0x8000u16.to_le_bytes()); // method
279        block[64 + 40..64 + 48].copy_from_slice(&130_461_864_497_281_120u64.to_le_bytes());
280        block[64 + 48..].copy_from_slice(&entry_bytes);
281        block
282    }
283
284    #[test]
285    fn parse_full_block() {
286        // A volume-header-block entry (0x000f) + a password VMK.
287        let mut vh_data = Vec::new();
288        vh_data.extend_from_slice(&0x0211_0800u64.to_le_bytes()); // block offset
289        vh_data.extend_from_slice(&0x0051_5a00u64.to_le_bytes()); // block size
290        let vh = entry_bytes(
291            ENTRY_TYPE_VOLUME_HEADER,
292            ENTRY_TYPE_VOLUME_HEADER,
293            1,
294            &vh_data,
295        );
296
297        let mut vmk_data = vec![0u8; 28];
298        vmk_data[26..28].copy_from_slice(&PROTECTION_PASSWORD.to_le_bytes());
299        let vmk = entry_bytes(ENTRY_TYPE_VMK, VALUE_TYPE_VMK, 1, &vmk_data);
300
301        let block = build_block(&[vh, vmk]);
302        let m = FveMetadata::parse(&block, 512).unwrap();
303        assert_eq!(m.encryption_method, 0x8000);
304        assert_eq!(m.volume_guid, [0xAB; 16]);
305        assert_eq!(m.creation_time, 130_461_864_497_281_120);
306        assert_eq!(m.encrypted_volume_size, 0x0400_0000);
307        assert_eq!(m.volume_header_offset, 0x0211_0800);
308        assert_eq!(m.volume_header_size, 0x0051_5a00); // from the 0x000f entry
309        assert_eq!(m.entries.len(), 2);
310        assert_eq!(m.protector_types(), vec![PROTECTION_PASSWORD]);
311    }
312
313    #[test]
314    fn parse_returns_none_without_signature() {
315        let block = vec![0u8; 128];
316        assert!(FveMetadata::parse(&block, 512).is_none());
317    }
318
319    #[test]
320    fn volume_header_size_falls_back_to_sector_count() {
321        // No 0x000f entry: size = num_volume_header_sectors * bytes_per_sector.
322        let block = build_block(&[]);
323        let m = FveMetadata::parse(&block, 512).unwrap();
324        assert_eq!(m.volume_header_size, 16 * 512);
325        assert_eq!(m.volume_header_offset, 0x0211_0800);
326    }
327
328    #[test]
329    fn truncated_block_does_not_panic() {
330        let mut block = build_block(&[]);
331        block.truncate(70);
332        let _ = FveMetadata::parse(&block, 512);
333    }
334}