Skip to main content

filevault/
volume_header.rs

1//! The 512-byte CoreStorage physical volume header (partition start, LE fields).
2//!
3//! See `docs/RESEARCH.md` — every offset below is cross-checked against libfvde
4//! and the dfvfs `fvdetest` ground truth.
5
6use crate::error::FileVaultError;
7use crate::read::{bytes16, le_u16, le_u32, le_u64};
8
9/// CoreStorage signature `"CS"` (bytes 0x43 0x53 at offsets 88,89) read
10/// little-endian at offset 88 -> 0x5343.
11const CS_SIGNATURE_LE: u16 = 0x5343;
12/// Encryption method code for AES-XTS-128.
13const ENCRYPTION_METHOD_AES_XTS_128: u32 = 2;
14
15/// Number of metadata block-number slots in the header (offset 104, 4 × u64).
16const METADATA_BLOCK_SLOTS: usize = 4;
17
18/// The parsed physical volume header.
19#[derive(Debug, Clone)]
20pub struct VolumeHeader {
21    /// CoreStorage block size in bytes (typically 4096); the multiplier that
22    /// turns a block number into a byte offset.
23    pub block_size: u32,
24    /// Bytes per sector (typically 512); the AES-XTS unit for LV decryption.
25    pub bytes_per_sector: u32,
26    /// Physical volume size in bytes (header offset 64).
27    pub physical_volume_size: u64,
28    /// Metadata block numbers (header offset 104, 4 × u64). Multiply by
29    /// `block_size` for a byte offset.
30    pub metadata_block_numbers: [u64; METADATA_BLOCK_SLOTS],
31    /// The 16-byte metadata XTS key1 (header offset 176, first 16 bytes of the
32    /// 128-byte key data).
33    pub key_data: [u8; 16],
34    /// The physical volume identifier (header offset 304, on-disk byte order) —
35    /// the metadata XTS key2.
36    pub physical_volume_identifier: [u8; 16],
37}
38
39impl VolumeHeader {
40    /// Parse the 512-byte header from the start of a CoreStorage partition.
41    ///
42    /// # Errors
43    /// - [`FileVaultError::NotCoreStorage`] if the `"CS"` signature is absent.
44    /// - [`FileVaultError::UnsupportedEncryptionMethod`] if not AES-XTS-128.
45    pub fn parse(data: &[u8]) -> Result<Self, FileVaultError> {
46        let signature = le_u16(data, 88);
47        if signature != CS_SIGNATURE_LE {
48            return Err(FileVaultError::NotCoreStorage { found: signature });
49        }
50
51        let encryption_method = le_u32(data, 172);
52        if encryption_method != ENCRYPTION_METHOD_AES_XTS_128 {
53            return Err(FileVaultError::UnsupportedEncryptionMethod {
54                found: encryption_method,
55            });
56        }
57
58        let mut metadata_block_numbers = [0u64; METADATA_BLOCK_SLOTS];
59        for (i, slot) in metadata_block_numbers.iter_mut().enumerate() {
60            *slot = le_u64(data, 104 + i * 8);
61        }
62
63        Ok(VolumeHeader {
64            block_size: le_u32(data, 96),
65            bytes_per_sector: le_u32(data, 48),
66            physical_volume_size: le_u64(data, 64),
67            metadata_block_numbers,
68            key_data: bytes16(data, 176),
69            physical_volume_identifier: bytes16(data, 304),
70        })
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// Build a 512-byte header carrying the dfvfs `fvdetest` ground-truth field
79    /// values (RESEARCH.md Tier-2 table).
80    fn ground_truth_header() -> [u8; 512] {
81        let mut h = [0u8; 512];
82        h[88] = b'C';
83        h[89] = b'S';
84        h[48..52].copy_from_slice(&512u32.to_le_bytes());
85        h[96..100].copy_from_slice(&4096u32.to_le_bytes());
86        h[64..72].copy_from_slice(&536_829_952u64.to_le_bytes());
87        h[172..176].copy_from_slice(&2u32.to_le_bytes());
88        for (i, block) in [1u64, 1025, 129_013, 130_037].iter().enumerate() {
89            h[104 + i * 8..104 + i * 8 + 8].copy_from_slice(&block.to_le_bytes());
90        }
91        h[176..192].copy_from_slice(&hex("18eaeb7da9ab0852ead69e9dabc86f59"));
92        h[304..320].copy_from_slice(&hex("3273a0553b8b47e8b970df35eecda81b"));
93        h
94    }
95
96    fn hex(s: &str) -> [u8; 16] {
97        let mut out = [0u8; 16];
98        for (i, byte) in out.iter_mut().enumerate() {
99            *byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap();
100        }
101        out
102    }
103
104    #[test]
105    fn parses_ground_truth_fields() {
106        let header = VolumeHeader::parse(&ground_truth_header()).unwrap();
107        assert_eq!(header.block_size, 4096);
108        assert_eq!(header.bytes_per_sector, 512);
109        assert_eq!(header.physical_volume_size, 536_829_952);
110        assert_eq!(header.metadata_block_numbers, [1, 1025, 129_013, 130_037]);
111        assert_eq!(header.key_data, hex("18eaeb7da9ab0852ead69e9dabc86f59"));
112        assert_eq!(
113            header.physical_volume_identifier,
114            hex("3273a0553b8b47e8b970df35eecda81b")
115        );
116    }
117
118    #[test]
119    fn rejects_non_corestorage() {
120        let mut h = ground_truth_header();
121        h[88] = b'X';
122        let err = VolumeHeader::parse(&h).unwrap_err();
123        assert!(
124            matches!(err, FileVaultError::NotCoreStorage { found } if found & 0x00ff == u16::from(b'X'))
125        );
126    }
127
128    #[test]
129    fn rejects_unsupported_encryption_method() {
130        let mut h = ground_truth_header();
131        h[172..176].copy_from_slice(&7u32.to_le_bytes());
132        let err = VolumeHeader::parse(&h).unwrap_err();
133        assert!(matches!(
134            err,
135            FileVaultError::UnsupportedEncryptionMethod { found: 7 }
136        ));
137    }
138
139    #[test]
140    fn short_input_is_not_corestorage() {
141        assert!(matches!(
142            VolumeHeader::parse(&[0u8; 10]),
143            Err(FileVaultError::NotCoreStorage { .. })
144        ));
145    }
146}