feat(opsec-ux): implement HF-01 to HF-04 and VFS carrier protection

- HF-01: eliminate visual leaks between decoy and hidden vaults during mount
- HF-02: add secure interactive BIP-39 recovery prompt avoiding shell history
- HF-03: implement BIP-39 normalization, word index error pinpointing, and Levenshtein typo suggestions
- HF-04: add --stealth mode for silent mounting in high-risk environments
- VFS: enforce write, truncate, delete, rename, and directory removal protection for carrier node in decoy vault
This commit is contained in:
2026-09-10 13:27:01 +02:00
parent 99813ae2a3
commit 541190cff4
5 changed files with 464 additions and 129 deletions
+124 -94
View File
@@ -120,6 +120,7 @@ pub async fn mount_container(
idle_timeout: Option<u64>,
lock_on_screen_lock: bool,
anti_leak: bool,
stealth: bool,
) -> Result<()> {
let drive_str = format_drive(drive_letter);
@@ -130,8 +131,10 @@ pub async fn mount_container(
);
}
println!();
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
if !stealth {
println!();
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
}
let db = Database::open(container_path)
.context("Konnte Container-Datenbank nicht öffnen")?;
@@ -141,10 +144,14 @@ pub async fn mount_container(
let (dek, carrier_dek, carrier_node_id, version, vault_id) = match auth {
ContainerAuth::Password(ref password) => {
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...");
if !stealth {
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...");
}
match meta.authenticate(password) {
Some(keys) => {
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
if !stealth {
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
}
let vault_id = keys.slot_id();
let ver = keys.version();
let c_dek = keys.carrier_dek();
@@ -157,10 +164,14 @@ pub async fn mount_container(
}
}
ContainerAuth::RecoveryKey(ref phrase) => {
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
if !stealth {
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
}
let dek = mnemonic_to_dek(phrase)
.context("Ungültiger 24-Wort Notfallschlüssel")?;
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
if !stealth {
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
}
// Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann
let is_hidden = {
@@ -212,15 +223,17 @@ pub async fn mount_container(
let bound_addr = listener.local_addr()?;
let bound_port = bound_addr.port();
ui::step(
4,
4,
"🌐",
&format!(
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
bound_port, drive_str
),
);
if !stealth {
ui::step(
4,
4,
"🌐",
&format!(
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
bound_port, drive_str
),
);
}
let (shutdown_tx, shutdown_rx) = watch::channel(false);
@@ -239,17 +252,9 @@ pub async fn mount_container(
return Err(e);
}
// Optional automatisch im Windows Explorer öffnen (mit DFIR ShellBag-Schutz für Hidden Vault)
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
if open_explorer {
if vault_id == 1 {
println!(
" {} {}",
ui::yellow("[!]"),
ui::dim("OpSec-Schutz: Explorer-Auto-Open für Hidden Vault unterdrückt (verhindert persistente ShellBag-Spuren in UsrClass.dat).")
);
} else {
let _ = crate::windows::open_in_explorer(drive_letter);
}
let _ = crate::windows::open_in_explorer(drive_letter);
}
// System-Tray Initialisierung
@@ -324,32 +329,33 @@ pub async fn mount_container(
None
};
println!();
println!("┌─────────────────────────────────────────────────────────────┐");
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
println!("└─────────────────────────────────────────────────────────────┘");
println!();
println!(" Container: {}", container_path.display());
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
if vault_id == 1 {
println!("Vault: {} (Verschlüsselte Dateinamen)", ui::magenta("Hidden Vault (Slot 1)"));
if stealth {
println!("Sanctum: Netzlaufwerk {} bereit.", drive_str);
} else {
println!();
println!("┌─────────────────────────────────────────────────────────────┐");
println!(" ✔ Sanctum Container erfolgreich gemountet │");
println!("└─────────────────────────────────────────────────────────────┘");
println!();
println!(" • Container: {}", container_path.display());
println!("Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
if let Some(secs) = idle_timeout {
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
}
if lock_on_screen_lock {
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
}
if anti_leak {
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
}
if enable_tray {
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
}
println!();
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
println!();
}
if let Some(secs) = idle_timeout {
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
}
if lock_on_screen_lock {
println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv");
}
if anti_leak {
println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)");
}
if enable_tray {
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
}
println!();
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
println!();
// Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT)
let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1);
@@ -359,64 +365,88 @@ pub async fn mount_container(
tokio::select! {
res = tokio::signal::ctrl_c() => {
let _ = res;
println!();
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
if !stealth {
println!();
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
}
}
_ = console_close_rx.recv() => {
println!();
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
if !stealth {
println!();
println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]"));
}
}
_ = tray_shutdown_rx.recv() => {
println!();
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
if !stealth {
println!();
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
}
}
_ = session_lock_rx.recv() => {
println!();
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
if !stealth {
println!();
println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]"));
}
}
_ = idle_shutdown_rx.recv() => {
println!();
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
}
}
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
let _ = std::io::Write::flush(&mut std::io::stdout());
// Automatisches Unmount
if let Err(e) = unmount_drive(drive_letter) {
println!("{}", ui::yellow(&format!("Warnung ({e})")));
} else {
println!("{}", ui::green("OK"));
}
// HTTP Server beenden
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
if let Ok(freelist) = db.freelist_count() {
if freelist > 0 {
print!(" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist);
let _ = std::io::Write::flush(&mut std::io::stdout());
match db.incremental_vacuum(None) {
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))),
if !stealth {
println!();
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
}
}
}
// SQLite WAL Checkpoint erzwingen
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
let _ = std::io::Write::flush(&mut std::io::stdout());
if let Err(e) = db.checkpoint() {
println!("{}", ui::red(&format!("Fehler ({e})")));
if stealth {
let _ = unmount_drive(drive_letter);
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
if let Ok(freelist) = db.freelist_count() {
if freelist > 0 {
let _ = db.incremental_vacuum(None);
}
}
let _ = db.checkpoint();
println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str);
} else {
println!("{}", ui::green("OK"));
}
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
let _ = std::io::Write::flush(&mut std::io::stdout());
println!();
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green(""));
println!();
// Automatisches Unmount
if let Err(e) = unmount_drive(drive_letter) {
println!("{}", ui::yellow(&format!("Warnung ({e})")));
} else {
println!("{}", ui::green("OK"));
}
// HTTP Server beenden
let _ = shutdown_tx.send(true);
let _ = server_handle.await;
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
if let Ok(freelist) = db.freelist_count() {
if freelist > 0 {
print!(" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist);
let _ = std::io::Write::flush(&mut std::io::stdout());
match db.incremental_vacuum(None) {
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))),
}
}
}
// SQLite WAL Checkpoint erzwingen
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
let _ = std::io::Write::flush(&mut std::io::stdout());
if let Err(e) = db.checkpoint() {
println!("{}", ui::red(&format!("Fehler ({e})")));
} else {
println!("{}", ui::green("OK"));
}
println!();
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green(""));
println!();
}
Ok(())
}