1use base64::engine::general_purpose::STANDARD as BASE64;
13use base64::Engine;
14
15use crate::error::FileVaultError;
16
17#[derive(Debug, Clone)]
19pub struct EncryptionContext {
20 pub salt: [u8; 16],
22 pub iterations: u32,
26 pub wrapped_kek: [u8; 24],
28 pub wrapped_vmk: [u8; 24],
31 pub protectors: Vec<Protector>,
33 pub conversion_status: Option<String>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Protector {
40 pub user_type: u32,
42 pub kind: ProtectorKind,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ProtectorKind {
49 Password,
51 Recovery,
53 Institutional,
55 Unknown,
58}
59
60impl ProtectorKind {
61 #[must_use]
63 pub fn from_user_type(user_type: u32) -> Self {
64 match user_type {
65 0x1000_0001 => ProtectorKind::Password,
66 0x1000_0009 => ProtectorKind::Recovery,
67 0x1000_0005 | 0x1000_000f => ProtectorKind::Institutional,
68 _ => ProtectorKind::Unknown,
69 }
70 }
71
72 #[must_use]
74 pub fn label(self) -> &'static str {
75 match self {
76 ProtectorKind::Password => "password",
77 ProtectorKind::Recovery => "recovery",
78 ProtectorKind::Institutional => "institutional",
79 ProtectorKind::Unknown => "unknown",
80 }
81 }
82}
83
84const PW_SALT_OFFSET: usize = 8;
87const PW_WRAPPED_KEK_OFFSET: usize = 32;
88const PW_ITERATIONS_OFFSET: usize = 168;
89const KEK_WRAPPED_VMK_OFFSET: usize = 8;
91const MAX_BASE64_BLOB: usize = 4096;
93
94fn extract_data(metadata: &[u8], key_name: &str) -> Option<Vec<u8>> {
97 let value = extract_tagged(metadata, key_name, b"<data ID=", b"</data>")?;
98 let trimmed: Vec<u8> = value
99 .iter()
100 .copied()
101 .filter(|b| !b.is_ascii_whitespace())
102 .collect();
103 if trimmed.len() > MAX_BASE64_BLOB {
104 return None;
105 }
106 BASE64.decode(&trimmed).ok()
107}
108
109fn extract_string(metadata: &[u8], key_name: &str) -> Option<String> {
111 let value = extract_tagged(metadata, key_name, b"<string ID=", b"</string>")?;
112 String::from_utf8(value).ok()
113}
114
115fn extract_integer_from(metadata: &[u8], key_name: &str, from: usize) -> Option<(u32, usize)> {
118 let (value, end) = extract_tagged_from(metadata, key_name, b"<integer", b"</integer>", from)?;
119 let text = std::str::from_utf8(&value).ok()?.trim();
121 let parsed = if let Some(hex) = text.strip_prefix("0x") {
122 u32::from_str_radix(hex, 16).ok()?
123 } else {
124 text.parse::<u32>().ok()?
125 };
126 Some((parsed, end))
127}
128
129fn extract_tagged(
132 metadata: &[u8],
133 key_name: &str,
134 open_tag: &[u8],
135 close_tag: &[u8],
136) -> Option<Vec<u8>> {
137 extract_tagged_from(metadata, key_name, open_tag, close_tag, 0).map(|(v, _)| v)
138}
139
140fn extract_tagged_from(
141 metadata: &[u8],
142 key_name: &str,
143 open_tag: &[u8],
144 close_tag: &[u8],
145 from: usize,
146) -> Option<(Vec<u8>, usize)> {
147 let mut key = Vec::with_capacity(key_name.len() + 11);
148 key.extend_from_slice(b"<key>");
149 key.extend_from_slice(key_name.as_bytes());
150 key.extend_from_slice(b"</key>");
151
152 let start = from.checked_add(find_sub(metadata.get(from..)?, &key)?)?;
153 let after_key = start.checked_add(key.len())?;
154 let open_rel = find_sub(metadata.get(after_key..)?, open_tag)?;
155 let open_abs = after_key.checked_add(open_rel)?;
156 let gt_rel = find_sub(metadata.get(open_abs..)?, b">")?;
158 let inner_start = open_abs.checked_add(gt_rel)?.checked_add(1)?;
159 let close_rel = find_sub(metadata.get(inner_start..)?, close_tag)?;
160 let inner_end = inner_start.checked_add(close_rel)?;
161 let inner = metadata.get(inner_start..inner_end)?.to_vec();
162 Some((inner, inner_end.checked_add(close_tag.len())?))
163}
164
165fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
167 if needle.is_empty() || needle.len() > haystack.len() {
168 return None;
169 }
170 haystack
171 .windows(needle.len())
172 .position(|window| window == needle)
173}
174
175fn extract_protectors(metadata: &[u8]) -> Vec<Protector> {
177 let mut out = Vec::new();
178 let mut cursor = 0;
179 while let Some((user_type, end)) = extract_integer_from(metadata, "UserType", cursor) {
180 out.push(Protector {
181 user_type,
182 kind: ProtectorKind::from_user_type(user_type),
183 });
184 cursor = end;
185 if out.len() >= 64 {
186 break; }
188 }
189 out
190}
191
192impl EncryptionContext {
193 pub fn extract(metadata: &[u8]) -> Result<Self, FileVaultError> {
200 let pw = extract_data(metadata, "PassphraseWrappedKEKStruct").ok_or(
201 FileVaultError::MetadataStructureMissing {
202 what: "PassphraseWrappedKEKStruct",
203 },
204 )?;
205
206 let salt = slice16(&pw, PW_SALT_OFFSET).ok_or(FileVaultError::OutOfRange {
207 what: "PassphraseWrappedKEKStruct salt",
208 })?;
209 let wrapped_kek =
210 slice24(&pw, PW_WRAPPED_KEK_OFFSET).ok_or(FileVaultError::OutOfRange {
211 what: "PassphraseWrappedKEKStruct wrapped KEK",
212 })?;
213 let iterations =
214 read_u32_le(&pw, PW_ITERATIONS_OFFSET).ok_or(FileVaultError::OutOfRange {
215 what: "PassphraseWrappedKEKStruct iterations",
216 })?;
217
218 let vmk_struct = extract_kek_wrapped_aes_xts(metadata).ok_or(
221 FileVaultError::MetadataStructureMissing {
222 what: "AES-XTS KEKWrappedVolumeKeyStruct",
223 },
224 )?;
225 let wrapped_vmk =
226 slice24(&vmk_struct, KEK_WRAPPED_VMK_OFFSET).ok_or(FileVaultError::OutOfRange {
227 what: "KEKWrappedVolumeKeyStruct wrapped VMK",
228 })?;
229
230 Ok(EncryptionContext {
231 salt,
232 iterations,
233 wrapped_kek,
234 wrapped_vmk,
235 protectors: extract_protectors(metadata),
236 conversion_status: extract_string(metadata, "ConversionStatus"),
237 })
238 }
239}
240
241fn extract_kek_wrapped_aes_xts(metadata: &[u8]) -> Option<Vec<u8>> {
245 let marker = b"<string ID=";
246 let mut cursor = 0;
248 loop {
249 let (algo, end) =
250 extract_tagged_from(metadata, "BlockAlgorithm", marker, b"</string>", cursor)?;
251 if algo == b"AES-XTS" {
252 return extract_data(metadata.get(end..)?, "KEKWrappedVolumeKeyStruct");
253 }
254 cursor = end;
255 }
256}
257
258#[derive(Debug, Clone)]
260pub struct LogicalVolumeInfo {
261 pub size: u64,
263 pub family_uuid: String,
265 pub lv_identifier: Option<String>,
267 pub name: Option<String>,
269}
270
271impl LogicalVolumeInfo {
272 pub fn extract(metadata: &[u8]) -> Result<Self, FileVaultError> {
277 let family_uuid = extract_string(metadata, "com.apple.corestorage.lv.familyUUID").ok_or(
278 FileVaultError::MetadataStructureMissing {
279 what: "com.apple.corestorage.lv.familyUUID",
280 },
281 )?;
282 let size = extract_lv_size(metadata).unwrap_or(0);
283 let lv_identifier = extract_string(metadata, "com.apple.corestorage.lv.uuid");
284 let name = extract_string(metadata, "com.apple.corestorage.lv.name");
285 Ok(LogicalVolumeInfo {
286 size,
287 family_uuid,
288 lv_identifier,
289 name,
290 })
291 }
292}
293
294fn extract_lv_size(metadata: &[u8]) -> Option<u64> {
299 if let Some((value, _)) = extract_u64_integer(metadata, "com.apple.corestorage.lv.size") {
302 return Some(value);
303 }
304 None
305}
306
307fn extract_u64_integer(metadata: &[u8], key_name: &str) -> Option<(u64, usize)> {
309 let (value, end) = extract_tagged_from(metadata, key_name, b"<integer", b"</integer>", 0)?;
310 let text = std::str::from_utf8(&value).ok()?.trim();
311 let parsed = if let Some(hex) = text.strip_prefix("0x") {
312 u64::from_str_radix(hex, 16).ok()?
313 } else {
314 text.parse::<u64>().ok()?
315 };
316 Some((parsed, end))
317}
318
319fn slice16(data: &[u8], off: usize) -> Option<[u8; 16]> {
320 let s = data.get(off..off.checked_add(16)?)?;
321 let mut out = [0u8; 16];
322 out.copy_from_slice(s);
323 Some(out)
324}
325
326fn slice24(data: &[u8], off: usize) -> Option<[u8; 24]> {
327 let s = data.get(off..off.checked_add(24)?)?;
328 let mut out = [0u8; 24];
329 out.copy_from_slice(s);
330 Some(out)
331}
332
333fn read_u32_le(data: &[u8], off: usize) -> Option<u32> {
334 let s = data.get(off..off.checked_add(4)?)?;
335 Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn hex(s: &str) -> Vec<u8> {
343 (0..s.len() / 2)
344 .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap())
345 .collect()
346 }
347
348 fn passphrase_struct() -> Vec<u8> {
351 let mut s = vec![0u8; 284];
352 s[PW_SALT_OFFSET..PW_SALT_OFFSET + 16]
353 .copy_from_slice(&hex("9bfcf480e4d9ad0eddd9ac6f47b85955"));
354 s[PW_WRAPPED_KEK_OFFSET..PW_WRAPPED_KEK_OFFSET + 24]
355 .copy_from_slice(&hex("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1"));
356 s[PW_ITERATIONS_OFFSET..PW_ITERATIONS_OFFSET + 4].copy_from_slice(&90506u32.to_le_bytes());
357 s
358 }
359
360 fn kek_wrapped_struct(wrapped_vmk_hex: &str) -> Vec<u8> {
362 let mut s = vec![0u8; 256];
363 s[KEK_WRAPPED_VMK_OFFSET..KEK_WRAPPED_VMK_OFFSET + 24]
364 .copy_from_slice(&hex(wrapped_vmk_hex));
365 s
366 }
367
368 fn b64(bytes: &[u8]) -> String {
369 BASE64.encode(bytes)
370 }
371
372 fn build_context_plist() -> Vec<u8> {
373 let pw = b64(&passphrase_struct());
374 let none_kek = b64(&kek_wrapped_struct(
376 "000000000000000000000000000000000000000000000000",
377 ));
378 let xts_kek = b64(&kek_wrapped_struct(
379 "9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1",
380 ));
381 format!(
382 "<dict ID=\"0\"><key>CryptoUsers</key><array ID=\"2\">\
383 <dict ID=\"3\">\
384 <key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{pw}</data>\
385 <key>UserType</key><integer size=\"32\" ID=\"6\">0x10000001</integer>\
386 <key>BlockAlgorithm</key><string ID=\"7\">None</string>\
387 <key>KEKWrappedVolumeKeyStruct</key><data ID=\"8\">{none_kek}</data>\
388 <key>BlockAlgorithm</key><string ID=\"9\">AES-XTS</string>\
389 <key>KEKWrappedVolumeKeyStruct</key><data ID=\"10\">{xts_kek}</data>\
390 </dict>\
391 <dict ID=\"11\">\
392 <key>UserType</key><integer size=\"32\" ID=\"12\">0x10000009</integer>\
393 </dict>\
394 </array>\
395 <key>ConversionStatus</key><string ID=\"20\">Complete</string></dict>"
396 )
397 .into_bytes()
398 }
399
400 #[test]
401 fn extracts_encryption_context() {
402 let meta = build_context_plist();
403 let ctx = EncryptionContext::extract(&meta).unwrap();
404 assert_eq!(
405 ctx.salt,
406 *b"\x9b\xfc\xf4\x80\xe4\xd9\xad\x0e\xdd\xd9\xac\x6f\x47\xb8\x59\x55"
407 );
408 assert_eq!(ctx.iterations, 90506);
409 assert_eq!(
410 ctx.wrapped_kek.to_vec(),
411 hex("ebbc1f64b9684eb4b26bfba3f85578677bab8cfafaf7b2c1")
412 );
413 assert_eq!(
415 ctx.wrapped_vmk.to_vec(),
416 hex("9a5b30e99f902ed8e2f03989e5f9c1543ed60512aa1dc9d1")
417 );
418 assert_eq!(ctx.conversion_status.as_deref(), Some("Complete"));
419 }
420
421 #[test]
422 fn enumerates_protectors() {
423 let meta = build_context_plist();
424 let ctx = EncryptionContext::extract(&meta).unwrap();
425 assert_eq!(ctx.protectors.len(), 2);
426 assert_eq!(ctx.protectors[0].kind, ProtectorKind::Password);
427 assert_eq!(ctx.protectors[0].user_type, 0x1000_0001);
428 assert_eq!(ctx.protectors[1].kind, ProtectorKind::Recovery);
429 }
430
431 #[test]
432 fn missing_passphrase_struct_is_error() {
433 let meta = b"<dict><key>Nothing</key></dict>".to_vec();
434 assert!(matches!(
435 EncryptionContext::extract(&meta),
436 Err(FileVaultError::MetadataStructureMissing {
437 what: "PassphraseWrappedKEKStruct"
438 })
439 ));
440 }
441
442 #[test]
443 fn missing_aes_xts_kek_is_error() {
444 let pw = b64(&passphrase_struct());
445 let meta = format!(
446 "<key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{pw}</data>\
447 <key>BlockAlgorithm</key><string ID=\"9\">None</string>\
448 <key>KEKWrappedVolumeKeyStruct</key><data ID=\"10\">{}</data>",
449 b64(&kek_wrapped_struct(
450 "000000000000000000000000000000000000000000000000"
451 ))
452 )
453 .into_bytes();
454 assert!(matches!(
455 EncryptionContext::extract(&meta),
456 Err(FileVaultError::MetadataStructureMissing {
457 what: "AES-XTS KEKWrappedVolumeKeyStruct"
458 })
459 ));
460 }
461
462 #[test]
463 fn protector_kind_classification() {
464 assert_eq!(
465 ProtectorKind::from_user_type(0x1000_0001),
466 ProtectorKind::Password
467 );
468 assert_eq!(
469 ProtectorKind::from_user_type(0x1000_0009),
470 ProtectorKind::Recovery
471 );
472 assert_eq!(
473 ProtectorKind::from_user_type(0x1000_000f),
474 ProtectorKind::Institutional
475 );
476 assert_eq!(
477 ProtectorKind::from_user_type(0xdead),
478 ProtectorKind::Unknown
479 );
480 assert_eq!(ProtectorKind::Password.label(), "password");
481 assert_eq!(ProtectorKind::Recovery.label(), "recovery");
482 assert_eq!(ProtectorKind::Institutional.label(), "institutional");
483 assert_eq!(ProtectorKind::Unknown.label(), "unknown");
484 }
485
486 #[test]
487 fn extracts_logical_volume_info() {
488 let meta = b"<dict ID=\"0\">\
489 <key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">1F01CA34-5F6C-4123-AC0C-B0A256889DB2</string>\
490 <key>com.apple.corestorage.lv.name</key><string ID=\"6\">TestLV</string>\
491 <key>com.apple.corestorage.lv.size</key><integer size=\"64\" ID=\"7\">0xa000000</integer>\
492 <key>com.apple.corestorage.lv.uuid</key><string ID=\"8\">420AF122-CF73-4A30-8B0A-A593A65FBEF5</string>\
493 </dict>"
494 .to_vec();
495 let lv = LogicalVolumeInfo::extract(&meta).unwrap();
496 assert_eq!(lv.family_uuid, "1F01CA34-5F6C-4123-AC0C-B0A256889DB2");
497 assert_eq!(lv.size, 167_772_160);
498 assert_eq!(lv.name.as_deref(), Some("TestLV"));
499 assert_eq!(
500 lv.lv_identifier.as_deref(),
501 Some("420AF122-CF73-4A30-8B0A-A593A65FBEF5")
502 );
503 }
504
505 #[test]
506 fn missing_family_uuid_is_error() {
507 let meta = b"<dict><key>other</key></dict>".to_vec();
508 assert!(matches!(
509 LogicalVolumeInfo::extract(&meta),
510 Err(FileVaultError::MetadataStructureMissing {
511 what: "com.apple.corestorage.lv.familyUUID"
512 })
513 ));
514 }
515
516 #[test]
517 fn lv_size_absent_is_zero_not_fabricated() {
518 let meta = b"<key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">1F01CA34-5F6C-4123-AC0C-B0A256889DB2</string>".to_vec();
519 let lv = LogicalVolumeInfo::extract(&meta).unwrap();
520 assert_eq!(lv.size, 0);
521 assert!(lv.name.is_none());
522 }
523
524 #[test]
525 fn oversized_base64_blob_is_rejected() {
526 let big = "A".repeat(MAX_BASE64_BLOB + 4);
528 let meta = format!("<key>PassphraseWrappedKEKStruct</key><data ID=\"4\">{big}</data>")
529 .into_bytes();
530 assert!(matches!(
531 EncryptionContext::extract(&meta),
532 Err(FileVaultError::MetadataStructureMissing { .. })
533 ));
534 }
535
536 #[test]
537 fn decimal_integer_values_parse() {
538 let meta = b"<key>UserType</key><integer size=\"32\" ID=\"6\">268435457</integer>".to_vec();
540 assert_eq!(
541 extract_integer_from(&meta, "UserType", 0).unwrap().0,
542 268_435_457
543 );
544
545 let lv_meta = b"<dict>\
546 <key>com.apple.corestorage.lv.familyUUID</key><string ID=\"1\">1F01CA34-5F6C-4123-AC0C-B0A256889DB2</string>\
547 <key>com.apple.corestorage.lv.size</key><integer size=\"64\" ID=\"7\">167772160</integer>\
548 </dict>".to_vec();
549 let lv = LogicalVolumeInfo::extract(&lv_meta).unwrap();
550 assert_eq!(lv.size, 167_772_160);
551 }
552}