use std::io::{Read, Seek};
use std::sync::{Arc, Mutex, PoisonError};
use forensic_vfs::adapters::SourceCursor;
use forensic_vfs::{
Credential, CredentialSource, CryptoLayer, CryptoScheme, DynSource, ImageSource, VfsError,
VfsResult,
};
use crate::{FileVaultError, FileVaultVolume};
pub struct FileVaultLayer {
encrypted: DynSource,
len: u64,
}
impl FileVaultLayer {
pub fn new(encrypted: DynSource) -> Self {
let len = encrypted.len();
Self { encrypted, len }
}
}
impl CryptoLayer for FileVaultLayer {
fn scheme(&self) -> CryptoScheme {
CryptoScheme::FileVault
}
fn open(&self, creds: &dyn CredentialSource) -> VfsResult<DynSource> {
let cands = creds.credentials_for(CryptoScheme::FileVault, "");
if cands.is_empty() {
return Err(VfsError::NeedCredentials {
scheme: "filevault",
target: String::new(),
});
}
let mut last_err = None;
for cred in &cands {
let Credential::Password(p) = cred else {
continue; };
let cursor = SourceCursor::new(Arc::clone(&self.encrypted), 0, self.len);
match FileVaultVolume::unlock_with_password(cursor, p) {
Ok(vol) => return Ok(Arc::new(FileVaultSource::new(vol))),
Err(e) => last_err = Some(e),
}
}
Err(last_err.as_ref().map_or(
VfsError::NeedCredentials {
scheme: "filevault",
target: String::new(),
},
map_fvde_err,
))
}
}
fn map_fvde_err(e: &FileVaultError) -> VfsError {
VfsError::Decode {
layer: "filevault",
offset: 0,
detail: e.to_string(),
bytes: forensic_vfs::SmallHex::new(&[]),
}
}
struct FileVaultSource<R: Read + Seek> {
inner: Mutex<FileVaultVolume<R>>,
len: u64,
}
impl<R: Read + Seek> FileVaultSource<R> {
fn new(vol: FileVaultVolume<R>) -> Self {
let len = vol.size();
Self {
inner: Mutex::new(vol),
len,
}
}
}
impl<R: Read + Seek + Send> ImageSource for FileVaultSource<R> {
fn len(&self) -> u64 {
self.len
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
let avail = self.len.saturating_sub(offset);
if avail == 0 {
return Ok(0);
}
let want = (buf.len() as u64).min(avail) as usize;
let Some(dst) = buf.get_mut(..want) else {
return Ok(0); };
let mut guard = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
guard.read_at(offset, dst).map_err(|e| map_fvde_err(&e))
}
}
#[cfg(test)]
mod tests {
use super::FileVaultLayer;
use forensic_vfs::adapters::FileSource;
use forensic_vfs::{
Credential, CredentialSource, CryptoLayer, CryptoScheme, DynSource, VfsError,
};
use sha2::{Digest, Sha256};
use std::sync::Arc;
struct FixedCreds(Vec<Credential>);
impl CredentialSource for FixedCreds {
fn credentials_for(&self, _scheme: CryptoScheme, _target: &str) -> Vec<Credential> {
self.0.clone()
}
}
fn encrypted() -> Option<DynSource> {
let path = std::env::var("FVDE_ORACLE_IMAGE")
.unwrap_or_else(|_| "/tmp/fvde-oracle/fvde_cs_p1.raw".to_string());
let src = FileSource::open(&path).ok()?;
Some(Arc::new(src))
}
fn sha256_hex(data: &[u8]) -> String {
use std::fmt::Write;
Sha256::digest(data).iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
#[test]
fn filevault_cryptolayer_decrypts_fvdetest() {
let Some(enc) = encrypted() else {
eprintln!("skip: no FileVault image (set FVDE_ORACLE_IMAGE)"); return; };
let layer = FileVaultLayer::new(enc);
assert_eq!(layer.scheme(), CryptoScheme::FileVault);
let creds = FixedCreds(vec![Credential::Password("fvde-TEST".to_string())]);
let dec: DynSource = layer.open(&creds).expect("unlock fvdetest");
assert_eq!(dec.len(), 167_772_160, "LV size (pyfvde ground truth)");
let mut sector = [0u8; 512];
assert_eq!(dec.read_at(1024, &mut sector).expect("read decrypted"), 512);
assert_eq!(
sha256_hex(§or),
"ebedb80407fc8bfdd3cce9c68de94efece7ed748df1babf35deeaacf008990af"
);
assert!(layer.open(&FixedCreds(vec![])).is_err());
}
use crate::test_support::{build_image, PASSWORD};
use forensic_vfs::adapters::SeekPoolSource;
use std::io::Cursor;
fn synthetic_source() -> DynSource {
let image = build_image();
let len = image.len() as u64;
Arc::new(SeekPoolSource::single(Cursor::new(image), len))
}
#[test]
fn synthetic_open_decrypts_and_reads() {
let layer = FileVaultLayer::new(synthetic_source());
assert_eq!(layer.scheme(), CryptoScheme::FileVault);
let creds = FixedCreds(vec![Credential::Password(PASSWORD.to_string())]);
let dec: DynSource = layer.open(&creds).expect("unlock synthetic");
assert_eq!(dec.len(), 0x4000);
let mut buf = [0u8; 512];
assert_eq!(dec.read_at(0, &mut buf).expect("read decrypted"), 512);
let expected: Vec<u8> = (0..512).map(|i| (i & 0xff) as u8).collect();
assert_eq!(&buf[..], &expected[..]);
assert_eq!(dec.read_at(0x4000, &mut buf).expect("eof read"), 0);
assert_eq!(dec.read_at(0x9000, &mut buf).expect("past-eof read"), 0);
let mut tail = [0u8; 512];
let n = dec.read_at(0x4000 - 100, &mut tail).expect("clamped read");
assert_eq!(n, 100);
}
#[test]
fn synthetic_wrong_password_is_decode_error() {
let layer = FileVaultLayer::new(synthetic_source());
let creds = FixedCreds(vec![Credential::Password("wrong".to_string())]);
let err = layer.open(&creds).err().expect("wrong password must fail");
assert!(matches!(
err,
VfsError::Decode {
layer: "filevault",
..
}
));
}
#[test]
fn non_password_credential_is_skipped_then_needs_credentials() {
let layer = FileVaultLayer::new(synthetic_source());
let creds = FixedCreds(vec![Credential::KeyBytes(vec![0u8; 16])]);
let err = layer.open(&creds).err().expect("no usable credential");
assert!(matches!(
err,
VfsError::NeedCredentials {
scheme: "filevault",
..
}
));
}
#[test]
fn empty_credentials_needs_credentials() {
let layer = FileVaultLayer::new(synthetic_source());
let err = layer.open(&FixedCreds(vec![])).err().expect("empty creds");
assert!(matches!(
err,
VfsError::NeedCredentials {
scheme: "filevault",
..
}
));
}
}