1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum KeybagTag {
24 Unknown = 0x00,
25 WrappingKey = 0x01,
26 VolumeKey = 0x02,
27 VolumeUnlockRecords = 0x03,
28 VolumePassphraseHint = 0x04,
29 UserPayload = 0xf8,
30}
31
32impl KeybagTag {
33 #[must_use]
35 pub fn from_u16(tag: u16) -> Self {
36 match tag {
37 0x01 => Self::WrappingKey,
38 0x02 => Self::VolumeKey,
39 0x03 => Self::VolumeUnlockRecords,
40 0x04 => Self::VolumePassphraseHint,
41 0xf8 => Self::UserPayload,
42 _ => Self::Unknown,
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub struct EncryptionState {
51 pub encrypted: bool,
52 pub tags_present: Vec<KeybagTag>,
53 pub has_passphrase_hint: bool,
54 pub unknown_tags: Vec<(u16, u64)>,
58}
59
60const OBJ_PHYS_LEN: usize = 32;
70const KL_NKEYS: usize = OBJ_PHYS_LEN + 2; const KL_ENTRIES_OFF: usize = OBJ_PHYS_LEN + 16; const KE_TAG: usize = 16; const KE_KEYLEN: usize = 18; const KE_HEADER_LEN: usize = 24; const MAX_KEYBAG_ENTRIES: usize = 4096;
77
78pub fn read_keybag(data: &[u8]) -> crate::Result<EncryptionState> {
87 let nkeys = (crate::bytes::le_u16(data, KL_NKEYS) as usize).min(MAX_KEYBAG_ENTRIES);
88 let mut tags_present = Vec::new();
89 let mut unknown_tags = Vec::new();
90 let mut off = KL_ENTRIES_OFF;
91 for _ in 0..nkeys {
92 if off + KE_HEADER_LEN > data.len() {
94 break;
95 }
96 let raw_tag = crate::bytes::le_u16(data, off + KE_TAG);
97 let tag = KeybagTag::from_u16(raw_tag);
98 let keylen = crate::bytes::le_u16(data, off + KE_KEYLEN) as usize;
99 if tag == KeybagTag::Unknown {
100 unknown_tags.push((raw_tag, off as u64));
101 }
102 if !tags_present.contains(&tag) {
103 tags_present.push(tag);
104 }
105 let entry_len = (KE_HEADER_LEN + keylen + 15) & !15;
107 off += entry_len.max(16);
108 }
109 let has_passphrase_hint = tags_present.contains(&KeybagTag::VolumePassphraseHint);
110 let encrypted = tags_present.iter().any(|t| {
113 matches!(
114 t,
115 KeybagTag::WrappingKey | KeybagTag::VolumeKey | KeybagTag::VolumeUnlockRecords
116 )
117 });
118 Ok(EncryptionState {
119 encrypted,
120 tags_present,
121 has_passphrase_hint,
122 unknown_tags,
123 })
124}
125
126#[derive(Debug, Clone)]
129#[non_exhaustive]
130pub struct WrappedKek {
131 pub wrapped_key: Vec<u8>,
134 pub iterations: u32,
136 pub salt: Vec<u8>,
138}
139
140pub fn parse_wrapped_kek(data: &[u8]) -> crate::Result<WrappedKek> {
156 const TAG_WRAPPED_KEK: u8 = 0x83;
157 const TAG_ITERATIONS: u8 = 0x84;
158 const TAG_SALT: u8 = 0x85;
159 const WRAPPED_KEK_LEN: usize = 40;
161 const SALT_LEN: usize = 16;
162
163 let bad = |field: &'static str, value: u64, cap: u64| crate::ApfsError::FieldOutOfRange {
167 structure: "wrapped_kek_object",
168 field,
169 value,
170 cap,
171 };
172
173 let mut wrapped_key: Option<Vec<u8>> = None;
174 let mut iterations: Option<u32> = None;
175 let mut salt: Option<Vec<u8>> = None;
176
177 let mut off = 0usize;
178 while off < data.len() {
179 let tag = *data
180 .get(off)
181 .ok_or_else(|| bad("tag", off as u64, data.len() as u64))?;
182 off += 1;
183 let first = *data
184 .get(off)
185 .ok_or_else(|| bad("length", off as u64, data.len() as u64))?;
186 off += 1;
187
188 let len = if first & 0x80 == 0 {
189 first as usize
190 } else if first == 0x81 {
191 let b = *data
192 .get(off)
193 .ok_or_else(|| bad("length_1byte", off as u64, data.len() as u64))?;
194 off += 1;
195 b as usize
196 } else if first == 0x82 {
197 let hi = *data
198 .get(off)
199 .ok_or_else(|| bad("length_2byte", off as u64, data.len() as u64))?;
200 let lo = *data
201 .get(off + 1)
202 .ok_or_else(|| bad("length_2byte", off as u64, data.len() as u64))?;
203 off += 2;
204 ((hi as usize) << 8) | lo as usize
205 } else {
206 return Err(bad("ber_length_form", u64::from(first), 0x82));
207 };
208
209 let end = off
210 .checked_add(len)
211 .ok_or_else(|| bad("value_length", len as u64, data.len() as u64))?;
212 let value = data
213 .get(off..end)
214 .ok_or_else(|| bad("value_end", end as u64, data.len() as u64))?;
215 off = end;
216
217 match tag {
218 TAG_WRAPPED_KEK => {
219 if value.len() != WRAPPED_KEK_LEN {
220 return Err(bad(
221 "wrapped_kek_len",
222 value.len() as u64,
223 WRAPPED_KEK_LEN as u64,
224 ));
225 }
226 wrapped_key = Some(value.to_vec());
227 }
228 TAG_ITERATIONS => {
229 if value.is_empty() || value.len() > 8 {
230 return Err(bad("iterations_len", value.len() as u64, 8));
231 }
232 let mut n: u64 = 0;
235 for b in value {
236 n = (n << 8) | u64::from(*b);
237 }
238 iterations = Some(u32::try_from(n).unwrap_or(u32::MAX));
239 }
240 TAG_SALT => {
241 if value.len() != SALT_LEN {
242 return Err(bad("salt_len", value.len() as u64, SALT_LEN as u64));
243 }
244 salt = Some(value.to_vec());
245 }
246 _ => {}
247 }
248 }
249
250 Ok(WrappedKek {
251 wrapped_key: wrapped_key.ok_or_else(|| bad("wrapped_kek_tag_0x83", 0, 1))?,
252 iterations: iterations.ok_or_else(|| bad("iterations_tag_0x84", 0, 1))?,
253 salt: salt.ok_or_else(|| bad("salt_tag_0x85", 0, 1))?,
254 })
255}
256
257pub fn aes_key_unwrap(kek: &[u8; 32], wrapped: &[u8; 40]) -> crate::Result<Vec<u8>> {
267 use aes_kw::KekAes256;
268 let kek = KekAes256::from(*kek);
269 kek.unwrap_vec(wrapped)
270 .map_err(|_| crate::ApfsError::FieldOutOfRange {
271 structure: "aes_key_wrap",
272 field: "integrity_check",
273 value: 0,
274 cap: 1,
275 })
276}
277
278#[must_use]
280pub fn derive_key_from_password(
281 password: &[u8],
282 salt: &[u8],
283 iterations: u32,
284 out_len: usize,
285) -> Vec<u8> {
286 use hmac::Hmac;
287 use sha2::Sha256;
288
289 let mut out = vec![0u8; out_len];
290 let iters = iterations.max(1);
293 if pbkdf2::pbkdf2::<Hmac<Sha256>>(password, salt, iters, &mut out).is_err() {
298 return Vec::new();
299 }
300 out
301}
302
303fn ber_find(data: &[u8], want_tag: u8) -> Option<&[u8]> {
308 let mut off = 0usize;
309 while off + 2 <= data.len() {
310 let tag = *data.get(off)?;
311 let first = *data.get(off + 1)?;
312 off += 2;
313 let len = if first & 0x80 == 0 {
314 first as usize
315 } else if first == 0x81 {
316 let b = *data.get(off)?;
317 off += 1;
318 b as usize
319 } else if first == 0x82 {
320 let hi = *data.get(off)?;
321 let lo = *data.get(off + 1)?;
322 off += 2;
323 ((hi as usize) << 8) | lo as usize
324 } else {
325 return None;
326 };
327 let end = off.checked_add(len)?;
328 let value = data.get(off..end)?;
329 if tag == want_tag {
330 return Some(value);
331 }
332 off = end;
333 }
334 None
335}
336
337const NX_BLOCK_SIZE: usize = 0x024;
339const NX_CONTAINER_UUID: usize = 0x048;
340const NX_KEYBAG_BLOCK: usize = 0x510;
341const NX_KEYBAG_BLOCKS: usize = 0x518;
342const KEYBAG_SECTOR: usize = 512;
344
345pub fn decrypt_container_keybag(image: &[u8]) -> crate::Result<Vec<u8>> {
362 let uuid: [u8; 16] = image
363 .get(NX_CONTAINER_UUID..NX_CONTAINER_UUID + 16)
364 .and_then(|s| s.try_into().ok())
365 .ok_or(crate::ApfsError::FieldOutOfRange {
366 structure: "nx_superblock",
367 field: "container_uuid",
368 value: image.len() as u64,
369 cap: (NX_CONTAINER_UUID + 16) as u64,
370 })?;
371 decrypt_container_keybag_with_uuid(image, &uuid)
372}
373
374pub fn decrypt_container_keybag_with_uuid(image: &[u8], uuid: &[u8; 16]) -> crate::Result<Vec<u8>> {
380 let bad = |field: &'static str, value: u64, cap: u64| crate::ApfsError::FieldOutOfRange {
381 structure: "nx_keylocker",
382 field,
383 value,
384 cap,
385 };
386
387 if image.get(32..36) != Some(b"NXSB") {
388 return Err(bad("magic", 0, 1));
389 }
390 let block_size = crate::bytes::le_u32(image, NX_BLOCK_SIZE) as usize;
391 if block_size == 0 || block_size % KEYBAG_SECTOR != 0 {
392 return Err(bad("block_size", block_size as u64, KEYBAG_SECTOR as u64));
393 }
394 let paddr = crate::bytes::le_u64(image, NX_KEYBAG_BLOCK) as usize;
395 let blocks = crate::bytes::le_u64(image, NX_KEYBAG_BLOCKS) as usize;
396 if paddr == 0 || blocks == 0 {
397 return Err(bad("keylocker_extent", paddr as u64, blocks as u64));
398 }
399
400 let start = paddr
401 .checked_mul(block_size)
402 .ok_or_else(|| bad("keybag_offset", paddr as u64, block_size as u64))?;
403 let len = blocks
404 .checked_mul(block_size)
405 .ok_or_else(|| bad("keybag_length", blocks as u64, block_size as u64))?;
406 let end = start
407 .checked_add(len)
408 .ok_or_else(|| bad("keybag_end", start as u64, len as u64))?;
409 let ct = image
410 .get(start..end)
411 .ok_or_else(|| bad("keybag_past_image", end as u64, image.len() as u64))?;
412
413 Ok(decrypt_keybag_area(ct, uuid, start / KEYBAG_SECTOR))
418}
419
420#[derive(Debug, Clone)]
422#[non_exhaustive]
423pub struct VolumeRecords {
424 pub wrapped_vek: Vec<u8>,
426 pub volume_keybag_block: u64,
428 pub volume_keybag_blocks: u64,
430}
431
432pub fn volume_records(keybag: &[u8], volume_uuid: &[u8; 16]) -> crate::Result<VolumeRecords> {
443 const WRAPPED_VEK_LEN: usize = 40;
445 const PRANGE_LEN: usize = 16;
447 const TAG_SEQUENCE: u8 = 0x30;
449 const TAG_NESTED_OBJECT: u8 = 0xa3;
451 const TAG_WRAPPED_KEY: u8 = 0x83;
453
454 let bad = |field: &'static str, value: u64, cap: u64| crate::ApfsError::FieldOutOfRange {
455 structure: "container_keybag",
456 field,
457 value,
458 cap,
459 };
460
461 let nkeys = (crate::bytes::le_u16(keybag, KL_NKEYS) as usize).min(MAX_KEYBAG_ENTRIES);
462 let mut wrapped_vek: Option<Vec<u8>> = None;
463 let mut extent: Option<(u64, u64)> = None;
464
465 let mut off = KL_ENTRIES_OFF;
466 for _ in 0..nkeys {
467 if off + KE_HEADER_LEN > keybag.len() {
468 break;
469 }
470 let entry_uuid = keybag.get(off..off + 16).unwrap_or(&[]);
471 let raw_tag = crate::bytes::le_u16(keybag, off + KE_TAG);
472 let keylen = crate::bytes::le_u16(keybag, off + KE_KEYLEN) as usize;
473 let data_off = off + KE_HEADER_LEN;
474 let data = keybag.get(data_off..data_off + keylen).unwrap_or(&[]);
475
476 if entry_uuid == volume_uuid.as_slice() {
477 match KeybagTag::from_u16(raw_tag) {
478 KeybagTag::VolumeKey => {
479 let seq = ber_find(data, TAG_SEQUENCE).unwrap_or(data);
485 let obj = ber_find(seq, TAG_NESTED_OBJECT)
486 .ok_or_else(|| bad("volume_key_missing_0xa3", data.len() as u64, 0xa3))?;
487 let inner = ber_find(obj, TAG_WRAPPED_KEY)
488 .ok_or_else(|| bad("volume_key_missing_0x83", obj.len() as u64, 0x83))?;
489 if inner.len() != WRAPPED_VEK_LEN {
490 return Err(bad(
491 "wrapped_vek_len",
492 inner.len() as u64,
493 WRAPPED_VEK_LEN as u64,
494 ));
495 }
496 wrapped_vek = Some(inner.to_vec());
497 }
498 KeybagTag::VolumeUnlockRecords => {
499 if data.len() < PRANGE_LEN {
500 return Err(bad(
501 "unlock_records_len",
502 data.len() as u64,
503 PRANGE_LEN as u64,
504 ));
505 }
506 extent = Some((crate::bytes::le_u64(data, 0), crate::bytes::le_u64(data, 8)));
507 }
508 _ => {}
509 }
510 }
511 off += ((KE_HEADER_LEN + keylen + 15) & !15).max(16);
512 }
513
514 let (blk, cnt) = extent.ok_or_else(|| bad("volume_unlock_records_absent", 0, 1))?;
515 Ok(VolumeRecords {
516 wrapped_vek: wrapped_vek.ok_or_else(|| bad("volume_key_absent", 0, 1))?,
517 volume_keybag_block: blk,
518 volume_keybag_blocks: cnt,
519 })
520}
521
522#[derive(Debug, Clone)]
524#[non_exhaustive]
525pub struct UnlockedVolume {
526 pub vek: Vec<u8>,
528}
529
530pub fn unlock_volume(
547 image: &[u8],
548 volume_uuid: &[u8; 16],
549 password: &str,
550) -> crate::Result<UnlockedVolume> {
551 let bad = |field: &'static str, value: u64, cap: u64| crate::ApfsError::FieldOutOfRange {
552 structure: "unlock_volume",
553 field,
554 value,
555 cap,
556 };
557
558 let container_kb = decrypt_container_keybag(image)?;
559 let rec = volume_records(&container_kb, volume_uuid)?;
560
561 let block_size = crate::bytes::le_u32(image, NX_BLOCK_SIZE) as usize;
562 let start = (rec.volume_keybag_block as usize)
563 .checked_mul(block_size)
564 .ok_or_else(|| bad("vkb_offset", rec.volume_keybag_block, block_size as u64))?;
565 let len = (rec.volume_keybag_blocks as usize)
566 .checked_mul(block_size)
567 .ok_or_else(|| bad("vkb_length", rec.volume_keybag_blocks, block_size as u64))?;
568 let end = start
569 .checked_add(len)
570 .ok_or_else(|| bad("vkb_end", start as u64, len as u64))?;
571 let ct = image
572 .get(start..end)
573 .ok_or_else(|| bad("vkb_past_image", end as u64, image.len() as u64))?;
574
575 let volume_kb = decrypt_keybag_area(ct, volume_uuid, start / KEYBAG_SECTOR);
577
578 unlock_with_volume_keybag(&volume_kb, &rec, password)
579}
580
581pub fn unlock_with_volume_keybag(
593 volume_keybag: &[u8],
594 records: &VolumeRecords,
595 password: &str,
596) -> crate::Result<UnlockedVolume> {
597 let bad = |field: &'static str, value: u64, cap: u64| crate::ApfsError::FieldOutOfRange {
598 structure: "unlock_with_volume_keybag",
599 field,
600 value,
601 cap,
602 };
603
604 let obj = find_kek_object(volume_keybag)
605 .ok_or_else(|| bad("kek_object_absent", volume_keybag.len() as u64, 1))?;
606 let kek_info = parse_wrapped_kek(obj)?;
607
608 let derived_v =
609 derive_key_from_password(password.as_bytes(), &kek_info.salt, kek_info.iterations, 32);
610 let derived: [u8; 32] = derived_v
611 .as_slice()
612 .try_into()
613 .map_err(|_| bad("derived_key_len", derived_v.len() as u64, 32))?;
614 let wrapped_kek: [u8; 40] = kek_info
615 .wrapped_key
616 .as_slice()
617 .try_into()
618 .map_err(|_| bad("wrapped_kek_len", kek_info.wrapped_key.len() as u64, 40))?;
619
620 let kek_v = aes_key_unwrap(&derived, &wrapped_kek)?;
623 let kek: [u8; 32] = kek_v
624 .as_slice()
625 .try_into()
626 .map_err(|_| bad("kek_len", kek_v.len() as u64, 32))?;
627 let wrapped_vek: [u8; 40] = records
628 .wrapped_vek
629 .as_slice()
630 .try_into()
631 .map_err(|_| bad("wrapped_vek_len", records.wrapped_vek.len() as u64, 40))?;
632
633 Ok(UnlockedVolume {
634 vek: aes_key_unwrap(&kek, &wrapped_vek)?,
635 })
636}
637
638pub const OMAP_VAL_ENCRYPTED: u32 = 0x0000_0004;
641
642pub const APFS_CRYPTO_SECTOR: usize = 0x200;
644
645pub fn decrypt_volume_area(data: &mut [u8], vek: &[u8; 32], tweak_block: u64, block_size: usize) {
662 if tweak_block == 0 || block_size < APFS_CRYPTO_SECTOR {
663 return;
664 }
665 use aes::cipher::KeyInit;
666 use xts_mode::{get_tweak_default, Xts128};
667
668 let (k1, k2) = vek.split_at(16);
669 let (Ok(k1), Ok(k2)) = (<&[u8; 16]>::try_from(k1), <&[u8; 16]>::try_from(k2)) else {
670 return; };
672 let xts = Xts128::new(aes::Aes128::new(k1.into()), aes::Aes128::new(k2.into()));
673
674 let sectors_per_block = (block_size / APFS_CRYPTO_SECTOR) as u64;
677 let first_sector = tweak_block.saturating_mul(sectors_per_block);
678 xts.decrypt_area(
679 data,
680 APFS_CRYPTO_SECTOR,
681 u128::from(first_sector),
682 get_tweak_default,
683 );
684}
685
686pub fn decrypt_keybag_area(ct: &[u8], uuid: &[u8; 16], first_sector: usize) -> Vec<u8> {
700 xts_decrypt_area(ct, uuid, first_sector)
701}
702
703fn xts_decrypt_area(ct: &[u8], uuid: &[u8; 16], first_sector: usize) -> Vec<u8> {
705 use aes::cipher::KeyInit;
706 use xts_mode::{get_tweak_default, Xts128};
707 let xts = Xts128::new(aes::Aes128::new(uuid.into()), aes::Aes128::new(uuid.into()));
708 let mut buf = ct.to_vec();
709 xts.decrypt_area(
710 &mut buf,
711 KEYBAG_SECTOR,
712 first_sector as u128,
713 get_tweak_default,
714 );
715 buf
716}
717
718fn find_kek_object(keybag: &[u8]) -> Option<&[u8]> {
724 let nkeys = (crate::bytes::le_u16(keybag, KL_NKEYS) as usize).min(MAX_KEYBAG_ENTRIES);
725 let mut off = KL_ENTRIES_OFF;
726 for _ in 0..nkeys {
727 if off + KE_HEADER_LEN > keybag.len() {
728 break;
729 }
730 let raw_tag = crate::bytes::le_u16(keybag, off + KE_TAG);
731 let keylen = crate::bytes::le_u16(keybag, off + KE_KEYLEN) as usize;
732 let data_off = off + KE_HEADER_LEN;
733 let tag = KeybagTag::from_u16(raw_tag);
734 if tag == KeybagTag::WrappingKey || tag == KeybagTag::VolumeUnlockRecords {
735 if let Some(data) = keybag.get(data_off..data_off + keylen) {
736 let seq = ber_find(data, 0x30).unwrap_or(data);
737 if let Some(obj) = ber_find(seq, 0xa3) {
738 return Some(obj);
739 }
740 }
741 }
742 off += ((KE_HEADER_LEN + keylen + 15) & !15).max(16);
743 }
744 None
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 fn keybag(entries: &[(u16, usize)]) -> Vec<u8> {
756 let mut data = vec![0u8; OBJ_PHYS_LEN + 16];
761 data[24..28].copy_from_slice(b"keys"); data[OBJ_PHYS_LEN..OBJ_PHYS_LEN + 2].copy_from_slice(&1u16.to_le_bytes()); data[OBJ_PHYS_LEN + 2..OBJ_PHYS_LEN + 4]
764 .copy_from_slice(&(entries.len() as u16).to_le_bytes()); for &(tag, keylen) in entries {
766 let mut e = vec![0u8; 24 + keylen];
767 e[16..18].copy_from_slice(&tag.to_le_bytes()); e[18..20].copy_from_slice(&(keylen as u16).to_le_bytes()); let padded = (e.len() + 15) & !15; e.resize(padded, 0);
771 data.extend_from_slice(&e);
772 }
773 let nbytes = data.len() as u32;
774 data[4..8].copy_from_slice(&nbytes.to_le_bytes()); data
776 }
777
778 #[test]
779 fn reads_volume_key_and_hint_tags() {
780 let kb = keybag(&[(0x02, 32), (0x04, 8)]);
782 let st = read_keybag(&kb).expect("parse keybag");
783 assert!(st.encrypted, "a keybag with a volume key is encrypted");
784 assert!(st.tags_present.contains(&KeybagTag::VolumeKey));
785 assert!(st.tags_present.contains(&KeybagTag::VolumePassphraseHint));
786 assert!(st.has_passphrase_hint);
787 }
788
789 #[test]
790 fn from_u16_maps_every_known_tag() {
791 assert_eq!(KeybagTag::from_u16(0x01), KeybagTag::WrappingKey);
794 assert_eq!(KeybagTag::from_u16(0x02), KeybagTag::VolumeKey);
795 assert_eq!(KeybagTag::from_u16(0x03), KeybagTag::VolumeUnlockRecords);
796 assert_eq!(KeybagTag::from_u16(0x04), KeybagTag::VolumePassphraseHint);
797 assert_eq!(KeybagTag::from_u16(0xf8), KeybagTag::UserPayload);
798 assert_eq!(KeybagTag::from_u16(0x99), KeybagTag::Unknown);
799 }
800
801 #[test]
802 fn wrapping_key_and_unlock_records_are_encrypted() {
803 let kb = keybag(&[(0x01, 32), (0x03, 16)]);
806 let st = read_keybag(&kb).expect("parse keybag");
807 assert!(st.encrypted, "wrapping-key / unlock-records ⇒ encrypted");
808 assert!(st.tags_present.contains(&KeybagTag::WrappingKey));
809 assert!(st.tags_present.contains(&KeybagTag::VolumeUnlockRecords));
810 }
811
812 #[test]
813 fn empty_keybag_reports_not_encrypted() {
814 let kb = keybag(&[]);
815 let st = read_keybag(&kb).expect("parse empty keybag");
816 assert!(!st.encrypted);
817 assert!(st.tags_present.is_empty());
818 assert!(!st.has_passphrase_hint);
819 }
820
821 #[test]
822 fn unknown_tag_maps_to_unknown_and_records_raw_value() {
823 let kb = keybag(&[(0x55, 4)]);
826 let st = read_keybag(&kb).expect("parse keybag");
827 assert!(st.tags_present.contains(&KeybagTag::Unknown));
828 assert_eq!(st.unknown_tags, vec![(0x55u16, KL_ENTRIES_OFF as u64)]);
832 }
833
834 #[test]
835 fn header_claiming_more_entries_than_the_blob_stops_early() {
836 let mut data = vec![0u8; 24];
840 data[0..2].copy_from_slice(&1u16.to_le_bytes()); data[2..4].copy_from_slice(&4u16.to_le_bytes()); let st = read_keybag(&data).expect("parse truncated keybag");
843 assert!(st.tags_present.is_empty(), "no entry fits → nothing parsed");
844 }
845
846 #[test]
854 fn wrapped_kek_object_yields_salt_iterations_and_wrapped_key() {
855 let mut blob = Vec::new();
856 blob.push(0x82u8);
858 blob.push(8u8);
859 blob.extend_from_slice(&[2, 0, 0, 0, 0, 0, 0, 0]);
860 blob.push(0x83u8);
862 blob.push(0x81u8);
863 blob.push(40u8);
864 let wrapped: Vec<u8> = (0..40u8)
865 .map(|i| i.wrapping_mul(7).wrapping_add(3))
866 .collect();
867 blob.extend_from_slice(&wrapped);
868 blob.push(0x84u8);
870 blob.push(3u8);
871 blob.extend_from_slice(&[0x01, 0xE8, 0x48]); blob.push(0x85u8);
874 blob.push(16u8);
875 let salt: Vec<u8> = (0..16u8).map(|i| i ^ 0x5A).collect();
876 blob.extend_from_slice(&salt);
877
878 let kek = parse_wrapped_kek(&blob).expect("a well-formed wrapped-KEK object must parse");
879 assert_eq!(
880 kek.wrapped_key.as_slice(),
881 wrapped.as_slice(),
882 "wrapped KEK bytes"
883 );
884 assert_eq!(kek.iterations, 125_000, "PBKDF2 iteration count");
885 assert_eq!(kek.salt.as_slice(), salt.as_slice(), "PBKDF2 salt");
886 }
887
888 #[test]
898 fn wrapped_kek_object_rejects_a_wrong_sized_wrapped_key() {
899 let mut blob = Vec::new();
900 blob.push(0x83u8);
901 blob.push(32u8); blob.extend_from_slice(&[0u8; 32]);
903 blob.push(0x84u8);
904 blob.push(2u8);
905 blob.extend_from_slice(&[0x27, 0x10]); blob.push(0x85u8);
907 blob.push(16u8);
908 blob.extend_from_slice(&[0u8; 16]); let err = parse_wrapped_kek(&blob)
911 .expect_err("a 32-byte wrapped key must be rejected; only 40 is valid");
912 let msg = format!("{err}");
914 assert!(
915 msg.contains("wrapped_kek_len"),
916 "must be rejected for the wrapped-key SIZE, got: {msg}"
917 );
918 }
919
920 #[test]
925 fn aes_key_unwrap_matches_rfc3394_vector() {
926 let kek: [u8; 32] = [
927 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
928 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B,
929 0x1C, 0x1D, 0x1E, 0x1F,
930 ];
931 let wrapped: [u8; 40] = [
932 0x28, 0xC9, 0xF4, 0x04, 0xC4, 0xB8, 0x10, 0xF4, 0xCB, 0xCC, 0xB3, 0x5C, 0xFB, 0x87,
933 0xF8, 0x26, 0x3F, 0x57, 0x86, 0xE2, 0xD8, 0x0E, 0xD3, 0x26, 0xCB, 0xC7, 0xF0, 0xE7,
934 0x1A, 0x99, 0xF4, 0x3B, 0xFB, 0x98, 0x8B, 0x9B, 0x7A, 0x02, 0xDD, 0x21,
935 ];
936 let expected: [u8; 32] = [
937 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
938 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
939 0x0C, 0x0D, 0x0E, 0x0F,
940 ];
941 let got = aes_key_unwrap(&kek, &wrapped).expect("RFC 3394 vector must unwrap");
942 assert_eq!(
943 got.as_slice(),
944 expected.as_slice(),
945 "RFC 3394 s4.6 unwrapped key"
946 );
947 }
948
949 #[test]
953 fn aes_key_unwrap_rejects_a_wrong_kek() {
954 let wrong = [0xAAu8; 32];
955 let wrapped: [u8; 40] = [
956 0x28, 0xC9, 0xF4, 0x04, 0xC4, 0xB8, 0x10, 0xF4, 0xCB, 0xCC, 0xB3, 0x5C, 0xFB, 0x87,
957 0xF8, 0x26, 0x3F, 0x57, 0x86, 0xE2, 0xD8, 0x0E, 0xD3, 0x26, 0xCB, 0xC7, 0xF0, 0xE7,
958 0x1A, 0x99, 0xF4, 0x3B, 0xFB, 0x98, 0x8B, 0x9B, 0x7A, 0x02, 0xDD, 0x21,
959 ];
960 assert!(
961 aes_key_unwrap(&wrong, &wrapped).is_err(),
962 "a wrong KEK must fail the AES-KW integrity check, never return bytes"
963 );
964 }
965
966 #[test]
974 fn pbkdf2_sha256_matches_rfc7914_vector() {
975 let got = derive_key_from_password(b"Password", b"NaCl", 80_000, 64);
976 let expected: [u8; 64] = [
977 0x4D, 0xDC, 0xD8, 0xF6, 0x0B, 0x98, 0xBE, 0x21, 0x83, 0x0C, 0xEE, 0x5E, 0xF2, 0x27,
978 0x01, 0xF9, 0x64, 0x1A, 0x44, 0x18, 0xD0, 0x4C, 0x04, 0x14, 0xAE, 0xFF, 0x08, 0x87,
979 0x6B, 0x34, 0xAB, 0x56, 0xA1, 0xD4, 0x25, 0xA1, 0x22, 0x58, 0x33, 0x54, 0x9A, 0xDB,
980 0x84, 0x1B, 0x51, 0xC9, 0xB3, 0x17, 0x6A, 0x27, 0x2B, 0xDE, 0xBB, 0xA1, 0xD0, 0x78,
981 0x47, 0x8F, 0x62, 0xB3, 0x97, 0xF3, 0x3C, 0x8D,
982 ];
983 assert_eq!(
984 got.as_slice(),
985 expected.as_slice(),
986 "RFC 7914 s11 PBKDF2-SHA256 c=80000"
987 );
988 }
989
990 #[cfg(test)]
993 fn fixture_image() -> Vec<u8> {
994 use std::io::Read as _;
995 let gz = include_bytes!("../../tests/data/filevault/apfs-native-filevault.raw.gz");
996 let mut out = Vec::new();
997 flate2::read::GzDecoder::new(&gz[..])
998 .read_to_end(&mut out)
999 .expect("fixture must decompress");
1000 out
1001 }
1002
1003 #[test]
1010 fn container_keybag_decrypts_to_a_parseable_keybag() {
1011 let img = fixture_image();
1012 let sb = &img[..4096];
1013 assert_eq!(
1014 &sb[32..36],
1015 b"NXSB",
1016 "fixture must start with a container superblock"
1017 );
1018
1019 let kb = decrypt_container_keybag(&img).expect("container keybag must decrypt");
1020 let state = read_keybag(&kb).expect("decrypted keybag must parse");
1021
1022 assert!(
1024 state.tags_present.contains(&KeybagTag::VolumeKey)
1025 || state.tags_present.contains(&KeybagTag::VolumeUnlockRecords),
1026 "decrypted container keybag must carry VolumeKey or VolumeUnlockRecords, got {:?}",
1027 state.tags_present
1028 );
1029 }
1030
1031 #[test]
1035 fn container_keybag_with_a_wrong_uuid_does_not_parse_as_a_keybag() {
1036 let img = fixture_image();
1037 let kb = decrypt_container_keybag_with_uuid(&img, &[0xAA; 16])
1038 .expect("decryption itself should run; only the RESULT must be wrong");
1039 let parsed = read_keybag(&kb);
1040 let bogus = match parsed {
1041 Err(_) => true,
1042 Ok(s) => {
1043 !s.tags_present.contains(&KeybagTag::VolumeKey)
1044 && !s.tags_present.contains(&KeybagTag::VolumeUnlockRecords)
1045 }
1046 };
1047 assert!(
1048 bogus,
1049 "a wrong UUID must not produce a keybag carrying real volume tags"
1050 );
1051 }
1052
1053 #[cfg(test)]
1056 const FIXTURE_VOLUME_UUID: [u8; 16] = [
1057 0x75, 0xAA, 0xF7, 0xF9, 0xBF, 0x5A, 0x4A, 0x4F, 0x86, 0xC0, 0x3E, 0xF9, 0x09, 0x36, 0xBD,
1058 0x7C,
1059 ];
1060
1061 #[test]
1070 fn container_keybag_yields_this_volumes_wrapped_vek_and_keybag_extent() {
1071 let img = fixture_image();
1072 let kb = decrypt_container_keybag(&img).expect("container keybag must decrypt");
1073 let rec = volume_records(&kb, &FIXTURE_VOLUME_UUID)
1074 .expect("the fixture's volume must be present in its container keybag");
1075
1076 assert_eq!(
1077 rec.wrapped_vek.len(),
1078 40,
1079 "wrapped VEK must be RFC 3394 output for a 256-bit key"
1080 );
1081 assert!(
1082 rec.volume_keybag_block > 0,
1083 "volume keybag block must be set"
1084 );
1085 assert!(
1086 rec.volume_keybag_blocks > 0,
1087 "volume keybag length must be non-zero"
1088 );
1089 let end = (rec.volume_keybag_block as usize + rec.volume_keybag_blocks as usize) * 4096;
1090 assert!(
1091 end <= img.len(),
1092 "volume keybag extent must lie inside the container"
1093 );
1094 }
1095
1096 #[test]
1100 fn container_keybag_refuses_an_unknown_volume_uuid() {
1101 let img = fixture_image();
1102 let kb = decrypt_container_keybag(&img).expect("container keybag must decrypt");
1103 assert!(
1104 volume_records(&kb, &[0x11; 16]).is_err(),
1105 "an absent volume UUID must be an error, never another volume's keys"
1106 );
1107 }
1108
1109 #[test]
1114 fn unlock_volume_recovers_the_vek_with_the_correct_password() {
1115 let img = fixture_image();
1116 let u = unlock_volume(&img, &FIXTURE_VOLUME_UUID, "apfs-FV-TEST-2026")
1117 .expect("the fixture must unlock with its recorded password");
1118 assert_eq!(u.vek.len(), 32, "VEK is two AES-128-XTS keys, 32 bytes");
1119 assert!(u.vek.iter().any(|&b| b != 0), "VEK must not be all zeros");
1120 }
1121
1122 #[test]
1126 fn unlock_volume_refuses_a_wrong_password() {
1127 let img = fixture_image();
1128 assert!(
1129 unlock_volume(&img, &FIXTURE_VOLUME_UUID, "not-the-password").is_err(),
1130 "a wrong password must fail the integrity check, never return a key"
1131 );
1132 }
1133
1134 #[test]
1144 fn keybag_area_decrypts_identically_from_a_ranged_read() {
1145 let img = fixture_image();
1146
1147 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1149 let paddr = crate::bytes::le_u64(&img, NX_KEYBAG_BLOCK) as usize;
1150 let blocks = crate::bytes::le_u64(&img, NX_KEYBAG_BLOCKS) as usize;
1151 let uuid: [u8; 16] = img[NX_CONTAINER_UUID..NX_CONTAINER_UUID + 16]
1152 .try_into()
1153 .expect("16 bytes");
1154
1155 let start = paddr * block_size;
1156 let end = start + blocks * block_size;
1157
1158 let ranged = decrypt_keybag_area(&img[start..end], &uuid, start / KEYBAG_SECTOR);
1160 let whole = decrypt_container_keybag(&img).expect("whole-image path must decrypt");
1161
1162 assert_eq!(
1163 ranged, whole,
1164 "a ranged read must decrypt to the same bytes as the whole-image path"
1165 );
1166 assert_eq!(
1170 ranged.get(24..28),
1171 Some(&b"syek"[..]),
1172 "and those bytes must be a container keybag, not noise"
1173 );
1174 }
1175
1176 #[cfg(test)]
1182 fn ranged_unlock(img: &[u8], volume_uuid: &[u8; 16], password: &str) -> (Vec<u8>, usize) {
1183 let mut bytes_read = 0usize;
1184
1185 let sb = &img[..4096];
1187 bytes_read += sb.len();
1188 let block_size = crate::bytes::le_u32(sb, NX_BLOCK_SIZE) as usize;
1189 let uuid: [u8; 16] = sb[NX_CONTAINER_UUID..NX_CONTAINER_UUID + 16]
1190 .try_into()
1191 .expect("16 bytes");
1192 let cs = crate::bytes::le_u64(sb, NX_KEYBAG_BLOCK) as usize * block_size;
1193 let ce = cs + crate::bytes::le_u64(sb, NX_KEYBAG_BLOCKS) as usize * block_size;
1194
1195 bytes_read += ce - cs;
1197 let ckb = decrypt_keybag_area(&img[cs..ce], &uuid, cs / KEYBAG_SECTOR);
1198 let rec = volume_records(&ckb, volume_uuid).expect("volume must be listed");
1199
1200 let vs = rec.volume_keybag_block as usize * block_size;
1202 let ve = vs + rec.volume_keybag_blocks as usize * block_size;
1203 bytes_read += ve - vs;
1204 let vkb = decrypt_keybag_area(&img[vs..ve], volume_uuid, vs / KEYBAG_SECTOR);
1205
1206 let vek = unlock_with_volume_keybag(&vkb, &rec, password)
1207 .map(|u| u.vek)
1208 .unwrap_or_default();
1209 (vek, bytes_read)
1210 }
1211
1212 #[test]
1221 fn the_full_unlock_chain_works_from_ranged_reads_alone() {
1222 let img = fixture_image();
1223 let (got, bytes_read) = ranged_unlock(&img, &FIXTURE_VOLUME_UUID, "apfs-FV-TEST-2026");
1224 let want = unlock_volume(&img, &FIXTURE_VOLUME_UUID, "apfs-FV-TEST-2026")
1225 .expect("whole-image path must unlock");
1226
1227 assert_eq!(
1228 got, want.vek,
1229 "the ranged chain must recover the same VEK as the whole-image path"
1230 );
1231 assert!(
1232 bytes_read <= 64 * 1024,
1233 "the unwrap chain must touch a trivial slice of the image, read {bytes_read} bytes"
1234 );
1235
1236 use sha2::{Digest as _, Sha256};
1247 assert_eq!(
1248 Sha256::digest(&got)[..],
1249 hex_literal_vek_fingerprint()[..],
1250 "the fixture's VEK changed; a shared-path bug moves both sides at once"
1251 );
1252 }
1253
1254 #[cfg(test)]
1257 fn hex_literal_vek_fingerprint() -> [u8; 32] {
1258 [
1259 0x12, 0xed, 0x22, 0xee, 0x47, 0xf9, 0x27, 0x28, 0x8c, 0x6e, 0xb2, 0x44, 0xf6, 0xa6,
1260 0xd3, 0x54, 0xa2, 0xb5, 0xed, 0x14, 0x18, 0xa6, 0x34, 0x11, 0x64, 0x9d, 0x4e, 0x4a,
1261 0xbc, 0xa5, 0xb1, 0x43,
1262 ]
1263 }
1264
1265 #[test]
1269 fn the_ranged_chain_refuses_a_wrong_password() {
1270 let img = fixture_image();
1271 let (vek, _) = ranged_unlock(&img, &FIXTURE_VOLUME_UUID, "not-the-password");
1272 assert!(
1273 vek.is_empty(),
1274 "a wrong password must fail the ranged chain, never return a key"
1275 );
1276 }
1277
1278 #[test]
1283 fn keybag_area_with_a_wrong_sector_index_yields_noise() {
1284 let img = fixture_image();
1285 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1286 let paddr = crate::bytes::le_u64(&img, NX_KEYBAG_BLOCK) as usize;
1287 let blocks = crate::bytes::le_u64(&img, NX_KEYBAG_BLOCKS) as usize;
1288 let uuid: [u8; 16] = img[NX_CONTAINER_UUID..NX_CONTAINER_UUID + 16]
1289 .try_into()
1290 .expect("16 bytes");
1291 let start = paddr * block_size;
1292 let end = start + blocks * block_size;
1293
1294 let wrong = decrypt_keybag_area(&img[start..end], &uuid, start / KEYBAG_SECTOR + 1);
1295 assert_eq!(
1296 wrong.len(),
1297 end - start,
1298 "a decrypted area must be the same length as its ciphertext"
1299 );
1300 assert_ne!(
1301 wrong.get(24..28),
1302 Some(&b"syek"[..]),
1303 "a wrong sector index must not still produce a valid keybag magic"
1304 );
1305 }
1306
1307 #[test]
1317 fn the_encrypted_fs_tree_root_decrypts_to_a_checksum_valid_node() {
1318 let img = fixture_image();
1319 let vek_v = unlock_volume(&img, &FIXTURE_VOLUME_UUID, "apfs-FV-TEST-2026")
1320 .expect("fixture must unlock")
1321 .vek;
1322 let vek: [u8; 32] = vek_v.as_slice().try_into().expect("VEK is 32 bytes");
1323
1324 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1325 let mut cur = std::io::Cursor::new(&img[..]);
1326
1327 let (entry, _vol) = fs_tree_root(&img, &mut cur, block_size);
1329
1330 assert_ne!(
1331 entry.flags & OMAP_VAL_ENCRYPTED,
1332 0,
1333 "the fixture's fs-tree root must be flagged encrypted, else this test proves nothing"
1334 );
1335
1336 let mut node = read_block_at(&img, entry.paddr, block_size);
1337 assert_ne!(
1338 crate::object::fletcher64_checksum(&node),
1339 crate::object::fletcher64_stored(&node),
1340 "ciphertext must NOT already checksum, or the block was never encrypted"
1341 );
1342
1343 decrypt_volume_area(&mut node, &vek, entry.paddr, block_size);
1344
1345 assert_eq!(
1346 crate::object::fletcher64_checksum(&node),
1347 crate::object::fletcher64_stored(&node),
1348 "the decrypted fs-tree root must pass its own Fletcher-64 checksum"
1349 );
1350 assert!(
1351 crate::btree::parse_node_header(&node).is_some(),
1352 "and must parse as a B-tree node"
1353 );
1354 }
1355
1356 #[test]
1360 fn a_wrong_vek_does_not_produce_a_checksum_valid_node() {
1361 let img = fixture_image();
1362 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1363 let mut cur = std::io::Cursor::new(&img[..]);
1364 let (entry, _vol) = fs_tree_root(&img, &mut cur, block_size);
1365
1366 let mut node = read_block_at(&img, entry.paddr, block_size);
1367 decrypt_volume_area(&mut node, &[0xAB; 32], entry.paddr, block_size);
1368 assert_ne!(
1369 crate::object::fletcher64_checksum(&node),
1370 crate::object::fletcher64_stored(&node),
1371 "a wrong VEK must not yield a checksum-valid node"
1372 );
1373 }
1374
1375 #[cfg(test)]
1381 fn fs_tree_root(
1382 img: &[u8],
1383 cur: &mut std::io::Cursor<&[u8]>,
1384 block_size: usize,
1385 ) -> (crate::omap::OmapEntry, crate::volume::ApfsVolume) {
1386 let nx = crate::container::NxSuperblock::parse(&img[..block_size])
1387 .expect("container superblock parses");
1388 let nx_omap = crate::omap::ObjectMap::parse(&read_block_at(img, nx.omap_oid, block_size))
1389 .expect("container omap parses");
1390 let fs_oid = *nx.fs_oids.first().expect("at least one volume");
1391 let vol_entry = nx_omap
1392 .resolve(cur, fs_oid, u64::MAX, block_size)
1393 .expect("volume oid resolves");
1394 let vol =
1395 crate::volume::ApfsVolume::parse(&read_block_at(img, vol_entry.paddr, block_size))
1396 .expect("APSB parses (it is plaintext)");
1397 let vol_omap =
1398 crate::omap::ObjectMap::parse(&read_block_at(img, vol.omap_oid(), block_size))
1399 .expect("volume omap parses");
1400 let entry = vol_omap
1401 .resolve(cur, vol.root_tree_oid(), u64::MAX, block_size)
1402 .expect("fs-tree root resolves");
1403 (entry, vol)
1404 }
1405
1406 #[cfg(test)]
1408 fn read_block_at(img: &[u8], paddr: u64, block_size: usize) -> Vec<u8> {
1409 let start = paddr as usize * block_size;
1410 img[start..start + block_size].to_vec()
1411 }
1412
1413 #[cfg(test)]
1417 const MARKER: &str = "APFS-FILEVAULT-GROUND-TRUTH-MARKER-0123456789";
1418
1419 #[test]
1431 fn a_file_on_an_encrypted_volume_reads_back_as_its_known_plaintext() {
1432 let img = fixture_image();
1433
1434 assert!(
1435 !img.windows(MARKER.len()).any(|w| w == MARKER.as_bytes()),
1436 "the marker must NOT appear in the raw image, or the volume was never encrypted \
1437 and this test would pass without decrypting anything"
1438 );
1439
1440 let vek_v = unlock_volume(&img, &FIXTURE_VOLUME_UUID, "apfs-FV-TEST-2026")
1441 .expect("fixture must unlock")
1442 .vek;
1443 let vek: [u8; 32] = vek_v.as_slice().try_into().expect("VEK is 32 bytes");
1444
1445 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1446 let mut cur = std::io::Cursor::new(&img[..]);
1447 let (_, mut vol) = fs_tree_root(&img, &mut cur, block_size);
1448 vol.set_vek(vek);
1449
1450 let inode = crate::dir::open_path(&mut cur, &vol, "marker.txt", block_size)
1451 .expect("marker.txt must be found on the decrypted fs-tree");
1452 let data = crate::extent::read_data(&mut cur, &vol, &inode, block_size)
1453 .expect("its contents must decrypt");
1454
1455 assert_eq!(
1456 String::from_utf8_lossy(&data).trim_end(),
1457 MARKER,
1458 "the decrypted file must be byte-for-byte what macOS wrote"
1459 );
1460 }
1461
1462 #[test]
1466 fn the_same_file_is_unreadable_without_the_key() {
1467 let img = fixture_image();
1468 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1469 let mut cur = std::io::Cursor::new(&img[..]);
1470 let (_, vol) = fs_tree_root(&img, &mut cur, block_size);
1471
1472 let r = crate::dir::open_path(&mut cur, &vol, "marker.txt", block_size)
1473 .and_then(|i| crate::extent::read_data(&mut cur, &vol, &i, block_size));
1474 match r {
1475 Err(_) => {}
1476 Ok(data) => panic!(
1477 "a locked volume must refuse, not answer; got {} bytes",
1478 data.len()
1479 ),
1480 }
1481 }
1482
1483 #[cfg(test)]
1488 const DFVFS_CONTAINER_OFFSET: usize = 20480;
1489
1490 #[cfg(test)]
1492 fn dfvfs_container() -> Vec<u8> {
1493 use std::io::Read as _;
1494 let gz = include_bytes!("../../tests/data/filevault/dfvfs-apfs-encrypted.dmg.gz");
1495 let mut out = Vec::new();
1496 flate2::read::GzDecoder::new(&gz[..])
1497 .read_to_end(&mut out)
1498 .expect("dfVFS fixture must decompress");
1499 out.split_off(DFVFS_CONTAINER_OFFSET)
1500 }
1501
1502 #[test]
1526 fn tier1_third_party_encrypted_image_reads_against_a_third_party_answer_key() {
1527 let img = dfvfs_container();
1528
1529 for name in [b"passwords.txt".as_slice(), b"another_file".as_slice()] {
1532 assert!(
1533 !img.windows(name.len()).any(|w| w == name),
1534 "{} appears in plaintext; this image is not really encrypted",
1535 String::from_utf8_lossy(name)
1536 );
1537 }
1538
1539 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1540
1541 let container_kb = decrypt_container_keybag(&img).expect("container keybag decrypts");
1543 let volume_uuid: [u8; 16] = container_kb[48..64].try_into().expect("16 bytes");
1544
1545 let vek_v = unlock_volume(&img, &volume_uuid, "apfs-TEST")
1546 .expect("dfVFS's published password must unlock dfVFS's image")
1547 .vek;
1548 let vek: [u8; 32] = vek_v.as_slice().try_into().expect("VEK is 32 bytes");
1549
1550 let mut cur = std::io::Cursor::new(&img[..]);
1551 let (_, mut vol) = fs_tree_root(&img, &mut cur, block_size);
1552 vol.set_vek(vek);
1553
1554 let mut names: Vec<String> = crate::dir::list_dir(&mut cur, &vol, 2, block_size)
1556 .expect("root directory must list")
1557 .into_iter()
1558 .map(|e| e.name)
1559 .collect();
1560 names.sort();
1561 assert_eq!(
1562 names,
1563 vec![".fseventsd", "a_directory", "a_link", "passwords.txt"],
1564 "root listing must match dfVFS's own expectations"
1565 );
1566
1567 let inode = crate::dir::open_path(&mut cur, &vol, "a_directory/another_file", block_size)
1569 .expect("/a_directory/another_file must resolve");
1570 assert_eq!(
1571 inode.oid, 21,
1572 "dfVFS's APFSFileEntryTestEncrypted pins this file's inode at 21"
1573 );
1574 let data = crate::extent::read_data(&mut cur, &vol, &inode, block_size)
1575 .expect("its contents must decrypt");
1576
1577 assert_eq!(
1590 data.as_slice(),
1591 b"This is another file.\n",
1592 "the decrypted bytes must equal what dfVFS's generator wrote"
1593 );
1594
1595 let pw = crate::dir::open_path(&mut cur, &vol, "passwords.txt", block_size)
1599 .expect("/passwords.txt must resolve");
1600 let pw_data = crate::extent::read_data(&mut cur, &vol, &pw, block_size)
1601 .expect("passwords.txt must decrypt");
1602 assert!(
1603 pw_data.starts_with(b"place,user,password\n"),
1604 "passwords.txt must decrypt to the generator's heredoc, got {:?}",
1605 String::from_utf8_lossy(&pw_data[..pw_data.len().min(40)])
1606 );
1607 assert!(
1608 pw_data.windows(23).any(|w| w == b"bank,joesmith,superrich"),
1609 "and must contain the generator's second line"
1610 );
1611 }
1612
1613 #[test]
1616 fn tier1_third_party_image_refuses_a_wrong_password() {
1617 let img = dfvfs_container();
1618 let container_kb = decrypt_container_keybag(&img).expect("container keybag decrypts");
1619 let volume_uuid: [u8; 16] = container_kb[48..64].try_into().expect("16 bytes");
1620 assert!(
1621 unlock_volume(&img, &volume_uuid, "apfs-WRONG").is_err(),
1622 "a wrong password must be refused, never answered with a key"
1623 );
1624 }
1625
1626 #[test]
1635 fn wrapped_kek_accepts_a_two_byte_ber_length() {
1636 let mut blob = vec![0x83, 0x82, 0x00, 40];
1638 blob.extend_from_slice(&[0xAA; 40]);
1639 blob.extend_from_slice(&[0x84, 0x01, 0x0A]); blob.push(0x85);
1641 blob.push(16);
1642 blob.extend_from_slice(&[0xBB; 16]);
1643
1644 let k = parse_wrapped_kek(&blob).expect("a 2-byte length is legal BER");
1645 assert_eq!(k.wrapped_key.len(), 40);
1646 assert_eq!(k.iterations, 10);
1647 assert_eq!(k.salt.len(), 16);
1648 }
1649
1650 #[test]
1653 fn wrapped_kek_rejects_an_unsupported_ber_length_form() {
1654 let blob = vec![0x83, 0x84, 0, 0, 0, 40];
1656 let e = parse_wrapped_kek(&blob).expect_err("unsupported length form must fail");
1657 assert!(
1658 format!("{e:?}").contains("ber_length_form"),
1659 "the error must name the offending form, got {e:?}"
1660 );
1661 }
1662
1663 #[test]
1665 fn wrapped_kek_rejects_a_truncated_two_byte_length() {
1666 let blob = vec![0x83, 0x82, 0x00]; assert!(
1668 parse_wrapped_kek(&blob).is_err(),
1669 "a truncated length must be refused, never over-read"
1670 );
1671 }
1672
1673 #[test]
1675 fn key_derivation_with_zero_iterations_is_floored_not_zero() {
1676 let k = derive_key_from_password(b"pw", &[0u8; 16], 0, 32);
1679 assert_eq!(k.len(), 32);
1680 assert!(k.iter().any(|&b| b != 0), "must still derive real material");
1681 }
1682
1683 #[test]
1685 fn container_keybag_rejects_a_non_apfs_image() {
1686 let img = vec![0u8; 4096];
1687 let e = decrypt_container_keybag(&img).expect_err("no NXSB must fail");
1688 assert!(format!("{e:?}").contains("magic"), "got {e:?}");
1689 }
1690
1691 #[test]
1694 fn container_keybag_rejects_an_unusable_block_size() {
1695 let mut img = vec![0u8; 8192];
1696 img[32..36].copy_from_slice(b"NXSB");
1697 img[NX_BLOCK_SIZE..NX_BLOCK_SIZE + 4].copy_from_slice(&777u32.to_le_bytes());
1698 let e = decrypt_container_keybag(&img).expect_err("odd block size must fail");
1699 assert!(format!("{e:?}").contains("block_size"), "got {e:?}");
1700 }
1701
1702 #[test]
1705 fn container_keybag_rejects_an_absent_keylocker() {
1706 let mut img = vec![0u8; 8192];
1707 img[32..36].copy_from_slice(b"NXSB");
1708 img[NX_BLOCK_SIZE..NX_BLOCK_SIZE + 4].copy_from_slice(&4096u32.to_le_bytes());
1709 let e = decrypt_container_keybag(&img).expect_err("absent keylocker must fail");
1711 assert!(format!("{e:?}").contains("keylocker_extent"), "got {e:?}");
1712 }
1713
1714 #[test]
1716 fn container_keybag_rejects_an_extent_past_the_image() {
1717 let mut img = vec![0u8; 8192];
1718 img[32..36].copy_from_slice(b"NXSB");
1719 img[NX_BLOCK_SIZE..NX_BLOCK_SIZE + 4].copy_from_slice(&4096u32.to_le_bytes());
1720 img[NX_KEYBAG_BLOCK..NX_KEYBAG_BLOCK + 8].copy_from_slice(&9999u64.to_le_bytes());
1721 img[NX_KEYBAG_BLOCKS..NX_KEYBAG_BLOCKS + 8].copy_from_slice(&1u64.to_le_bytes());
1722 assert!(
1723 decrypt_container_keybag(&img).is_err(),
1724 "an extent past the image must be refused"
1725 );
1726 }
1727
1728 #[test]
1731 fn volume_area_decryption_is_a_no_op_for_the_sentinel_and_short_blocks() {
1732 let original = vec![0x5Au8; 4096];
1733
1734 let mut a = original.clone();
1735 decrypt_volume_area(&mut a, &[7u8; 32], 0, 4096);
1736 assert_eq!(a, original, "tweak 0 means not encrypted: leave it alone");
1737
1738 let mut b = original.clone();
1739 decrypt_volume_area(&mut b, &[7u8; 32], 5, 256);
1740 assert_eq!(
1741 b, original,
1742 "a block smaller than a sector cannot be XTS-addressed"
1743 );
1744 }
1745
1746 #[test]
1748 fn volume_records_stops_at_the_end_of_a_short_keybag() {
1749 let mut kb = vec![0u8; OBJ_PHYS_LEN + 16];
1750 kb[KL_NKEYS..KL_NKEYS + 2].copy_from_slice(&50u16.to_le_bytes());
1751 assert!(
1752 volume_records(&kb, &[0x11; 16]).is_err(),
1753 "no matching volume in a truncated keybag must be an error, not a guess"
1754 );
1755 }
1756
1757 #[test]
1759 fn unlock_with_volume_keybag_reports_a_missing_kek_object() {
1760 let records = VolumeRecords {
1761 wrapped_vek: vec![0u8; 40],
1762 volume_keybag_block: 1,
1763 volume_keybag_blocks: 1,
1764 };
1765 let empty = vec![0u8; OBJ_PHYS_LEN + 16];
1766 let e =
1767 unlock_with_volume_keybag(&empty, &records, "pw").expect_err("no KEK object must fail");
1768 assert!(format!("{e:?}").contains("kek_object_absent"), "got {e:?}");
1769 }
1770
1771 #[test]
1774 fn unlock_with_volume_keybag_rejects_a_wrong_sized_wrapped_vek() {
1775 let img = fixture_image();
1776 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1777 let container_kb = decrypt_container_keybag(&img).expect("keybag decrypts");
1778 let mut rec = volume_records(&container_kb, &FIXTURE_VOLUME_UUID).expect("records");
1779 rec.wrapped_vek.truncate(8); let vs = rec.volume_keybag_block as usize * block_size;
1782 let ve = vs + rec.volume_keybag_blocks as usize * block_size;
1783 let vkb = decrypt_keybag_area(&img[vs..ve], &FIXTURE_VOLUME_UUID, vs / KEYBAG_SECTOR);
1784
1785 let e = unlock_with_volume_keybag(&vkb, &rec, "apfs-FV-TEST-2026")
1786 .expect_err("a short wrapped VEK must fail");
1787 assert!(format!("{e:?}").contains("wrapped_vek_len"), "got {e:?}");
1788 }
1789
1790 #[test]
1793 fn unlock_volume_rejects_an_unparseable_image() {
1794 assert!(
1795 unlock_volume(&[0u8; 4096], &[0x22; 16], "pw").is_err(),
1796 "a non-APFS image is an image error, not a password error"
1797 );
1798 }
1799
1800 #[cfg(test)]
1804 fn container_keybag_for(uuid: &[u8; 16], entries: &[(u16, Vec<u8>)]) -> Vec<u8> {
1805 let mut data = vec![0u8; OBJ_PHYS_LEN + 16];
1806 data[24..28].copy_from_slice(b"syek");
1807 data[OBJ_PHYS_LEN..OBJ_PHYS_LEN + 2].copy_from_slice(&1u16.to_le_bytes());
1808 data[KL_NKEYS..KL_NKEYS + 2].copy_from_slice(&(entries.len() as u16).to_le_bytes());
1809 for (tag, payload) in entries {
1810 let mut e = vec![0u8; KE_HEADER_LEN + payload.len()];
1811 e[..16].copy_from_slice(uuid);
1812 e[KE_TAG..KE_TAG + 2].copy_from_slice(&tag.to_le_bytes());
1813 e[KE_KEYLEN..KE_KEYLEN + 2].copy_from_slice(&(payload.len() as u16).to_le_bytes());
1814 e[KE_HEADER_LEN..].copy_from_slice(payload);
1815 let padded = (e.len() + 15) & !15;
1816 e.resize(padded, 0);
1817 data.extend_from_slice(&e);
1818 }
1819 data
1820 }
1821
1822 #[test]
1825 fn volume_records_rejects_a_wrong_sized_wrapped_vek() {
1826 let uuid = [0x33u8; 16];
1827 let inner = vec![0x83, 8, 0, 0, 0, 0, 0, 0, 0, 0];
1829 let a3 = {
1830 let mut v = vec![0xa3, inner.len() as u8];
1831 v.extend_from_slice(&inner);
1832 v
1833 };
1834 let seq = {
1835 let mut v = vec![0x30, a3.len() as u8];
1836 v.extend_from_slice(&a3);
1837 v
1838 };
1839 let kb = container_keybag_for(&uuid, &[(0x02, seq)]);
1840 let e = volume_records(&kb, &uuid).expect_err("a short wrapped VEK must be refused");
1841 assert!(format!("{e:?}").contains("wrapped_vek_len"), "got {e:?}");
1842 }
1843
1844 #[test]
1847 fn volume_records_rejects_short_unlock_records() {
1848 let uuid = [0x44u8; 16];
1849 let kb = container_keybag_for(&uuid, &[(0x03, vec![0u8; 4])]);
1850 let e = volume_records(&kb, &uuid).expect_err("a short prange must be refused");
1851 assert!(format!("{e:?}").contains("unlock_records_len"), "got {e:?}");
1852 }
1853
1854 #[test]
1856 fn volume_records_rejects_a_volume_key_entry_with_no_wrapped_key() {
1857 let uuid = [0x55u8; 16];
1858 let a3 = vec![0xa3, 2, 0x99, 0];
1860 let seq = {
1861 let mut v = vec![0x30, a3.len() as u8];
1862 v.extend_from_slice(&a3);
1863 v
1864 };
1865 let kb = container_keybag_for(&uuid, &[(0x02, seq)]);
1866 assert!(
1867 volume_records(&kb, &uuid).is_err(),
1868 "a volume key entry with no 0x83 must be refused"
1869 );
1870 }
1871
1872 #[test]
1875 fn a_volume_keybag_with_an_unsupported_ber_length_yields_no_kek() {
1876 let uuid = [0x66u8; 16];
1877 let seq = vec![0x30, 0x84, 0, 0, 0, 4, 0xa3, 2, 0x83, 0];
1879 let kb = container_keybag_for(&uuid, &[(0x01, seq)]);
1880 let records = VolumeRecords {
1881 wrapped_vek: vec![0u8; 40],
1882 volume_keybag_block: 1,
1883 volume_keybag_blocks: 1,
1884 };
1885 let e = unlock_with_volume_keybag(&kb, &records, "pw")
1886 .expect_err("an unreadable KEK object must fail");
1887 assert!(format!("{e:?}").contains("kek_object_absent"), "got {e:?}");
1888 }
1889
1890 #[test]
1893 fn a_volume_keybag_with_a_two_byte_ber_length_is_walked() {
1894 let uuid = [0x77u8; 16];
1895 let mut a3 = vec![0xa3, 0x81, 44];
1896 a3.extend_from_slice(&[0x83, 40]);
1897 a3.extend_from_slice(&[0xCC; 40]);
1898 a3.extend_from_slice(&[0x84, 0x01, 0x05]); let mut seq = vec![0x30, 0x82, 0x00, a3.len() as u8];
1900 seq.extend_from_slice(&a3);
1901 let kb = container_keybag_for(&uuid, &[(0x01, seq)]);
1902 let records = VolumeRecords {
1903 wrapped_vek: vec![0u8; 40],
1904 volume_keybag_block: 1,
1905 volume_keybag_blocks: 1,
1906 };
1907 let e = unlock_with_volume_keybag(&kb, &records, "pw")
1910 .expect_err("a KEK object without a salt cannot derive a key");
1911 assert!(
1912 !format!("{e:?}").contains("kek_object_absent"),
1913 "the 0x82 length must have been walked, got {e:?}"
1914 );
1915 }
1916
1917 #[test]
1942 fn tier1_symlink_target_reads_through_decryption() {
1943 let img = dfvfs_container();
1944 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1945 let container_kb = decrypt_container_keybag(&img).expect("container keybag decrypts");
1946 let volume_uuid: [u8; 16] = container_kb[48..64].try_into().expect("16 bytes");
1947 let vek: [u8; 32] = unlock_volume(&img, &volume_uuid, "apfs-TEST")
1948 .expect("dfVFS's password unlocks dfVFS's image")
1949 .vek
1950 .as_slice()
1951 .try_into()
1952 .expect("VEK is 32 bytes");
1953
1954 let mut cur = std::io::Cursor::new(&img[..]);
1955 let (_, mut vol) = fs_tree_root(&img, &mut cur, block_size);
1956 vol.set_vek(vek);
1957
1958 let link =
1959 crate::dir::open_path(&mut cur, &vol, "a_link", block_size).expect("a_link resolves");
1960 assert_eq!(
1961 link.oid, 22,
1962 "dfVFS pins a_link's inode at 22 on this image"
1963 );
1964
1965 let target = crate::xattr::symlink_target(&mut cur, &vol, link.oid, block_size)
1966 .expect("symlink target reads")
1967 .expect("a_link is a symlink and must have a target");
1968 assert_eq!(
1969 target, "a_directory/a_file",
1970 "dfVFS pins the linked entry's name as a_file on the ENCRYPTED image \
1971 (it is another_file on the plaintext one)"
1972 );
1973 }
1974
1975 #[test]
1982 fn the_encrypted_corpus_carries_no_xattrs_or_resource_fork() {
1983 let img = dfvfs_container();
1984 let block_size = crate::bytes::le_u32(&img, NX_BLOCK_SIZE) as usize;
1985 let container_kb = decrypt_container_keybag(&img).expect("container keybag decrypts");
1986 let volume_uuid: [u8; 16] = container_kb[48..64].try_into().expect("16 bytes");
1987 let vek: [u8; 32] = unlock_volume(&img, &volume_uuid, "apfs-TEST")
1988 .expect("unlocks")
1989 .vek
1990 .as_slice()
1991 .try_into()
1992 .expect("VEK is 32 bytes");
1993
1994 let mut cur = std::io::Cursor::new(&img[..]);
1995 let (_, mut vol) = fs_tree_root(&img, &mut cur, block_size);
1996 vol.set_vek(vek);
1997
1998 let dir = crate::dir::open_path(&mut cur, &vol, "a_directory", block_size)
1999 .expect("a_directory resolves");
2000 let mut names: Vec<String> = crate::dir::list_dir(&mut cur, &vol, dir.oid, block_size)
2001 .expect("lists")
2002 .into_iter()
2003 .map(|e| e.name)
2004 .collect();
2005 names.sort();
2006 assert_eq!(
2007 names,
2008 vec!["a_file", "another_file"],
2009 "no a_resourcefork here, unlike the plaintext image"
2010 );
2011 }
2012}