ext4 com diário e entradas apagadas; LUKS1/2 e BitLocker (todas as imagens de referência do cryptsetup)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1ac320193
commit
86344c0536
15 files changed
+2583
-80
No files matched your search
@@ -20,6 +20,11 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"]
|
|||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
bzip2 = "0.6"
|
bzip2 = "0.6"
|
||||||
|
aes = "0.8"
|
||||||
|
hmac = "0.12"
|
||||||
|
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
||||||
|
ripemd = "0.1"
|
||||||
|
argon2 = "0.5"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|||||||
@@ -0,0 +1,470 @@
|
|||||||
|
//! BitLocker (Windows 7 em diante, inclusive BitLocker To Go): AES-CBC com ou sem difusor
|
||||||
|
//! Elephant, e AES-XTS. Protetores: senha, chave de recuperação, chave de inicialização (.BEK),
|
||||||
|
//! chave limpa (proteção suspensa) e chave do volume já conhecida. TPM não é suportado.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::source::{read_vec, SubSource};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> usize {
|
||||||
|
u16::from_le_bytes([b[o], b[o + 1]]) as usize
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
const GUID_NORMAL: [u8; 16] = [0x3b, 0xd6, 0x67, 0x49, 0x29, 0x2e, 0xd8, 0x4a, 0x83, 0x99, 0xf6, 0xa3, 0x39, 0xe3, 0xd0, 0x01];
|
||||||
|
const GUID_EOW: [u8; 16] = [0x3b, 0x4d, 0xa8, 0x92, 0x80, 0xdd, 0x0e, 0x4d, 0x9e, 0x4e, 0xb1, 0xe3, 0x28, 0x4e, 0xae, 0xd8];
|
||||||
|
|
||||||
|
/// Posição da tabela de deslocamentos FVE, se for BitLocker (normal ou To Go).
|
||||||
|
fn offsets_pos(vbr: &[u8]) -> Option<usize> {
|
||||||
|
if vbr.len() >= 11 && &vbr[3..11] == b"-FVE-FS-" {
|
||||||
|
return Some(176);
|
||||||
|
}
|
||||||
|
// BitLocker To Go: parece FAT ("MSWIN4.1"), mas guarda o GUID do BitLocker no setor de boot.
|
||||||
|
if vbr.len() >= 464 && (vbr[424..440] == GUID_NORMAL || vbr[424..440] == GUID_EOW) {
|
||||||
|
return Some(440);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(vbr: &[u8]) -> bool {
|
||||||
|
offsets_pos(vbr).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- Elephant
|
||||||
|
|
||||||
|
fn rotl(x: u32, n: u32) -> u32 {
|
||||||
|
x.rotate_left(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RA: [u32; 4] = [9, 0, 13, 0];
|
||||||
|
const RB: [u32; 4] = [0, 10, 0, 25];
|
||||||
|
|
||||||
|
fn words(buf: &[u8]) -> Vec<u32> {
|
||||||
|
buf.chunks_exact(4).map(|c| u32::from_le_bytes(c.try_into().unwrap())).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decifragem (ordem do dm-crypt do Linux): somas em ordem crescente.
|
||||||
|
fn diffuser_a_decrypt(d: &mut [u32]) {
|
||||||
|
let n = d.len();
|
||||||
|
for _ in 0..5 {
|
||||||
|
for i in 0..n {
|
||||||
|
let a = d[(i + n - 2) % n];
|
||||||
|
let b = rotl(d[(i + n - 5) % n], RA[i % 4]);
|
||||||
|
d[i] = d[i].wrapping_add(a ^ b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diffuser_b_decrypt(d: &mut [u32]) {
|
||||||
|
let n = d.len();
|
||||||
|
for _ in 0..3 {
|
||||||
|
for i in 0..n {
|
||||||
|
let a = d[(i + 2) % n];
|
||||||
|
let b = rotl(d[(i + 5) % n], RB[i % 4]);
|
||||||
|
d[i] = d[i].wrapping_add(a ^ b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decifra um setor AES-CBC + difusor Elephant (BitLocker Vista/7 sem XTS).
|
||||||
|
pub fn elephant_decrypt(key: &AesAny, tweak: &AesAny, buf: &mut [u8], byte_off: u64) {
|
||||||
|
let mut iv = [0u8; 16];
|
||||||
|
iv[..8].copy_from_slice(&byte_off.to_le_bytes());
|
||||||
|
key.encrypt(&mut iv);
|
||||||
|
cbc_decrypt(key, &iv, buf);
|
||||||
|
let mut d = words(buf);
|
||||||
|
diffuser_b_decrypt(&mut d);
|
||||||
|
diffuser_a_decrypt(&mut d);
|
||||||
|
for (i, w) in d.iter().enumerate() {
|
||||||
|
buf[i * 4..i * 4 + 4].copy_from_slice(&w.to_le_bytes());
|
||||||
|
}
|
||||||
|
// Chave do setor: duas cifragens do deslocamento com a chave de ajuste.
|
||||||
|
let mut k0 = [0u8; 16];
|
||||||
|
k0[..8].copy_from_slice(&byte_off.to_le_bytes());
|
||||||
|
let mut k1 = k0;
|
||||||
|
k1[15] = 0x80;
|
||||||
|
tweak.encrypt(&mut k0);
|
||||||
|
tweak.encrypt(&mut k1);
|
||||||
|
let mut sk = [0u8; 32];
|
||||||
|
sk[..16].copy_from_slice(&k0);
|
||||||
|
sk[16..].copy_from_slice(&k1);
|
||||||
|
for (i, b) in buf.iter_mut().enumerate() {
|
||||||
|
*b ^= sk[i % 32];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ CCM
|
||||||
|
|
||||||
|
/// AES-CCM (nonce de 12 bytes, tag de 16): `blob` = tag cifrada (16) + dados cifrados.
|
||||||
|
fn ccm_decrypt(key: &AesAny, nonce: &[u8], blob: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
if nonce.len() != 12 || blob.len() < 16 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ctr = |i: u32| -> [u8; 16] {
|
||||||
|
let mut a = [0u8; 16];
|
||||||
|
a[0] = 2;
|
||||||
|
a[1..13].copy_from_slice(nonce);
|
||||||
|
a[13..16].copy_from_slice(&i.to_be_bytes()[1..]);
|
||||||
|
key.encrypt(&mut a);
|
||||||
|
a
|
||||||
|
};
|
||||||
|
let s0 = ctr(0);
|
||||||
|
let mut tag = [0u8; 16];
|
||||||
|
for i in 0..16 {
|
||||||
|
tag[i] = blob[i] ^ s0[i];
|
||||||
|
}
|
||||||
|
let ct = &blob[16..];
|
||||||
|
let mut pt = Vec::with_capacity(ct.len());
|
||||||
|
for (bi, ch) in ct.chunks(16).enumerate() {
|
||||||
|
let s = ctr(bi as u32 + 1);
|
||||||
|
pt.extend(ch.iter().zip(s.iter()).map(|(a, b)| a ^ b));
|
||||||
|
}
|
||||||
|
// CBC-MAC sobre B0 + dados.
|
||||||
|
let mut b0 = [0u8; 16];
|
||||||
|
b0[0] = 0x3A;
|
||||||
|
b0[1..13].copy_from_slice(nonce);
|
||||||
|
b0[13..16].copy_from_slice(&(pt.len() as u32).to_be_bytes()[1..]);
|
||||||
|
key.encrypt(&mut b0);
|
||||||
|
let mut x = b0;
|
||||||
|
for ch in pt.chunks(16) {
|
||||||
|
for i in 0..ch.len() {
|
||||||
|
x[i] ^= ch[i];
|
||||||
|
}
|
||||||
|
key.encrypt(&mut x);
|
||||||
|
}
|
||||||
|
(x == tag).then_some(pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------- chaves
|
||||||
|
|
||||||
|
fn stretch(initial: &[u8; 32], salt: &[u8]) -> [u8; 32] {
|
||||||
|
let mut last = [0u8; 32];
|
||||||
|
let mut buf = [0u8; 88];
|
||||||
|
buf[32..64].copy_from_slice(initial);
|
||||||
|
buf[64..80].copy_from_slice(salt);
|
||||||
|
for i in 0u64..0x10_0000 {
|
||||||
|
buf[..32].copy_from_slice(&last);
|
||||||
|
buf[80..88].copy_from_slice(&i.to_le_bytes());
|
||||||
|
last.copy_from_slice(&Sha256::digest(buf));
|
||||||
|
}
|
||||||
|
last
|
||||||
|
}
|
||||||
|
|
||||||
|
fn password_key(pw: &str, salt: &[u8]) -> [u8; 32] {
|
||||||
|
let utf16: Vec<u8> = pw.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
|
||||||
|
let h1 = Sha256::digest(&utf16);
|
||||||
|
let mut init = [0u8; 32];
|
||||||
|
init.copy_from_slice(&Sha256::digest(h1));
|
||||||
|
stretch(&init, salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chave de recuperação: 8 blocos de 6 dígitos, cada um múltiplo de 11.
|
||||||
|
fn recovery_key(rp: &str, salt: &[u8]) -> Option<[u8; 32]> {
|
||||||
|
let digits: String = rp.chars().filter(|c| c.is_ascii_digit()).collect();
|
||||||
|
if digits.len() != 48 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut raw = [0u8; 16];
|
||||||
|
for i in 0..8 {
|
||||||
|
let n: u32 = digits[i * 6..i * 6 + 6].parse().ok()?;
|
||||||
|
if n % 11 != 0 || n / 11 > 0xFFFF {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
raw[i * 2..i * 2 + 2].copy_from_slice(&((n / 11) as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
let mut init = [0u8; 32];
|
||||||
|
init.copy_from_slice(&Sha256::digest(raw));
|
||||||
|
Some(stretch(&init, salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------- metadados
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Entry {
|
||||||
|
etype: usize,
|
||||||
|
vtype: usize,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_entries(b: &[u8]) -> Vec<Entry> {
|
||||||
|
let mut v = vec![];
|
||||||
|
let mut o = 0;
|
||||||
|
while o + 8 <= b.len() {
|
||||||
|
let size = le16(b, o);
|
||||||
|
if size < 8 || o + size > b.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
v.push(Entry {
|
||||||
|
etype: le16(b, o + 2),
|
||||||
|
vtype: le16(b, o + 4),
|
||||||
|
data: b[o + 8..o + size].to_vec(),
|
||||||
|
});
|
||||||
|
o += size;
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Vmk {
|
||||||
|
protection: usize,
|
||||||
|
nested: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vmk {
|
||||||
|
fn ccm(&self) -> Option<&Entry> {
|
||||||
|
self.nested.iter().find(|e| e.vtype == 5)
|
||||||
|
}
|
||||||
|
fn stretch_salt(&self) -> Option<&[u8]> {
|
||||||
|
self.nested.iter().find(|e| e.vtype == 3).filter(|e| e.data.len() >= 20).map(|e| &e.data[4..20])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fve {
|
||||||
|
method: usize,
|
||||||
|
volume_header_offset: u64,
|
||||||
|
volume_header_size: u64,
|
||||||
|
encrypted_size: u64,
|
||||||
|
meta_size: u64,
|
||||||
|
vmks: Vec<Vmk>,
|
||||||
|
fvek: Option<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_fve(src: &dyn Source, offset: u64) -> Option<Fve> {
|
||||||
|
let h = read_vec(src, offset, 64 + 48).ok()?;
|
||||||
|
if h.len() < 112 || &h[..8] != b"-FVE-FS-" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let version = le16(&h, 10);
|
||||||
|
if version != 2 {
|
||||||
|
return None; // Vista (versão 1) não suportado
|
||||||
|
}
|
||||||
|
let encrypted_size = le64(&h, 16);
|
||||||
|
let hdr_sectors = le32(&h, 28) as u64;
|
||||||
|
let volume_header_offset = le64(&h, 56);
|
||||||
|
let meta_size = le32(&h, 64) as usize;
|
||||||
|
if !(48..=1 << 20).contains(&meta_size) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let method = le16(&h, 64 + 36);
|
||||||
|
let all = read_vec(src, offset + 64, meta_size).ok()?;
|
||||||
|
if all.len() < meta_size {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let entries = parse_entries(&all[48..]);
|
||||||
|
let mut vmks = vec![];
|
||||||
|
let mut fvek = None;
|
||||||
|
let (mut vh_off, mut vh_size) = (volume_header_offset, hdr_sectors * 512);
|
||||||
|
for e in entries {
|
||||||
|
match (e.etype, e.vtype) {
|
||||||
|
(15, 15) if e.data.len() >= 16 => {
|
||||||
|
vh_off = le64(&e.data, 0);
|
||||||
|
vh_size = le64(&e.data, 8);
|
||||||
|
}
|
||||||
|
(2, 8) if e.data.len() > 28 => vmks.push(Vmk {
|
||||||
|
protection: le16(&e.data, 26),
|
||||||
|
nested: parse_entries(&e.data[28..]),
|
||||||
|
}),
|
||||||
|
(3, 5) => fvek = Some(e),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Fve {
|
||||||
|
method,
|
||||||
|
volume_header_offset: vh_off,
|
||||||
|
volume_header_size: vh_size,
|
||||||
|
encrypted_size,
|
||||||
|
meta_size: 0x10000,
|
||||||
|
vmks,
|
||||||
|
fvek,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chave de um arquivo .BEK (chave de inicialização).
|
||||||
|
fn bek_key(file: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
if file.len() < 48 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let es = parse_entries(&file[48..]);
|
||||||
|
for e in &es {
|
||||||
|
if e.vtype == 9 && e.data.len() > 24 {
|
||||||
|
for n in parse_entries(&e.data[24..]) {
|
||||||
|
if n.vtype == 1 && n.data.len() >= 36 {
|
||||||
|
return Some(n.data[4..36].to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_keys(v: &Vmk, secrets: &[Secret]) -> Vec<Vec<u8>> {
|
||||||
|
let mut out: Vec<Vec<u8>> = vec![];
|
||||||
|
// Chave limpa: o próprio protetor guarda a chave em claro.
|
||||||
|
if v.protection == 0 {
|
||||||
|
for n in &v.nested {
|
||||||
|
if n.vtype == 1 && n.data.len() >= 36 {
|
||||||
|
out.push(n.data[4..36].to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for s in secrets {
|
||||||
|
match s {
|
||||||
|
Secret::Password(p) if v.protection == 0x2000 => {
|
||||||
|
if let Some(salt) = v.stretch_salt() {
|
||||||
|
out.push(password_key(p, salt).to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Secret::RecoveryKey(r) if v.protection == 0x0800 => {
|
||||||
|
if let Some(k) = v.stretch_salt().and_then(|salt| recovery_key(r, salt)) {
|
||||||
|
out.push(k.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Secret::KeyFile(f) if v.protection == 0x0200 => {
|
||||||
|
if let Some(k) = bek_key(f) {
|
||||||
|
out.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_key_entry(key: &[u8], e: &Entry) -> Option<Vec<u8>> {
|
||||||
|
let aes = AesAny::new(key)?;
|
||||||
|
if e.data.len() < 12 + 16 + 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let pt = ccm_decrypt(&aes, &e.data[..12], &e.data[12..])?;
|
||||||
|
// payload = cabeçalho de entrada (8) + método (4) + chave
|
||||||
|
(pt.len() > 12).then(|| pt[12..].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let vbr = read_vec(src.as_ref(), 0, 512).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if !detect(&vbr) || vbr.len() < 512 {
|
||||||
|
return Err(UnlockError::Corrupt("não é BitLocker".into()));
|
||||||
|
}
|
||||||
|
let bps = le16(&vbr, 11).max(512) as u64;
|
||||||
|
let pos = offsets_pos(&vbr).unwrap_or(176);
|
||||||
|
// Três cópias dos metadados; usa a primeira legível.
|
||||||
|
let mut fve = None;
|
||||||
|
let mut meta_offsets = vec![];
|
||||||
|
for i in 0..3 {
|
||||||
|
let off = le64(&vbr, pos + i * 8);
|
||||||
|
if off != 0 {
|
||||||
|
meta_offsets.push(off);
|
||||||
|
}
|
||||||
|
if off != 0 && off < src.size() {
|
||||||
|
if let Some(f) = read_fve(src.as_ref(), off) {
|
||||||
|
fve = Some(f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fve = fve.ok_or_else(|| UnlockError::Unsupported("metadados FVE ilegíveis ou do Windows Vista".into()))?;
|
||||||
|
let fvek_entry = fve.fvek.as_ref().ok_or_else(|| UnlockError::Corrupt("sem FVEK".into()))?;
|
||||||
|
|
||||||
|
let mut volume_key: Option<Vec<u8>> = None;
|
||||||
|
for s in secrets {
|
||||||
|
if let Secret::VolumeKey(k) = s {
|
||||||
|
volume_key = Some(k.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut vmk_found: Option<Vec<u8>> = None;
|
||||||
|
if volume_key.is_none() {
|
||||||
|
'outer: for v in &fve.vmks {
|
||||||
|
let Some(ccm) = v.ccm() else { continue };
|
||||||
|
for k in candidate_keys(v, secrets) {
|
||||||
|
if let Some(vmk) = decrypt_key_entry(&k, ccm) {
|
||||||
|
if vmk.len() >= 32 {
|
||||||
|
vmk_found = Some(vmk[..32].to_vec());
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fvek_bytes = if let Some(k) = volume_key {
|
||||||
|
k
|
||||||
|
} else {
|
||||||
|
let vmk = vmk_found.ok_or(UnlockError::WrongSecret)?;
|
||||||
|
decrypt_key_entry(&vmk, fvek_entry).ok_or_else(|| UnlockError::Corrupt("FVEK não decifra com a VMK".into()))?
|
||||||
|
};
|
||||||
|
let k = &fvek_bytes;
|
||||||
|
let bad = || UnlockError::Corrupt("tamanho de chave incompatível com o método".into());
|
||||||
|
let (cipher, name) = match fve.method {
|
||||||
|
0x8000 | 0x8001 => {
|
||||||
|
let n = if fve.method == 0x8000 { 16 } else { 32 };
|
||||||
|
if k.len() < 32 + n {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
(
|
||||||
|
SectorCipher::Elephant {
|
||||||
|
key: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
||||||
|
tweak: AesAny::new(&k[32..32 + n]).ok_or_else(bad)?,
|
||||||
|
},
|
||||||
|
if n == 16 { "AES-128-CBC + difusor" } else { "AES-256-CBC + difusor" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
0x8002 | 0x8003 => {
|
||||||
|
let n = if fve.method == 0x8002 { 16 } else { 32 };
|
||||||
|
if k.len() < n {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let key = AesAny::new(&k[..n]).ok_or_else(bad)?;
|
||||||
|
(SectorCipher::Cbc { key: key.clone(), iv: Iv::Eboiv(key) }, if n == 16 { "AES-128-CBC" } else { "AES-256-CBC" })
|
||||||
|
}
|
||||||
|
0x8004 | 0x8005 => {
|
||||||
|
let n = if fve.method == 0x8004 { 16 } else { 32 };
|
||||||
|
if k.len() < 2 * n {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let iv = if bps > 512 { Iv::Unit(bps) } else { Iv::Plain64 };
|
||||||
|
(
|
||||||
|
SectorCipher::Xts {
|
||||||
|
k1: AesAny::new(&k[..n]).ok_or_else(bad)?,
|
||||||
|
k2: AesAny::new(&k[n..2 * n]).ok_or_else(bad)?,
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
if n == 16 { "AES-128-XTS" } else { "AES-256-XTS" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
m => return Err(UnlockError::Unsupported(format!("método de cifragem BitLocker {m:#x}"))),
|
||||||
|
};
|
||||||
|
let len = src.size();
|
||||||
|
let mut cs = CryptSource::new(src.clone(), cipher, 0, len, bps);
|
||||||
|
let mut patches = vec![];
|
||||||
|
// Áreas de metadados FVE: o Windows não as expõe como dados.
|
||||||
|
for off in meta_offsets {
|
||||||
|
patches.push(Patch { at: off, len: fve.meta_size, from: 0, decrypt_from: None, zero: true });
|
||||||
|
}
|
||||||
|
if fve.volume_header_size > 0 && fve.volume_header_offset > 0 {
|
||||||
|
// O início do volume original foi movido para outro lugar e cifrado lá.
|
||||||
|
patches.push(Patch { at: fve.volume_header_offset, len: fve.volume_header_size, from: 0, decrypt_from: None, zero: true });
|
||||||
|
patches.push(Patch {
|
||||||
|
at: 0,
|
||||||
|
len: fve.volume_header_size,
|
||||||
|
from: fve.volume_header_offset,
|
||||||
|
decrypt_from: Some(fve.volume_header_offset),
|
||||||
|
zero: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cs = cs.with_patches(patches);
|
||||||
|
if fve.encrypted_size > 0 && fve.encrypted_size < len {
|
||||||
|
cs = cs.with_plain_from(fve.encrypted_size);
|
||||||
|
}
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: format!("BitLocker ({name})"),
|
||||||
|
volume_key: Some(fvek_bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock_window(base: Arc<dyn Source>, start: u64, len: u64, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
unlock(Arc::new(SubSource::new(base, start, len)), secrets)
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,418 @@
|
|||||||
|
//! LUKS1 e LUKS2 (cifras AES; PBKDF2 e Argon2; hashes SHA-1/256/512 e RIPEMD-160).
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::source::{read_vec, SubSource};
|
||||||
|
use sha1::Sha1;
|
||||||
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
|
pub const MAGIC: &[u8; 6] = b"LUKS\xBA\xBE";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Hash {
|
||||||
|
Sha1,
|
||||||
|
Sha256,
|
||||||
|
Sha512,
|
||||||
|
Ripemd160,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash {
|
||||||
|
pub fn parse(s: &str) -> Option<Hash> {
|
||||||
|
Some(match s.to_ascii_lowercase().as_str() {
|
||||||
|
"sha1" => Hash::Sha1,
|
||||||
|
"sha256" => Hash::Sha256,
|
||||||
|
"sha512" => Hash::Sha512,
|
||||||
|
"ripemd160" | "rmd160" => Hash::Ripemd160,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn size(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Hash::Sha1 | Hash::Ripemd160 => 20,
|
||||||
|
Hash::Sha256 => 32,
|
||||||
|
Hash::Sha512 => 64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn digest(self, parts: &[&[u8]]) -> Vec<u8> {
|
||||||
|
fn run<D: Digest>(parts: &[&[u8]]) -> Vec<u8> {
|
||||||
|
let mut d = D::new();
|
||||||
|
for p in parts {
|
||||||
|
d.update(p);
|
||||||
|
}
|
||||||
|
d.finalize().to_vec()
|
||||||
|
}
|
||||||
|
match self {
|
||||||
|
Hash::Sha1 => run::<Sha1>(parts),
|
||||||
|
Hash::Sha256 => run::<Sha256>(parts),
|
||||||
|
Hash::Sha512 => run::<Sha512>(parts),
|
||||||
|
Hash::Ripemd160 => run::<ripemd::Ripemd160>(parts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn pbkdf2(self, pass: &[u8], salt: &[u8], iterations: u32, len: usize) -> Vec<u8> {
|
||||||
|
let mut out = vec![0u8; len];
|
||||||
|
let it = iterations.max(1);
|
||||||
|
match self {
|
||||||
|
Hash::Sha1 => pbkdf2::pbkdf2_hmac::<Sha1>(pass, salt, it, &mut out),
|
||||||
|
Hash::Sha256 => pbkdf2::pbkdf2_hmac::<Sha256>(pass, salt, it, &mut out),
|
||||||
|
Hash::Sha512 => pbkdf2::pbkdf2_hmac::<Sha512>(pass, salt, it, &mut out),
|
||||||
|
Hash::Ripemd160 => pbkdf2::pbkdf2_hmac::<ripemd::Ripemd160>(pass, salt, it, &mut out),
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Difusão do "anti-forensic splitter".
|
||||||
|
fn diffuse(block: &mut [u8], h: Hash) {
|
||||||
|
let ds = h.size();
|
||||||
|
let mut i = 0u32;
|
||||||
|
let mut off = 0;
|
||||||
|
while off < block.len() {
|
||||||
|
let n = ds.min(block.len() - off);
|
||||||
|
let d = h.digest(&[&i.to_be_bytes(), &block[off..off + n]]);
|
||||||
|
block[off..off + n].copy_from_slice(&d[..n]);
|
||||||
|
off += n;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompõe a chave a partir das `stripes` faixas (AFmerge).
|
||||||
|
pub fn af_merge(src: &[u8], block: usize, stripes: usize, h: Hash) -> Vec<u8> {
|
||||||
|
let mut buf = vec![0u8; block];
|
||||||
|
for i in 0..stripes.saturating_sub(1) {
|
||||||
|
for (b, s) in buf.iter_mut().zip(&src[i * block..(i + 1) * block]) {
|
||||||
|
*b ^= *s;
|
||||||
|
}
|
||||||
|
diffuse(&mut buf, h);
|
||||||
|
}
|
||||||
|
let last = &src[(stripes - 1) * block..stripes * block];
|
||||||
|
buf.iter().zip(last).map(|(a, b)| a ^ b).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cifra descrita por texto como "aes-xts-plain64" ou "aes-cbc-essiv:sha256".
|
||||||
|
pub fn parse_cipher(spec: &str, key: &[u8]) -> std::result::Result<SectorCipher, UnlockError> {
|
||||||
|
let (alg, rest) = spec.split_once('-').ok_or_else(|| UnlockError::Unsupported(spec.into()))?;
|
||||||
|
if !alg.eq_ignore_ascii_case("aes") {
|
||||||
|
return Err(UnlockError::Unsupported(format!("cifra {alg} (apenas AES)")));
|
||||||
|
}
|
||||||
|
let (mode, ivs) = rest.split_once('-').unwrap_or((rest, "plain"));
|
||||||
|
let bad = || UnlockError::Unsupported(format!("chave inválida para {spec}"));
|
||||||
|
let mk_iv = |k: &[u8]| -> std::result::Result<Iv, UnlockError> {
|
||||||
|
let ivl = ivs.to_ascii_lowercase();
|
||||||
|
Ok(match ivl.as_str() {
|
||||||
|
"plain64" => Iv::Plain64,
|
||||||
|
"plain" | "plain32" => Iv::Plain32,
|
||||||
|
"plain64be" => Iv::Plain64Be,
|
||||||
|
s if s.starts_with("essiv:") => {
|
||||||
|
let h = Hash::parse(&s[6..]).ok_or_else(|| UnlockError::Unsupported(spec.into()))?;
|
||||||
|
Iv::Essiv(AesAny::new(&h.digest(&[k])).ok_or_else(bad)?)
|
||||||
|
}
|
||||||
|
_ => return Err(UnlockError::Unsupported(format!("IV {ivs}"))),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
match mode.to_ascii_lowercase().as_str() {
|
||||||
|
"xts" => {
|
||||||
|
if key.len() % 2 != 0 {
|
||||||
|
return Err(bad());
|
||||||
|
}
|
||||||
|
let h = key.len() / 2;
|
||||||
|
Ok(SectorCipher::Xts {
|
||||||
|
k1: AesAny::new(&key[..h]).ok_or_else(bad)?,
|
||||||
|
k2: AesAny::new(&key[h..]).ok_or_else(bad)?,
|
||||||
|
iv: mk_iv(&key[..h])?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"cbc" => Ok(SectorCipher::Cbc {
|
||||||
|
key: AesAny::new(key).ok_or_else(bad)?,
|
||||||
|
iv: mk_iv(key)?,
|
||||||
|
}),
|
||||||
|
m => Err(UnlockError::Unsupported(format!("modo {m}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn passwords(secrets: &[Secret]) -> Vec<Vec<u8>> {
|
||||||
|
secrets
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| match s {
|
||||||
|
Secret::Password(p) => Some(p.as_bytes().to_vec()),
|
||||||
|
Secret::KeyFile(k) => Some(k.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn volume_keys(secrets: &[Secret]) -> Vec<Vec<u8>> {
|
||||||
|
secrets
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| match s {
|
||||||
|
Secret::VolumeKey(k) => Some(k.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect(head: &[u8]) -> bool {
|
||||||
|
head.len() >= 8 && &head[..6] == MAGIC
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let head = read_vec(src.as_ref(), 0, 8).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if !detect(&head) {
|
||||||
|
return Err(UnlockError::Corrupt("não é LUKS".into()));
|
||||||
|
}
|
||||||
|
match u16::from_be_bytes([head[6], head[7]]) {
|
||||||
|
1 => unlock_v1(src, secrets),
|
||||||
|
2 => unlock_v2(src, secrets),
|
||||||
|
v => Err(UnlockError::Unsupported(format!("LUKS versão {v}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn be32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_be_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cstr(b: &[u8]) -> String {
|
||||||
|
let n = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||||
|
String::from_utf8_lossy(&b[..n]).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_v1(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let h = read_vec(src.as_ref(), 0, 592).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if h.len() < 592 {
|
||||||
|
return Err(UnlockError::Corrupt("cabeçalho LUKS1 truncado".into()));
|
||||||
|
}
|
||||||
|
let cipher_name = cstr(&h[8..40]);
|
||||||
|
let cipher_mode = cstr(&h[40..72]);
|
||||||
|
let hash = Hash::parse(&cstr(&h[72..104])).ok_or_else(|| UnlockError::Unsupported(format!("hash {}", cstr(&h[72..104]))))?;
|
||||||
|
let payload = be32(&h, 104) as u64 * 512;
|
||||||
|
let key_bytes = be32(&h, 108) as usize;
|
||||||
|
let mk_digest = &h[112..132];
|
||||||
|
let mk_salt = &h[132..164];
|
||||||
|
let mk_iter = be32(&h, 164);
|
||||||
|
let spec = format!("{cipher_name}-{cipher_mode}");
|
||||||
|
if key_bytes == 0 || key_bytes > 128 {
|
||||||
|
return Err(UnlockError::Corrupt("tamanho de chave inválido".into()));
|
||||||
|
}
|
||||||
|
let check = |mk: &[u8]| hash.pbkdf2(mk, mk_salt, mk_iter, 20) == mk_digest;
|
||||||
|
let finish = |mk: Vec<u8>| -> UnlockResult {
|
||||||
|
let cipher = parse_cipher(&spec, &mk)?;
|
||||||
|
let len = src.size().saturating_sub(payload);
|
||||||
|
let cs = CryptSource::new(src.clone(), cipher, payload, len, 512);
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: format!("LUKS1 ({spec})"),
|
||||||
|
volume_key: Some(mk),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for vk in volume_keys(secrets) {
|
||||||
|
if vk.len() == key_bytes && check(&vk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pws = passwords(secrets);
|
||||||
|
for slot in 0..8 {
|
||||||
|
let o = 208 + slot * 48;
|
||||||
|
if be32(&h, o) != 0x00AC_71F3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let iter = be32(&h, o + 4);
|
||||||
|
let salt = &h[o + 8..o + 40];
|
||||||
|
let km_off = be32(&h, o + 40) as u64 * 512;
|
||||||
|
let stripes = be32(&h, o + 44) as usize;
|
||||||
|
if stripes == 0 || stripes > 100_000 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let km_len = (key_bytes * stripes).div_ceil(512) * 512;
|
||||||
|
let Ok(mut km) = read_vec(src.as_ref(), km_off, km_len) else { continue };
|
||||||
|
if km.len() < km_len {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for pw in &pws {
|
||||||
|
let dk = hash.pbkdf2(pw, salt, iter, key_bytes);
|
||||||
|
let cipher = parse_cipher(&spec, &dk)?;
|
||||||
|
let mut m = km.clone();
|
||||||
|
for (i, s) in m.chunks_exact_mut(512).enumerate() {
|
||||||
|
cipher.decrypt_sector(s, i as u64, i as u64 * 512);
|
||||||
|
}
|
||||||
|
let mk = af_merge(&m, key_bytes, stripes, hash);
|
||||||
|
if check(&mk) {
|
||||||
|
return finish(mk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
km.clear();
|
||||||
|
}
|
||||||
|
Err(UnlockError::WrongSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn b64(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut acc = 0u32;
|
||||||
|
let mut bits = 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' => 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jstr<'a>(v: &'a serde_json::Value, k: &str) -> Option<&'a str> {
|
||||||
|
v.get(k)?.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Números no JSON do LUKS2 vêm como texto (inteiros grandes) ou número.
|
||||||
|
fn jnum(v: &serde_json::Value, k: &str) -> Option<u64> {
|
||||||
|
let x = v.get(k)?;
|
||||||
|
x.as_u64().or_else(|| x.as_str()?.parse().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock_v2(src: Arc<dyn Source>, secrets: &[Secret]) -> UnlockResult {
|
||||||
|
let h = read_vec(src.as_ref(), 0, 4096).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
if h.len() < 4096 {
|
||||||
|
return Err(UnlockError::Corrupt("cabeçalho LUKS2 truncado".into()));
|
||||||
|
}
|
||||||
|
let hdr_size = u64::from_be_bytes(h[8..16].try_into().unwrap());
|
||||||
|
if !(16384..=(4 << 20)).contains(&hdr_size) {
|
||||||
|
return Err(UnlockError::Corrupt("tamanho de cabeçalho LUKS2 inválido".into()));
|
||||||
|
}
|
||||||
|
let area = read_vec(src.as_ref(), 4096, (hdr_size - 4096) as usize).map_err(|e| UnlockError::Corrupt(e.to_string()))?;
|
||||||
|
let end = area.iter().position(|&c| c == 0).unwrap_or(area.len());
|
||||||
|
let json: serde_json::Value = serde_json::from_slice(&area[..end]).map_err(|e| UnlockError::Corrupt(format!("JSON: {e}")))?;
|
||||||
|
let segments = json.get("segments").and_then(|s| s.as_object()).ok_or_else(|| UnlockError::Corrupt("sem segments".into()))?;
|
||||||
|
let (_, seg) = segments
|
||||||
|
.iter()
|
||||||
|
.find(|(_, s)| jstr(s, "type") == Some("crypt"))
|
||||||
|
.ok_or_else(|| UnlockError::Unsupported("sem segmento cifrado".into()))?;
|
||||||
|
let seg_off = jnum(seg, "offset").ok_or_else(|| UnlockError::Corrupt("offset do segmento".into()))?;
|
||||||
|
let seg_len = match seg.get("size").and_then(|s| s.as_str()) {
|
||||||
|
Some("dynamic") | None => src.size().saturating_sub(seg_off),
|
||||||
|
Some(n) => n.parse().unwrap_or(0),
|
||||||
|
};
|
||||||
|
let seg_enc = jstr(seg, "encryption").unwrap_or("aes-xts-plain64").to_string();
|
||||||
|
let sector = jnum(seg, "sector_size").unwrap_or(512);
|
||||||
|
let iv_tweak = jnum(seg, "iv_tweak").unwrap_or(0);
|
||||||
|
let digests = json.get("digests").and_then(|d| d.as_object()).ok_or_else(|| UnlockError::Corrupt("sem digests".into()))?;
|
||||||
|
|
||||||
|
let check = |vk: &[u8]| -> bool {
|
||||||
|
for (_, d) in digests {
|
||||||
|
if jstr(d, "type") != Some("pbkdf2") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (Some(hash), Some(salt), Some(want), Some(it)) = (
|
||||||
|
jstr(d, "hash").and_then(Hash::parse),
|
||||||
|
jstr(d, "salt").and_then(b64),
|
||||||
|
jstr(d, "digest").and_then(b64),
|
||||||
|
jnum(d, "iterations"),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if hash.pbkdf2(vk, &salt, it as u32, want.len()) == want {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
};
|
||||||
|
let finish = |vk: Vec<u8>| -> UnlockResult {
|
||||||
|
let cipher = parse_cipher(&seg_enc, &vk)?;
|
||||||
|
let cs = CryptSource::new(src.clone(), cipher, seg_off, seg_len, sector).with_iv_tweak(iv_tweak);
|
||||||
|
Ok(Unlocked {
|
||||||
|
source: Arc::new(cs),
|
||||||
|
description: format!("LUKS2 ({seg_enc})"),
|
||||||
|
volume_key: Some(vk),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
for vk in volume_keys(secrets) {
|
||||||
|
if check(&vk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pws = passwords(secrets);
|
||||||
|
let slots = json.get("keyslots").and_then(|d| d.as_object()).ok_or_else(|| UnlockError::Corrupt("sem keyslots".into()))?;
|
||||||
|
let mut unsupported: Option<String> = None;
|
||||||
|
for (_, slot) in slots {
|
||||||
|
if jstr(slot, "type") != Some("luks2") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let key_size = jnum(slot, "key_size").unwrap_or(0) as usize;
|
||||||
|
let af = slot.get("af");
|
||||||
|
let stripes = af.and_then(|a| jnum(a, "stripes")).unwrap_or(4000) as usize;
|
||||||
|
let af_hash = af.and_then(|a| jstr(a, "hash")).and_then(Hash::parse).unwrap_or(Hash::Sha256);
|
||||||
|
let ar = slot.get("area");
|
||||||
|
let (Some(a_off), Some(a_size)) = (ar.and_then(|a| jnum(a, "offset")), ar.and_then(|a| jnum(a, "size"))) else { continue };
|
||||||
|
let a_enc = ar.and_then(|a| jstr(a, "encryption")).unwrap_or("aes-xts-plain64");
|
||||||
|
let a_ksize = ar.and_then(|a| jnum(a, "key_size")).unwrap_or(64) as usize;
|
||||||
|
let kdf = slot.get("kdf");
|
||||||
|
let Some(kdf) = kdf else { continue };
|
||||||
|
let salt = jstr(kdf, "salt").and_then(b64).unwrap_or_default();
|
||||||
|
let km_len = (key_size * stripes).div_ceil(512) * 512;
|
||||||
|
if key_size == 0 || stripes == 0 || km_len as u64 > a_size {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(km) = read_vec(src.as_ref(), a_off, km_len) else { continue };
|
||||||
|
if km.len() < km_len {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for pw in &pws {
|
||||||
|
let dk: Vec<u8> = match jstr(kdf, "type") {
|
||||||
|
Some("pbkdf2") => {
|
||||||
|
let hh = jstr(kdf, "hash").and_then(Hash::parse).unwrap_or(Hash::Sha256);
|
||||||
|
hh.pbkdf2(pw, &salt, jnum(kdf, "iterations").unwrap_or(1000) as u32, a_ksize)
|
||||||
|
}
|
||||||
|
Some(t @ ("argon2i" | "argon2id")) => {
|
||||||
|
let alg = if t == "argon2i" { argon2::Algorithm::Argon2i } else { argon2::Algorithm::Argon2id };
|
||||||
|
let params = match argon2::Params::new(
|
||||||
|
jnum(kdf, "memory").unwrap_or(65536) as u32,
|
||||||
|
jnum(kdf, "time").unwrap_or(4) as u32,
|
||||||
|
jnum(kdf, "cpus").unwrap_or(1) as u32,
|
||||||
|
Some(a_ksize),
|
||||||
|
) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
unsupported = Some(format!("parâmetros Argon2: {e}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut out = vec![0u8; a_ksize];
|
||||||
|
if argon2::Argon2::new(alg, argon2::Version::V0x13, params).hash_password_into(pw, &salt, &mut out).is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
Some(t) => {
|
||||||
|
unsupported = Some(format!("KDF {t}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let cipher = parse_cipher(a_enc, &dk)?;
|
||||||
|
let mut m = km.clone();
|
||||||
|
for (i, s) in m.chunks_exact_mut(512).enumerate() {
|
||||||
|
cipher.decrypt_sector(s, i as u64, i as u64 * 512);
|
||||||
|
}
|
||||||
|
let vk = af_merge(&m, key_size, stripes, af_hash);
|
||||||
|
if check(&vk) {
|
||||||
|
return finish(vk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match unsupported {
|
||||||
|
Some(u) => Err(UnlockError::Unsupported(u)),
|
||||||
|
None => Err(UnlockError::WrongSecret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atalho: abre uma janela da origem como LUKS.
|
||||||
|
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,346 @@
|
|||||||
|
//! Cifras de disco (somente leitura) e desbloqueio de volumes cifrados.
|
||||||
|
//!
|
||||||
|
//! O volume desbloqueado é exposto como uma `Source` comum: o restante do Guará
|
||||||
|
//! (varredura, recuperação) enxerga o sistema de arquivos em claro, sem saber
|
||||||
|
//! que existe cifra por baixo.
|
||||||
|
|
||||||
|
pub mod bitlocker;
|
||||||
|
pub mod filevault;
|
||||||
|
pub mod luks;
|
||||||
|
|
||||||
|
use crate::source::Source;
|
||||||
|
use aes::cipher::generic_array::GenericArray;
|
||||||
|
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
|
||||||
|
use aes::{Aes128, Aes192, Aes256};
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// AES com tamanho de chave escolhido em tempo de execução.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum AesAny {
|
||||||
|
A128(Aes128),
|
||||||
|
A192(Aes192),
|
||||||
|
A256(Aes256),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AesAny {
|
||||||
|
pub fn new(key: &[u8]) -> Option<Self> {
|
||||||
|
Some(match key.len() {
|
||||||
|
16 => AesAny::A128(Aes128::new_from_slice(key).ok()?),
|
||||||
|
24 => AesAny::A192(Aes192::new_from_slice(key).ok()?),
|
||||||
|
32 => AesAny::A256(Aes256::new_from_slice(key).ok()?),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn encrypt(&self, b: &mut [u8; 16]) {
|
||||||
|
let g = GenericArray::from_mut_slice(b);
|
||||||
|
match self {
|
||||||
|
AesAny::A128(c) => c.encrypt_block(g),
|
||||||
|
AesAny::A192(c) => c.encrypt_block(g),
|
||||||
|
AesAny::A256(c) => c.encrypt_block(g),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn decrypt(&self, b: &mut [u8; 16]) {
|
||||||
|
let g = GenericArray::from_mut_slice(b);
|
||||||
|
match self {
|
||||||
|
AesAny::A128(c) => c.decrypt_block(g),
|
||||||
|
AesAny::A192(c) => c.decrypt_block(g),
|
||||||
|
AesAny::A256(c) => c.decrypt_block(g),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xor16(a: &mut [u8; 16], b: &[u8]) {
|
||||||
|
for i in 0..16 {
|
||||||
|
a[i] ^= b[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Como o vetor de inicialização de cada setor é calculado.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Iv {
|
||||||
|
/// Número do setor (de 512 bytes) em 64 bits little-endian.
|
||||||
|
Plain64,
|
||||||
|
/// Número do setor em 32 bits little-endian.
|
||||||
|
Plain32,
|
||||||
|
Plain64Be,
|
||||||
|
/// ESSIV: AES(SHA-256(chave), setor).
|
||||||
|
Essiv(AesAny),
|
||||||
|
/// Número da unidade de dados (setor de `usize` bytes) em 64 bits little-endian.
|
||||||
|
Unit(u64),
|
||||||
|
/// BitLocker (Windows 10 1511+): AES(chave, offset em bytes).
|
||||||
|
Eboiv(AesAny),
|
||||||
|
/// BitLocker CBC com difusor Elephant: IV = AES(chave, offset em bytes).
|
||||||
|
Bitlocker(AesAny),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iv {
|
||||||
|
fn make(&self, sector_512: u64, byte_off: u64) -> [u8; 16] {
|
||||||
|
let mut iv = [0u8; 16];
|
||||||
|
match self {
|
||||||
|
Iv::Plain64 => iv[..8].copy_from_slice(§or_512.to_le_bytes()),
|
||||||
|
Iv::Plain32 => iv[..4].copy_from_slice(&(sector_512 as u32).to_le_bytes()),
|
||||||
|
Iv::Plain64Be => iv[8..].copy_from_slice(§or_512.to_be_bytes()),
|
||||||
|
Iv::Unit(u) => iv[..8].copy_from_slice(&(byte_off / u).to_le_bytes()),
|
||||||
|
Iv::Essiv(k) => {
|
||||||
|
iv[..8].copy_from_slice(§or_512.to_le_bytes());
|
||||||
|
k.encrypt(&mut iv);
|
||||||
|
}
|
||||||
|
Iv::Eboiv(k) | Iv::Bitlocker(k) => {
|
||||||
|
iv[..8].copy_from_slice(&byte_off.to_le_bytes());
|
||||||
|
k.encrypt(&mut iv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cifra aplicada setor a setor.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SectorCipher {
|
||||||
|
Xts { k1: AesAny, k2: AesAny, iv: Iv },
|
||||||
|
Cbc { key: AesAny, iv: Iv },
|
||||||
|
/// BitLocker AES-CBC com difusor Elephant (chave de dados + chave de ajuste).
|
||||||
|
Elephant { key: AesAny, tweak: AesAny },
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gf_double(t: &mut [u8; 16]) {
|
||||||
|
let mut carry = 0u8;
|
||||||
|
for b in t.iter_mut() {
|
||||||
|
let next = *b >> 7;
|
||||||
|
*b = (*b << 1) | carry;
|
||||||
|
carry = next;
|
||||||
|
}
|
||||||
|
if carry != 0 {
|
||||||
|
t[0] ^= 0x87;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectorCipher {
|
||||||
|
/// Decifra um setor no lugar. `sector_512` conta setores de 512 bytes; `byte_off` é a posição
|
||||||
|
/// em bytes relativa ao início dos dados cifrados.
|
||||||
|
pub fn decrypt_sector(&self, buf: &mut [u8], sector_512: u64, byte_off: u64) {
|
||||||
|
match self {
|
||||||
|
SectorCipher::Xts { k1, k2, iv } => {
|
||||||
|
let mut t = iv.make(sector_512, byte_off);
|
||||||
|
k2.encrypt(&mut t);
|
||||||
|
for ch in buf.chunks_exact_mut(16) {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
b.copy_from_slice(ch);
|
||||||
|
xor16(&mut b, &t);
|
||||||
|
k1.decrypt(&mut b);
|
||||||
|
xor16(&mut b, &t);
|
||||||
|
ch.copy_from_slice(&b);
|
||||||
|
gf_double(&mut t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SectorCipher::Cbc { key, iv } => {
|
||||||
|
cbc_decrypt(key, &iv.make(sector_512, byte_off), buf);
|
||||||
|
}
|
||||||
|
SectorCipher::Elephant { key, tweak } => {
|
||||||
|
bitlocker::elephant_decrypt(key, tweak, buf, byte_off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cbc_decrypt(key: &AesAny, iv: &[u8; 16], buf: &mut [u8]) {
|
||||||
|
let mut prev = *iv;
|
||||||
|
for ch in buf.chunks_exact_mut(16) {
|
||||||
|
let mut c = [0u8; 16];
|
||||||
|
c.copy_from_slice(ch);
|
||||||
|
let mut b = c;
|
||||||
|
key.decrypt(&mut b);
|
||||||
|
xor16(&mut b, &prev);
|
||||||
|
ch.copy_from_slice(&b);
|
||||||
|
prev = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cbc_encrypt(key: &AesAny, iv: &[u8; 16], buf: &mut [u8]) {
|
||||||
|
let mut prev = *iv;
|
||||||
|
for ch in buf.chunks_exact_mut(16) {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
b.copy_from_slice(ch);
|
||||||
|
xor16(&mut b, &prev);
|
||||||
|
key.encrypt(&mut b);
|
||||||
|
ch.copy_from_slice(&b);
|
||||||
|
prev = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Volume cifrado visto em claro: `len` bytes que começam em `data_offset` na origem.
|
||||||
|
pub struct CryptSource {
|
||||||
|
inner: Arc<dyn Source>,
|
||||||
|
cipher: SectorCipher,
|
||||||
|
data_offset: u64,
|
||||||
|
len: u64,
|
||||||
|
sector: u64,
|
||||||
|
/// Setores antes deste limite estão em claro (BitLocker parcialmente cifrado) ou
|
||||||
|
/// são cobertos por uma cópia (os primeiros setores do BitLocker).
|
||||||
|
patches: Vec<Patch>,
|
||||||
|
/// A partir deste byte (relativo aos dados) o conteúdo ainda não foi cifrado.
|
||||||
|
plain_from: u64,
|
||||||
|
/// Somado ao número do setor no cálculo do IV (iv_tweak do LUKS2).
|
||||||
|
iv_tweak: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trecho do volume lido de outro lugar da origem (BitLocker move o início do volume).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Patch {
|
||||||
|
/// Início no volume em claro.
|
||||||
|
pub at: u64,
|
||||||
|
pub len: u64,
|
||||||
|
/// Onde estão os dados, na origem, já em claro.
|
||||||
|
pub from: u64,
|
||||||
|
/// Se os bytes de origem precisam ser decifrados (usando a posição `from_crypt`).
|
||||||
|
pub decrypt_from: Option<u64>,
|
||||||
|
/// Devolve zeros (áreas de metadados do BitLocker).
|
||||||
|
pub zero: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptSource {
|
||||||
|
pub fn new(inner: Arc<dyn Source>, cipher: SectorCipher, data_offset: u64, len: u64, sector: u64) -> Self {
|
||||||
|
let sector = sector.max(512);
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
cipher,
|
||||||
|
data_offset,
|
||||||
|
len: len / sector * sector,
|
||||||
|
sector,
|
||||||
|
patches: vec![],
|
||||||
|
plain_from: u64::MAX,
|
||||||
|
iv_tweak: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_patches(mut self, p: Vec<Patch>) -> Self {
|
||||||
|
self.patches = p;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn with_iv_tweak(mut self, t: u64) -> Self {
|
||||||
|
self.iv_tweak = t;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn with_plain_from(mut self, off: u64) -> Self {
|
||||||
|
self.plain_from = off;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sector_plain(&self, rel: u64, out: &mut [u8]) -> io::Result<()> {
|
||||||
|
// `rel` alinhado ao setor; `out.len() == sector`.
|
||||||
|
for p in &self.patches {
|
||||||
|
if rel >= p.at && rel < p.at + p.len {
|
||||||
|
if p.zero {
|
||||||
|
out.fill(0);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let src_off = p.from + (rel - p.at);
|
||||||
|
crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + src_off, out)?;
|
||||||
|
if let Some(pos) = p.decrypt_from {
|
||||||
|
let r = pos + (rel - p.at);
|
||||||
|
self.cipher.decrypt_sector(out, r / 512, r);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::source::read_exact_at(self.inner.as_ref(), self.data_offset + rel, out)?;
|
||||||
|
if rel < self.plain_from {
|
||||||
|
self.cipher.decrypt_sector(out, rel / 512 + self.iv_tweak, rel);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for CryptSource {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.len || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.len - offset) as usize);
|
||||||
|
let s = self.sector;
|
||||||
|
let mut done = 0usize;
|
||||||
|
let mut tmp = vec![0u8; s as usize];
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let rel = pos / s * s;
|
||||||
|
let within = (pos - rel) as usize;
|
||||||
|
let n = (s as usize - within).min(want - done);
|
||||||
|
self.read_sector_plain(rel, &mut tmp)?;
|
||||||
|
buf[done..done + n].copy_from_slice(&tmp[within..within + n]);
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Algo que o usuário fornece para abrir um volume.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Secret {
|
||||||
|
Password(String),
|
||||||
|
/// Chave de recuperação numérica do BitLocker (8 grupos de 6 dígitos).
|
||||||
|
RecoveryKey(String),
|
||||||
|
/// Conteúdo de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
||||||
|
KeyFile(Vec<u8>),
|
||||||
|
/// Chave do volume já conhecida (FVEK/master key).
|
||||||
|
VolumeKey(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Secret {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Secret::Password(_) => "Secret::Password(..)",
|
||||||
|
Secret::RecoveryKey(_) => "Secret::RecoveryKey(..)",
|
||||||
|
Secret::KeyFile(_) => "Secret::KeyFile(..)",
|
||||||
|
Secret::VolumeKey(_) => "Secret::VolumeKey(..)",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UnlockError {
|
||||||
|
/// Nenhuma das senhas/chaves abriu o volume.
|
||||||
|
WrongSecret,
|
||||||
|
Unsupported(String),
|
||||||
|
Corrupt(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UnlockError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
UnlockError::WrongSecret => write!(f, "senha ou chave incorreta"),
|
||||||
|
UnlockError::Unsupported(s) => write!(f, "não suportado: {s}"),
|
||||||
|
UnlockError::Corrupt(s) => write!(f, "cabeçalho inválido: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UnlockError> for crate::Error {
|
||||||
|
fn from(e: UnlockError) -> Self {
|
||||||
|
crate::Error::msg(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Volume aberto.
|
||||||
|
pub struct Unlocked {
|
||||||
|
pub source: Arc<dyn Source>,
|
||||||
|
pub description: String,
|
||||||
|
/// Chave do volume (para reabrir sem a senha, se o usuário permitir guardá-la).
|
||||||
|
pub volume_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type UnlockResult = std::result::Result<Unlocked, UnlockError>;
|
||||||
|
|
||||||
|
pub fn to_hex(b: &[u8]) -> String {
|
||||||
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hex(s: &str) -> Option<Vec<u8>> {
|
||||||
|
let s = s.trim();
|
||||||
|
if s.len() % 2 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(0..s.len() / 2).map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()).collect()
|
||||||
|
}
|
||||||
+831
-73
@@ -1,90 +1,848 @@
|
|||||||
//! ext2/3/4: lê apenas o mapa de blocos livres, para o carving varrer só o
|
//! ext2/3/4: arquivos apagados com nome, por três caminhos que se completam:
|
||||||
//! que está livre. (Inodes apagados de ext4 têm a árvore de extents zerada,
|
//!
|
||||||
//! então a recuperação é por assinatura.)
|
//! 1. inodes com `dtime` cujo mapa de extents ainda tem sobras legíveis;
|
||||||
|
//! 2. cópias antigas do inode guardadas no diário (jbd2), de antes da exclusão;
|
||||||
|
//! 3. entradas de diretório "escondidas" na folga do `rec_len` (dão o nome).
|
||||||
|
//!
|
||||||
|
//! Também devolve o mapa de blocos livres, para o carving varrer só o que está livre.
|
||||||
|
|
||||||
use super::{merge_extents, Ctx, VolumeScan};
|
use super::{make_found, merge_extents, Ctx, NewFound, VolumeScan};
|
||||||
use crate::source::read_vec;
|
use crate::signatures;
|
||||||
use crate::types::Extent;
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::{Confidence, DataLoc, Extent, Origin};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
const EXT4_EXTENTS_FL: u32 = 0x80000;
|
||||||
let mut res = VolumeScan::default();
|
const EXT4_INLINE_DATA_FL: u32 = 0x1000_0000;
|
||||||
let Some(sb) = read_vec(ctx.src, base + 1024, 1024)
|
const EXT_MAGIC: u16 = 0xF30A;
|
||||||
.ok()
|
const JBD2_MAGIC: u32 = 0xC03B_3998;
|
||||||
.filter(|s| s.len() == 1024)
|
|
||||||
else {
|
fn le16(b: &[u8], o: usize) -> u16 {
|
||||||
return res;
|
b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0)
|
||||||
};
|
}
|
||||||
let le32 = |o: usize| u32::from_le_bytes(sb[o..o + 4].try_into().unwrap()) as u64;
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
let le16 = |o: usize| u16::from_le_bytes(sb[o..o + 2].try_into().unwrap()) as usize;
|
b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
|
||||||
if le16(56) != 0xEF53 {
|
}
|
||||||
return res;
|
fn be32(b: &[u8], o: usize) -> u32 {
|
||||||
|
b.get(o..o + 4).map(|s| u32::from_be_bytes(s.try_into().unwrap())).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
struct Group {
|
||||||
|
block_bitmap: u64,
|
||||||
|
inode_bitmap: u64,
|
||||||
|
inode_table: u64,
|
||||||
|
flags: u16,
|
||||||
|
itable_unused: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Inode {
|
||||||
|
pub mode: u16,
|
||||||
|
pub size: u64,
|
||||||
|
pub atime: u32,
|
||||||
|
pub ctime: u32,
|
||||||
|
pub mtime: u32,
|
||||||
|
pub dtime: u32,
|
||||||
|
pub links: u16,
|
||||||
|
pub flags: u32,
|
||||||
|
pub block: [u8; 60],
|
||||||
|
pub crtime: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inode {
|
||||||
|
fn parse(b: &[u8]) -> Option<Inode> {
|
||||||
|
if b.len() < 128 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut block = [0u8; 60];
|
||||||
|
block.copy_from_slice(&b[40..100]);
|
||||||
|
let mode = le16(b, 0);
|
||||||
|
let mut size = le32(b, 4) as u64;
|
||||||
|
if mode & 0xF000 == 0x8000 {
|
||||||
|
size |= (le32(b, 0x6C) as u64) << 32;
|
||||||
|
}
|
||||||
|
Some(Inode {
|
||||||
|
mode,
|
||||||
|
size,
|
||||||
|
atime: le32(b, 8),
|
||||||
|
ctime: le32(b, 12),
|
||||||
|
mtime: le32(b, 16),
|
||||||
|
dtime: le32(b, 20),
|
||||||
|
links: le16(b, 26),
|
||||||
|
flags: le32(b, 32),
|
||||||
|
block,
|
||||||
|
crtime: if b.len() >= 0x98 { le32(b, 0x90) } else { 0 },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
let bs = 1024u64 << le32(24).min(6);
|
fn is_reg(&self) -> bool {
|
||||||
let incompat = le32(96);
|
self.mode & 0xF000 == 0x8000
|
||||||
let is64 = incompat & 0x80 != 0;
|
|
||||||
let mut blocks = le32(4);
|
|
||||||
if is64 {
|
|
||||||
blocks |= le32(336) << 32;
|
|
||||||
}
|
}
|
||||||
let first_data = le32(20);
|
fn is_dir(&self) -> bool {
|
||||||
let bpg = le32(32).max(1);
|
self.mode & 0xF000 == 0x4000
|
||||||
let desc_size = if is64 { le16(254).max(64) } else { 32 };
|
}
|
||||||
let groups = blocks.saturating_sub(first_data).div_ceil(bpg);
|
}
|
||||||
let gdt_block = first_data + 1;
|
|
||||||
let gdt_len = groups * desc_size as u64;
|
struct Ext<'a> {
|
||||||
let Some(gdt) = read_vec(ctx.src, base + gdt_block * bs, gdt_len as usize)
|
src: &'a dyn Source,
|
||||||
.ok()
|
base: u64,
|
||||||
.filter(|g| g.len() as u64 == gdt_len)
|
bs: u64,
|
||||||
else {
|
blocks: u64,
|
||||||
res.warnings
|
first_data: u64,
|
||||||
.push("ext: tabela de grupos ilegível; varrendo a partição inteira.".into());
|
bpg: u64,
|
||||||
return res;
|
ipg: u64,
|
||||||
};
|
inode_size: u64,
|
||||||
let mut free: Vec<Extent> = vec![];
|
inodes: u64,
|
||||||
for g in 0..groups {
|
groups: Vec<Group>,
|
||||||
|
journal_inum: u32,
|
||||||
|
incompat: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sequência de (bloco lógico, bloco físico, quantidade, não-inicializado).
|
||||||
|
type ExtentList = Vec<(u64, u64, u64, bool)>;
|
||||||
|
|
||||||
|
impl<'a> Ext<'a> {
|
||||||
|
fn open(src: &'a dyn Source, base: u64) -> Option<Self> {
|
||||||
|
let sb = read_vec(src, base + 1024, 1024).ok().filter(|s| s.len() == 1024)?;
|
||||||
|
if le16(&sb, 56) != 0xEF53 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let log = le32(&sb, 24);
|
||||||
|
if log > 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bs = 1024u64 << log;
|
||||||
|
let incompat = le32(&sb, 96);
|
||||||
|
let is64 = incompat & 0x80 != 0;
|
||||||
|
let mut blocks = le32(&sb, 4) as u64;
|
||||||
|
if is64 {
|
||||||
|
blocks |= (le32(&sb, 336) as u64) << 32;
|
||||||
|
}
|
||||||
|
let first_data = le32(&sb, 20) as u64;
|
||||||
|
let bpg = (le32(&sb, 32) as u64).max(1);
|
||||||
|
let ipg = (le32(&sb, 40) as u64).max(1);
|
||||||
|
let rev = le32(&sb, 76);
|
||||||
|
let inode_size = if rev >= 1 { le16(&sb, 88) as u64 } else { 128 };
|
||||||
|
if !(128..=4096).contains(&inode_size) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let desc_size = if is64 { (le16(&sb, 254) as u64).max(64) } else { 32 };
|
||||||
|
let ngroups = blocks.saturating_sub(first_data).div_ceil(bpg);
|
||||||
|
let gdt_block = first_data + 1;
|
||||||
|
let gdt = read_vec(src, base + gdt_block * bs, (ngroups * desc_size) as usize).ok()?;
|
||||||
|
if (gdt.len() as u64) < ngroups * desc_size {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut groups = Vec::with_capacity(ngroups as usize);
|
||||||
|
for g in 0..ngroups as usize {
|
||||||
|
let d = &gdt[g * desc_size as usize..];
|
||||||
|
let mut gr = Group {
|
||||||
|
block_bitmap: le32(d, 0) as u64,
|
||||||
|
inode_bitmap: le32(d, 4) as u64,
|
||||||
|
inode_table: le32(d, 8) as u64,
|
||||||
|
flags: le16(d, 0x12),
|
||||||
|
itable_unused: le16(d, 0x1C) as u32,
|
||||||
|
};
|
||||||
|
if is64 && desc_size >= 64 {
|
||||||
|
gr.block_bitmap |= (le32(d, 0x20) as u64) << 32;
|
||||||
|
gr.inode_bitmap |= (le32(d, 0x24) as u64) << 32;
|
||||||
|
gr.inode_table |= (le32(d, 0x28) as u64) << 32;
|
||||||
|
gr.itable_unused |= (le16(d, 0x32) as u32) << 16;
|
||||||
|
}
|
||||||
|
groups.push(gr);
|
||||||
|
}
|
||||||
|
Some(Self {
|
||||||
|
src,
|
||||||
|
base,
|
||||||
|
bs,
|
||||||
|
blocks,
|
||||||
|
first_data,
|
||||||
|
bpg,
|
||||||
|
ipg,
|
||||||
|
inode_size,
|
||||||
|
inodes: le32(&sb, 0) as u64,
|
||||||
|
groups,
|
||||||
|
journal_inum: le32(&sb, 224),
|
||||||
|
incompat,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_block(&self, blk: u64) -> Option<Vec<u8>> {
|
||||||
|
if blk >= self.blocks {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let v = read_vec(self.src, self.base + blk * self.bs, self.bs as usize).ok()?;
|
||||||
|
(v.len() as u64 == self.bs).then_some(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inode_bytes(&self, ino: u64) -> Option<Vec<u8>> {
|
||||||
|
if ino == 0 || ino > self.inodes {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let g = ((ino - 1) / self.ipg) as usize;
|
||||||
|
let idx = (ino - 1) % self.ipg;
|
||||||
|
let grp = self.groups.get(g)?;
|
||||||
|
let off = self.base + grp.inode_table * self.bs + idx * self.inode_size;
|
||||||
|
let v = read_vec(self.src, off, self.inode_size as usize).ok()?;
|
||||||
|
(v.len() as u64 == self.inode_size).then_some(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_inode(&self, ino: u64) -> Option<Inode> {
|
||||||
|
Inode::parse(&self.inode_bytes(ino)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extents de uma árvore ext4 (raiz em `node`, 60 bytes ou um bloco).
|
||||||
|
fn walk_extent_node(&self, node: &[u8], depth_limit: u32, out: &mut ExtentList, strict: bool) -> bool {
|
||||||
|
if node.len() < 12 || le16(node, 0) != EXT_MAGIC {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let entries = le16(node, 2) as usize;
|
||||||
|
let max = le16(node, 4) as usize;
|
||||||
|
let depth = le16(node, 6) as u32;
|
||||||
|
if entries > max || depth > depth_limit || (strict && entries == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for i in 0..entries {
|
||||||
|
let e = match node.get(12 + i * 12..24 + i * 12) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
if depth == 0 {
|
||||||
|
let lblk = le32(e, 0) as u64;
|
||||||
|
let mut len = le16(e, 4) as u64;
|
||||||
|
let uninit = len > 32768;
|
||||||
|
if uninit {
|
||||||
|
len -= 32768;
|
||||||
|
}
|
||||||
|
let start = ((le16(e, 6) as u64) << 32) | le32(e, 8) as u64;
|
||||||
|
if len == 0 || start == 0 || start + len > self.blocks {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.push((lblk, start, len, uninit));
|
||||||
|
} else {
|
||||||
|
let child = ((le16(e, 8) as u64) << 32) | le32(e, 4) as u64;
|
||||||
|
let Some(b) = self.read_block(child) else { return false };
|
||||||
|
if !self.walk_extent_node(&b, depth - 1, out, true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blocos de um inode ext2/3 (mapa de blocos clássico).
|
||||||
|
fn block_map(&self, blk: &[u8; 60], max_blocks: u64) -> ExtentList {
|
||||||
|
let mut v: Vec<u64> = vec![];
|
||||||
|
let ptr = |i: usize| le32(blk, i * 4) as u64;
|
||||||
|
for i in 0..12 {
|
||||||
|
v.push(ptr(i));
|
||||||
|
}
|
||||||
|
let ppb = self.bs / 4;
|
||||||
|
let ind = |b: u64, v: &mut Vec<u64>| {
|
||||||
|
if b == 0 {
|
||||||
|
v.extend(std::iter::repeat(0).take(ppb as usize));
|
||||||
|
} else if let Some(d) = self.read_block(b) {
|
||||||
|
v.extend((0..ppb as usize).map(|i| le32(&d, i * 4) as u64));
|
||||||
|
} else {
|
||||||
|
v.extend(std::iter::repeat(0).take(ppb as usize));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if v.len() as u64 > max_blocks {
|
||||||
|
v.truncate(max_blocks as usize);
|
||||||
|
} else {
|
||||||
|
ind(ptr(12), &mut v);
|
||||||
|
if (v.len() as u64) < max_blocks {
|
||||||
|
if ptr(13) != 0 {
|
||||||
|
if let Some(d) = self.read_block(ptr(13)) {
|
||||||
|
for i in 0..ppb as usize {
|
||||||
|
ind(le32(&d, i * 4) as u64, &mut v);
|
||||||
|
if v.len() as u64 >= max_blocks {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v.len() as u64) < max_blocks && ptr(14) != 0 {
|
||||||
|
if let Some(d1) = self.read_block(ptr(14)) {
|
||||||
|
'o: for i in 0..ppb as usize {
|
||||||
|
if let Some(d2) = self.read_block(le32(&d1, i * 4) as u64) {
|
||||||
|
for j in 0..ppb as usize {
|
||||||
|
ind(le32(&d2, j * 4) as u64, &mut v);
|
||||||
|
if v.len() as u64 >= max_blocks {
|
||||||
|
break 'o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.truncate(max_blocks as usize);
|
||||||
|
let mut out: ExtentList = vec![];
|
||||||
|
for (l, &p) in v.iter().enumerate() {
|
||||||
|
if p == 0 || p >= self.blocks {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match out.last_mut() {
|
||||||
|
Some(last) if last.1 + last.2 == p && last.0 + last.2 == l as u64 => last.2 += 1,
|
||||||
|
_ => out.push((l as u64, p, 1, false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extents do arquivo (ordenados pelo bloco lógico), cortados em `size` bytes.
|
||||||
|
fn file_extents(&self, ino: &Inode, size: u64) -> Vec<Extent> {
|
||||||
|
let need = size.div_ceil(self.bs);
|
||||||
|
let mut list: ExtentList = vec![];
|
||||||
|
if ino.flags & EXT4_EXTENTS_FL != 0 {
|
||||||
|
if !self.walk_extent_node(&ino.block, 5, &mut list, false) {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
} else if ino.flags & EXT4_INLINE_DATA_FL == 0 {
|
||||||
|
list = self.block_map(&ino.block, need);
|
||||||
|
}
|
||||||
|
list.sort_by_key(|e| e.0);
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut left = size;
|
||||||
|
let mut cur_l = 0u64;
|
||||||
|
for (l, p, n, uninit) in list {
|
||||||
|
if left == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if l > cur_l {
|
||||||
|
let hole = ((l - cur_l) * self.bs).min(left);
|
||||||
|
out.push(Extent { offset: 0, len: hole, sparse: true });
|
||||||
|
left -= hole;
|
||||||
|
if left == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let take = (n * self.bs).min(left);
|
||||||
|
out.push(Extent { offset: self.base + p * self.bs, len: take, sparse: uninit });
|
||||||
|
left -= take;
|
||||||
|
cur_l = l + n;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_file(&self, ino: &Inode, max: u64) -> Vec<u8> {
|
||||||
|
let size = ino.size.min(max);
|
||||||
|
let mut out = Vec::with_capacity(size as usize);
|
||||||
|
for e in self.file_extents(ino, size) {
|
||||||
|
if e.sparse {
|
||||||
|
out.extend(std::iter::repeat(0).take(e.len as usize));
|
||||||
|
} else if let Ok(v) = read_vec(self.src, e.offset, e.len as usize) {
|
||||||
|
out.extend(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mapa de blocos livres (bitmap), em extents absolutos.
|
||||||
|
fn free_map(&self, ctx: &Ctx) -> (Vec<Extent>, Vec<bool>) {
|
||||||
|
let mut free: Vec<Extent> = vec![];
|
||||||
|
let mut allocated_all: Vec<bool> = vec![];
|
||||||
|
let total_groups = self.groups.len() as u64;
|
||||||
|
for (g, gr) in self.groups.iter().enumerate() {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let gstart = self.first_data + g as u64 * self.bpg;
|
||||||
|
let gblocks = self.bpg.min(self.blocks.saturating_sub(gstart));
|
||||||
|
let mut used_bits = vec![true; gblocks as usize];
|
||||||
|
if gr.flags & 0x2 != 0 {
|
||||||
|
used_bits.fill(false);
|
||||||
|
free.push(Extent::new(self.base + gstart * self.bs, gblocks * self.bs));
|
||||||
|
} else if let Some(bm) = read_vec(self.src, self.base + gr.block_bitmap * self.bs, gblocks.div_ceil(8) as usize)
|
||||||
|
.ok()
|
||||||
|
.filter(|b| b.len() as u64 == gblocks.div_ceil(8))
|
||||||
|
{
|
||||||
|
let mut run: Option<u64> = None;
|
||||||
|
for i in 0..gblocks {
|
||||||
|
let used = bm[(i / 8) as usize] >> (i % 8) & 1 == 1;
|
||||||
|
used_bits[i as usize] = used;
|
||||||
|
match (used, run) {
|
||||||
|
(false, None) => run = Some(i),
|
||||||
|
(true, Some(s)) => {
|
||||||
|
free.push(Extent::new(self.base + (gstart + s) * self.bs, (i - s) * self.bs));
|
||||||
|
run = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(s) = run {
|
||||||
|
free.push(Extent::new(self.base + (gstart + s) * self.bs, (gblocks - s) * self.bs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allocated_all.extend(used_bits);
|
||||||
|
if g % 64 == 0 {
|
||||||
|
(ctx.progress)(g as u64, total_groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(merge_extents(free), allocated_all)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_allocated(&self, alloc: &[bool], blk: u64) -> bool {
|
||||||
|
blk.checked_sub(self.first_data)
|
||||||
|
.and_then(|i| alloc.get(i as usize).copied())
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DirEnt {
|
||||||
|
ino: u64,
|
||||||
|
name: String,
|
||||||
|
live: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê as entradas de um bloco de diretório, inclusive as escondidas na folga.
|
||||||
|
fn parse_dir_block(b: &[u8], max_ino: u64, out: &mut Vec<DirEnt>) {
|
||||||
|
let mut off = 0usize;
|
||||||
|
while off + 8 <= b.len() {
|
||||||
|
let ino = le32(b, off) as u64;
|
||||||
|
let rec = le16(b, off + 4) as usize;
|
||||||
|
let nlen = b[off + 6] as usize;
|
||||||
|
if rec < 8 || rec % 4 != 0 || off + rec > b.len() || 8 + nlen > rec {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let real = (8 + nlen).div_ceil(4) * 4;
|
||||||
|
if ino != 0 && nlen > 0 && ino <= max_ino {
|
||||||
|
let name = String::from_utf8_lossy(&b[off + 8..off + 8 + nlen]).to_string();
|
||||||
|
out.push(DirEnt { ino, name, live: true });
|
||||||
|
}
|
||||||
|
// Folga: entradas apagadas ficam coladas depois do fim "real" desta.
|
||||||
|
let mut s = off + if ino != 0 || nlen > 0 { real } else { 8 };
|
||||||
|
let end = off + rec;
|
||||||
|
if ino == 0 {
|
||||||
|
s = end; // entrada nula: sem folga confiável
|
||||||
|
}
|
||||||
|
while s + 8 <= end {
|
||||||
|
let dino = le32(b, s) as u64;
|
||||||
|
let drec = le16(b, s + 4) as usize;
|
||||||
|
let dn = b[s + 6] as usize;
|
||||||
|
let ok = dino != 0
|
||||||
|
&& dino <= max_ino
|
||||||
|
&& dn > 0
|
||||||
|
&& drec >= 8 + dn
|
||||||
|
&& drec % 4 == 0
|
||||||
|
&& s + drec <= end
|
||||||
|
&& b[s + 8..s + 8 + dn].iter().all(|&c| c >= 0x20 && c != b'/' && c != 0x7f);
|
||||||
|
if !ok {
|
||||||
|
s += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(DirEnt {
|
||||||
|
ino: dino,
|
||||||
|
name: String::from_utf8_lossy(&b[s + 8..s + 8 + dn]).to_string(),
|
||||||
|
live: false,
|
||||||
|
});
|
||||||
|
s += (8 + dn).div_ceil(4) * 4;
|
||||||
|
}
|
||||||
|
off += rec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Names {
|
||||||
|
/// inode -> (pasta, nome) de entradas vivas.
|
||||||
|
live: HashMap<u64, (String, String)>,
|
||||||
|
/// inode -> (pasta, nome) de entradas apagadas (mais recente por varredura).
|
||||||
|
dead: HashMap<u64, (String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_dirs(ext: &Ext, ctx: &Ctx) -> Names {
|
||||||
|
let mut names = Names { live: HashMap::new(), dead: HashMap::new() };
|
||||||
|
let mut queue: Vec<(u64, String)> = vec![(2, String::new())];
|
||||||
|
let mut seen: HashSet<u64> = HashSet::new();
|
||||||
|
let mut visited = 0usize;
|
||||||
|
while let Some((dino, path)) = queue.pop() {
|
||||||
|
if !seen.insert(dino) || (ctx.cancelled)() || visited > 2_000_000 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited += 1;
|
||||||
|
let Some(ino) = ext.read_inode(dino).filter(|i| i.is_dir()) else { continue };
|
||||||
|
let mut ents = vec![];
|
||||||
|
for e in ext.file_extents(&ino, ino.size.min(64 << 20)) {
|
||||||
|
if e.sparse {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut o = 0;
|
||||||
|
while o < e.len {
|
||||||
|
if let Ok(b) = read_vec(ext.src, e.offset + o, ext.bs as usize) {
|
||||||
|
if b.len() as u64 == ext.bs {
|
||||||
|
parse_dir_block(&b, ext.inodes, &mut ents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o += ext.bs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for en in ents {
|
||||||
|
if en.name == "." || en.name == ".." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if en.live {
|
||||||
|
names.live.entry(en.ino).or_insert_with(|| (path.clone(), en.name.clone()));
|
||||||
|
queue.push((en.ino, format!("{}/{}", path, en.name)));
|
||||||
|
} else {
|
||||||
|
names.dead.entry(en.ino).or_insert_with(|| (path.clone(), en.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- diário
|
||||||
|
|
||||||
|
/// Cópias de inodes guardadas no diário: inode -> lista (ordem do diário) de registros brutos.
|
||||||
|
fn journal_inodes(ext: &Ext, ctx: &Ctx) -> HashMap<u64, Vec<Vec<u8>>> {
|
||||||
|
let mut out: HashMap<u64, Vec<Vec<u8>>> = HashMap::new();
|
||||||
|
if ext.journal_inum == 0 {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let Some(jino) = ext.read_inode(ext.journal_inum as u64) else { return out };
|
||||||
|
let jext = ext.file_extents(&jino, jino.size.min(1 << 30));
|
||||||
|
// Blocos físicos do diário em ordem lógica.
|
||||||
|
let mut jblocks: Vec<u64> = vec![];
|
||||||
|
for e in &jext {
|
||||||
|
if e.sparse {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for k in 0..e.len / ext.bs {
|
||||||
|
jblocks.push((e.offset - ext.base) / ext.bs + k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if jblocks.len() < 4 {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let Some(jsb) = ext.read_block(jblocks[0]) else { return out };
|
||||||
|
if be32(&jsb, 0) != JBD2_MAGIC {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let jbs = be32(&jsb, 12) as u64;
|
||||||
|
if jbs != ext.bs {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let incompat = be32(&jsb, 40);
|
||||||
|
let is64 = incompat & 0x2 != 0;
|
||||||
|
let csum_v3 = incompat & 0x10 != 0;
|
||||||
|
let csum_v2 = incompat & 0x8 != 0;
|
||||||
|
let tag_bytes = if csum_v3 { 16 } else { 8 + if is64 { 4 } else { 0 } };
|
||||||
|
let have_csum = csum_v2 || csum_v3;
|
||||||
|
// Tabela de inodes: faixa de blocos de cada grupo.
|
||||||
|
let itable_blocks = (ext.ipg * ext.inode_size).div_ceil(ext.bs);
|
||||||
|
let mut tables: Vec<(u64, u64, u64)> = vec![]; // (início, fim, primeiro inode)
|
||||||
|
for (g, gr) in ext.groups.iter().enumerate() {
|
||||||
|
tables.push((gr.inode_table, gr.inode_table + itable_blocks, g as u64 * ext.ipg + 1));
|
||||||
|
}
|
||||||
|
let n = jblocks.len();
|
||||||
|
let first = (be32(&jsb, 20) as usize).clamp(1, n);
|
||||||
|
let mut seqs: Vec<(u32, usize)> = vec![]; // (sequência, índice do descritor)
|
||||||
|
let mut idx = first;
|
||||||
|
// Varredura física: qualquer bloco que seja descritor, em ordem do diário.
|
||||||
|
while idx < n {
|
||||||
if (ctx.cancelled)() {
|
if (ctx.cancelled)() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let d = &gdt[(g * desc_size as u64) as usize..];
|
let Some(d) = ext.read_block(jblocks[idx]) else {
|
||||||
let mut bb = u32::from_le_bytes(d[0..4].try_into().unwrap()) as u64;
|
idx += 1;
|
||||||
if is64 && desc_size >= 64 {
|
|
||||||
bb |= (u32::from_le_bytes(d[0x20..0x24].try_into().unwrap()) as u64) << 32;
|
|
||||||
}
|
|
||||||
let flags = u16::from_le_bytes(d[0x12..0x14].try_into().unwrap());
|
|
||||||
let gstart = first_data + g * bpg;
|
|
||||||
let gblocks = bpg.min(blocks.saturating_sub(gstart));
|
|
||||||
if flags & 0x2 != 0 {
|
|
||||||
// BLOCK_UNINIT: grupo sem bitmap válido; trata como livre.
|
|
||||||
free.push(Extent::new(base + gstart * bs, gblocks * bs));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(bm) = read_vec(ctx.src, base + bb * bs, (gblocks.div_ceil(8)) as usize).ok()
|
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let mut run: Option<u64> = None;
|
if be32(&d, 0) == JBD2_MAGIC && be32(&d, 4) == 1 {
|
||||||
for i in 0..gblocks {
|
seqs.push((be32(&d, 8), idx));
|
||||||
let used = bm
|
}
|
||||||
.get((i / 8) as usize)
|
idx += 1;
|
||||||
.map(|b| b >> (i % 8) & 1 == 1)
|
}
|
||||||
.unwrap_or(true);
|
seqs.sort_by_key(|s| s.0);
|
||||||
match (used, run) {
|
for (_seq, didx) in seqs {
|
||||||
(false, None) => run = Some(i),
|
let Some(d) = ext.read_block(jblocks[didx]) else { continue };
|
||||||
(true, Some(s)) => {
|
let limit = d.len() - if have_csum { 4 } else { 0 };
|
||||||
free.push(Extent::new(base + (gstart + s) * bs, (i - s) * bs));
|
let mut off = 12usize;
|
||||||
run = None;
|
let mut data_idx = didx + 1;
|
||||||
|
loop {
|
||||||
|
if off + tag_bytes > limit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut blk = be32(&d, off) as u64;
|
||||||
|
let (flags, hi) = if csum_v3 {
|
||||||
|
(be32(&d, off + 4), be32(&d, off + 8))
|
||||||
|
} else {
|
||||||
|
(u16::from_be_bytes([d[off + 6], d[off + 7]]) as u32, if is64 { be32(&d, off + 8) } else { 0 })
|
||||||
|
};
|
||||||
|
blk |= (hi as u64) << 32;
|
||||||
|
off += tag_bytes;
|
||||||
|
if flags & 2 == 0 {
|
||||||
|
off += 16;
|
||||||
|
}
|
||||||
|
let di = if data_idx >= n { first + (data_idx - n) } else { data_idx };
|
||||||
|
data_idx += 1;
|
||||||
|
if flags & 4 == 0 {
|
||||||
|
if let Some(t) = tables.iter().position(|t| blk >= t.0 && blk < t.1) {
|
||||||
|
if let Some(mut data) = ext.read_block(jblocks[di.min(n - 1)]) {
|
||||||
|
if flags & 1 != 0 {
|
||||||
|
data[0..4].copy_from_slice(&JBD2_MAGIC.to_be_bytes());
|
||||||
|
}
|
||||||
|
let per_block = (ext.bs / ext.inode_size) as usize;
|
||||||
|
let first_ino = tables[t].2 + (blk - tables[t].0) * per_block as u64;
|
||||||
|
for k in 0..per_block {
|
||||||
|
let rec = &data[k * ext.inode_size as usize..(k + 1) * ext.inode_size as usize];
|
||||||
|
if rec[..128].iter().any(|&x| x != 0) {
|
||||||
|
out.entry(first_ino + k as u64).or_default().push(rec.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
}
|
||||||
|
if flags & 8 != 0 {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(s) = run {
|
}
|
||||||
free.push(Extent::new(base + (gstart + s) * bs, (gblocks - s) * bs));
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------- varredura
|
||||||
|
|
||||||
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
|
let mut res = VolumeScan::default();
|
||||||
|
let Some(ext) = Ext::open(ctx.src, base) else {
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
let (free, alloc) = ext.free_map(ctx);
|
||||||
|
let _ = part_len;
|
||||||
|
res.free = Some(free);
|
||||||
|
|
||||||
|
let names = walk_dirs(&ext, ctx);
|
||||||
|
let journal = journal_inodes(&ext, ctx);
|
||||||
|
let mut emitted: HashSet<u64> = HashSet::new();
|
||||||
|
let mut seen_extents: HashSet<(u64, u64)> = HashSet::new();
|
||||||
|
|
||||||
|
// 1) Inodes apagados na tabela atual.
|
||||||
|
let total = ext.groups.len() as u64;
|
||||||
|
for (g, gr) in ext.groups.iter().enumerate() {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
if g % 64 == 0 {
|
if gr.flags & 0x1 != 0 {
|
||||||
(ctx.progress)(g, groups);
|
continue; // INODE_UNINIT
|
||||||
|
}
|
||||||
|
let used = ext.ipg.saturating_sub(gr.itable_unused as u64).min(ext.ipg);
|
||||||
|
let Some(table) = read_vec(ext.src, ext.base + gr.inode_table * ext.bs, (used * ext.inode_size) as usize).ok() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for i in 0..(table.len() as u64 / ext.inode_size) {
|
||||||
|
let ino_no = g as u64 * ext.ipg + i + 1;
|
||||||
|
let raw = &table[(i * ext.inode_size) as usize..((i + 1) * ext.inode_size) as usize];
|
||||||
|
let Some(ino) = Inode::parse(raw) else { continue };
|
||||||
|
if names.live.contains_key(&ino_no) || ino_no < 11 || ino.mode == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let deleted = ino.dtime != 0 || ino.links == 0;
|
||||||
|
if !deleted || !ino.is_reg() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Melhor fonte: a cópia do diário anterior à exclusão; senão o que sobrou no inode.
|
||||||
|
let mut cand: Option<(Inode, &str)> = None;
|
||||||
|
if let Some(snaps) = journal.get(&ino_no) {
|
||||||
|
for raw in snaps.iter().rev() {
|
||||||
|
if let Some(j) = Inode::parse(raw) {
|
||||||
|
if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 {
|
||||||
|
let ex = ext.file_extents(&j, j.size);
|
||||||
|
if ex.iter().any(|e| !e.sparse) {
|
||||||
|
cand = Some((j, "diário"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cand.is_none() {
|
||||||
|
cand = Some((ino.clone(), "inode"));
|
||||||
|
}
|
||||||
|
if let Some((c, src_kind)) = cand {
|
||||||
|
if emit_inode(ctx, &ext, &alloc, &names, ino_no, &c, src_kind, &mut seen_extents) {
|
||||||
|
emitted.insert(ino_no);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g % 16 == 0 {
|
||||||
|
(ctx.progress)(g as u64, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Inodes que só existem no diário (liberados e já reaproveitados/zerados).
|
||||||
|
for (ino_no, snaps) in &journal {
|
||||||
|
if emitted.contains(ino_no) || names.live.contains_key(ino_no) || *ino_no < 11 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
let cur = ext.read_inode(*ino_no);
|
||||||
|
// Se o inode atual está vivo e é outro arquivo, não mexe.
|
||||||
|
if cur.as_ref().map(|c| c.dtime == 0 && c.links > 0 && c.mode != 0).unwrap_or(false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for raw in snaps.iter().rev() {
|
||||||
|
if let Some(j) = Inode::parse(raw) {
|
||||||
|
if j.is_reg() && j.size > 0 && j.dtime == 0 && j.links > 0 {
|
||||||
|
if emit_inode(ctx, &ext, &alloc, &names, *ino_no, &j, "diário", &mut seen_extents) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = part_len;
|
|
||||||
res.free = Some(merge_extents(free));
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn emit_inode(
|
||||||
|
ctx: &mut Ctx,
|
||||||
|
ext: &Ext,
|
||||||
|
alloc: &[bool],
|
||||||
|
names: &Names,
|
||||||
|
ino_no: u64,
|
||||||
|
ino: &Inode,
|
||||||
|
src_kind: &str,
|
||||||
|
seen: &mut HashSet<(u64, u64)>,
|
||||||
|
) -> bool {
|
||||||
|
let mut extents: Vec<Extent>;
|
||||||
|
let mut size = ino.size;
|
||||||
|
let mut estimated = false;
|
||||||
|
if ino.flags & EXT4_EXTENTS_FL != 0 {
|
||||||
|
let mut list: ExtentList = vec![];
|
||||||
|
if !ext.walk_extent_node(&ino.block, 5, &mut list, true) {
|
||||||
|
// Sobras de extent em inode truncado: cabeçalho zerado, registros ainda lá.
|
||||||
|
list = remnant_extents(ext, &ino.block);
|
||||||
|
}
|
||||||
|
if list.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
list.sort_by_key(|e| e.0);
|
||||||
|
if size == 0 {
|
||||||
|
size = list.iter().map(|e| e.2).sum::<u64>() * ext.bs;
|
||||||
|
estimated = true;
|
||||||
|
}
|
||||||
|
extents = layout_from_list(ext, &list, size);
|
||||||
|
} else {
|
||||||
|
if size == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
extents = ext.file_extents(ino, size);
|
||||||
|
}
|
||||||
|
if extents.is_empty() || extents.iter().all(|e| e.sparse) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let first = extents.iter().find(|e| !e.sparse).map(|e| (e.offset, e.len)).unwrap();
|
||||||
|
if !seen.insert((first.0, first.1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Quantos blocos de dados continuam livres? Reaproveitados => baixa confiança.
|
||||||
|
let mut reused = 0u64;
|
||||||
|
let mut total_blocks = 0u64;
|
||||||
|
for e in extents.iter().filter(|e| !e.sparse) {
|
||||||
|
let b0 = (e.offset - ext.base) / ext.bs;
|
||||||
|
for k in 0..e.len.div_ceil(ext.bs) {
|
||||||
|
total_blocks += 1;
|
||||||
|
if ext.block_allocated(alloc, b0 + k) {
|
||||||
|
reused += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut note = Some(format!("recuperado do {src_kind} do ext4"));
|
||||||
|
let mut conf = Confidence::High;
|
||||||
|
if reused > 0 {
|
||||||
|
conf = if reused * 2 > total_blocks { Confidence::Low } else { Confidence::Medium };
|
||||||
|
note = Some(format!("{reused} de {total_blocks} blocos já foram reaproveitados por outros arquivos"));
|
||||||
|
}
|
||||||
|
if estimated {
|
||||||
|
// Tamanho desconhecido: tenta achar o fim real pelo conteúdo.
|
||||||
|
if let Some(h) = extents
|
||||||
|
.iter()
|
||||||
|
.find(|e| !e.sparse)
|
||||||
|
.and_then(|e| read_vec(ext.src, e.offset, 128).ok())
|
||||||
|
.and_then(|head| signatures::detect(ext.src, first.0, &head, size))
|
||||||
|
{
|
||||||
|
if h.len <= size && h.len > 0 {
|
||||||
|
size = h.len;
|
||||||
|
extents = super::fat::trim_extents(extents, size);
|
||||||
|
estimated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if estimated {
|
||||||
|
conf = conf.min(Confidence::Medium);
|
||||||
|
note = Some("tamanho exato perdido na exclusão; usando todos os blocos do arquivo".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (dir, name) = match names.dead.get(&ino_no).or_else(|| names.live.get(&ino_no)) {
|
||||||
|
Some((d, n)) => (Some(d.clone()), n.clone()),
|
||||||
|
None => (None, format!("inode_{ino_no}")),
|
||||||
|
};
|
||||||
|
let ts = |t: u32| (t != 0).then_some(t as i64);
|
||||||
|
let nf = NewFound {
|
||||||
|
name,
|
||||||
|
dir,
|
||||||
|
size,
|
||||||
|
origin: Origin::Ext,
|
||||||
|
data: DataLoc::Extents { extents },
|
||||||
|
modified: ts(ino.mtime),
|
||||||
|
created: ts(ino.crtime),
|
||||||
|
accessed: ts(ino.atime),
|
||||||
|
base: conf,
|
||||||
|
note: note.as_deref(),
|
||||||
|
};
|
||||||
|
let f = make_found(ctx, nf);
|
||||||
|
(ctx.emit)(f);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_from_list(ext: &Ext, list: &ExtentList, size: u64) -> Vec<Extent> {
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut left = size;
|
||||||
|
let mut cur = 0u64;
|
||||||
|
for &(l, p, n, uninit) in list {
|
||||||
|
if left == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if l > cur {
|
||||||
|
let hole = ((l - cur) * ext.bs).min(left);
|
||||||
|
out.push(Extent { offset: 0, len: hole, sparse: true });
|
||||||
|
left -= hole;
|
||||||
|
if left == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let take = (n * ext.bs).min(left);
|
||||||
|
out.push(Extent { offset: ext.base + p * ext.bs, len: take, sparse: uninit });
|
||||||
|
left -= take;
|
||||||
|
cur = l + n;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê registros de extent deixados em i_block depois que o cabeçalho foi zerado.
|
||||||
|
fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList {
|
||||||
|
let mut out: ExtentList = vec![];
|
||||||
|
if le16(block, 0) != EXT_MAGIC || le16(block, 6) != 0 {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let max = (le16(block, 4) as usize).min(4);
|
||||||
|
let mut next_l = 0u64;
|
||||||
|
for i in 0..max {
|
||||||
|
let e = &block[12 + i * 12..24 + i * 12];
|
||||||
|
let lblk = le32(e, 0) as u64;
|
||||||
|
let mut len = le16(e, 4) as u64;
|
||||||
|
let uninit = len > 32768;
|
||||||
|
if uninit {
|
||||||
|
len -= 32768;
|
||||||
|
}
|
||||||
|
let start = ((le16(e, 6) as u64) << 32) | le32(e, 8) as u64;
|
||||||
|
if len == 0 || start < ext.first_data || start + len > ext.blocks || lblk < next_l {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
next_l = lblk + len;
|
||||||
|
out.push((lblk, start, len, uninit));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
pub mod carve;
|
pub mod carve;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
|
pub mod crypt;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod ewf;
|
pub mod ewf;
|
||||||
pub mod explorer;
|
pub mod explorer;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
|
|||||||
return Some((FsKind::Exfat, Some(vol << shift)));
|
return Some((FsKind::Exfat, Some(vol << shift)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if &b[3..11] == b"-FVE-FS-" {
|
if crate::crypt::bitlocker::detect(b) {
|
||||||
return Some((FsKind::Bitlocker, None));
|
return Some((FsKind::Bitlocker, None));
|
||||||
}
|
}
|
||||||
if b.starts_with(b"LUKS\xBA\xBE") {
|
if b.starts_with(b"LUKS\xBA\xBE") {
|
||||||
|
|||||||
@@ -292,12 +292,7 @@ footer{margin-top:34px;color:#5b6f6d;font-size:12px}@media print{body{background
|
|||||||
human(f.size),
|
human(f.size),
|
||||||
iso(f.modified),
|
iso(f.modified),
|
||||||
iso(f.created),
|
iso(f.created),
|
||||||
match f.origin {
|
f.origin.label(),
|
||||||
Origin::Carved => "assinatura",
|
|
||||||
Origin::Ntfs => "NTFS",
|
|
||||||
Origin::Fat => "FAT",
|
|
||||||
Origin::Exfat => "exFAT",
|
|
||||||
},
|
|
||||||
f.first_offset().map(|o| o.to_string()).unwrap_or_default()
|
f.first_offset().map(|o| o.to_string()).unwrap_or_default()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,9 +114,32 @@ pub enum Origin {
|
|||||||
Ntfs,
|
Ntfs,
|
||||||
Fat,
|
Fat,
|
||||||
Exfat,
|
Exfat,
|
||||||
|
Ext,
|
||||||
|
Btrfs,
|
||||||
|
Xfs,
|
||||||
|
Hfs,
|
||||||
|
Apfs,
|
||||||
|
Refs,
|
||||||
Carved,
|
Carved,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Origin {
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Origin::Carved => "assinatura",
|
||||||
|
Origin::Ntfs => "NTFS",
|
||||||
|
Origin::Fat => "FAT",
|
||||||
|
Origin::Exfat => "exFAT",
|
||||||
|
Origin::Ext => "ext4",
|
||||||
|
Origin::Btrfs => "Btrfs",
|
||||||
|
Origin::Xfs => "XFS",
|
||||||
|
Origin::Hfs => "HFS+",
|
||||||
|
Origin::Apfs => "APFS",
|
||||||
|
Origin::Refs => "ReFS",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Um arquivo encontrado (apagado, perdido ou reconstruído por assinatura).
|
/// Um arquivo encontrado (apagado, perdido ou reconstruído por assinatura).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Found {
|
pub struct Found {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//! BitLocker: imagens reais do conjunto de testes do cryptsetup (GitLab), com senhas conhecidas.
|
||||||
|
//! Baixe com `scripts/fetch-testdata.sh`; sem elas os testes são pulados.
|
||||||
|
|
||||||
|
use guara_core::crypt::{bitlocker, Secret, UnlockError};
|
||||||
|
use guara_core::partition::detect_fs_in;
|
||||||
|
use guara_core::source::{read_vec, FileSource, Source};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn dir() -> Option<PathBuf> {
|
||||||
|
let d = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/bitlk-images");
|
||||||
|
d.join("images.conf").exists().then_some(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conf(d: &PathBuf) -> BTreeMap<String, BTreeMap<String, String>> {
|
||||||
|
let mut m: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
for l in std::fs::read_to_string(d.join("images.conf")).unwrap().lines() {
|
||||||
|
let l = l.trim();
|
||||||
|
if let Some(n) = l.strip_prefix('[').and_then(|x| x.strip_suffix(']')) {
|
||||||
|
cur = n.to_string();
|
||||||
|
} else if let Some((k, v)) = l.split_once('=') {
|
||||||
|
m.entry(cur.clone()).or_default().insert(k.into(), v.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_fs(src: &dyn Source, what: &str) {
|
||||||
|
// Imagens "encrypt-on-write" não têm sistema de arquivos reconhecível (UUID vazio no images.conf).
|
||||||
|
if what.contains("-eow ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let head = read_vec(src, 0, 4096).unwrap();
|
||||||
|
let fs = detect_fs_in(&head);
|
||||||
|
assert!(fs.is_some(), "{what}: setor 0 aberto não é um sistema de arquivos: {:02x?}", &head[..32]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opens_all_reference_images_with_password_and_recovery_key() {
|
||||||
|
let Some(d) = dir() else {
|
||||||
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut opened = 0;
|
||||||
|
for (name, c) in conf(&d) {
|
||||||
|
let path = d.join(format!("{name}.img"));
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let src: Arc<dyn Source> = Arc::new(FileSource::open(&path).unwrap());
|
||||||
|
let pw = c.get("PW").cloned().unwrap_or_default();
|
||||||
|
let rp = c.get("RP").cloned().unwrap_or_default();
|
||||||
|
// Proteção suspensa (chave limpa): abre sem segredo nenhum.
|
||||||
|
if let Ok(u) = bitlocker::unlock(src.clone(), &[]) {
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (chave limpa)"));
|
||||||
|
eprintln!("ok {name}: chave limpa");
|
||||||
|
opened += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match bitlocker::unlock(src.clone(), &[Secret::Password("errada".into())]) {
|
||||||
|
Err(UnlockError::WrongSecret) | Err(UnlockError::Unsupported(_)) => {}
|
||||||
|
Err(e) => panic!("{name}: {e}"),
|
||||||
|
Ok(_) => panic!("{name}: abriu com senha errada"),
|
||||||
|
}
|
||||||
|
let mut views = vec![];
|
||||||
|
if !pw.is_empty() {
|
||||||
|
let u = bitlocker::unlock(src.clone(), &[Secret::Password(pw.clone())]).unwrap_or_else(|e| panic!("{name} senha: {e}"));
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (senha)"));
|
||||||
|
views.push(u);
|
||||||
|
}
|
||||||
|
if !rp.is_empty() {
|
||||||
|
let u = bitlocker::unlock(src.clone(), &[Secret::RecoveryKey(rp.clone())]).unwrap_or_else(|e| panic!("{name} recuperação: {e}"));
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (recuperação)"));
|
||||||
|
views.push(u);
|
||||||
|
}
|
||||||
|
if let Some(g) = c.get("SK_VMK_GUID") {
|
||||||
|
let f = d.join(format!("{}.BEK", g.to_uppercase()));
|
||||||
|
if f.exists() {
|
||||||
|
let u = bitlocker::unlock(src.clone(), &[Secret::KeyFile(std::fs::read(f).unwrap())]).unwrap_or_else(|e| panic!("{name} BEK: {e}"));
|
||||||
|
check_fs(u.source.as_ref(), &format!("{name} (BEK)"));
|
||||||
|
views.push(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(!views.is_empty(), "{name}: nenhum protetor testável");
|
||||||
|
let first = read_vec(views[0].source.as_ref(), 0, 1 << 20).unwrap();
|
||||||
|
for v in &views[1..] {
|
||||||
|
assert!(read_vec(v.source.as_ref(), 0, 1 << 20).unwrap() == first, "{name}: protetores divergem");
|
||||||
|
}
|
||||||
|
opened += 1;
|
||||||
|
eprintln!("ok {name}: {} ({} protetores)", views[0].description, views.len());
|
||||||
|
}
|
||||||
|
assert!(opened > 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
//! ext4: arquivos apagados com nome, a partir de imagens criadas com mke2fs/debugfs.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn have(tool: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {tool}")])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debugfs(img: &Path, cmds: &[&str]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for c in cmds {
|
||||||
|
let o = Command::new("debugfs").arg("-w").arg("-R").arg(c).arg(img).output().unwrap();
|
||||||
|
out.push_str(&String::from_utf8_lossy(&o.stdout));
|
||||||
|
out.push_str(&String::from_utf8_lossy(&o.stderr));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inode_of(img: &Path, path: &str) -> u64 {
|
||||||
|
let o = debugfs(img, &[&format!("stat {path}")]);
|
||||||
|
let l = o.lines().find(|l| l.starts_with("Inode:")).unwrap_or_else(|| panic!("{o}"));
|
||||||
|
l.split_whitespace().nth(1).unwrap().parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(seed: u32, len: usize) -> Vec<u8> {
|
||||||
|
let mut x = 88172645463325252u64 ^ seed as u64;
|
||||||
|
(0..len)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Img {
|
||||||
|
_dir: tempfile::TempDir,
|
||||||
|
img: PathBuf,
|
||||||
|
files: Vec<(&'static str, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(opts: &[&str]) -> Option<Img> {
|
||||||
|
if !have("mke2fs") || !have("debugfs") {
|
||||||
|
eprintln!("SKIP: e2fsprogs ausente");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("docs/sub")).unwrap();
|
||||||
|
std::fs::create_dir_all(root.join("fotos")).unwrap();
|
||||||
|
let files = vec![
|
||||||
|
("docs/relatorio.dat", content(1, 150_000)),
|
||||||
|
("docs/sub/contrato.dat", content(2, 9_000)),
|
||||||
|
("fotos/viagem.dat", content(3, 700_000)),
|
||||||
|
("fotos/fica.dat", content(4, 30_000)),
|
||||||
|
];
|
||||||
|
for (p, c) in &files {
|
||||||
|
std::fs::write(root.join(p), c).unwrap();
|
||||||
|
}
|
||||||
|
let img = dir.path().join("e.img");
|
||||||
|
let f = std::fs::File::create(&img).unwrap();
|
||||||
|
f.set_len(24 << 20).unwrap();
|
||||||
|
let st = Command::new("mke2fs")
|
||||||
|
.args(["-q", "-F", "-d"])
|
||||||
|
.arg(&root)
|
||||||
|
.args(opts)
|
||||||
|
.arg(&img)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(st.status.success(), "{}", String::from_utf8_lossy(&st.stderr));
|
||||||
|
Some(Img { _dir: dir, img, files })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deep(img: &Path) -> common::Collect {
|
||||||
|
init();
|
||||||
|
let opts = ScanOptions { mode: ScanMode::Deep, ..ScanOptions::default() };
|
||||||
|
scan_path(img, &opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recovered(img: &Path, f: &Found) -> Vec<u8> {
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let rep = recover_all(img, std::slice::from_ref(f), d.path());
|
||||||
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() { PathBuf::from(p) } else { d.path().join(p) };
|
||||||
|
std::fs::read(p).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_by_debugfs_keeps_extents_and_name() {
|
||||||
|
let Some(i) = build(&["-t", "ext4"]) else { return };
|
||||||
|
debugfs(&i.img, &["rm /docs/relatorio.dat", "rm /fotos/viagem.dat"]);
|
||||||
|
let c = deep(&i.img);
|
||||||
|
for (p, data) in i.files.iter().filter(|f| f.0 == "docs/relatorio.dat" || f.0 == "fotos/viagem.dat") {
|
||||||
|
let name = p.rsplit('/').next().unwrap();
|
||||||
|
let f = c.found.iter().find(|f| f.name == name && f.origin == Origin::Ext).unwrap_or_else(|| panic!("{name} não encontrado: {:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
||||||
|
assert_eq!(f.size, data.len() as u64);
|
||||||
|
assert_eq!(f.dir.as_deref(), Some(&format!("/{}", p.rsplit_once('/').unwrap().0)[..]));
|
||||||
|
assert!(recovered(&i.img, f) == *data, "{name}");
|
||||||
|
}
|
||||||
|
// O arquivo que não foi apagado não aparece.
|
||||||
|
assert!(!c.found.iter().any(|f| f.name == "fica.dat" && f.origin == Origin::Ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncated_inode_recovered_by_remnant_extents() {
|
||||||
|
// Simula o que o kernel faz ao apagar: tamanho zerado e contagem de extents zerada.
|
||||||
|
let Some(i) = build(&["-t", "ext4"]) else { return };
|
||||||
|
let ino = inode_of(&i.img, "/docs/sub/contrato.dat");
|
||||||
|
debugfs(
|
||||||
|
&i.img,
|
||||||
|
&[
|
||||||
|
"rm /docs/sub/contrato.dat",
|
||||||
|
&format!("sif <{ino}> size 0"),
|
||||||
|
&format!("sif <{ino}> block[0] 0xF30A"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let c = deep(&i.img);
|
||||||
|
let f = c.found.iter().find(|f| f.name == "contrato.dat").expect("contrato.dat");
|
||||||
|
assert_eq!(f.origin, Origin::Ext);
|
||||||
|
// Tamanho exato é perdido: recupera todos os blocos (9000 bytes arredondados para 3 blocos de 4K).
|
||||||
|
assert_eq!(f.size, 12288);
|
||||||
|
let got = recovered(&i.img, f);
|
||||||
|
assert_eq!(&got[..9000], &i.files[1].1[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn journal_copy_restores_exact_size() {
|
||||||
|
if !have("debugfs") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(i) = build(&["-t", "ext4", "-O", "has_journal"]) else { return };
|
||||||
|
// Bloco da tabela de inodes que contém o inode do arquivo.
|
||||||
|
let ino = inode_of(&i.img, "/fotos/viagem.dat");
|
||||||
|
let o = debugfs(&i.img, &["stats"]);
|
||||||
|
let _ = o;
|
||||||
|
let blk_line = debugfs(&i.img, &[&format!("imap <{ino}>")]);
|
||||||
|
// "Inode 12 is part of block group 0\n\tlocated at block 1058, offset 0x0300"
|
||||||
|
let blk: u64 = blk_line
|
||||||
|
.split("located at block ")
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|s| s.split(',').next())
|
||||||
|
.and_then(|s| s.trim().parse().ok())
|
||||||
|
.unwrap_or_else(|| panic!("imap: {blk_line}"));
|
||||||
|
let bs = 4096u64;
|
||||||
|
let raw = std::fs::read(&i.img).unwrap();
|
||||||
|
let old = &raw[(blk * bs) as usize..((blk + 1) * bs) as usize];
|
||||||
|
let bin = i.img.with_extension("blk");
|
||||||
|
std::fs::write(&bin, old).unwrap();
|
||||||
|
let script = i.img.with_extension("cmds");
|
||||||
|
std::fs::write(&script, format!("jo\njw -b {blk} {}\njc\njc\n", bin.display())).unwrap();
|
||||||
|
let o = Command::new("debugfs").arg("-w").arg("-f").arg(&script).arg(&i.img).output().unwrap();
|
||||||
|
let j = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));
|
||||||
|
if j.contains("Unknown request") || j.contains("not supported") {
|
||||||
|
eprintln!("SKIP: debugfs sem suporte a escrita no diário: {j}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Exclui e zera como o kernel faria.
|
||||||
|
debugfs(
|
||||||
|
&i.img,
|
||||||
|
&[
|
||||||
|
"rm /fotos/viagem.dat",
|
||||||
|
&format!("sif <{ino}> size 0"),
|
||||||
|
&format!("sif <{ino}> block[0] 0xF30A"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let c = deep(&i.img);
|
||||||
|
let f = c.found.iter().find(|f| f.name == "viagem.dat").unwrap_or_else(|| panic!("sem viagem.dat: {:?}", c.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||||
|
assert_eq!(f.size, 700_000, "{:?}", f.note);
|
||||||
|
assert!(recovered(&i.img, f) == i.files[2].1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext2_blockmap_deleted_is_not_invented() {
|
||||||
|
// ext2 zera os ponteiros ao apagar; sem diário não há o que recuperar por metadados,
|
||||||
|
// mas a varredura não pode falhar nem listar o arquivo que continua existindo.
|
||||||
|
let Some(i) = build(&["-t", "ext2"]) else { return };
|
||||||
|
let c = deep(&i.img);
|
||||||
|
assert!(!c.found.iter().any(|f| f.origin == Origin::Ext));
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
//! LUKS1/LUKS2: imagens criadas pelo cryptsetup; o conteúdo é cifrado por uma
|
||||||
|
//! implementação independente (python cryptography) com a chave real do volume.
|
||||||
|
|
||||||
|
use guara_core::crypt::{luks, Secret, UnlockError};
|
||||||
|
use guara_core::source::{read_vec, FileSource, Source};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn have(tool: &str) -> bool {
|
||||||
|
Command::new("sh")
|
||||||
|
.args(["-c", &format!("command -v {tool}")])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn py_ok() -> bool {
|
||||||
|
Command::new("python3")
|
||||||
|
.args(["-c", "import cryptography"])
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_stdin(cmd: &mut Command, input: &str) -> bool {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut ch = cmd.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn().unwrap();
|
||||||
|
ch.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
|
||||||
|
ch.wait().unwrap().success()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plain_data(len: usize) -> Vec<u8> {
|
||||||
|
let mut x = 0x9E3779B97F4A7C15u64;
|
||||||
|
(0..len)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 32) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Case<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
format_args: Vec<&'a str>,
|
||||||
|
cipher: &'a str,
|
||||||
|
sector: u32,
|
||||||
|
ver2: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload_offset(img: &Path, ver2: bool) -> u64 {
|
||||||
|
let o = Command::new("cryptsetup").arg("luksDump").arg(img).output().unwrap();
|
||||||
|
let t = String::from_utf8_lossy(&o.stdout).to_string();
|
||||||
|
if ver2 {
|
||||||
|
let l = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap();
|
||||||
|
l.split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap()
|
||||||
|
} else {
|
||||||
|
let l = t.lines().find(|l| l.starts_with("Payload offset")).unwrap();
|
||||||
|
l.split(':').nth(1).unwrap().trim().parse::<u64>().unwrap() * 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_case(c: &Case) {
|
||||||
|
if !have("cryptsetup") || !py_ok() {
|
||||||
|
eprintln!("SKIP: cryptsetup/python3-cryptography ausentes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let img = dir.path().join("l.img");
|
||||||
|
let plain = dir.path().join("plain.bin");
|
||||||
|
let data = plain_data(3 * 1024 * 1024 + 4096);
|
||||||
|
std::fs::write(&plain, &data).unwrap();
|
||||||
|
std::fs::File::create(&img).unwrap().set_len(24 << 20).unwrap();
|
||||||
|
let pw = "senha com acentuação ç";
|
||||||
|
let mut fmt = Command::new("cryptsetup");
|
||||||
|
fmt.args(["luksFormat", "--batch-mode", "--key-file", "-"]);
|
||||||
|
fmt.args(&c.format_args);
|
||||||
|
fmt.arg(&img);
|
||||||
|
assert!(run_stdin(&mut fmt, pw), "luksFormat falhou ({})", c.name);
|
||||||
|
let vkf = dir.path().join("vk.bin");
|
||||||
|
let mut dump = Command::new("cryptsetup");
|
||||||
|
dump.args(["luksDump", "--dump-volume-key", "--batch-mode", "--key-file", "-", "--volume-key-file"]);
|
||||||
|
dump.arg(&vkf).arg(&img);
|
||||||
|
assert!(run_stdin(&mut dump, pw), "dump da chave falhou ({})", c.name);
|
||||||
|
let off = payload_offset(&img, c.ver2);
|
||||||
|
let st = Command::new("python3")
|
||||||
|
.arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/support/luks_encrypt.py"))
|
||||||
|
.arg(&img)
|
||||||
|
.arg(&plain)
|
||||||
|
.arg(&vkf)
|
||||||
|
.arg(off.to_string())
|
||||||
|
.arg(c.cipher)
|
||||||
|
.arg(c.sector.to_string())
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(st.success());
|
||||||
|
|
||||||
|
let src: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
|
||||||
|
// Senha errada primeiro.
|
||||||
|
match luks::unlock(src.clone(), &[Secret::Password("errada".into())]) {
|
||||||
|
Err(UnlockError::WrongSecret) => {}
|
||||||
|
Err(e) => panic!("{}: erro inesperado {e}", c.name),
|
||||||
|
Ok(_) => panic!("{}: abriu com senha errada", c.name),
|
||||||
|
}
|
||||||
|
let u = luks::unlock(src.clone(), &[Secret::Password("x".into()), Secret::Password(pw.into())])
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {e}", c.name));
|
||||||
|
let got = read_vec(u.source.as_ref(), 0, data.len()).unwrap();
|
||||||
|
assert!(got == data, "{}: conteúdo difere", c.name);
|
||||||
|
// Também abre com a chave do volume.
|
||||||
|
let vk = std::fs::read(&vkf).unwrap();
|
||||||
|
let u2 = luks::unlock(src, &[Secret::VolumeKey(vk)]).unwrap();
|
||||||
|
assert_eq!(read_vec(u2.source.as_ref(), 1000, 5000).unwrap(), &data[1000..6000]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn luks1_xts() {
|
||||||
|
run_case(&Case {
|
||||||
|
name: "luks1-xts",
|
||||||
|
format_args: vec!["--type", "luks1", "--pbkdf-force-iterations", "1000", "-c", "aes-xts-plain64", "-s", "512"],
|
||||||
|
cipher: "aes-xts-plain64",
|
||||||
|
sector: 512,
|
||||||
|
ver2: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn luks1_cbc_essiv_sha1() {
|
||||||
|
run_case(&Case {
|
||||||
|
name: "luks1-cbc-essiv",
|
||||||
|
format_args: vec!["--type", "luks1", "--pbkdf-force-iterations", "1000", "-c", "aes-cbc-essiv:sha256", "-s", "256", "-h", "sha1"],
|
||||||
|
cipher: "aes-cbc-essiv:sha256",
|
||||||
|
sector: 512,
|
||||||
|
ver2: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn luks2_argon2id_xts() {
|
||||||
|
run_case(&Case {
|
||||||
|
name: "luks2-argon2id",
|
||||||
|
format_args: vec!["--type", "luks2", "--pbkdf", "argon2id", "--pbkdf-force-iterations", "4", "--pbkdf-memory", "64", "-c", "aes-xts-plain64", "-s", "512"],
|
||||||
|
cipher: "aes-xts-plain64",
|
||||||
|
sector: 4096,
|
||||||
|
ver2: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn luks2_pbkdf2_sector512_aes256() {
|
||||||
|
run_case(&Case {
|
||||||
|
name: "luks2-pbkdf2",
|
||||||
|
format_args: vec!["--type", "luks2", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "-c", "aes-xts-plain64", "-s", "256", "--sector-size", "512"],
|
||||||
|
cipher: "aes-xts-plain64",
|
||||||
|
sector: 512,
|
||||||
|
ver2: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Cifra um volume em claro dentro de uma imagem LUKS, com cryptography (referência independente).
|
||||||
|
uso: luks_encrypt.py IMG PLAIN VOLUME_KEY_FILE PAYLOAD_OFFSET CIPHER SECTOR_SIZE"""
|
||||||
|
import sys, hashlib
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
img, plain, vkf, off, cipher, ss = sys.argv[1:7]
|
||||||
|
off = int(off); ss = int(ss)
|
||||||
|
vk = open(vkf, 'rb').read()
|
||||||
|
data = open(plain, 'rb').read()
|
||||||
|
data += bytes((-len(data)) % ss)
|
||||||
|
out = bytearray()
|
||||||
|
for i in range(0, len(data), ss):
|
||||||
|
sector512 = i // 512
|
||||||
|
blk = data[i:i+ss]
|
||||||
|
if 'xts' in cipher:
|
||||||
|
tweak = sector512.to_bytes(16, 'little')
|
||||||
|
e = Cipher(algorithms.AES(vk), modes.XTS(tweak)).encryptor()
|
||||||
|
else:
|
||||||
|
if 'essiv:sha256' in cipher:
|
||||||
|
salt = hashlib.sha256(vk).digest()
|
||||||
|
iv = Cipher(algorithms.AES(salt), modes.ECB()).encryptor().update(sector512.to_bytes(16, 'little'))
|
||||||
|
else:
|
||||||
|
iv = sector512.to_bytes(16, 'little')
|
||||||
|
e = Cipher(algorithms.AES(vk), modes.CBC(iv)).encryptor()
|
||||||
|
out += e.update(blk) + e.finalize()
|
||||||
|
with open(img, 'r+b') as f:
|
||||||
|
f.seek(off)
|
||||||
|
f.write(out)
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Baixa imagens de referência (BitLocker e FileVault2) do conjunto de testes do cryptsetup.
|
||||||
|
# Ficam em target/testdata (não versionado). Sem elas, os testes correspondentes são pulados.
|
||||||
|
set -euo pipefail
|
||||||
|
dest="$(cd "$(dirname "$0")/.." && pwd)/target/testdata"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
base=https://gitlab.com/cryptsetup/cryptsetup/-/raw/main/tests
|
||||||
|
for n in bitlk-images fvault2-images; do
|
||||||
|
[ -d "$dest/$n" ] && continue
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
curl -fsSL "$base/$n.tar.xz" -o "$tmp/$n.tar.xz"
|
||||||
|
tar xJf "$tmp/$n.tar.xz" -C "$dest"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
done
|
||||||
|
echo "ok: $dest"
|
||||||
Reference in new issue
Block a user