Skip to main content

filevault/
metadata.rs

1//! Locate and decrypt the CoreStorage encrypted-metadata region.
2//!
3//! Pipeline (see `docs/RESEARCH.md`, cross-checked against libfvde
4//! `libfvde_metadata_read_type_0x0011`):
5//! 1. Read the plaintext metadata region starting at
6//!    `metadata_block_numbers[0] * block_size`; its first block has type
7//!    `0x0011` and its payload carries `metadata_size` (region length) at
8//!    payload offset 0 and a volume-groups-descriptor offset at payload 156.
9//! 2. At that region-relative descriptor offset, read the encrypted-metadata
10//!    block count (+8) and the 48-bit primary / secondary block numbers
11//!    (+32 / +40). Verified on the fvdetest ground truth: count = 6144,
12//!    primary = 2049.
13//! 3. AES-XTS-128 decrypt the encrypted-metadata region: key1 = header key_data,
14//!    key2 = physical-volume identifier, unit = 8192 bytes, tweak = 0-based unit
15//!    index within the region.
16
17use crate::error::FileVaultError;
18use crate::read::{le_u16, le_u32, le_u64};
19use crate::volume_header::VolumeHeader;
20use crate::xts;
21
22/// Block type of the first plaintext metadata block (the one whose payload holds
23/// the volume-groups descriptor pointing at the encrypted metadata).
24const BLOCK_TYPE_ENCRYPTED_METADATA_POINTER: u16 = 0x0011;
25/// AES-XTS unit size for the encrypted metadata region.
26const METADATA_UNIT_SIZE: usize = 8192;
27/// Fixed 64-byte metadata-block header (checksum, version, type, serial,
28/// transaction id, object id, number, block size, …).
29pub const BLOCK_HEADER_SIZE: usize = 64;
30/// Offset of the metadata-block header type field.
31const BLOCK_HEADER_TYPE_OFFSET: usize = 10;
32/// Payload offset (from the header end) of the u32 metadata region size.
33const PAYLOAD_METADATA_SIZE_OFFSET: usize = 0;
34/// Payload offset of the u32 volume-groups-descriptor offset (region-relative).
35const PAYLOAD_VG_DESCRIPTOR_OFFSET: usize = 156;
36/// Within the volume-groups descriptor: encrypted-metadata block count (u64).
37const VG_COUNT_OFFSET: usize = 8;
38/// Within the descriptor: 48-bit primary encrypted-metadata block number (u64,
39/// top 16 bits are the physical-volume index and are masked off).
40const VG_PRIMARY_BLOCK_OFFSET: usize = 32;
41/// Mask keeping the low 48 bits of a CoreStorage block-number field.
42const BLOCK_NUMBER_MASK: u64 = 0x0000_ffff_ffff_ffff;
43/// A sane upper bound on the plaintext metadata region (blocks).
44const MAX_METADATA_BLOCKS: u64 = 1 << 20;
45/// A sane upper bound on the encrypted-metadata region (blocks).
46const MAX_ENCRYPTED_METADATA_BLOCKS: u64 = 1 << 20;
47
48/// The located encrypted-metadata region, plus everything needed to read it.
49#[derive(Debug, Clone)]
50pub struct EncryptedMetadataLocation {
51    /// Byte offset of the primary encrypted-metadata region in the image.
52    pub primary_offset: u64,
53    /// Number of blocks in the region.
54    pub block_count: u64,
55    /// Region length in bytes (`block_count * block_size`), whole 8192 units.
56    pub length: u64,
57}
58
59/// Byte offset and length of the plaintext metadata region (block mbn[0]).
60#[must_use]
61pub fn plaintext_metadata_region(header: &VolumeHeader) -> (u64, u64) {
62    let block = header.metadata_block_numbers.first().copied().unwrap_or(0);
63    let offset = block.saturating_mul(u64::from(header.block_size));
64    (offset, 0)
65}
66
67/// Read the region size (bytes) from the plaintext metadata region's first
68/// block payload, capping against an allocation bomb.
69///
70/// # Errors
71/// [`FileVaultError::MetadataStructureMissing`] if the first block is not a
72/// `0x0011` block, [`FileVaultError::OutOfRange`] if the size is absurd.
73pub fn plaintext_metadata_size(
74    header: &VolumeHeader,
75    first_block: &[u8],
76) -> Result<u64, FileVaultError> {
77    let block_type = le_u16(first_block, BLOCK_HEADER_TYPE_OFFSET);
78    if block_type != BLOCK_TYPE_ENCRYPTED_METADATA_POINTER {
79        return Err(FileVaultError::MetadataStructureMissing {
80            what: "0x0011 encrypted-metadata pointer block",
81        });
82    }
83    let size = u64::from(le_u32(
84        first_block,
85        BLOCK_HEADER_SIZE + PAYLOAD_METADATA_SIZE_OFFSET,
86    ));
87    let block_size = u64::from(header.block_size);
88    if block_size == 0 {
89        return Err(FileVaultError::OutOfRange {
90            what: "block size is zero",
91        });
92    }
93    if size == 0 || size / block_size > MAX_METADATA_BLOCKS {
94        return Err(FileVaultError::OutOfRange {
95            what: "plaintext metadata region size",
96        });
97    }
98    Ok(size)
99}
100
101/// Locate the encrypted-metadata region from the full plaintext metadata region.
102///
103/// `region` is the whole plaintext metadata (`metadata_size` bytes) beginning at
104/// the `0x0011` block. The volume-groups-descriptor offset is read from the
105/// first block's payload (offset 156) and is *region-relative*.
106///
107/// # Errors
108/// [`FileVaultError::MetadataStructureMissing`] / [`FileVaultError::OutOfRange`]
109/// on a malformed or out-of-range descriptor.
110pub fn locate_encrypted_metadata(
111    header: &VolumeHeader,
112    region: &[u8],
113) -> Result<EncryptedMetadataLocation, FileVaultError> {
114    let block_type = le_u16(region, BLOCK_HEADER_TYPE_OFFSET);
115    if block_type != BLOCK_TYPE_ENCRYPTED_METADATA_POINTER {
116        return Err(FileVaultError::MetadataStructureMissing {
117            what: "0x0011 encrypted-metadata pointer block",
118        });
119    }
120
121    let descriptor_offset =
122        le_u32(region, BLOCK_HEADER_SIZE + PAYLOAD_VG_DESCRIPTOR_OFFSET) as usize;
123
124    // The descriptor must sit within the region with room for its fields.
125    if descriptor_offset
126        .checked_add(VG_PRIMARY_BLOCK_OFFSET + 8)
127        .map_or(true, |end| end > region.len())
128    {
129        return Err(FileVaultError::MetadataStructureMissing {
130            what: "volume-groups descriptor (offset out of region)",
131        });
132    }
133
134    let block_count = le_u64(region, descriptor_offset + VG_COUNT_OFFSET);
135    let primary_block =
136        le_u64(region, descriptor_offset + VG_PRIMARY_BLOCK_OFFSET) & BLOCK_NUMBER_MASK;
137
138    if block_count == 0 || block_count > MAX_ENCRYPTED_METADATA_BLOCKS {
139        return Err(FileVaultError::OutOfRange {
140            what: "encrypted-metadata block count",
141        });
142    }
143
144    let block_size = u64::from(header.block_size);
145    let primary_offset =
146        primary_block
147            .checked_mul(block_size)
148            .ok_or(FileVaultError::OutOfRange {
149                what: "encrypted-metadata primary offset",
150            })?;
151    let length = block_count
152        .checked_mul(block_size)
153        .ok_or(FileVaultError::OutOfRange {
154            what: "encrypted-metadata length",
155        })?;
156
157    Ok(EncryptedMetadataLocation {
158        primary_offset,
159        block_count,
160        length,
161    })
162}
163
164/// Decrypt an encrypted-metadata region in place over 8192-byte AES-XTS-128
165/// units, tweak = 0-based unit index. Only whole units are decrypted; a trailing
166/// partial (never present for a valid region) is left untouched.
167pub fn decrypt_metadata(header: &VolumeHeader, ciphertext: &mut [u8]) {
168    xts::decrypt_units(
169        ciphertext,
170        &header.key_data,
171        &header.physical_volume_identifier,
172        METADATA_UNIT_SIZE,
173        0,
174    );
175}
176
177/// Block type of the segment-descriptor block (logical→physical map).
178const BLOCK_TYPE_SEGMENT_DESCRIPTOR: u16 = 0x0305;
179/// Payload offset of the first `0x0305` entry.
180const SEG_FIRST_ENTRY_PAYLOAD_OFFSET: usize = 8;
181/// Stride between `0x0305` entries.
182const SEG_ENTRY_STRIDE: usize = 40;
183/// Within a `0x0305` entry: logical block number (u64).
184const SEG_LOGICAL_BLOCK_OFFSET: usize = 8;
185/// Within an entry: number of blocks (u32).
186const SEG_NUM_BLOCKS_OFFSET: usize = 16;
187/// Within an entry: 48-bit physical block number (u64; top 16 bits masked).
188const SEG_PHYSICAL_BLOCK_OFFSET: usize = 32;
189/// A sane cap on segment-descriptor entries (guards a lying count).
190const MAX_SEGMENTS: u32 = 1 << 16;
191
192/// One logical→physical segment mapping decoded from a `0x0305` block.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct SegmentEntry {
195    /// Logical block number where this segment begins.
196    pub logical_block: u64,
197    /// First physical block of the segment.
198    pub physical_block: u64,
199    /// Number of blocks in the segment.
200    pub number_of_blocks: u32,
201}
202
203/// Parse the segment-descriptor (`0x0305`) block from decrypted metadata,
204/// returning the (logical→physical) segment entries.
205///
206/// Finds the first `0x0305` block in `metadata`, reads its entry count
207/// (capped), and decodes each 40-byte entry. Returns an empty vector if no
208/// segment-descriptor block is present (a per-artifact miss).
209#[must_use]
210pub fn parse_segments(metadata: &[u8], block_size: usize) -> Vec<SegmentEntry> {
211    let Some(block_offset) =
212        find_block_of_type(metadata, block_size, BLOCK_TYPE_SEGMENT_DESCRIPTOR)
213    else {
214        return Vec::new();
215    };
216    let payload = block_offset + BLOCK_HEADER_SIZE;
217    let count = le_u32(metadata, payload).min(MAX_SEGMENTS);
218
219    let mut out = Vec::new();
220    for i in 0..count as usize {
221        let entry = payload + SEG_FIRST_ENTRY_PAYLOAD_OFFSET + i * SEG_ENTRY_STRIDE;
222        // Stop if the entry would run past this block's payload — a lying
223        // `number_of_entries` must not read into the next block or out of bounds.
224        if entry + SEG_ENTRY_STRIDE > block_offset + block_size {
225            break;
226        }
227        let physical_block =
228            le_u64(metadata, entry + SEG_PHYSICAL_BLOCK_OFFSET) & BLOCK_NUMBER_MASK;
229        out.push(SegmentEntry {
230            logical_block: le_u64(metadata, entry + SEG_LOGICAL_BLOCK_OFFSET),
231            physical_block,
232            number_of_blocks: le_u32(metadata, entry + SEG_NUM_BLOCKS_OFFSET),
233        });
234    }
235    out
236}
237
238/// Locate a block of `block_type` in `metadata`, returning its byte offset.
239#[must_use]
240fn find_block_of_type(metadata: &[u8], block_size: usize, block_type: u16) -> Option<usize> {
241    if block_size == 0 {
242        return None; // cov:unreachable: block_size comes from a validated header (>=512)
243    }
244    let mut offset = 0;
245    while offset + block_size <= metadata.len() {
246        if le_u16(metadata, offset + BLOCK_HEADER_TYPE_OFFSET) == block_type {
247            return Some(offset);
248        }
249        offset += block_size;
250    }
251    None
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn header(block_size: u32, mbn0: u64) -> VolumeHeader {
259        VolumeHeader {
260            block_size,
261            bytes_per_sector: 512,
262            physical_volume_size: 0,
263            metadata_block_numbers: [mbn0, 0, 0, 0],
264            key_data: [0u8; 16],
265            physical_volume_identifier: [0u8; 16],
266        }
267    }
268
269    /// Build a plaintext metadata region: block 0 is a 0x0011 pointer whose
270    /// payload carries metadata_size@0 and the descriptor offset@156; the
271    /// descriptor (at a region offset) carries count@+8 and primary@+32.
272    fn build_region(block_size: usize, count: u64, primary: u64) -> Vec<u8> {
273        let mut region = vec![0u8; block_size * 4];
274        // Block 0 header: type 0x0011 at offset 10.
275        region[10..12].copy_from_slice(&BLOCK_TYPE_ENCRYPTED_METADATA_POINTER.to_le_bytes());
276        // Payload starts at BLOCK_HEADER_SIZE (64).
277        let payload = BLOCK_HEADER_SIZE;
278        // metadata_size = whole region.
279        region[payload..payload + 4].copy_from_slice(&((block_size as u32) * 4).to_le_bytes());
280        // Descriptor offset (region-relative): put it at 2*block_size.
281        let desc = 2 * block_size;
282        region[payload + PAYLOAD_VG_DESCRIPTOR_OFFSET..payload + PAYLOAD_VG_DESCRIPTOR_OFFSET + 4]
283            .copy_from_slice(&(desc as u32).to_le_bytes());
284        region[desc + VG_COUNT_OFFSET..desc + VG_COUNT_OFFSET + 8]
285            .copy_from_slice(&count.to_le_bytes());
286        // primary with a physical-volume index in the top 16 bits to prove masking.
287        let primary_field = primary | (0x1234u64 << 48);
288        region[desc + VG_PRIMARY_BLOCK_OFFSET..desc + VG_PRIMARY_BLOCK_OFFSET + 8]
289            .copy_from_slice(&primary_field.to_le_bytes());
290        region
291    }
292
293    #[test]
294    fn locates_encrypted_metadata_masking_volume_index() {
295        let h = header(4096, 1);
296        let region = build_region(4096, 6144, 2049);
297        let loc = locate_encrypted_metadata(&h, &region).unwrap();
298        assert_eq!(loc.block_count, 6144);
299        assert_eq!(loc.primary_offset, 2049 * 4096);
300        assert_eq!(loc.length, 6144 * 4096);
301    }
302
303    #[test]
304    fn rejects_wrong_first_block_type() {
305        let h = header(4096, 1);
306        let mut region = build_region(4096, 6144, 2049);
307        region[10..12].copy_from_slice(&0x0010u16.to_le_bytes());
308        assert!(matches!(
309            locate_encrypted_metadata(&h, &region),
310            Err(FileVaultError::MetadataStructureMissing { .. })
311        ));
312    }
313
314    #[test]
315    fn rejects_absurd_block_count() {
316        let h = header(4096, 1);
317        let region = build_region(4096, u64::from(u32::MAX), 2049);
318        assert!(matches!(
319            locate_encrypted_metadata(&h, &region),
320            Err(FileVaultError::OutOfRange { .. })
321        ));
322    }
323
324    #[test]
325    fn descriptor_offset_out_of_region_is_missing() {
326        let h = header(4096, 1);
327        let mut region = build_region(4096, 6144, 2049);
328        let payload = BLOCK_HEADER_SIZE;
329        // Point the descriptor beyond the region.
330        let beyond = region.len() as u32 + 100;
331        region[payload + PAYLOAD_VG_DESCRIPTOR_OFFSET..payload + PAYLOAD_VG_DESCRIPTOR_OFFSET + 4]
332            .copy_from_slice(&beyond.to_le_bytes());
333        assert!(matches!(
334            locate_encrypted_metadata(&h, &region),
335            Err(FileVaultError::MetadataStructureMissing { .. })
336        ));
337    }
338
339    #[test]
340    fn plaintext_size_reads_from_pointer_block() {
341        let h = header(4096, 1);
342        let region = build_region(4096, 6144, 2049);
343        let size = plaintext_metadata_size(&h, &region[..4096]).unwrap();
344        assert_eq!(size, 4096 * 4);
345    }
346
347    #[test]
348    fn plaintext_size_rejects_non_pointer_block() {
349        let h = header(4096, 1);
350        let block = vec![0u8; 4096];
351        assert!(matches!(
352            plaintext_metadata_size(&h, &block),
353            Err(FileVaultError::MetadataStructureMissing { .. })
354        ));
355    }
356
357    #[test]
358    fn plaintext_region_offset_is_block_scaled() {
359        let h = header(4096, 3);
360        assert_eq!(plaintext_metadata_region(&h), (3 * 4096, 0));
361    }
362
363    #[test]
364    fn parses_single_segment() {
365        let block_size = 4096usize;
366        let mut meta = vec![0u8; block_size];
367        meta[10..12].copy_from_slice(&BLOCK_TYPE_SEGMENT_DESCRIPTOR.to_le_bytes());
368        let payload = BLOCK_HEADER_SIZE;
369        meta[payload..payload + 4].copy_from_slice(&1u32.to_le_bytes()); // one entry
370        let entry = payload + SEG_FIRST_ENTRY_PAYLOAD_OFFSET;
371        meta[entry + SEG_LOGICAL_BLOCK_OFFSET..entry + SEG_LOGICAL_BLOCK_OFFSET + 8]
372            .copy_from_slice(&0u64.to_le_bytes());
373        meta[entry + SEG_NUM_BLOCKS_OFFSET..entry + SEG_NUM_BLOCKS_OFFSET + 4]
374            .copy_from_slice(&40960u32.to_le_bytes());
375        let phys = 16384u64 | (0x1234u64 << 48);
376        meta[entry + SEG_PHYSICAL_BLOCK_OFFSET..entry + SEG_PHYSICAL_BLOCK_OFFSET + 8]
377            .copy_from_slice(&phys.to_le_bytes());
378        let segs = parse_segments(&meta, block_size);
379        assert_eq!(segs.len(), 1);
380        assert_eq!(segs[0].physical_block, 16384);
381        assert_eq!(segs[0].number_of_blocks, 40960);
382        assert_eq!(segs[0].logical_block, 0);
383    }
384
385    #[test]
386    fn no_segment_block_yields_empty() {
387        let meta = vec![0u8; 4096];
388        assert!(parse_segments(&meta, 4096).is_empty());
389    }
390
391    #[test]
392    fn decrypt_metadata_roundtrips() {
393        let mut h = header(4096, 1);
394        h.key_data = [0x11u8; 16];
395        h.physical_volume_identifier = [0x22u8; 16];
396        let plain: Vec<u8> = (0..8192u32).map(|i| (i & 0xff) as u8).collect();
397        let mut buf = plain.clone();
398        crate::xts::encrypt_units(
399            &mut buf,
400            &h.key_data,
401            &h.physical_volume_identifier,
402            8192,
403            0,
404        );
405        decrypt_metadata(&h, &mut buf);
406        assert_eq!(buf, plain);
407    }
408
409    #[test]
410    fn plaintext_size_rejects_zero_block_size() {
411        let h = header(0, 1);
412        let region = build_region(4096, 6144, 2049);
413        assert!(matches!(
414            plaintext_metadata_size(&h, &region[..4096]),
415            Err(FileVaultError::OutOfRange {
416                what: "block size is zero"
417            })
418        ));
419    }
420
421    #[test]
422    fn plaintext_size_rejects_absurd_size() {
423        // block_size 512 with size u32::MAX -> size/512 > MAX_METADATA_BLOCKS.
424        let h = header(512, 1);
425        let mut block = vec![0u8; 4096];
426        block[10..12].copy_from_slice(&BLOCK_TYPE_ENCRYPTED_METADATA_POINTER.to_le_bytes());
427        block[BLOCK_HEADER_SIZE..BLOCK_HEADER_SIZE + 4].copy_from_slice(&u32::MAX.to_le_bytes());
428        assert!(matches!(
429            plaintext_metadata_size(&h, &block),
430            Err(FileVaultError::OutOfRange {
431                what: "plaintext metadata region size"
432            })
433        ));
434    }
435
436    #[test]
437    fn parse_segments_zero_block_size_is_empty() {
438        // find_block_of_type is reached with block_size == 0 -> None -> empty.
439        assert!(parse_segments(&[0u8; 4096], 0).is_empty());
440    }
441
442    #[test]
443    fn parse_segments_stops_on_lying_count() {
444        // A 0x0305 block claiming more entries than the block can hold must stop
445        // at the block boundary, not read past it.
446        let block_size = 4096usize;
447        let mut meta = vec![0u8; block_size];
448        meta[10..12].copy_from_slice(&BLOCK_TYPE_SEGMENT_DESCRIPTOR.to_le_bytes());
449        let payload = BLOCK_HEADER_SIZE;
450        // Claim 1000 entries; only (4096-64-8)/40 ~= 100 fit.
451        meta[payload..payload + 4].copy_from_slice(&1000u32.to_le_bytes());
452        let segs = parse_segments(&meta, block_size);
453        // Never panics; yields at most what fits.
454        assert!(segs.len() < 1000);
455    }
456}