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