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;
35pub mod unlock;
36pub mod volume;
37pub mod volume_header;
38pub mod xts;
39
40pub use context::{EncryptionContext, LogicalVolumeInfo, Protector, ProtectorKind};
41pub use error::FileVaultError;
42pub use volume::DecryptedVolume;
43pub use volume_header::VolumeHeader;
44
45const VOLUME_HEADER_LEN: usize = 512;
47
48#[derive(Debug, Clone)]
54pub struct FileVaultInfo {
55 pub physical_volume_identifier: String,
57 pub pbkdf2_iterations: u32,
59 pub pbkdf2_salt: [u8; 16],
61 pub family_uuid: String,
63 pub lv_identifier: Option<String>,
65 pub lv_name: Option<String>,
67 pub lv_size: u64,
69 pub encryption_method: &'static str,
71 pub conversion_status: Option<String>,
73 pub protectors: Vec<Protector>,
75}
76
77#[derive(Debug)]
80pub struct FileVaultVolume<R: Read + Seek> {
81 decrypted: DecryptedVolume<R>,
82 info: FileVaultInfo,
83}
84
85impl<R: Read + Seek> FileVaultVolume<R> {
86 pub fn unlock_with_password(mut reader: R, password: &str) -> Result<Self, FileVaultError> {
93 let mut header_bytes = [0u8; VOLUME_HEADER_LEN];
94 reader.seek(SeekFrom::Start(0))?;
95 read_exact_or_err(&mut reader, &mut header_bytes)?;
96 let header = VolumeHeader::parse(&header_bytes)?;
97
98 let plaintext = decrypt_metadata_region(&mut reader, &header)?;
99 let context = EncryptionContext::extract(&plaintext)?;
100 let lv = LogicalVolumeInfo::extract(&plaintext)?;
101
102 let family_uuid_bytes =
103 parse_uuid_bytes(&lv.family_uuid).ok_or(FileVaultError::MetadataStructureMissing {
104 what: "parseable family UUID",
105 })?;
106 let keys = unlock::derive_volume_keys(password, &context, &family_uuid_bytes)?;
107
108 let (physical_base, size) = resolve_lv_extent(&plaintext, &header, &lv);
109
110 let info = build_info(&header, &context, &lv, size);
111 let decrypted = DecryptedVolume::new(reader, keys, physical_base, size);
112 Ok(FileVaultVolume { decrypted, info })
113 }
114
115 pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, FileVaultError> {
121 self.decrypted.read_at(offset, buf)
122 }
123
124 #[must_use]
126 pub fn size(&self) -> u64 {
127 self.decrypted.size()
128 }
129
130 #[must_use]
132 pub fn info(&self) -> &FileVaultInfo {
133 &self.info
134 }
135
136 #[must_use]
138 pub fn into_decrypted(self) -> DecryptedVolume<R> {
139 self.decrypted
140 }
141}
142
143impl<R: Read + Seek> Read for FileVaultVolume<R> {
144 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
145 self.decrypted.read(buf)
146 }
147}
148
149impl<R: Read + Seek> Seek for FileVaultVolume<R> {
150 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
151 self.decrypted.seek(pos)
152 }
153}
154
155pub fn parse_info<R: Read + Seek>(mut reader: R) -> Result<FileVaultInfo, FileVaultError> {
161 let mut header_bytes = [0u8; VOLUME_HEADER_LEN];
162 reader.seek(SeekFrom::Start(0))?;
163 read_exact_or_err(&mut reader, &mut header_bytes)?;
164 let header = VolumeHeader::parse(&header_bytes)?;
165
166 let plaintext = decrypt_metadata_region(&mut reader, &header)?;
167 let context = EncryptionContext::extract(&plaintext)?;
168 let lv = LogicalVolumeInfo::extract(&plaintext)?;
169 let (_, size) = resolve_lv_extent(&plaintext, &header, &lv);
170 Ok(build_info(&header, &context, &lv, size))
171}
172
173fn decrypt_metadata_region<R: Read + Seek>(
176 reader: &mut R,
177 header: &VolumeHeader,
178) -> Result<Vec<u8>, FileVaultError> {
179 let (region_offset, _) = metadata::plaintext_metadata_region(header);
180
181 let block_size = header.block_size as usize;
183 if block_size == 0 {
184 return Err(FileVaultError::OutOfRange {
185 what: "block size is zero",
186 });
187 }
188 let mut first_block = vec![0u8; block_size];
189 reader.seek(SeekFrom::Start(region_offset))?;
190 read_exact_or_err(reader, &mut first_block)?;
191
192 let region_size = metadata::plaintext_metadata_size(header, &first_block)?;
193 let mut region = vec![0u8; region_size as usize];
194 reader.seek(SeekFrom::Start(region_offset))?;
195 read_exact_or_err(reader, &mut region)?;
196
197 let location = metadata::locate_encrypted_metadata(header, ®ion)?;
198 let mut ciphertext = vec![0u8; location.length as usize];
199 reader.seek(SeekFrom::Start(location.primary_offset))?;
200 read_exact_or_err(reader, &mut ciphertext)?;
201
202 metadata::decrypt_metadata(header, &mut ciphertext);
203 Ok(ciphertext)
204}
205
206fn resolve_lv_extent(
210 metadata_bytes: &[u8],
211 header: &VolumeHeader,
212 lv: &LogicalVolumeInfo,
213) -> (u64, u64) {
214 let block_size = u64::from(header.block_size);
215 let segments = metadata::parse_segments(metadata_bytes, header.block_size as usize);
216 if let Some(first) = segments.first() {
217 let base = first.physical_block.saturating_mul(block_size);
218 let size = if lv.size != 0 {
219 lv.size
220 } else {
221 u64::from(first.number_of_blocks).saturating_mul(block_size)
222 };
223 (base, size)
224 } else {
225 (0, lv.size)
226 }
227}
228
229fn build_info(
231 header: &VolumeHeader,
232 context: &EncryptionContext,
233 lv: &LogicalVolumeInfo,
234 size: u64,
235) -> FileVaultInfo {
236 FileVaultInfo {
237 physical_volume_identifier: format_uuid(&header.physical_volume_identifier),
238 pbkdf2_iterations: context.iterations,
239 pbkdf2_salt: context.salt,
240 family_uuid: lv.family_uuid.clone(),
241 lv_identifier: lv.lv_identifier.clone(),
242 lv_name: lv.name.clone(),
243 lv_size: size,
244 encryption_method: "AES-XTS-128",
245 conversion_status: context.conversion_status.clone(),
246 protectors: context.protectors.clone(),
247 }
248}
249
250fn read_exact_or_err<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<(), FileVaultError> {
253 reader.read_exact(buf).map_err(FileVaultError::Io)
254}
255
256fn format_uuid(bytes: &[u8; 16]) -> String {
258 uuid::Uuid::from_bytes(*bytes).hyphenated().to_string()
259}
260
261fn parse_uuid_bytes(text: &str) -> Option<[u8; 16]> {
263 uuid::Uuid::parse_str(text).ok().map(|u| *u.as_bytes())
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use aes_kw::KekAes128;
270 use base64::engine::general_purpose::STANDARD as B64;
271 use base64::Engine;
272 use hmac::Hmac;
273 use pbkdf2::pbkdf2;
274 use sha2::{Digest, Sha256};
275 use std::io::Cursor;
276
277 const BLOCK: usize = 4096;
278 const PASSWORD: &str = "synthetic-pass";
279 const SALT: [u8; 16] = [0xAB; 16];
280 const ITERS: u32 = 1000;
281 const KEK: [u8; 16] = [0x10; 16];
282 const VMK: [u8; 16] = [0x20; 16];
283 const KEY1: [u8; 16] = [0x30; 16]; const PV_ID: [u8; 16] = [0x40; 16]; const FAMILY: [u8; 16] = [
286 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
287 0x10,
288 ];
289 const LV_PHYS_BLOCK: u64 = 16; const LV_BLOCKS: u32 = 4; fn wrap(kek: &[u8; 16], key: &[u8; 16]) -> [u8; 24] {
293 let k = KekAes128::from(*kek);
294 let mut out = [0u8; 24];
295 k.wrap(key, &mut out).unwrap();
296 out
297 }
298
299 fn set_block_type(block: &mut [u8], t: u16) {
300 block[10..12].copy_from_slice(&t.to_le_bytes());
301 }
302
303 fn build_decrypted_metadata() -> Vec<u8> {
306 let passphrase_key = {
307 let mut pk = [0u8; 16];
308 pbkdf2::<Hmac<Sha256>>(PASSWORD.as_bytes(), &SALT, ITERS, &mut pk).unwrap();
309 pk
310 };
311 let wrapped_kek = wrap(&passphrase_key, &KEK);
312 let wrapped_vmk = wrap(&KEK, &VMK);
313
314 let mut pw = vec![0u8; 284];
316 pw[8..24].copy_from_slice(&SALT);
317 pw[32..56].copy_from_slice(&wrapped_kek);
318 pw[168..172].copy_from_slice(&ITERS.to_le_bytes());
319 let mut kekw = vec![0u8; 256];
321 kekw[8..32].copy_from_slice(&wrapped_vmk);
322
323 let plist = format!(
324 "<dict ID=\"0\"><key>CryptoUsers</key><array ID=\"2\"><dict ID=\"3\">\
325 <key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{}</data>\
326 <key>UserType</key><integer size=\"32\" ID=\"5\">0x10000001</integer>\
327 <key>BlockAlgorithm</key><string ID=\"6\">AES-XTS</string>\
328 <key>KEKWrappedVolumeKeyStruct</key><data ID=\"7\">{}</data>\
329 </dict></array>\
330 <key>ConversionStatus</key><string ID=\"8\">Complete</string></dict>",
331 B64.encode(&pw),
332 B64.encode(&kekw),
333 );
334
335 let mut meta = vec![0u8; BLOCK * 3];
336 set_block_type(&mut meta[0..BLOCK], 0x0013);
338 let body = plist.as_bytes();
339 meta[64..64 + body.len()].copy_from_slice(body);
340
341 set_block_type(&mut meta[BLOCK..2 * BLOCK], 0x001a);
343 let fam_uuid = uuid::Uuid::from_bytes(FAMILY).hyphenated().to_string();
344 let lv_plist = format!(
345 "<dict ID=\"0\">\
346 <key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">{fam_uuid}</string>\
347 <key>com.apple.corestorage.lv.name</key><string ID=\"2\">SynthLV</string>\
348 <key>com.apple.corestorage.lv.uuid</key><string ID=\"3\">00000000-0000-0000-0000-000000000001</string>\
349 <key>com.apple.corestorage.lv.size</key><integer size=\"64\" ID=\"4\">0x4000</integer></dict>"
350 );
351 let lb = lv_plist.as_bytes();
352 meta[BLOCK + 64..BLOCK + 64 + lb.len()].copy_from_slice(lb);
353
354 let base = 2 * BLOCK;
356 set_block_type(&mut meta[base..base + BLOCK], 0x0305);
357 let payload = base + 64;
358 meta[payload..payload + 4].copy_from_slice(&1u32.to_le_bytes());
359 let entry = payload + 8;
360 meta[entry + 8..entry + 16].copy_from_slice(&0u64.to_le_bytes()); meta[entry + 16..entry + 20].copy_from_slice(&LV_BLOCKS.to_le_bytes());
362 let phys = LV_PHYS_BLOCK | (0x99u64 << 48);
363 meta[entry + 32..entry + 40].copy_from_slice(&phys.to_le_bytes());
364 meta
365 }
366
367 fn build_image() -> Vec<u8> {
370 let enc_meta_block: u64 = 8;
373 let plaintext_meta_block: u64 = 1;
374
375 let plain_meta = build_decrypted_metadata();
377 let mut enc_meta = plain_meta.clone();
378 crate::xts::encrypt_units(&mut enc_meta, &KEY1, &PV_ID, 8192, 0);
379
380 let meta_size = BLOCK as u32; let mut region = vec![0u8; BLOCK];
383 set_block_type(&mut region, 0x0011);
384 region[64..68].copy_from_slice(&meta_size.to_le_bytes()); let desc_off = 200u32;
387 region[64 + 156..64 + 160].copy_from_slice(&desc_off.to_le_bytes());
388 let d = desc_off as usize;
389 region[d + 8..d + 16]
390 .copy_from_slice(&(plain_meta.len() as u64 / BLOCK as u64).to_le_bytes()); region[d + 32..d + 40].copy_from_slice(&enc_meta_block.to_le_bytes()); let tweak_key = {
395 let mut h = Sha256::new();
396 h.update(VMK);
397 h.update(FAMILY);
398 let dg = h.finalize();
399 let mut tk = [0u8; 16];
400 tk.copy_from_slice(&dg[..16]);
401 tk
402 };
403 let lv_len = (LV_BLOCKS as usize) * BLOCK;
404 let lv_plain: Vec<u8> = (0..lv_len).map(|i| (i & 0xff) as u8).collect();
405 let mut lv_cipher = lv_plain.clone();
406 crate::xts::encrypt_units(&mut lv_cipher, &VMK, &tweak_key, 512, 0);
407
408 let lv_block: u64 = LV_PHYS_BLOCK;
410 let total = (lv_block as usize + LV_BLOCKS as usize) * BLOCK;
411 let mut image = vec![0u8; total];
412
413 image[88] = b'C';
415 image[89] = b'S';
416 image[48..52].copy_from_slice(&512u32.to_le_bytes());
417 image[96..100].copy_from_slice(&(BLOCK as u32).to_le_bytes());
418 image[64..72].copy_from_slice(&(total as u64).to_le_bytes());
419 image[172..176].copy_from_slice(&2u32.to_le_bytes());
420 image[104..112].copy_from_slice(&plaintext_meta_block.to_le_bytes());
421 image[176..192].copy_from_slice(&KEY1);
422 image[304..320].copy_from_slice(&PV_ID);
423
424 let po = plaintext_meta_block as usize * BLOCK;
426 image[po..po + region.len()].copy_from_slice(®ion);
427 let eo = enc_meta_block as usize * BLOCK;
429 image[eo..eo + enc_meta.len()].copy_from_slice(&enc_meta);
430 let lo = lv_block as usize * BLOCK;
432 image[lo..lo + lv_cipher.len()].copy_from_slice(&lv_cipher);
433
434 image
435 }
436
437 #[test]
438 fn unlock_synthetic_volume_end_to_end() {
439 let image = build_image();
440 let mut vol = FileVaultVolume::unlock_with_password(Cursor::new(image), PASSWORD).unwrap();
441 assert_eq!(vol.size(), 0x4000);
442 assert_eq!(vol.info().pbkdf2_iterations, ITERS);
443 assert_eq!(vol.info().lv_name.as_deref(), Some("SynthLV"));
444 assert_eq!(vol.info().encryption_method, "AES-XTS-128");
445 assert_eq!(vol.info().conversion_status.as_deref(), Some("Complete"));
446 assert_eq!(vol.info().protectors.len(), 1);
447
448 let mut buf = [0u8; 512];
450 assert_eq!(vol.read_at(0, &mut buf).unwrap(), 512);
451 let expected: Vec<u8> = (0..512).map(|i| (i & 0xff) as u8).collect();
452 assert_eq!(&buf[..], &expected[..]);
453
454 assert_eq!(vol.seek(SeekFrom::Start(512)).unwrap(), 512);
456 let mut b2 = [0u8; 16];
457 assert!(std::io::Read::read(&mut vol, &mut b2).unwrap() > 0);
458
459 let image2 = build_image();
461 let info = parse_info(Cursor::new(image2)).unwrap();
462 assert_eq!(info.family_uuid, vol.info().family_uuid);
463 assert_eq!(info.lv_size, 0x4000);
464 assert_eq!(
465 info.physical_volume_identifier,
466 uuid::Uuid::from_bytes(PV_ID).hyphenated().to_string()
467 );
468
469 let mut dec = vol.into_decrypted();
471 assert_eq!(dec.size(), 0x4000);
472 let mut b3 = [0u8; 16];
473 assert_eq!(dec.read_at(0, &mut b3).unwrap(), 16);
474 }
475
476 #[test]
477 fn unlock_wrong_password_fails() {
478 let image = build_image();
479 assert!(matches!(
480 FileVaultVolume::unlock_with_password(Cursor::new(image), "nope"),
481 Err(FileVaultError::KeyUnwrap { .. })
482 ));
483 }
484
485 #[test]
486 fn unlock_non_corestorage_fails() {
487 let image = vec![0u8; 4096];
488 assert!(matches!(
489 FileVaultVolume::unlock_with_password(Cursor::new(image), PASSWORD),
490 Err(FileVaultError::NotCoreStorage { .. })
491 ));
492 }
493
494 #[test]
495 fn truncated_image_errors_loudly() {
496 let mut image = vec![0u8; 600];
498 image[88] = b'C';
499 image[89] = b'S';
500 image[96..100].copy_from_slice(&(BLOCK as u32).to_le_bytes());
501 image[172..176].copy_from_slice(&2u32.to_le_bytes());
502 image[104..112].copy_from_slice(&1u64.to_le_bytes());
503 let err = parse_info(Cursor::new(image)).unwrap_err();
504 assert!(matches!(err, FileVaultError::Io(_)));
505 }
506
507 #[test]
508 fn zero_block_size_header_errors() {
509 let mut image = vec![0u8; 4096];
511 image[88] = b'C';
512 image[89] = b'S';
513 image[96..100].copy_from_slice(&0u32.to_le_bytes());
514 image[172..176].copy_from_slice(&2u32.to_le_bytes());
515 let err = parse_info(Cursor::new(image)).unwrap_err();
516 assert!(matches!(err, FileVaultError::OutOfRange { .. }));
517 }
518
519 #[test]
520 fn format_and_parse_uuid_roundtrip() {
521 let s = format_uuid(&FAMILY);
522 assert_eq!(parse_uuid_bytes(&s), Some(FAMILY));
523 assert_eq!(parse_uuid_bytes("not-a-uuid"), None);
524 }
525
526 #[test]
527 fn resolve_extent_uses_segment_blocks_when_lv_size_zero() {
528 let header = VolumeHeader {
530 block_size: BLOCK as u32,
531 bytes_per_sector: 512,
532 physical_volume_size: 0,
533 metadata_block_numbers: [1, 0, 0, 0],
534 key_data: KEY1,
535 physical_volume_identifier: PV_ID,
536 };
537 let mut meta = vec![0u8; BLOCK];
539 meta[10..12].copy_from_slice(&0x0305u16.to_le_bytes());
540 let payload = 64;
541 meta[payload..payload + 4].copy_from_slice(&1u32.to_le_bytes());
542 let entry = payload + 8;
543 meta[entry + 16..entry + 20].copy_from_slice(&2u32.to_le_bytes());
544 meta[entry + 32..entry + 40].copy_from_slice(&5u64.to_le_bytes());
545 let lv = LogicalVolumeInfo {
546 size: 0,
547 family_uuid: "x".to_string(),
548 lv_identifier: None,
549 name: None,
550 };
551 let (base, size) = resolve_lv_extent(&meta, &header, &lv);
552 assert_eq!(base, 5 * BLOCK as u64);
553 assert_eq!(size, 2 * BLOCK as u64);
554 }
555
556 #[test]
557 fn resolve_extent_no_segment_falls_back_to_lv_size() {
558 let header = VolumeHeader {
559 block_size: BLOCK as u32,
560 bytes_per_sector: 512,
561 physical_volume_size: 0,
562 metadata_block_numbers: [1, 0, 0, 0],
563 key_data: KEY1,
564 physical_volume_identifier: PV_ID,
565 };
566 let meta = vec![0u8; BLOCK]; let lv = LogicalVolumeInfo {
568 size: 12345,
569 family_uuid: "x".to_string(),
570 lv_identifier: None,
571 name: None,
572 };
573 let (base, size) = resolve_lv_extent(&meta, &header, &lv);
574 assert_eq!(base, 0);
575 assert_eq!(size, 12345);
576 }
577}