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:
+83
-26
@@ -76,8 +76,10 @@ enum Commands {
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
|
||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage)
|
||||
#[arg(long)]
|
||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage).
|
||||
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel sicher und interaktiv
|
||||
/// einzugeben (verhindert Klartext-Einträge in der PowerShell-Historie).
|
||||
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||
recovery_key: Option<String>,
|
||||
|
||||
/// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer
|
||||
@@ -99,6 +101,10 @@ enum Commands {
|
||||
/// Deaktiviert das Blockieren und Verbergen von Windows Explorer Metadaten (Thumbs.db, desktop.ini)
|
||||
#[arg(long, default_value_t = false)]
|
||||
no_anti_leak: bool,
|
||||
|
||||
/// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben)
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
stealth: bool,
|
||||
},
|
||||
|
||||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||
@@ -114,8 +120,9 @@ enum Commands {
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (erlaubt Reset bei vergessenem Passwort)
|
||||
#[arg(long)]
|
||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (erlaubt Reset bei vergessenem Passwort).
|
||||
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel interaktiv einzugeben.
|
||||
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||
recovery_key: Option<String>,
|
||||
},
|
||||
|
||||
@@ -162,8 +169,9 @@ enum Commands {
|
||||
#[arg(long)]
|
||||
header_file: Option<PathBuf>,
|
||||
|
||||
/// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort
|
||||
#[arg(long)]
|
||||
/// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort.
|
||||
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel interaktiv einzugeben.
|
||||
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||
recovery_key: Option<String>,
|
||||
|
||||
/// Ziel-Slot für die Wiederherstellung (0 = Decoy/Standard, 1 = Hidden Vault; Standard: 0)
|
||||
@@ -228,6 +236,19 @@ pub fn parse_size_string(s: &str) -> Result<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest den 24-Wort BIP-39 Notfallschlüssel interaktiv und maskiert ein,
|
||||
/// ohne dass Eingaben in der PowerShell-Historie (ConsoleHost_history.txt) landen.
|
||||
fn prompt_recovery_key_interactive() -> Result<Zeroizing<String>> {
|
||||
println!(" [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.", ui::cyan("ℹ"));
|
||||
println!(" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der PowerShell-Historie.", ui::dim("OpSec"));
|
||||
let phrase = rpassword::prompt_password("24-Wort Notfallschlüssel: ")
|
||||
.context("Fehler beim Einlesen des Notfallschlüssels")?;
|
||||
if phrase.trim().is_empty() {
|
||||
bail!("Der Notfallschlüssel darf nicht leer sein.");
|
||||
}
|
||||
Ok(Zeroizing::new(phrase))
|
||||
}
|
||||
|
||||
fn handle_init(
|
||||
container_path: &Path,
|
||||
with_hidden: bool,
|
||||
@@ -506,8 +527,13 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
let (dek, target_slot_id, carrier_dek, carrier_node_id) = if let Some(phrase) = recovery_key {
|
||||
let actual_phrase = if phrase.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(phrase.to_string())
|
||||
};
|
||||
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
||||
let d = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
let d = mnemonic_to_dek(&actual_phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||||
(d, 0, None, None)
|
||||
} else {
|
||||
@@ -717,7 +743,37 @@ fn handle_restore_header(
|
||||
ui::step(2, 2, "💾", "Header in Container-Datenbank zurückgeschrieben!");
|
||||
println!();
|
||||
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
|
||||
} else if let Some(key) = recovery_key {
|
||||
} else {
|
||||
let key_str = if let Some(key) = recovery_key {
|
||||
if key.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(key.to_string())
|
||||
}
|
||||
} else {
|
||||
// Interaktiver Auswahldialog statt Fehlermeldung
|
||||
println!(" Keine Wiederherstellungsquelle als CLI-Argument angegeben.");
|
||||
println!(" Bitte wählen Sie die gewünschte Wiederherstellungsmethode:");
|
||||
println!(" [1] Sicherungsdatei verwenden (.sanctum.hdr)");
|
||||
println!(" [2] 24-Wort BIP-39 Notfallschlüssel interaktiv eingeben");
|
||||
print!(" Auswahl [1/2]: ");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let mut choice = String::new();
|
||||
std::io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!(" Pfad zur .sanctum.hdr Sicherungsdatei: ");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let mut p = String::new();
|
||||
std::io::stdin().read_line(&mut p)?;
|
||||
let hdr_path = PathBuf::from(p.trim());
|
||||
return handle_restore_header(container_path, Some(&hdr_path), None, slot);
|
||||
}
|
||||
"2" => prompt_recovery_key_interactive()?,
|
||||
_ => bail!("Ungültige Auswahl. Wiederherstellung abgebrochen."),
|
||||
}
|
||||
};
|
||||
|
||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", slot);
|
||||
let new_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort festlegen: ")
|
||||
@@ -738,12 +794,10 @@ fn handle_restore_header(
|
||||
}
|
||||
|
||||
ui::step(1, 2, "🔑", "Dekodiere DEK & leite neuen KEK ab...");
|
||||
restore_slot_from_recovery_key(container_path, key, &new_password, slot)?;
|
||||
restore_slot_from_recovery_key(container_path, &key_str, &new_password, slot)?;
|
||||
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", slot));
|
||||
println!();
|
||||
println!("{}", ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!"));
|
||||
} else {
|
||||
bail!("Bitte geben Sie entweder --header-file <PFAD> oder --recovery-key \"<24 WÖRTER>\" an.");
|
||||
}
|
||||
println!();
|
||||
|
||||
@@ -774,12 +828,8 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
||||
.authenticate(&password)
|
||||
.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||
let dek = keys.dek().clone();
|
||||
let slot_id = keys.slot_id();
|
||||
|
||||
let phrase = dek_to_mnemonic(&dek)?;
|
||||
if slot_id == 1 {
|
||||
println!(" • Vault: {}", ui::magenta("Hidden Vault (Slot 1)"));
|
||||
}
|
||||
ui::print_recovery_phrase_card(&phrase);
|
||||
|
||||
Ok(())
|
||||
@@ -807,9 +857,7 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||||
|
||||
match meta.authenticate(&password) {
|
||||
Some(keys) => {
|
||||
let slot_id = keys.slot_id();
|
||||
let vault_desc = if slot_id == 1 { "Hidden Vault (Slot 1)" } else { "Decoy Vault (Slot 0)" };
|
||||
println!(" {} Master-Passwort verifiziert ({}). Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"), ui::cyan(vault_desc));
|
||||
println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"));
|
||||
Some(keys.dek().clone())
|
||||
}
|
||||
None => {
|
||||
@@ -863,21 +911,29 @@ async fn run() -> Result<()> {
|
||||
idle_timeout,
|
||||
no_screen_lock,
|
||||
no_anti_leak,
|
||||
stealth,
|
||||
} => {
|
||||
let drive_char = match drive {
|
||||
Some(ref d) => parse_drive_letter(d)?,
|
||||
None => {
|
||||
let auto_drive = sanctum::windows::find_next_available_drive()?;
|
||||
println!(
|
||||
" {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:",
|
||||
ui::cyan("ℹ"),
|
||||
auto_drive
|
||||
);
|
||||
if !stealth {
|
||||
println!(
|
||||
" {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:",
|
||||
ui::cyan("ℹ"),
|
||||
auto_drive
|
||||
);
|
||||
}
|
||||
auto_drive
|
||||
}
|
||||
};
|
||||
let auth = if let Some(key) = recovery_key {
|
||||
ContainerAuth::RecoveryKey(Zeroizing::new(key))
|
||||
let actual_key = if key.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(key)
|
||||
};
|
||||
ContainerAuth::RecoveryKey(actual_key)
|
||||
} else {
|
||||
let prompt_text = format!(
|
||||
"Master-Passwort für Container '{}' eingeben: ",
|
||||
@@ -890,8 +946,8 @@ async fn run() -> Result<()> {
|
||||
ContainerAuth::Password(password)
|
||||
};
|
||||
|
||||
let open_explorer = !no_open;
|
||||
let enable_tray = !no_tray;
|
||||
let open_explorer = !no_open && !stealth;
|
||||
let enable_tray = !no_tray && !stealth;
|
||||
let lock_on_screen_lock = !no_screen_lock;
|
||||
let anti_leak = !no_anti_leak;
|
||||
mount_container(
|
||||
@@ -904,6 +960,7 @@ async fn run() -> Result<()> {
|
||||
idle_timeout,
|
||||
lock_on_screen_lock,
|
||||
anti_leak,
|
||||
stealth,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user