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