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,
|
||||
groups: Vec<Group>,
|
||||
journal_inum: u32,
|
||||
incompat: u32,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
groups,
|
||||
journal_inum: le32(&sb, 224),
|
||||
incompat,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -327,19 +325,6 @@ impl<'a> Ext<'a> {
|
||||
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![];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
|
||||
//! crate escreve no dispositivo que está sendo recuperado.
|
||||
|
||||
pub mod assemble;
|
||||
pub mod carve;
|
||||
pub mod device;
|
||||
pub mod crypt;
|
||||
@@ -31,6 +32,7 @@ pub mod source;
|
||||
pub mod sqlite;
|
||||
pub mod thumb;
|
||||
pub mod types;
|
||||
pub mod volume;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use types::*;
|
||||
|
||||
@@ -13,7 +13,21 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
|
||||
if b.len() < 1100 {
|
||||
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).
|
||||
@@ -35,6 +49,27 @@ pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
|
||||
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) {
|
||||
return Some((FsKind::Bitlocker, None));
|
||||
}
|
||||
|
||||
@@ -83,6 +83,15 @@ pub fn run(
|
||||
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());
|
||||
|
||||
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
|
||||
|
||||
@@ -204,10 +204,25 @@ impl Engine {
|
||||
|
||||
// ---------------------------------------------------------------- varredura
|
||||
|
||||
pub fn start_scan(&self, path: &str, opts: ScanOptions) -> Result<String> {
|
||||
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||
let fp = crate::source::fingerprint(src.as_ref());
|
||||
let job = self.register(JobKind::Scan, path.to_string(), opts, Some(src));
|
||||
pub fn start_scan(&self, path: &str, mut opts: ScanOptions) -> Result<String> {
|
||||
let raw = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||
let fp = crate::source::fingerprint(raw.as_ref());
|
||||
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.session_file.lock().unwrap() = Some(new_session_path(path));
|
||||
spawn_scan(job.clone(), None);
|
||||
@@ -814,8 +829,8 @@ impl Engine {
|
||||
let mut last_err = None;
|
||||
for path in &tried {
|
||||
match open_path(Path::new(path)) {
|
||||
Ok(src) if crate::reconnect::matches(src.as_ref(), size, fp.as_deref()) => {
|
||||
*j.src.lock().unwrap() = Some(src);
|
||||
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(finish_source(&j, src));
|
||||
*j.src_path.lock().unwrap() = path.clone();
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -826,7 +841,7 @@ impl Engine {
|
||||
if let Some((path, src)) =
|
||||
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();
|
||||
return Ok(Some(path));
|
||||
}
|
||||
@@ -863,7 +878,7 @@ impl Engine {
|
||||
if fp.is_none() {
|
||||
*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.st.lock()
|
||||
.unwrap()
|
||||
@@ -1483,6 +1498,14 @@ impl Engine {
|
||||
|
||||
// ---------------------------------------------------------------- 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 {
|
||||
crate::settings::config_dir().join("sessoes")
|
||||
}
|
||||
|
||||
@@ -210,8 +210,17 @@ pub enum FsKind {
|
||||
Ext,
|
||||
Apfs,
|
||||
HfsPlus,
|
||||
Btrfs,
|
||||
Xfs,
|
||||
Refs,
|
||||
Bitlocker,
|
||||
Luks,
|
||||
/// FileVault 2 (Core Storage).
|
||||
Corestorage,
|
||||
/// Membro de volume LVM (physical volume).
|
||||
Lvm,
|
||||
/// Membro de RAID por software (mdadm).
|
||||
Raid,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -226,8 +235,14 @@ impl FsKind {
|
||||
FsKind::Ext => "ext2/3/4",
|
||||
FsKind::Apfs => "APFS",
|
||||
FsKind::HfsPlus => "HFS+",
|
||||
FsKind::Btrfs => "Btrfs",
|
||||
FsKind::Xfs => "XFS",
|
||||
FsKind::Refs => "ReFS",
|
||||
FsKind::Bitlocker => "BitLocker",
|
||||
FsKind::Luks => "LUKS",
|
||||
FsKind::Corestorage => "FileVault 2",
|
||||
FsKind::Lvm => "LVM",
|
||||
FsKind::Raid => "RAID (mdadm)",
|
||||
FsKind::Unknown => "desconhecido",
|
||||
}
|
||||
}
|
||||
@@ -237,6 +252,7 @@ impl FsKind {
|
||||
match self {
|
||||
FsKind::Bitlocker => Some("BitLocker"),
|
||||
FsKind::Luks => Some("LUKS"),
|
||||
FsKind::Corestorage => Some("FileVault 2"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -302,6 +318,12 @@ pub struct ScanOptions {
|
||||
/// Tamanho máximo aceito para arquivos carved (bytes).
|
||||
#[serde(default = "default_max_carve")]
|
||||
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 {
|
||||
@@ -315,6 +337,8 @@ impl Default for ScanOptions {
|
||||
include_allocated: false,
|
||||
categories: vec![],
|
||||
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