Montagem de discos virtuais: desbloqueio de LUKS, BitLocker e FileVault 2 integrado à varredura e à recuperação
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a2ce82315e
commit
c014526761
9 files changed
+457
-24
No files matched your search
@@ -0,0 +1,184 @@
|
|||||||
|
//! Monta o disco virtual a partir do que o usuário informou: membros extras (RAID/LVM) e
|
||||||
|
//! senhas ou chaves para volumes cifrados. É a única porta de entrada para varrer e recuperar
|
||||||
|
//! discos que não são "simples".
|
||||||
|
|
||||||
|
use crate::crypt::{self, Secret, UnlockError};
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::partition;
|
||||||
|
use crate::source::{open_path, Source, SubSource};
|
||||||
|
use crate::types::{FsKind, PartInfo};
|
||||||
|
use crate::volume::{Overlay, Region};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||||
|
pub enum SecretInput {
|
||||||
|
Password { value: String },
|
||||||
|
Recovery { value: String },
|
||||||
|
/// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
||||||
|
Keyfile { path: String },
|
||||||
|
/// Chave do volume em hexadecimal.
|
||||||
|
Key { hex: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chave de volume guardada na sessão (só quando o usuário pede para lembrar).
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SavedKey {
|
||||||
|
pub start: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub key_hex: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Assembly {
|
||||||
|
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
||||||
|
#[serde(default)]
|
||||||
|
pub members: Vec<String>,
|
||||||
|
/// Senhas e chaves: nunca vão para o disco junto com a sessão.
|
||||||
|
#[serde(default, skip_serializing)]
|
||||||
|
pub secrets: Vec<SecretInput>,
|
||||||
|
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
|
||||||
|
#[serde(default)]
|
||||||
|
pub remember_keys: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keys: Vec<SavedKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assembly {
|
||||||
|
pub fn is_plain(&self) -> bool {
|
||||||
|
self.members.is_empty() && self.secrets.is_empty() && self.keys.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct UnlockInfo {
|
||||||
|
pub start: u64,
|
||||||
|
pub len: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct LockedInfo {
|
||||||
|
pub start: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Assembled {
|
||||||
|
pub source: Arc<dyn Source>,
|
||||||
|
pub unlocked: Vec<UnlockInfo>,
|
||||||
|
pub locked: Vec<LockedInfo>,
|
||||||
|
/// Chaves derivadas, para guardar na sessão se o usuário permitir.
|
||||||
|
pub keys: Vec<SavedKey>,
|
||||||
|
/// Volumes lógicos / partições extras que não estão na tabela do disco.
|
||||||
|
pub extra_parts: Vec<PartInfo>,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_secrets(a: &Assembly) -> Result<Vec<Secret>> {
|
||||||
|
let mut v = vec![];
|
||||||
|
for s in &a.secrets {
|
||||||
|
v.push(match s {
|
||||||
|
SecretInput::Password { value } => Secret::Password(value.clone()),
|
||||||
|
SecretInput::Recovery { value } => Secret::RecoveryKey(value.clone()),
|
||||||
|
SecretInput::Keyfile { path } => Secret::KeyFile(
|
||||||
|
std::fs::read(path).map_err(|e| Error::msg(format!("não foi possível ler o arquivo de chave {path}: {e}")))?,
|
||||||
|
),
|
||||||
|
SecretInput::Key { hex } => Secret::VolumeKey(
|
||||||
|
crypt::from_hex(hex).ok_or_else(|| Error::msg("chave do volume em hexadecimal inválida"))?,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind_name(fs: FsKind) -> &'static str {
|
||||||
|
match fs {
|
||||||
|
FsKind::Bitlocker => "bitlocker",
|
||||||
|
FsKind::Luks => "luks",
|
||||||
|
FsKind::Corestorage => "filevault",
|
||||||
|
_ => "?",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_unlock(window: Arc<dyn Source>, fs: FsKind, secrets: &[Secret]) -> crypt::UnlockResult {
|
||||||
|
match fs {
|
||||||
|
FsKind::Bitlocker => crypt::bitlocker::unlock(window, secrets),
|
||||||
|
FsKind::Luks => crypt::luks::unlock(window, secrets),
|
||||||
|
FsKind::Corestorage => crypt::filevault::unlock(window, secrets),
|
||||||
|
_ => Err(UnlockError::Unsupported("tipo".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Abre `path` e aplica a montagem pedida.
|
||||||
|
pub fn assemble(path: &str, asm: &Assembly) -> Result<Assembled> {
|
||||||
|
let base = open_path(Path::new(path)).map_err(|e| Error::msg(format!("{path}: {e}")))?;
|
||||||
|
assemble_source(base, asm)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assembled> {
|
||||||
|
let secrets = to_secrets(asm)?;
|
||||||
|
let mut regions: Vec<Region> = vec![];
|
||||||
|
let mut out = Assembled {
|
||||||
|
source: base.clone(),
|
||||||
|
unlocked: vec![],
|
||||||
|
locked: vec![],
|
||||||
|
keys: vec![],
|
||||||
|
extra_parts: vec![],
|
||||||
|
notes: vec![],
|
||||||
|
};
|
||||||
|
// Até três rodadas: o que foi aberto pode conter outro volume cifrado.
|
||||||
|
for _ in 0..3 {
|
||||||
|
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
||||||
|
let layout = partition::analyze(current.as_ref(), false);
|
||||||
|
let mut progress = false;
|
||||||
|
out.locked.clear();
|
||||||
|
for p in &layout.partitions {
|
||||||
|
if p.encrypted.is_none() || regions.iter().any(|r| r.start == p.start) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
||||||
|
let window: Arc<dyn Source> = Arc::new(SubSource::new(current.clone(), p.start, len));
|
||||||
|
let mut all = secrets.clone();
|
||||||
|
for k in asm.keys.iter().filter(|k| k.start == p.start) {
|
||||||
|
if let Some(b) = crypt::from_hex(&k.key_hex) {
|
||||||
|
all.push(Secret::VolumeKey(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match try_unlock(window, p.fs, &all) {
|
||||||
|
Ok(u) => {
|
||||||
|
let ulen = u.source.size();
|
||||||
|
out.unlocked.push(UnlockInfo {
|
||||||
|
start: p.start,
|
||||||
|
len: ulen,
|
||||||
|
kind: kind_name(p.fs).into(),
|
||||||
|
description: u.description.clone(),
|
||||||
|
});
|
||||||
|
if let Some(k) = &u.volume_key {
|
||||||
|
out.keys.push(SavedKey { start: p.start, kind: kind_name(p.fs).into(), key_hex: crypt::to_hex(k) });
|
||||||
|
}
|
||||||
|
regions.push(Region { start: p.start, len: ulen, src: u.source, label: u.description });
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
Err(e) => out.locked.push(LockedInfo {
|
||||||
|
start: p.start,
|
||||||
|
kind: kind_name(p.fs).into(),
|
||||||
|
reason: match e {
|
||||||
|
UnlockError::WrongSecret if secrets.is_empty() && asm.keys.is_empty() => "volume cifrado: informe a senha, a chave de recuperação ou o arquivo de chave".into(),
|
||||||
|
other => other.to_string(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !progress {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !regions.is_empty() {
|
||||||
|
out.source = Arc::new(Overlay::new(base, regions));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
@@ -95,7 +95,6 @@ struct Ext<'a> {
|
|||||||
inodes: u64,
|
inodes: u64,
|
||||||
groups: Vec<Group>,
|
groups: Vec<Group>,
|
||||||
journal_inum: u32,
|
journal_inum: u32,
|
||||||
incompat: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sequência de (bloco lógico, bloco físico, quantidade, não-inicializado).
|
/// Sequência de (bloco lógico, bloco físico, quantidade, não-inicializado).
|
||||||
@@ -163,7 +162,6 @@ impl<'a> Ext<'a> {
|
|||||||
inodes: le32(&sb, 0) as u64,
|
inodes: le32(&sb, 0) as u64,
|
||||||
groups,
|
groups,
|
||||||
journal_inum: le32(&sb, 224),
|
journal_inum: le32(&sb, 224),
|
||||||
incompat,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,19 +325,6 @@ impl<'a> Ext<'a> {
|
|||||||
out
|
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.
|
/// Mapa de blocos livres (bitmap), em extents absolutos.
|
||||||
fn free_map(&self, ctx: &Ctx) -> (Vec<Extent>, Vec<bool>) {
|
fn free_map(&self, ctx: &Ctx) -> (Vec<Extent>, Vec<bool>) {
|
||||||
let mut free: Vec<Extent> = vec![];
|
let mut free: Vec<Extent> = vec![];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
|
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
|
||||||
//! crate escreve no dispositivo que está sendo recuperado.
|
//! crate escreve no dispositivo que está sendo recuperado.
|
||||||
|
|
||||||
|
pub mod assemble;
|
||||||
pub mod carve;
|
pub mod carve;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
pub mod crypt;
|
pub mod crypt;
|
||||||
@@ -31,6 +32,7 @@ pub mod source;
|
|||||||
pub mod sqlite;
|
pub mod sqlite;
|
||||||
pub mod thumb;
|
pub mod thumb;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
pub mod volume;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|||||||
@@ -13,7 +13,21 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
|
|||||||
if b.len() < 1100 {
|
if b.len() < 1100 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
detect_fs_in(&b)
|
if let Some(r) = detect_fs_in(&b) {
|
||||||
|
return Some(r);
|
||||||
|
}
|
||||||
|
// Assinaturas mais adiante no volume: Btrfs (64 KiB) e RAID mdadm 1.2 (4 KiB).
|
||||||
|
if let Ok(h) = read_vec(src, start + 0x10000, 0x100) {
|
||||||
|
if h.len() >= 0x80 && &h[0x40..0x48] == b"_BHRfS_M" {
|
||||||
|
return Some((FsKind::Btrfs, Some(u64::from_le_bytes(h[0x70..0x78].try_into().unwrap()))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(h) = read_vec(src, start + 4096, 16) {
|
||||||
|
if h.len() >= 4 && h[..4] == [0xFC, 0x4E, 0x2B, 0xA9] {
|
||||||
|
return Some((FsKind::Raid, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mesma detecção sobre um buffer que começa no setor candidato (>= 1100 bytes).
|
/// Mesma detecção sobre um buffer que começa no setor candidato (>= 1100 bytes).
|
||||||
@@ -35,6 +49,27 @@ 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.len() >= 520 && &b[512..520] == b"LABELONE" {
|
||||||
|
return Some((FsKind::Lvm, None));
|
||||||
|
}
|
||||||
|
if b[..4] == [0xFC, 0x4E, 0x2B, 0xA9] {
|
||||||
|
return Some((FsKind::Raid, None));
|
||||||
|
}
|
||||||
|
if &b[0..4] == b"XFSB" {
|
||||||
|
let bs = u32::from_be_bytes(b[4..8].try_into().unwrap()) as u64;
|
||||||
|
let blocks = u64::from_be_bytes(b[8..16].try_into().unwrap());
|
||||||
|
if bs.is_power_of_two() && (512..=65536).contains(&bs) {
|
||||||
|
return Some((FsKind::Xfs, Some(bs * blocks)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if &b[3..7] == b"ReFS" && &b[16..20] == b"FSRS" {
|
||||||
|
let sectors = u64::from_le_bytes(b[24..32].try_into().unwrap());
|
||||||
|
let bps = u32::from_le_bytes(b[32..36].try_into().unwrap()) as u64;
|
||||||
|
return Some((FsKind::Refs, Some(sectors * bps)));
|
||||||
|
}
|
||||||
|
if crate::crypt::filevault::detect(b) {
|
||||||
|
return Some((FsKind::Corestorage, Some(u64::from_le_bytes(b[64..72].try_into().unwrap()))));
|
||||||
|
}
|
||||||
if crate::crypt::bitlocker::detect(b) {
|
if crate::crypt::bitlocker::detect(b) {
|
||||||
return Some((FsKind::Bitlocker, None));
|
return Some((FsKind::Bitlocker, None));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ pub fn run(
|
|||||||
l
|
l
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut layout = layout;
|
||||||
|
// Volumes lógicos e outros volumes montados que não estão na tabela do disco.
|
||||||
|
for p in &opts.extra_parts {
|
||||||
|
if !layout.partitions.iter().any(|q| q.start == p.start) {
|
||||||
|
let mut p = p.clone();
|
||||||
|
p.index = layout.partitions.len();
|
||||||
|
layout.partitions.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
cp.layout = Some(layout.clone());
|
cp.layout = Some(layout.clone());
|
||||||
|
|
||||||
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
|
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
|
||||||
|
|||||||
@@ -204,10 +204,25 @@ impl Engine {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- varredura
|
// ---------------------------------------------------------------- varredura
|
||||||
|
|
||||||
pub fn start_scan(&self, path: &str, opts: ScanOptions) -> Result<String> {
|
pub fn start_scan(&self, path: &str, mut opts: ScanOptions) -> Result<String> {
|
||||||
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
let raw = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||||
let fp = crate::source::fingerprint(src.as_ref());
|
let fp = crate::source::fingerprint(raw.as_ref());
|
||||||
let job = self.register(JobKind::Scan, path.to_string(), opts, Some(src));
|
let asm = crate::assemble::assemble_source(raw, &opts.assembly)?;
|
||||||
|
opts.extra_parts = asm.extra_parts.clone();
|
||||||
|
if opts.assembly.remember_keys {
|
||||||
|
opts.assembly.keys = asm.keys.clone();
|
||||||
|
}
|
||||||
|
let job = self.register(JobKind::Scan, path.to_string(), opts, Some(asm.source.clone()));
|
||||||
|
{
|
||||||
|
let mut s = job.st.lock().unwrap();
|
||||||
|
for u in &asm.unlocked {
|
||||||
|
s.warnings.push(format!("Volume cifrado aberto na posição {}: {}.", u.start, u.description));
|
||||||
|
}
|
||||||
|
for l in &asm.locked {
|
||||||
|
s.warnings.push(format!("Volume cifrado na posição {} não foi aberto: {}.", l.start, l.reason));
|
||||||
|
}
|
||||||
|
s.warnings.extend(asm.notes.iter().cloned());
|
||||||
|
}
|
||||||
*job.fingerprint.lock().unwrap() = fp;
|
*job.fingerprint.lock().unwrap() = fp;
|
||||||
*job.session_file.lock().unwrap() = Some(new_session_path(path));
|
*job.session_file.lock().unwrap() = Some(new_session_path(path));
|
||||||
spawn_scan(job.clone(), None);
|
spawn_scan(job.clone(), None);
|
||||||
@@ -814,8 +829,8 @@ impl Engine {
|
|||||||
let mut last_err = None;
|
let mut last_err = None;
|
||||||
for path in &tried {
|
for path in &tried {
|
||||||
match open_path(Path::new(path)) {
|
match open_path(Path::new(path)) {
|
||||||
Ok(src) if crate::reconnect::matches(src.as_ref(), size, fp.as_deref()) => {
|
Ok(src) if crate::reconnect::matches(src.as_ref(), if j.opts.assembly.is_plain() { size } else { 0 }, fp.as_deref()) => {
|
||||||
*j.src.lock().unwrap() = Some(src);
|
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||||
*j.src_path.lock().unwrap() = path.clone();
|
*j.src_path.lock().unwrap() = path.clone();
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -826,7 +841,7 @@ impl Engine {
|
|||||||
if let Some((path, src)) =
|
if let Some((path, src)) =
|
||||||
crate::reconnect::find_matching_source(size, fp.as_deref(), &tried)
|
crate::reconnect::find_matching_source(size, fp.as_deref(), &tried)
|
||||||
{
|
{
|
||||||
*j.src.lock().unwrap() = Some(src);
|
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||||
*j.src_path.lock().unwrap() = path.clone();
|
*j.src_path.lock().unwrap() = path.clone();
|
||||||
return Ok(Some(path));
|
return Ok(Some(path));
|
||||||
}
|
}
|
||||||
@@ -863,7 +878,7 @@ impl Engine {
|
|||||||
if fp.is_none() {
|
if fp.is_none() {
|
||||||
*j.fingerprint.lock().unwrap() = crate::source::fingerprint(src.as_ref());
|
*j.fingerprint.lock().unwrap() = crate::source::fingerprint(src.as_ref());
|
||||||
}
|
}
|
||||||
*j.src.lock().unwrap() = Some(src);
|
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||||
*j.src_path.lock().unwrap() = path.to_string();
|
*j.src_path.lock().unwrap() = path.to_string();
|
||||||
j.st.lock()
|
j.st.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1483,6 +1498,14 @@ impl Engine {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- sessões automáticas
|
// ---------------------------------------------------------------- sessões automáticas
|
||||||
|
|
||||||
|
/// Reaplica ao disco reaberto a montagem (volumes desbloqueados, RAID) da sessão.
|
||||||
|
fn finish_source(j: &Job, raw: Arc<dyn Source>) -> Arc<dyn Source> {
|
||||||
|
match crate::assemble::assemble_source(raw.clone(), &j.opts.assembly) {
|
||||||
|
Ok(a) => a.source,
|
||||||
|
Err(_) => raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sessions_dir() -> PathBuf {
|
fn sessions_dir() -> PathBuf {
|
||||||
crate::settings::config_dir().join("sessoes")
|
crate::settings::config_dir().join("sessoes")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,8 +210,17 @@ pub enum FsKind {
|
|||||||
Ext,
|
Ext,
|
||||||
Apfs,
|
Apfs,
|
||||||
HfsPlus,
|
HfsPlus,
|
||||||
|
Btrfs,
|
||||||
|
Xfs,
|
||||||
|
Refs,
|
||||||
Bitlocker,
|
Bitlocker,
|
||||||
Luks,
|
Luks,
|
||||||
|
/// FileVault 2 (Core Storage).
|
||||||
|
Corestorage,
|
||||||
|
/// Membro de volume LVM (physical volume).
|
||||||
|
Lvm,
|
||||||
|
/// Membro de RAID por software (mdadm).
|
||||||
|
Raid,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,8 +235,14 @@ impl FsKind {
|
|||||||
FsKind::Ext => "ext2/3/4",
|
FsKind::Ext => "ext2/3/4",
|
||||||
FsKind::Apfs => "APFS",
|
FsKind::Apfs => "APFS",
|
||||||
FsKind::HfsPlus => "HFS+",
|
FsKind::HfsPlus => "HFS+",
|
||||||
|
FsKind::Btrfs => "Btrfs",
|
||||||
|
FsKind::Xfs => "XFS",
|
||||||
|
FsKind::Refs => "ReFS",
|
||||||
FsKind::Bitlocker => "BitLocker",
|
FsKind::Bitlocker => "BitLocker",
|
||||||
FsKind::Luks => "LUKS",
|
FsKind::Luks => "LUKS",
|
||||||
|
FsKind::Corestorage => "FileVault 2",
|
||||||
|
FsKind::Lvm => "LVM",
|
||||||
|
FsKind::Raid => "RAID (mdadm)",
|
||||||
FsKind::Unknown => "desconhecido",
|
FsKind::Unknown => "desconhecido",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,6 +252,7 @@ impl FsKind {
|
|||||||
match self {
|
match self {
|
||||||
FsKind::Bitlocker => Some("BitLocker"),
|
FsKind::Bitlocker => Some("BitLocker"),
|
||||||
FsKind::Luks => Some("LUKS"),
|
FsKind::Luks => Some("LUKS"),
|
||||||
|
FsKind::Corestorage => Some("FileVault 2"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,6 +318,12 @@ pub struct ScanOptions {
|
|||||||
/// Tamanho máximo aceito para arquivos carved (bytes).
|
/// Tamanho máximo aceito para arquivos carved (bytes).
|
||||||
#[serde(default = "default_max_carve")]
|
#[serde(default = "default_max_carve")]
|
||||||
pub max_carve_size: u64,
|
pub max_carve_size: u64,
|
||||||
|
/// Membros extras (RAID/LVM) e segredos para volumes cifrados.
|
||||||
|
#[serde(default)]
|
||||||
|
pub assembly: crate::assemble::Assembly,
|
||||||
|
/// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados).
|
||||||
|
#[serde(default)]
|
||||||
|
pub extra_parts: Vec<PartInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_carve() -> u64 {
|
fn default_max_carve() -> u64 {
|
||||||
@@ -315,6 +337,8 @@ impl Default for ScanOptions {
|
|||||||
include_allocated: false,
|
include_allocated: false,
|
||||||
categories: vec![],
|
categories: vec![],
|
||||||
max_carve_size: default_max_carve(),
|
max_carve_size: default_max_carve(),
|
||||||
|
assembly: Default::default(),
|
||||||
|
extra_parts: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! Disco virtual: a origem real com trechos substituídos por volumes desbloqueados,
|
||||||
|
//! membros de RAID montados ou volumes lógicos (LVM) anexados depois do fim do disco.
|
||||||
|
//!
|
||||||
|
//! Os deslocamentos continuam sendo "do disco": quem varre e quem recupera enxergam o mesmo
|
||||||
|
//! espaço de endereços, desde que a montagem seja refeita igual (a `Assembly` é determinística).
|
||||||
|
|
||||||
|
use crate::source::Source;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Region {
|
||||||
|
pub start: u64,
|
||||||
|
pub len: u64,
|
||||||
|
pub src: Arc<dyn Source>,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Overlay {
|
||||||
|
base: Arc<dyn Source>,
|
||||||
|
regions: Vec<Region>,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Overlay {
|
||||||
|
pub fn new(base: Arc<dyn Source>, mut regions: Vec<Region>) -> Self {
|
||||||
|
regions.sort_by_key(|r| r.start);
|
||||||
|
let size = regions.iter().map(|r| r.start + r.len).max().unwrap_or(0).max(base.size());
|
||||||
|
Self { base, regions, size }
|
||||||
|
}
|
||||||
|
pub fn regions(&self) -> &[Region] {
|
||||||
|
&self.regions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for Overlay {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let i = self.regions.partition_point(|r| r.start + r.len <= offset);
|
||||||
|
if let Some(r) = self.regions.get(i) {
|
||||||
|
if r.start <= offset {
|
||||||
|
let n = want.min((r.start + r.len - offset) as usize);
|
||||||
|
return r.src.read_at(offset - r.start, &mut buf[..n]);
|
||||||
|
}
|
||||||
|
// Antes da próxima região: lê da base só até ela.
|
||||||
|
let n = want.min((r.start - offset) as usize);
|
||||||
|
return self.read_base(offset, &mut buf[..n]);
|
||||||
|
}
|
||||||
|
self.read_base(offset, &mut buf[..want])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Overlay {
|
||||||
|
fn read_base(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.base.size() {
|
||||||
|
// Lacuna entre a base e uma região anexada.
|
||||||
|
buf.fill(0);
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
let n = buf.len().min((self.base.size() - offset) as usize);
|
||||||
|
let got = self.base.read_at(offset, &mut buf[..n])?;
|
||||||
|
if got < buf.len() && got == n {
|
||||||
|
buf[n..].fill(0);
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
Ok(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
//! Ponta a ponta: volume LUKS2 com ext4 dentro, arquivo apagado, senha informada ao Guará.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::assemble::{assemble_source, Assembly, SecretInput};
|
||||||
|
use guara_core::scan::{self, Outcome};
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn have(t: &str) -> bool {
|
||||||
|
Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_stdin(c: &mut Command, input: &str) -> bool {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut ch = c.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 data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
|
let mut x = seed | 1;
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_file_inside_luks2_is_recovered_with_password() {
|
||||||
|
if !(have("cryptsetup") && have("mke2fs") && have("debugfs") && have("python3")) {
|
||||||
|
eprintln!("SKIP: ferramentas ausentes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("docs")).unwrap();
|
||||||
|
let secret_doc = data(11, 120_000);
|
||||||
|
std::fs::write(root.join("docs/segredo.dat"), &secret_doc).unwrap();
|
||||||
|
std::fs::write(root.join("docs/fica.dat"), data(12, 5000)).unwrap();
|
||||||
|
let plain = dir.path().join("plain.img");
|
||||||
|
std::fs::File::create(&plain).unwrap().set_len(8 << 20).unwrap();
|
||||||
|
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
||||||
|
let dbg = |c: &str| {
|
||||||
|
Command::new("debugfs").arg("-w").arg("-R").arg(c).arg(&plain).output().unwrap()
|
||||||
|
};
|
||||||
|
dbg("rm /docs/segredo.dat");
|
||||||
|
|
||||||
|
let img = dir.path().join("luks.img");
|
||||||
|
std::fs::File::create(&img).unwrap().set_len(32 << 20).unwrap();
|
||||||
|
let pw = "correct horse";
|
||||||
|
let mut fmt = Command::new("cryptsetup");
|
||||||
|
fmt.args(["luksFormat", "--batch-mode", "--type", "luks2", "--pbkdf", "pbkdf2", "--pbkdf-force-iterations", "1000", "--sector-size", "512", "--key-file", "-"]).arg(&img);
|
||||||
|
assert!(run_stdin(&mut fmt, pw));
|
||||||
|
let vkf = dir.path().join("vk");
|
||||||
|
let mut d = Command::new("cryptsetup");
|
||||||
|
d.args(["luksDump", "--dump-volume-key", "--batch-mode", "--key-file", "-", "--volume-key-file"]).arg(&vkf).arg(&img);
|
||||||
|
assert!(run_stdin(&mut d, pw));
|
||||||
|
let out = Command::new("cryptsetup").arg("luksDump").arg(&img).output().unwrap();
|
||||||
|
let t = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
let off: u64 = t.lines().skip_while(|l| !l.starts_with("Data segments")).find(|l| l.contains("offset:")).unwrap().split(':').nth(1).unwrap().split_whitespace().next().unwrap().parse().unwrap();
|
||||||
|
assert!(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("aes-xts-plain64").arg("512")
|
||||||
|
.status().unwrap().success());
|
||||||
|
|
||||||
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&img).unwrap());
|
||||||
|
// Sem senha: o volume segue fechado e a varredura não inventa arquivos.
|
||||||
|
let locked = assemble_source(base.clone(), &Assembly::default()).unwrap();
|
||||||
|
assert_eq!(locked.unlocked.len(), 0);
|
||||||
|
assert_eq!(locked.locked.len(), 1);
|
||||||
|
|
||||||
|
let asm = Assembly { secrets: vec![SecretInput::Password { value: pw.into() }], ..Default::default() };
|
||||||
|
let open = assemble_source(base, &asm).unwrap();
|
||||||
|
assert_eq!(open.unlocked.len(), 1);
|
||||||
|
let mut sink = Collect::default();
|
||||||
|
let outc = scan::run(open.source.as_ref(), &ScanOptions { mode: ScanMode::Deep, ..Default::default() }, None, &[], &|| false, &mut sink);
|
||||||
|
assert!(matches!(outc, Outcome::Completed));
|
||||||
|
let f = sink.found.iter().find(|f| f.name == "segredo.dat").unwrap_or_else(|| panic!("não achou: {:?}", sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||||
|
assert_eq!(f.size, 120_000);
|
||||||
|
let dest = tempfile::tempdir().unwrap();
|
||||||
|
let refs = vec![f];
|
||||||
|
let rep = recover::recover(open.source.as_ref(), &refs, dest.path(), &|| false, &mut |_, _, _| {}).unwrap();
|
||||||
|
assert!(rep.failed.is_empty());
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) };
|
||||||
|
assert!(std::fs::read(p).unwrap() == secret_doc);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user