1#![forbid(unsafe_code)]
24#![allow(clippy::doc_markdown)]
27#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
28
29use std::io::{Read, Seek, SeekFrom};
30
31pub mod context;
32pub mod error;
33pub mod metadata;
34pub mod read;
35#[cfg(test)]
36mod test_support;
37pub mod unlock;
38#[cfg(feature = "vfs")]
39pub mod vfs;
40#[cfg(feature = "vfs")]
41pub use vfs::FileVaultLayer;
42pub mod volume;
43pub mod volume_header;
44pub mod xts;
45
46pub use context::{EncryptionContext, LogicalVolumeInfo, Protector, ProtectorKind};
47pub use error::FileVaultError;
48pub use volume::DecryptedVolume;
49pub use volume_header::VolumeHeader;
50
51const VOLUME_HEADER_LEN: usize = 512;
53
54#[derive(Debug, Clone)]
60pub struct FileVaultInfo {
61 pub physical_volume_identifier: String,
63 pub pbkdf2_iterations: u32,
65 pub pbkdf2_salt: [u8; 16],
67 pub family_uuid: String,
69 pub lv_identifier: Option<String>,
71 pub lv_name: Option<String>,
73 pub lv_size: u64,
75 pub encryption_method: &'static str,
77 pub conversion_status: Option<String>,
79 pub protectors: Vec<Protector>,
81}
82
83#[derive(Debug)]
86pub struct FileVaultVolume<R: Read + Seek> {
87 decrypted: DecryptedVolume<R>,
88 info: FileVaultInfo,
89}
90
91impl<R: Read + Seek> FileVaultVolume<R> {
92 pub fn unlock_with_password(mut reader: R, password: &str) -> Result<Self, FileVaultError> {
99 let mut header_bytes = [0u8; VOLUME_HEADER_LEN];
100 reader.seek(SeekFrom::Start(0))?;
101 read_exact_or_err(&mut reader, &mut header_bytes)?;
102 let header = VolumeHeader::parse(&header_bytes)?;
103
104 let plaintext = decrypt_metadata_region(&mut reader, &header)?;
105 let context = EncryptionContext::extract(&plaintext)?;
106 let lv = LogicalVolumeInfo::extract(&plaintext)?;
107
108 let family_uuid_bytes =
109 parse_uuid_bytes(&lv.family_uuid).ok_or(FileVaultError::MetadataStructureMissing {
110 what: "parseable family UUID",
111 })?;
112 let keys = unlock::derive_volume_keys(password, &context, &family_uuid_bytes)?;
113
114 let (physical_base, size) = resolve_lv_extent(&plaintext, &header, &lv);
115
116 let info = build_info(&header, &context, &lv, size);
117 let decrypted = DecryptedVolume::new(reader, keys, physical_base, size);
118 Ok(FileVaultVolume { decrypted, info })
119 }
120
121 pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, FileVaultError> {
127 self.decrypted.read_at(offset, buf)
128 }
129
130 #[must_use]
132 pub fn size(&self) -> u64 {
133 self.decrypted.size()
134 }
135
136 #[must_use]
138 pub fn info(&self) -> &FileVaultInfo {
139 &self.info
140 }
141
142 #[must_use]
144 pub fn into_decrypted(self) -> DecryptedVolume<R> {
145 self.decrypted
146 }
147}
148
149impl<R: Read + Seek> Read for FileVaultVolume<R> {
150 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
151 self.decrypted.read(buf)
152 }
153}
154
155impl<R: Read + Seek> Seek for FileVaultVolume<R> {
156 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
157 self.decrypted.seek(pos)
158 }
159}
160
161pub fn parse_info<R: Read + Seek>(mut reader: R) -> Result<FileVaultInfo, FileVaultError> {
167 let mut header_bytes = [0u8; VOLUME_HEADER_LEN];
168 reader.seek(SeekFrom::Start(0))?;
169 read_exact_or_err(&mut reader, &mut header_bytes)?;
170 let header = VolumeHeader::parse(&header_bytes)?;
171
172 let plaintext = decrypt_metadata_region(&mut reader, &header)?;
173 let context = EncryptionContext::extract(&plaintext)?;
174 let lv = LogicalVolumeInfo::extract(&plaintext)?;
175 let (_, size) = resolve_lv_extent(&plaintext, &header, &lv);
176 Ok(build_info(&header, &context, &lv, size))
177}
178
179fn decrypt_metadata_region<R: Read + Seek>(
182 reader: &mut R,
183 header: &VolumeHeader,
184) -> Result<Vec<u8>, FileVaultError> {
185 let (region_offset, _) = metadata::plaintext_metadata_region(header);
186
187 let block_size = header.block_size as usize;
189 if block_size == 0 {
190 return Err(FileVaultError::OutOfRange {
191 what: "block size is zero",
192 });
193 }
194 let mut first_block = vec![0u8; block_size];
195 reader.seek(SeekFrom::Start(region_offset))?;
196 read_exact_or_err(reader, &mut first_block)?;
197
198 let region_size = metadata::plaintext_metadata_size(header, &first_block)?;
199 let mut region = vec![0u8; region_size as usize];
200 reader.seek(SeekFrom::Start(region_offset))?;
201 read_exact_or_err(reader, &mut region)?;
202
203 let location = metadata::locate_encrypted_metadata(header, ®ion)?;
204 let mut ciphertext = vec![0u8; location.length as usize];
205 reader.seek(SeekFrom::Start(location.primary_offset))?;
206 read_exact_or_err(reader, &mut ciphertext)?;
207
208 metadata::decrypt_metadata(header, &mut ciphertext);
209 Ok(ciphertext)
210}
211
212fn resolve_lv_extent(
216 metadata_bytes: &[u8],
217 header: &VolumeHeader,
218 lv: &LogicalVolumeInfo,
219) -> (u64, u64) {
220 let block_size = u64::from(header.block_size);
221 let segments = metadata::parse_segments(metadata_bytes, header.block_size as usize);
222 if let Some(first) = segments.first() {
223 let base = first.physical_block.saturating_mul(block_size);
224 let size = if lv.size != 0 {
225 lv.size
226 } else {
227 u64::from(first.number_of_blocks).saturating_mul(block_size)
228 };
229 (base, size)
230 } else {
231 (0, lv.size)
232 }
233}
234
235fn build_info(
237 header: &VolumeHeader,
238 context: &EncryptionContext,
239 lv: &LogicalVolumeInfo,
240 size: u64,
241) -> FileVaultInfo {
242 FileVaultInfo {
243 physical_volume_identifier: format_uuid(&header.physical_volume_identifier),
244 pbkdf2_iterations: context.iterations,
245 pbkdf2_salt: context.salt,
246 family_uuid: lv.family_uuid.clone(),
247 lv_identifier: lv.lv_identifier.clone(),
248 lv_name: lv.name.clone(),
249 lv_size: size,
250 encryption_method: "AES-XTS-128",
251 conversion_status: context.conversion_status.clone(),
252 protectors: context.protectors.clone(),
253 }
254}
255
256fn read_exact_or_err<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<(), FileVaultError> {
259 reader.read_exact(buf).map_err(FileVaultError::Io)
260}
261
262fn format_uuid(bytes: &[u8; 16]) -> String {
264 uuid::Uuid::from_bytes(*bytes).hyphenated().to_string()
265}
266
267fn parse_uuid_bytes(text: &str) -> Option<[u8; 16]> {
269 uuid::Uuid::parse_str(text).ok().map(|u| *u.as_bytes())
270}
271
272#[cfg(test)]
273mod tests {
274 use super::test_support::{build_image, BLOCK, FAMILY, ITERS, KEY1, PASSWORD, PV_ID};
275 use super::*;
276 use std::io::Cursor;
277
278 #[test]
279 fn unlock_synthetic_volume_end_to_end() {
280 let image = build_image();
281 let mut vol = FileVaultVolume::unlock_with_password(Cursor::new(image), PASSWORD).unwrap();
282 assert_eq!(vol.size(), 0x4000);
283 assert_eq!(vol.info().pbkdf2_iterations, ITERS);
284 assert_eq!(vol.info().lv_name.as_deref(), Some("SynthLV"));
285 assert_eq!(vol.info().encryption_method, "AES-XTS-128");
286 assert_eq!(vol.info().conversion_status.as_deref(), Some("Complete"));
287 assert_eq!(vol.info().protectors.len(), 1);
288
289 let mut buf = [0u8; 512];
291 assert_eq!(vol.read_at(0, &mut buf).unwrap(), 512);
292 let expected: Vec<u8> = (0..512).map(|i| (i & 0xff) as u8).collect();
293 assert_eq!(&buf[..], &expected[..]);
294
295 assert_eq!(vol.seek(SeekFrom::Start(512)).unwrap(), 512);
297 let mut b2 = [0u8; 16];
298 assert!(std::io::Read::read(&mut vol, &mut b2).unwrap() > 0);
299
300 let image2 = build_image();
302 let info = parse_info(Cursor::new(image2)).unwrap();
303 assert_eq!(info.family_uuid, vol.info().family_uuid);
304 assert_eq!(info.lv_size, 0x4000);
305 assert_eq!(
306 info.physical_volume_identifier,
307 uuid::Uuid::from_bytes(PV_ID).hyphenated().to_string()
308 );
309
310 let mut dec = vol.into_decrypted();
312 assert_eq!(dec.size(), 0x4000);
313 let mut b3 = [0u8; 16];
314 assert_eq!(dec.read_at(0, &mut b3).unwrap(), 16);
315 }
316
317 #[test]
318 fn unlock_wrong_password_fails() {
319 let image = build_image();
320 assert!(matches!(
321 FileVaultVolume::unlock_with_password(Cursor::new(image), "nope"),
322 Err(FileVaultError::KeyUnwrap { .. })
323 ));
324 }
325
326 #[test]
327 fn unlock_non_corestorage_fails() {
328 let image = vec![0u8; 4096];
329 assert!(matches!(
330 FileVaultVolume::unlock_with_password(Cursor::new(image), PASSWORD),
331 Err(FileVaultError::NotCoreStorage { .. })
332 ));
333 }
334
335 #[test]
336 fn truncated_image_errors_loudly() {
337 let mut image = vec![0u8; 600];
339 image[88] = b'C';
340 image[89] = b'S';
341 image[96..100].copy_from_slice(&(BLOCK as u32).to_le_bytes());
342 image[172..176].copy_from_slice(&2u32.to_le_bytes());
343 image[104..112].copy_from_slice(&1u64.to_le_bytes());
344 let err = parse_info(Cursor::new(image)).unwrap_err();
345 assert!(matches!(err, FileVaultError::Io(_)));
346 }
347
348 #[test]
349 fn zero_block_size_header_errors() {
350 let mut image = vec![0u8; 4096];
352 image[88] = b'C';
353 image[89] = b'S';
354 image[96..100].copy_from_slice(&0u32.to_le_bytes());
355 image[172..176].copy_from_slice(&2u32.to_le_bytes());
356 let err = parse_info(Cursor::new(image)).unwrap_err();
357 assert!(matches!(err, FileVaultError::OutOfRange { .. }));
358 }
359
360 #[test]
361 fn format_and_parse_uuid_roundtrip() {
362 let s = format_uuid(&FAMILY);
363 assert_eq!(parse_uuid_bytes(&s), Some(FAMILY));
364 assert_eq!(parse_uuid_bytes("not-a-uuid"), None);
365 }
366
367 #[test]
368 fn resolve_extent_uses_segment_blocks_when_lv_size_zero() {
369 let header = VolumeHeader {
371 block_size: BLOCK as u32,
372 bytes_per_sector: 512,
373 physical_volume_size: 0,
374 metadata_block_numbers: [1, 0, 0, 0],
375 key_data: KEY1,
376 physical_volume_identifier: PV_ID,
377 };
378 let mut meta = vec![0u8; BLOCK];
380 meta[10..12].copy_from_slice(&0x0305u16.to_le_bytes());
381 let payload = 64;
382 meta[payload..payload + 4].copy_from_slice(&1u32.to_le_bytes());
383 let entry = payload + 8;
384 meta[entry + 16..entry + 20].copy_from_slice(&2u32.to_le_bytes());
385 meta[entry + 32..entry + 40].copy_from_slice(&5u64.to_le_bytes());
386 let lv = LogicalVolumeInfo {
387 size: 0,
388 family_uuid: "x".to_string(),
389 lv_identifier: None,
390 name: None,
391 };
392 let (base, size) = resolve_lv_extent(&meta, &header, &lv);
393 assert_eq!(base, 5 * BLOCK as u64);
394 assert_eq!(size, 2 * BLOCK as u64);
395 }
396
397 #[test]
398 fn resolve_extent_no_segment_falls_back_to_lv_size() {
399 let header = VolumeHeader {
400 block_size: BLOCK as u32,
401 bytes_per_sector: 512,
402 physical_volume_size: 0,
403 metadata_block_numbers: [1, 0, 0, 0],
404 key_data: KEY1,
405 physical_volume_identifier: PV_ID,
406 };
407 let meta = vec![0u8; BLOCK]; let lv = LogicalVolumeInfo {
409 size: 12345,
410 family_uuid: "x".to_string(),
411 lv_identifier: None,
412 name: None,
413 };
414 let (base, size) = resolve_lv_extent(&meta, &header, &lv);
415 assert_eq!(base, 0);
416 assert_eq!(size, 12345);
417 }
418}