#![forbid(unsafe_code)]
#![allow(clippy::doc_markdown)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::io::{Read, Seek, SeekFrom};
pub mod context;
pub mod error;
pub mod metadata;
pub mod read;
#[cfg(test)]
mod test_support;
pub mod unlock;
#[cfg(feature = "vfs")]
pub mod vfs;
pub mod volume;
pub mod volume_header;
pub mod xts;
pub use context::{EncryptionContext, LogicalVolumeInfo, Protector, ProtectorKind};
pub use error::FileVaultError;
pub use volume::DecryptedVolume;
pub use volume_header::VolumeHeader;
const VOLUME_HEADER_LEN: usize = 512;
#[derive(Debug, Clone)]
pub struct FileVaultInfo {
pub physical_volume_identifier: String,
pub pbkdf2_iterations: u32,
pub pbkdf2_salt: [u8; 16],
pub family_uuid: String,
pub lv_identifier: Option<String>,
pub lv_name: Option<String>,
pub lv_size: u64,
pub encryption_method: &'static str,
pub conversion_status: Option<String>,
pub protectors: Vec<Protector>,
}
#[derive(Debug)]
pub struct FileVaultVolume<R: Read + Seek> {
decrypted: DecryptedVolume<R>,
info: FileVaultInfo,
}
impl<R: Read + Seek> FileVaultVolume<R> {
pub fn unlock_with_password(mut reader: R, password: &str) -> Result<Self, FileVaultError> {
let mut header_bytes = [0u8; VOLUME_HEADER_LEN];
reader.seek(SeekFrom::Start(0))?;
read_exact_or_err(&mut reader, &mut header_bytes)?;
let header = VolumeHeader::parse(&header_bytes)?;
let plaintext = decrypt_metadata_region(&mut reader, &header)?;
let context = EncryptionContext::extract(&plaintext)?;
let lv = LogicalVolumeInfo::extract(&plaintext)?;
let family_uuid_bytes =
parse_uuid_bytes(&lv.family_uuid).ok_or(FileVaultError::MetadataStructureMissing {
what: "parseable family UUID",
})?;
let keys = unlock::derive_volume_keys(password, &context, &family_uuid_bytes)?;
let (physical_base, size) = resolve_lv_extent(&plaintext, &header, &lv);
let info = build_info(&header, &context, &lv, size);
let decrypted = DecryptedVolume::new(reader, keys, physical_base, size);
Ok(FileVaultVolume { decrypted, info })
}
pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<usize, FileVaultError> {
self.decrypted.read_at(offset, buf)
}
#[must_use]
pub fn size(&self) -> u64 {
self.decrypted.size()
}
#[must_use]
pub fn info(&self) -> &FileVaultInfo {
&self.info
}
#[must_use]
pub fn into_decrypted(self) -> DecryptedVolume<R> {
self.decrypted
}
}
impl<R: Read + Seek> Read for FileVaultVolume<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.decrypted.read(buf)
}
}
impl<R: Read + Seek> Seek for FileVaultVolume<R> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.decrypted.seek(pos)
}
}
pub fn parse_info<R: Read + Seek>(mut reader: R) -> Result<FileVaultInfo, FileVaultError> {
let mut header_bytes = [0u8; VOLUME_HEADER_LEN];
reader.seek(SeekFrom::Start(0))?;
read_exact_or_err(&mut reader, &mut header_bytes)?;
let header = VolumeHeader::parse(&header_bytes)?;
let plaintext = decrypt_metadata_region(&mut reader, &header)?;
let context = EncryptionContext::extract(&plaintext)?;
let lv = LogicalVolumeInfo::extract(&plaintext)?;
let (_, size) = resolve_lv_extent(&plaintext, &header, &lv);
Ok(build_info(&header, &context, &lv, size))
}
fn decrypt_metadata_region<R: Read + Seek>(
reader: &mut R,
header: &VolumeHeader,
) -> Result<Vec<u8>, FileVaultError> {
let (region_offset, _) = metadata::plaintext_metadata_region(header);
let block_size = header.block_size as usize;
if block_size == 0 {
return Err(FileVaultError::OutOfRange {
what: "block size is zero",
});
}
let mut first_block = vec![0u8; block_size];
reader.seek(SeekFrom::Start(region_offset))?;
read_exact_or_err(reader, &mut first_block)?;
let region_size = metadata::plaintext_metadata_size(header, &first_block)?;
let mut region = vec![0u8; region_size as usize];
reader.seek(SeekFrom::Start(region_offset))?;
read_exact_or_err(reader, &mut region)?;
let location = metadata::locate_encrypted_metadata(header, ®ion)?;
let mut ciphertext = vec![0u8; location.length as usize];
reader.seek(SeekFrom::Start(location.primary_offset))?;
read_exact_or_err(reader, &mut ciphertext)?;
metadata::decrypt_metadata(header, &mut ciphertext);
Ok(ciphertext)
}
fn resolve_lv_extent(
metadata_bytes: &[u8],
header: &VolumeHeader,
lv: &LogicalVolumeInfo,
) -> (u64, u64) {
let block_size = u64::from(header.block_size);
let segments = metadata::parse_segments(metadata_bytes, header.block_size as usize);
if let Some(first) = segments.first() {
let base = first.physical_block.saturating_mul(block_size);
let size = if lv.size != 0 {
lv.size
} else {
u64::from(first.number_of_blocks).saturating_mul(block_size)
};
(base, size)
} else {
(0, lv.size)
}
}
fn build_info(
header: &VolumeHeader,
context: &EncryptionContext,
lv: &LogicalVolumeInfo,
size: u64,
) -> FileVaultInfo {
FileVaultInfo {
physical_volume_identifier: format_uuid(&header.physical_volume_identifier),
pbkdf2_iterations: context.iterations,
pbkdf2_salt: context.salt,
family_uuid: lv.family_uuid.clone(),
lv_identifier: lv.lv_identifier.clone(),
lv_name: lv.name.clone(),
lv_size: size,
encryption_method: "AES-XTS-128",
conversion_status: context.conversion_status.clone(),
protectors: context.protectors.clone(),
}
}
fn read_exact_or_err<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<(), FileVaultError> {
reader.read_exact(buf).map_err(FileVaultError::Io)
}
fn format_uuid(bytes: &[u8; 16]) -> String {
uuid::Uuid::from_bytes(*bytes).hyphenated().to_string()
}
fn parse_uuid_bytes(text: &str) -> Option<[u8; 16]> {
uuid::Uuid::parse_str(text).ok().map(|u| *u.as_bytes())
}
#[cfg(test)]
mod tests {
use super::test_support::{build_image, BLOCK, FAMILY, ITERS, KEY1, PASSWORD, PV_ID};
use super::*;
use std::io::Cursor;
#[test]
fn unlock_synthetic_volume_end_to_end() {
let image = build_image();
let mut vol = FileVaultVolume::unlock_with_password(Cursor::new(image), PASSWORD).unwrap();
assert_eq!(vol.size(), 0x4000);
assert_eq!(vol.info().pbkdf2_iterations, ITERS);
assert_eq!(vol.info().lv_name.as_deref(), Some("SynthLV"));
assert_eq!(vol.info().encryption_method, "AES-XTS-128");
assert_eq!(vol.info().conversion_status.as_deref(), Some("Complete"));
assert_eq!(vol.info().protectors.len(), 1);
let mut buf = [0u8; 512];
assert_eq!(vol.read_at(0, &mut buf).unwrap(), 512);
let expected: Vec<u8> = (0..512).map(|i| (i & 0xff) as u8).collect();
assert_eq!(&buf[..], &expected[..]);
assert_eq!(vol.seek(SeekFrom::Start(512)).unwrap(), 512);
let mut b2 = [0u8; 16];
assert!(std::io::Read::read(&mut vol, &mut b2).unwrap() > 0);
let image2 = build_image();
let info = parse_info(Cursor::new(image2)).unwrap();
assert_eq!(info.family_uuid, vol.info().family_uuid);
assert_eq!(info.lv_size, 0x4000);
assert_eq!(
info.physical_volume_identifier,
uuid::Uuid::from_bytes(PV_ID).hyphenated().to_string()
);
let mut dec = vol.into_decrypted();
assert_eq!(dec.size(), 0x4000);
let mut b3 = [0u8; 16];
assert_eq!(dec.read_at(0, &mut b3).unwrap(), 16);
}
#[test]
fn unlock_wrong_password_fails() {
let image = build_image();
assert!(matches!(
FileVaultVolume::unlock_with_password(Cursor::new(image), "nope"),
Err(FileVaultError::KeyUnwrap { .. })
));
}
#[test]
fn unlock_non_corestorage_fails() {
let image = vec![0u8; 4096];
assert!(matches!(
FileVaultVolume::unlock_with_password(Cursor::new(image), PASSWORD),
Err(FileVaultError::NotCoreStorage { .. })
));
}
#[test]
fn truncated_image_errors_loudly() {
let mut image = vec![0u8; 600];
image[88] = b'C';
image[89] = b'S';
image[96..100].copy_from_slice(&(BLOCK as u32).to_le_bytes());
image[172..176].copy_from_slice(&2u32.to_le_bytes());
image[104..112].copy_from_slice(&1u64.to_le_bytes());
let err = parse_info(Cursor::new(image)).unwrap_err();
assert!(matches!(err, FileVaultError::Io(_)));
}
#[test]
fn zero_block_size_header_errors() {
let mut image = vec![0u8; 4096];
image[88] = b'C';
image[89] = b'S';
image[96..100].copy_from_slice(&0u32.to_le_bytes());
image[172..176].copy_from_slice(&2u32.to_le_bytes());
let err = parse_info(Cursor::new(image)).unwrap_err();
assert!(matches!(err, FileVaultError::OutOfRange { .. }));
}
#[test]
fn format_and_parse_uuid_roundtrip() {
let s = format_uuid(&FAMILY);
assert_eq!(parse_uuid_bytes(&s), Some(FAMILY));
assert_eq!(parse_uuid_bytes("not-a-uuid"), None);
}
#[test]
fn resolve_extent_uses_segment_blocks_when_lv_size_zero() {
let header = VolumeHeader {
block_size: BLOCK as u32,
bytes_per_sector: 512,
physical_volume_size: 0,
metadata_block_numbers: [1, 0, 0, 0],
key_data: KEY1,
physical_volume_identifier: PV_ID,
};
let mut meta = vec![0u8; BLOCK];
meta[10..12].copy_from_slice(&0x0305u16.to_le_bytes());
let payload = 64;
meta[payload..payload + 4].copy_from_slice(&1u32.to_le_bytes());
let entry = payload + 8;
meta[entry + 16..entry + 20].copy_from_slice(&2u32.to_le_bytes());
meta[entry + 32..entry + 40].copy_from_slice(&5u64.to_le_bytes());
let lv = LogicalVolumeInfo {
size: 0,
family_uuid: "x".to_string(),
lv_identifier: None,
name: None,
};
let (base, size) = resolve_lv_extent(&meta, &header, &lv);
assert_eq!(base, 5 * BLOCK as u64);
assert_eq!(size, 2 * BLOCK as u64);
}
#[test]
fn resolve_extent_no_segment_falls_back_to_lv_size() {
let header = VolumeHeader {
block_size: BLOCK as u32,
bytes_per_sector: 512,
physical_volume_size: 0,
metadata_block_numbers: [1, 0, 0, 0],
key_data: KEY1,
physical_volume_identifier: PV_ID,
};
let meta = vec![0u8; BLOCK]; let lv = LogicalVolumeInfo {
size: 12345,
family_uuid: "x".to_string(),
lv_identifier: None,
name: None,
};
let (base, size) = resolve_lv_extent(&meta, &header, &lv);
assert_eq!(base, 0);
assert_eq!(size, 12345);
}
}