FileVault 2 (Core Storage): confere com o hash SHA-256 de referência do cryptsetup
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
86344c0536
commit
a2ce82315e
2 files changed
+268
No files matched your search
@@ -0,0 +1,233 @@
|
|||||||
|
//! FileVault 2 sobre Core Storage (volumes HFS+ cifrados do macOS 10.7 a 10.12 e Fusion Drives).
|
||||||
|
//! Senha ou chave do volume; chave de recuperação institucional não é suportada.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::source::{read_vec, SubSource};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
const MD_BLOCK: usize = 8192;
|
||||||
|
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(head: &[u8]) -> bool {
|
||||||
|
head.len() >= 512 && head[88] == 0x43 && head[89] == 0x53 && head[10] == 0x10
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Desembrulha uma chave AES-128 de 24 bytes (RFC 3394).
|
||||||
|
fn unwrap_key(kek: &[u8], wrapped: &[u8]) -> Option<[u8; 16]> {
|
||||||
|
let aes = AesAny::new(kek)?;
|
||||||
|
if wrapped.len() != 24 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut a: [u8; 8] = wrapped[..8].try_into().unwrap();
|
||||||
|
let mut r2: [u8; 8] = wrapped[8..16].try_into().unwrap();
|
||||||
|
let mut r3: [u8; 8] = wrapped[16..24].try_into().unwrap();
|
||||||
|
for t in (1..=12u64).rev() {
|
||||||
|
let prev_r2 = r2;
|
||||||
|
let tb = t.to_be_bytes();
|
||||||
|
let mut blk = [0u8; 16];
|
||||||
|
for i in 0..8 {
|
||||||
|
blk[i] = a[i] ^ tb[i];
|
||||||
|
}
|
||||||
|
blk[8..].copy_from_slice(&r3);
|
||||||
|
aes.decrypt(&mut blk);
|
||||||
|
a.copy_from_slice(&blk[..8]);
|
||||||
|
r2.copy_from_slice(&blk[8..]);
|
||||||
|
r3 = prev_r2;
|
||||||
|
}
|
||||||
|
if a != [0xA6; 8] {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out[..8].copy_from_slice(&r2);
|
||||||
|
out[8..].copy_from_slice(&r3);
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn b64(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let (mut acc, mut bits) = (0u32, 0);
|
||||||
|
for c in s.bytes() {
|
||||||
|
let v = match c {
|
||||||
|
b'A'..=b'Z' => c - b'A',
|
||||||
|
b'a'..=b'z' => c - b'a' + 26,
|
||||||
|
b'0'..=b'9' => c - b'0' + 52,
|
||||||
|
b'+' => 62,
|
||||||
|
b'/' => 63,
|
||||||
|
b'=' | b'\n' | b'\r' | b' ' | b'\t' => continue,
|
||||||
|
_ => return None,
|
||||||
|
} as u32;
|
||||||
|
acc = (acc << 6) | v;
|
||||||
|
bits += 6;
|
||||||
|
if bits >= 8 {
|
||||||
|
bits -= 8;
|
||||||
|
out.push((acc >> bits) as u8);
|
||||||
|
acc &= (1 << bits) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valor de `<key>NOME</key><tipo ...>VALOR</tipo>` num plist simples (primeira ocorrência com valor).
|
||||||
|
fn plist_value(xml: &str, key: &str) -> Option<String> {
|
||||||
|
let k = format!("<key>{key}</key>");
|
||||||
|
let mut from = 0;
|
||||||
|
while let Some(p) = xml[from..].find(&k) {
|
||||||
|
let rest = &xml[from + p + k.len()..];
|
||||||
|
from += p + k.len();
|
||||||
|
let Some(open) = rest.find('<').filter(|&o| o == 0) else { continue };
|
||||||
|
let Some(gt) = rest[open..].find('>') else { continue };
|
||||||
|
if rest[open..open + gt].ends_with('/') {
|
||||||
|
continue; // <reference .../>
|
||||||
|
}
|
||||||
|
let Some(te) = rest[open + 1..].find(|c: char| c == ' ' || c == '>') else { continue };
|
||||||
|
let tag = &rest[open + 1..open + 1 + te];
|
||||||
|
let vstart = open + gt + 1;
|
||||||
|
if let Some(e) = rest[vstart..].find(&format!("</{tag}>")) {
|
||||||
|
let v = rest[vstart..vstart + e].trim();
|
||||||
|
if !v.is_empty() {
|
||||||
|
return Some(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xml_of(blk: &[u8], off_pos: usize) -> Option<String> {
|
||||||
|
let off = le32(blk, off_pos) as usize;
|
||||||
|
let size = le32(blk, off_pos + 4) as usize;
|
||||||
|
if off >= MD_BLOCK || size > MD_BLOCK - off {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(String::from_utf8_lossy(&blk[off..off + size]).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let corrupt = |s: &str| UnlockError::Corrupt(s.to_string());
|
||||||
|
let h = read_vec(src.as_ref(), 0, 512).map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if !detect(&h) {
|
||||||
|
return Err(corrupt("não é um volume Core Storage"));
|
||||||
|
}
|
||||||
|
let block_size = le32(&h, 96) as u64;
|
||||||
|
let disklbl = le64(&h, 104);
|
||||||
|
if block_size == 0 || block_size > 1 << 20 || le32(&h, 168) != 16 {
|
||||||
|
return Err(UnlockError::Unsupported("Core Storage com chave de metadados que não é AES-128".into()));
|
||||||
|
}
|
||||||
|
let mut md_key = [0u8; 32];
|
||||||
|
md_key[..16].copy_from_slice(&h[176..192]);
|
||||||
|
md_key[16..].copy_from_slice(&h[304..320]);
|
||||||
|
let (k1, k2) = (AesAny::new(&md_key[..16]).unwrap(), AesAny::new(&md_key[16..]).unwrap());
|
||||||
|
let md_cipher = SectorCipher::Xts { k1, k2, iv: Iv::Plain64 };
|
||||||
|
|
||||||
|
let label = read_vec(src.as_ref(), disklbl * block_size, MD_BLOCK).map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if label.len() < MD_BLOCK {
|
||||||
|
return Err(corrupt("rótulo do disco truncado"));
|
||||||
|
}
|
||||||
|
let vgd_off = le32(&label, 220) as u64;
|
||||||
|
let vgd = read_vec(src.as_ref(), disklbl * block_size + vgd_off, 40).map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if vgd.len() < 40 {
|
||||||
|
return Err(corrupt("descritor de grupos truncado"));
|
||||||
|
}
|
||||||
|
let md_blocks = le64(&vgd, 8);
|
||||||
|
let md_start = le64(&vgd, 32) * block_size;
|
||||||
|
|
||||||
|
let (mut iters, mut salt, mut wkek, mut wvk) = (0u32, vec![], vec![], vec![]);
|
||||||
|
let (mut lv_size, mut lv_off) = (0u64, 0u64);
|
||||||
|
let mut family = [0u8; 16];
|
||||||
|
let mut got = 0u8;
|
||||||
|
for i in 0..md_blocks.min(64) {
|
||||||
|
let mut b = read_vec(src.as_ref(), md_start + i * MD_BLOCK as u64, MD_BLOCK).map_err(|e| corrupt(&e.to_string()))?;
|
||||||
|
if b.len() < MD_BLOCK || b.iter().all(|&x| x == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// XTS sobre o bloco inteiro, com o número do bloco como ajuste inicial.
|
||||||
|
md_cipher.decrypt_sector(&mut b, i, 0);
|
||||||
|
match u16::from_le_bytes([b[10], b[11]]) {
|
||||||
|
0x0019 => {
|
||||||
|
if let Some(xml) = xml_of(&b, 112) {
|
||||||
|
let pwk = plist_value(&xml, "PassphraseWrappedKEKStruct").and_then(|s| b64(&s));
|
||||||
|
let kw = plist_value(&xml, "KEKWrappedVolumeKeyStruct").and_then(|s| b64(&s));
|
||||||
|
if let (Some(p), Some(k)) = (pwk, kw) {
|
||||||
|
if p.len() >= 172 && k.len() >= 32 {
|
||||||
|
salt = p[8..24].to_vec();
|
||||||
|
wkek = p[32..56].to_vec();
|
||||||
|
iters = le32(&p, 168);
|
||||||
|
wvk = k[8..32].to_vec();
|
||||||
|
got |= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x001a => {
|
||||||
|
if let Some(xml) = xml_of(&b, 128) {
|
||||||
|
if let Some(v) = plist_value(&xml, "com.apple.corestorage.lv.size") {
|
||||||
|
lv_size = u64::from_str_radix(v.trim_start_matches("0x"), 16).unwrap_or(0);
|
||||||
|
got |= 8;
|
||||||
|
if let Some(u) = plist_value(&xml, "com.apple.corestorage.lv.familyUUID") {
|
||||||
|
let hex: String = u.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||||
|
if let Some(b) = super::from_hex(&hex).filter(|b| b.len() == 16) {
|
||||||
|
family.copy_from_slice(&b);
|
||||||
|
got |= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x0305 => {
|
||||||
|
lv_off = le32(&b, 104) as u64 * block_size;
|
||||||
|
got |= 4;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got != 7 | 8 {
|
||||||
|
return Err(corrupt("metadados Core Storage incompletos"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let finish = |vk16: [u8; 16]| -> UnlockResult {
|
||||||
|
let mut full = [0u8; 32];
|
||||||
|
full[..16].copy_from_slice(&vk16);
|
||||||
|
full[16..].copy_from_slice(&Sha256::new().chain_update(vk16).chain_update(family).finalize()[..16]);
|
||||||
|
let cipher = SectorCipher::Xts {
|
||||||
|
k1: AesAny::new(&full[..16]).unwrap(),
|
||||||
|
k2: AesAny::new(&full[16..]).unwrap(),
|
||||||
|
iv: Iv::Plain64,
|
||||||
|
};
|
||||||
|
let cs = CryptSource::new(src.clone(), cipher, lv_off, lv_size, 512);
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: "FileVault 2 (Core Storage, AES-128-XTS)".into(),
|
||||||
|
volume_key: Some(vk16.to_vec()),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for s in secrets {
|
||||||
|
if let Secret::VolumeKey(k) = s {
|
||||||
|
if k.len() >= 16 {
|
||||||
|
let mut a = [0u8; 16];
|
||||||
|
a.copy_from_slice(&k[..16]);
|
||||||
|
return finish(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for s in secrets {
|
||||||
|
if let Secret::Password(p) = s {
|
||||||
|
let mut pk = [0u8; 16];
|
||||||
|
pbkdf2::pbkdf2_hmac::<Sha256>(p.as_bytes(), &salt, iters.max(1), &mut pk);
|
||||||
|
if let Some(kek) = unwrap_key(&pk, &wkek) {
|
||||||
|
if let Some(vk) = unwrap_key(&kek, &wvk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(UnlockError::WrongSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock_window(base: Arc<dyn Source>, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//! FileVault 2 (Core Storage): imagem de referência do cryptsetup (senha heslo123).
|
||||||
|
|
||||||
|
use guara_core::crypt::{filevault, Secret, UnlockError};
|
||||||
|
use guara_core::hashing::MultiHasher;
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opens_reference_image_and_matches_known_hash() {
|
||||||
|
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/fvault2-images/small");
|
||||||
|
if !p.exists() {
|
||||||
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let src: Arc<dyn Source> = Arc::new(FileSource::open(&p).unwrap());
|
||||||
|
assert!(matches!(
|
||||||
|
filevault::unlock(src.clone(), &[Secret::Password("errada".into())]),
|
||||||
|
Err(UnlockError::WrongSecret)
|
||||||
|
));
|
||||||
|
let u = filevault::unlock(src, &[Secret::Password("heslo123".into())]).unwrap();
|
||||||
|
assert_eq!(u.source.size(), 167_772_160);
|
||||||
|
assert_eq!(
|
||||||
|
guara_core::crypt::to_hex(u.volume_key.as_ref().unwrap()),
|
||||||
|
"20734d3389212774d7610c29d732880 9".replace(' ', "")
|
||||||
|
);
|
||||||
|
let mut h = MultiHasher::new();
|
||||||
|
let mut off = 0;
|
||||||
|
let mut buf = vec![0u8; 1 << 20];
|
||||||
|
while off < u.source.size() {
|
||||||
|
let n = u.source.read_at(off, &mut buf).unwrap();
|
||||||
|
h.update(&buf[..n]);
|
||||||
|
off += n as u64;
|
||||||
|
}
|
||||||
|
assert_eq!(h.finalize().sha256, "2c662e36c0f7e2f5583e6a939bbcbdc660805692d0fccaa45ad4052beb3b8e18");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user