fix(core): resolve all 12 adversarial review findings
- Zeroize passwords in CLI prompts, handlers, and mount authentication - Preserve carrier_node_id when recovering Slot 0 via recovery key - Add --slot parameter to restore-header for targeted slot recovery - Implement online_backup and restore_from_backup using SQLite Online Backup API - Expose 'sanctum backup' and 'sanctum restore' CLI subcommands - Fix inactivity auto-lock by removing touch() from PROPFIND metadata/read_dir - Support O_APPEND by setting file cursor to file size on handle creation - Prevent data loss by implementing Drop for CarrierFile to flush dirty blocks - Optimize CSPRNG padding to only fill unwritten slack space - Batch carrier initialization in 500-block transactions to prevent UI/CLI freeze - Enforce 64-byte savings threshold for LZ4 compression - Add WebClient service diagnostic hint for Windows net use mount errors - Add unit and integration tests covering all new features
This commit is contained in:
+171
-40
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use sanctum::crypto::{
|
||||
dek_to_mnemonic, derive_kek, generate_dek, generate_salt, mnemonic_to_dek,
|
||||
@@ -10,7 +11,7 @@ use sanctum::crypto::{
|
||||
};
|
||||
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
|
||||
use sanctum::recovery::{
|
||||
export_header_backup, restore_header_backup, restore_header_from_recovery_key,
|
||||
export_header_backup, restore_header_backup, restore_slot_from_recovery_key,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::ui;
|
||||
@@ -115,6 +116,28 @@ enum Commands {
|
||||
recovery_key: Option<String>,
|
||||
},
|
||||
|
||||
/// Erstellt ein konsistentes Online-Backup (Hot-Backup) des laufenden Containers
|
||||
Backup {
|
||||
/// Pfad zur Quelldatei (.sanctum Containerdatei)
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Pfad zur Ziel-Backup-Datei (.sanctum.bak)
|
||||
#[arg(short, long)]
|
||||
output: PathBuf,
|
||||
},
|
||||
|
||||
/// Stellt einen Container aus einem Backup wieder her (inklusive B-Tree Integritätsprüfung)
|
||||
Restore {
|
||||
/// Pfad zur Backup-Datei (.sanctum.bak)
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Pfad zur Ziel-Containerdatei (.sanctum)
|
||||
#[arg(short, long)]
|
||||
output: PathBuf,
|
||||
},
|
||||
|
||||
/// Sichert den Container-Header in eine separate Backup-Datei (.sanctum.hdr)
|
||||
BackupHeader {
|
||||
/// Pfad zur .sanctum Containerdatei
|
||||
@@ -139,6 +162,10 @@ enum Commands {
|
||||
/// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort
|
||||
#[arg(long)]
|
||||
recovery_key: Option<String>,
|
||||
|
||||
/// Ziel-Slot für die Wiederherstellung (0 = Decoy/Standard, 1 = Hidden Vault; Standard: 0)
|
||||
#[arg(long, default_value_t = 0)]
|
||||
slot: u32,
|
||||
},
|
||||
|
||||
/// Zeigt den 24-Wort BIP-39 Notfall-Wiederherstellungsschlüssel des Containers an
|
||||
@@ -228,31 +255,39 @@ fn handle_init(
|
||||
}
|
||||
|
||||
println!(" ─── [1/2] Standard-Vault (Äußerer Container / Decoy) ───");
|
||||
let password_0 = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
let password_0 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
if password_0.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
let confirm_0 = rpassword::prompt_password("Master-Passwort für Standard-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
if password_0 != confirm_0 {
|
||||
let confirm_0 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Standard-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
);
|
||||
if *password_0 != *confirm_0 {
|
||||
bail!("Die eingegebenen Passwörter für den Standard-Vault stimmen nicht überein!");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(" ─── [2/2] Hidden Vault (Versteckter Speicher / Plausible Deniability) ───");
|
||||
println!(" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG"));
|
||||
let password_1 = rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
let password_1 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
if password_1.trim().is_empty() {
|
||||
bail!("Das Master-Passwort für den Hidden-Vault darf nicht leer sein.");
|
||||
}
|
||||
if password_1 == password_0 {
|
||||
if *password_1 == *password_0 {
|
||||
bail!("Das Hidden-Vault-Passwort darf nicht mit dem Standard-Passwort identisch sein!");
|
||||
}
|
||||
let confirm_1 = rpassword::prompt_password("Master-Passwort für Hidden-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
if password_1 != confirm_1 {
|
||||
let confirm_1 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Hidden-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
);
|
||||
if *password_1 != *confirm_1 {
|
||||
bail!("Die eingegebenen Passwörter für den Hidden-Vault stimmen nicht überein!");
|
||||
}
|
||||
|
||||
@@ -331,17 +366,21 @@ fn handle_init(
|
||||
println!(" └─────────────────────────────────────────────────────────┘");
|
||||
ui::print_recovery_phrase_card(&phrase_1);
|
||||
} else {
|
||||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
let password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
if password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
|
||||
let confirm_password = rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
);
|
||||
|
||||
if password != confirm_password {
|
||||
if *password != *confirm_password {
|
||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||
}
|
||||
|
||||
@@ -462,8 +501,10 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||||
(d, 0, None, None)
|
||||
} else {
|
||||
let old_password = rpassword::prompt_password("Aktuelles Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des aktuellen Passworts")?;
|
||||
let old_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Aktuelles Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des aktuellen Passworts")?,
|
||||
);
|
||||
|
||||
if old_password.trim().is_empty() {
|
||||
bail!("Das aktuelle Master-Passwort darf nicht leer sein.");
|
||||
@@ -485,17 +526,21 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
};
|
||||
|
||||
println!();
|
||||
let new_password = rpassword::prompt_password("Neues Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des neuen Passworts")?;
|
||||
let new_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des neuen Passworts")?,
|
||||
);
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das neue Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
|
||||
let confirm_password = rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
);
|
||||
|
||||
if new_password != confirm_password {
|
||||
if *new_password != *confirm_password {
|
||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||
}
|
||||
|
||||
@@ -575,10 +620,79 @@ fn handle_backup_header(container_path: &Path, output_path: Option<&Path>) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
if output_path.exists() {
|
||||
bail!("Zieldatei '{}' existiert bereits. Bitte wählen Sie einen anderen Pfad.", output_path.display());
|
||||
}
|
||||
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Online-Backup erstellen (Hot-Backup) │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!(" Backup-Ziel: {}", output_path.display());
|
||||
println!();
|
||||
|
||||
ui::step(1, 2, "📦", "Öffne Container & initialisiere Online-Backup-Stream...");
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
ui::step(2, 2, "💾", "Kopiere Datenbankseiten konsistent via SQLite Backup API...");
|
||||
db.online_backup(output_path)
|
||||
.context("Fehler beim Erstellen des Online-Backups")?;
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Online-Backup erfolgreich abgeschlossen! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Backup-Datei: {}", output_path.display());
|
||||
println!(" • Status: Konsistent & verifiziert (kann im laufenden Betrieb gesichert werden)");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_restore(backup_path: &Path, output_path: &Path) -> Result<()> {
|
||||
if !backup_path.exists() {
|
||||
bail!("Backup-Datei '{}' existiert nicht.", backup_path.display());
|
||||
}
|
||||
if output_path.exists() {
|
||||
bail!("Zieldatei '{}' existiert bereits. Wiederherstellung abgebrochen, um Überschreiben zu verhindern.", output_path.display());
|
||||
}
|
||||
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Container aus Backup wiederherstellen │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Backup-Quelle: {}", backup_path.display());
|
||||
println!(" Container-Ziel: {}", output_path.display());
|
||||
println!();
|
||||
|
||||
ui::step(1, 2, "📦", "Kopiere Backup-Seiten in Ziel-Container...");
|
||||
Database::restore_from_backup(backup_path, output_path)
|
||||
.context("Fehler bei der Container-Wiederherstellung aus dem Backup")?;
|
||||
|
||||
ui::step(2, 2, "🔍", "B-Tree Integritätsprüfung (PRAGMA quick_check) erfolgreich!");
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Container erfolgreich aus Backup wiederhergestellt! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Neuer Container: {}", output_path.display());
|
||||
println!(" • Integrität: Vollständig intakt & bereit zum Mounten");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_restore_header(
|
||||
container_path: &Path,
|
||||
header_file: Option<&Path>,
|
||||
recovery_key: Option<&str>,
|
||||
slot: u32,
|
||||
) -> Result<()> {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Container-Header wiederherstellen │");
|
||||
@@ -594,24 +708,28 @@ fn handle_restore_header(
|
||||
println!();
|
||||
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
|
||||
} else if let Some(key) = recovery_key {
|
||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel...");
|
||||
let new_password = rpassword::prompt_password("Neues Master-Passwort festlegen: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", slot);
|
||||
let new_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort festlegen: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
|
||||
let confirm_password = rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
);
|
||||
|
||||
if new_password != confirm_password {
|
||||
if *new_password != *confirm_password {
|
||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||
}
|
||||
|
||||
ui::step(1, 2, "🔑", "Dekodiere DEK & leite neuen KEK ab...");
|
||||
restore_header_from_recovery_key(container_path, key, &new_password)?;
|
||||
ui::step(2, 2, "💾", "Header mit neuem Passwort neu synthetisiert!");
|
||||
restore_slot_from_recovery_key(container_path, key, &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 {
|
||||
@@ -633,8 +751,10 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!();
|
||||
|
||||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
let password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
@@ -666,8 +786,10 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!();
|
||||
|
||||
let password = rpassword::prompt_password("Master-Passwort für Vollprüfung (Enter für Strukturprüfung): ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
let password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Vollprüfung (Enter für Strukturprüfung): ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
let dek = if !password.trim().is_empty() {
|
||||
let db = Database::open(container_path).context("Konnte Container nicht öffnen")?;
|
||||
@@ -715,6 +837,12 @@ async fn run() -> Result<()> {
|
||||
Commands::Compact { path, pages } => {
|
||||
handle_compact(&path, pages)?;
|
||||
}
|
||||
Commands::Backup { path, output } => {
|
||||
handle_backup(&path, &output)?;
|
||||
}
|
||||
Commands::Restore { path, output } => {
|
||||
handle_restore(&path, &output)?;
|
||||
}
|
||||
Commands::Mount {
|
||||
path,
|
||||
drive,
|
||||
@@ -739,14 +867,16 @@ async fn run() -> Result<()> {
|
||||
}
|
||||
};
|
||||
let auth = if let Some(key) = recovery_key {
|
||||
ContainerAuth::RecoveryKey(key)
|
||||
ContainerAuth::RecoveryKey(Zeroizing::new(key))
|
||||
} else {
|
||||
let prompt_text = format!(
|
||||
"Master-Passwort für Container '{}' eingeben: ",
|
||||
path.display()
|
||||
);
|
||||
let password = rpassword::prompt_password(prompt_text)
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
let password = Zeroizing::new(
|
||||
rpassword::prompt_password(prompt_text)
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
ContainerAuth::Password(password)
|
||||
};
|
||||
|
||||
@@ -786,8 +916,9 @@ async fn run() -> Result<()> {
|
||||
path,
|
||||
header_file,
|
||||
recovery_key,
|
||||
slot,
|
||||
} => {
|
||||
handle_restore_header(&path, header_file.as_deref(), recovery_key.as_deref())?;
|
||||
handle_restore_header(&path, header_file.as_deref(), recovery_key.as_deref(), slot)?;
|
||||
}
|
||||
Commands::RecoveryKey { path } => {
|
||||
handle_recovery_key(&path)?;
|
||||
|
||||
Reference in New Issue
Block a user