805 lines
35 KiB
Rust
805 lines
35 KiB
Rust
use std::path::{Path, PathBuf};
|
||
|
||
use anyhow::{bail, Context, Result};
|
||
use clap::{Parser, Subcommand};
|
||
use tracing_subscriber::EnvFilter;
|
||
|
||
use sanctum::crypto::{
|
||
dek_to_mnemonic, derive_kek, generate_dek, generate_salt, mnemonic_to_dek, unwrap_dek,
|
||
wrap_dek, KdfParams, FORMAT_VERSION,
|
||
};
|
||
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
|
||
use sanctum::recovery::{
|
||
export_header_backup, restore_header_backup, restore_header_from_recovery_key,
|
||
};
|
||
use sanctum::storage::Database;
|
||
use sanctum::ui;
|
||
use sanctum::verify::verify_container;
|
||
|
||
#[derive(Parser)]
|
||
#[command(name = "sanctum")]
|
||
#[command(author = "Sanctum Systems & Security Engineering")]
|
||
#[command(about = "Sanctum: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland", long_about = None)]
|
||
#[command(version)]
|
||
struct Cli {
|
||
#[command(subcommand)]
|
||
command: Commands,
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
enum Commands {
|
||
/// Initialisiert einen neuen verschlüsselten Sanctum-Container (.sanctum)
|
||
Init {
|
||
/// Pfad zur zu erstellenden .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Erstellt zusätzlich einen plausibel abstreitbaren Hidden Vault (Plausible Deniability)
|
||
#[arg(long, default_value_t = false)]
|
||
with_hidden: bool,
|
||
},
|
||
|
||
/// Kompaktiert den Container-Speicherplatz (Incremental Vacuum) und bereinigt ungenutzte Seiten
|
||
Compact {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Maximale Anzahl an Seiten, die freigegeben werden sollen (Standard: alle freien Seiten)
|
||
#[arg(short, long)]
|
||
pages: Option<usize>,
|
||
},
|
||
|
||
/// Bindet einen Sanctum-Container als Windows-Netzlaufwerk via WebDAV ein
|
||
Mount {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt standardmäßig automatisch das nächste freie Laufwerk)
|
||
#[arg(short, long)]
|
||
drive: Option<String>,
|
||
|
||
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
||
#[arg(long)]
|
||
port: Option<u16>,
|
||
|
||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage)
|
||
#[arg(long)]
|
||
recovery_key: Option<String>,
|
||
|
||
/// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer
|
||
#[arg(long, default_value_t = false)]
|
||
no_open: bool,
|
||
|
||
/// Deaktiviert das Windows System-Tray Icon während des Mounts
|
||
#[arg(long, default_value_t = false)]
|
||
no_tray: bool,
|
||
|
||
/// Automatisches Aushängen und Sperren nach N Sekunden Inaktivität (z. B. 300 für 5 Minuten)
|
||
#[arg(long, value_name = "SECS")]
|
||
idle_timeout: Option<u64>,
|
||
|
||
/// Verhindert das automatische Sperren beim Sperren des Windows-Bildschirms (Win + L)
|
||
#[arg(long, default_value_t = false)]
|
||
no_screen_lock: bool,
|
||
|
||
/// Deaktiviert das Blockieren und Verbergen von Windows Explorer Metadaten (Thumbs.db, desktop.ini)
|
||
#[arg(long, default_value_t = false)]
|
||
no_anti_leak: bool,
|
||
},
|
||
|
||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||
Unmount {
|
||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:')
|
||
#[arg(short, long)]
|
||
drive: String,
|
||
},
|
||
|
||
/// Ändert das Master-Passwort eines bestehenden Sanctum-Containers
|
||
Passwd {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (erlaubt Reset bei vergessenem Passwort)
|
||
#[arg(long)]
|
||
recovery_key: Option<String>,
|
||
},
|
||
|
||
/// Sichert den Container-Header in eine separate Backup-Datei (.sanctum.hdr)
|
||
BackupHeader {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Optionaler Ausgabepfad (Standard: <CONTAINER>.hdr)
|
||
#[arg(short, long)]
|
||
output: Option<PathBuf>,
|
||
},
|
||
|
||
/// Stellt den Container-Header aus einem Backup oder via 24-Wort Notfallschlüssel wieder her
|
||
RestoreHeader {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Pfad zur Header-Backup-Datei (.sanctum.hdr)
|
||
#[arg(long)]
|
||
header_file: Option<PathBuf>,
|
||
|
||
/// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort
|
||
#[arg(long)]
|
||
recovery_key: Option<String>,
|
||
},
|
||
|
||
/// Zeigt den 24-Wort BIP-39 Notfall-Wiederherstellungsschlüssel des Containers an
|
||
RecoveryKey {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
},
|
||
|
||
/// Führt eine Integritätsprüfung (FSCK) und Bitrot-Erkennung auf dem Container durch
|
||
Verify {
|
||
/// Pfad zur .sanctum Containerdatei
|
||
#[arg(short, long)]
|
||
path: PathBuf,
|
||
|
||
/// Vollständige kryptografische AEAD-Prüfung aller Chunks
|
||
#[arg(long, default_value_t = true)]
|
||
full: bool,
|
||
},
|
||
|
||
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
||
Register,
|
||
|
||
/// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry
|
||
Unregister,
|
||
}
|
||
|
||
fn parse_drive_letter(s: &str) -> Result<char> {
|
||
let trimmed = s.trim();
|
||
let ch = trimmed
|
||
.chars()
|
||
.next()
|
||
.context("Laufwerksangabe darf nicht leer sein")?;
|
||
|
||
if !ch.is_ascii_alphabetic() {
|
||
bail!("Ungültiger Laufwerksbuchstabe: '{}'", trimmed);
|
||
}
|
||
|
||
Ok(ch.to_ascii_uppercase())
|
||
}
|
||
|
||
fn handle_init(container_path: &Path, with_hidden: bool) -> Result<()> {
|
||
if container_path.exists() {
|
||
bail!(
|
||
"Zieldatei '{}' existiert bereits. Initialisierung abgebrochen, um Überschreiben zu verhindern.",
|
||
container_path.display()
|
||
);
|
||
}
|
||
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Neuen verschlüsselten Container erstellen │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!(" Zieldatei: {}", container_path.display());
|
||
if with_hidden {
|
||
println!(" Modus: Dual-Vault (Standard + {})", ui::magenta("Hidden Vault"));
|
||
}
|
||
println!();
|
||
|
||
if with_hidden {
|
||
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")?;
|
||
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 {
|
||
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")?;
|
||
if password_1.trim().is_empty() {
|
||
bail!("Das Master-Passwort für den Hidden-Vault darf nicht leer sein.");
|
||
}
|
||
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 {
|
||
bail!("Die eingegebenen Passwörter für den Hidden-Vault stimmen nicht überein!");
|
||
}
|
||
|
||
println!();
|
||
ui::step(1, 4, "🔑", "Leite KEKs für Standard- und Hidden-Vault via Argon2id ab...");
|
||
let salt_0 = generate_salt();
|
||
let kdf_params_0 = KdfParams::default();
|
||
let kek_0 = derive_kek(&password_0, &salt_0, &kdf_params_0)?;
|
||
|
||
let salt_1 = generate_salt();
|
||
let kdf_params_1 = KdfParams::default();
|
||
let kek_1 = derive_kek(&password_1, &salt_1, &kdf_params_1)?;
|
||
|
||
ui::step(2, 4, "🎲", "Erzeuge getrennte DEKs für beide Vaults via CSPRNG...");
|
||
let dek_0 = generate_dek();
|
||
let dek_1 = generate_dek();
|
||
|
||
ui::step(3, 4, "🔒", "Verschlüssele beide DEKs unabhängig via AES-256-GCM...");
|
||
let (wrapped_dek_0, nonce_0, tag_0) = wrap_dek(&kek_0, &dek_0)?;
|
||
let (wrapped_dek_1, nonce_1, tag_1) = wrap_dek(&kek_1, &dek_1)?;
|
||
|
||
ui::step(4, 4, "📦", "Initialisiere SQLite-Container mit Dual-Slot Header & WAL-Modus...");
|
||
let db = Database::open(container_path)
|
||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||
|
||
db.init_schema_with_hidden(
|
||
&salt_0,
|
||
&kdf_params_0,
|
||
&wrapped_dek_0,
|
||
&nonce_0,
|
||
&tag_0,
|
||
Some((&salt_1, &kdf_params_1, &wrapped_dek_1, &nonce_1, &tag_1)),
|
||
)?;
|
||
|
||
db.checkpoint()?;
|
||
|
||
let phrase_0 = dek_to_mnemonic(&dek_0)?;
|
||
let phrase_1 = dek_to_mnemonic(&dek_1)?;
|
||
|
||
println!();
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ ✔ Sanctum Dual-Vault Container erfolgreich initialisiert! │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!();
|
||
println!(" • Container: {}", container_path.display());
|
||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
||
println!(" • Verschlüssel: AES-256-GCM + LZ4-Kompression + Dateinamens-Verschleierung");
|
||
println!(" • Deniability: Beide Slots besitzen identische Struktur und Bit-Entropie");
|
||
println!();
|
||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||
println!(" (Die Eingabe des jeweiligen Passworts bindet automatisch den passenden Vault ein)");
|
||
println!();
|
||
|
||
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||
println!(" │ NOTFALLSCHLÜSSEL — SLOT 0 (STANDARD / DECOY VAULT) │");
|
||
println!(" └─────────────────────────────────────────────────────────┘");
|
||
ui::print_recovery_phrase_card(&phrase_0);
|
||
|
||
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||
println!(" │ NOTFALLSCHLÜSSEL — SLOT 1 (HIDDEN VAULT) │");
|
||
println!(" └─────────────────────────────────────────────────────────┘");
|
||
ui::print_recovery_phrase_card(&phrase_1);
|
||
} else {
|
||
let password = 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")?;
|
||
|
||
if password != confirm_password {
|
||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||
}
|
||
|
||
println!();
|
||
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
||
let salt = generate_salt();
|
||
let kdf_params = KdfParams::default();
|
||
let kek = derive_kek(&password, &salt, &kdf_params)
|
||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||
|
||
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
||
let dek = generate_dek();
|
||
|
||
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
||
let (wrapped_dek, header_nonce, header_tag) =
|
||
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
||
|
||
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
||
let db = Database::open(container_path)
|
||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||
|
||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||
.context("Fehler bei der Schema-Initialisierung")?;
|
||
|
||
db.checkpoint()
|
||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||
|
||
let recovery_phrase =
|
||
dek_to_mnemonic(&dek).context("Fehler beim Erzeugen der Notfallphrase")?;
|
||
|
||
println!();
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!();
|
||
println!(" • Container: {}", container_path.display());
|
||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
||
println!(" • Deniability: Slot 1 mit CSPRNG-Zufallsrauschen gefüllt");
|
||
println!();
|
||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||
println!();
|
||
|
||
ui::print_recovery_phrase_card(&recovery_phrase);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
||
if !container_path.exists() {
|
||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||
}
|
||
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Storage-Kompaktierung (Anti-Forensics) │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!(" Container: {}", container_path.display());
|
||
println!();
|
||
|
||
let db = Database::open(container_path)
|
||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||
|
||
let freelist_before = db.freelist_count()
|
||
.context("Konnte Freelist-Größe nicht ermitteln")?;
|
||
|
||
println!(" • Freie Seiten in Freelist: {}", ui::cyan(&freelist_before.to_string()));
|
||
|
||
if freelist_before == 0 {
|
||
println!(" • Der Container ist bereits optimal kompaktiert. Keine Bereinigung nötig.");
|
||
println!();
|
||
return Ok(());
|
||
}
|
||
|
||
ui::step(1, 2, "🧹", "Führe Incremental-Vacuum aus...");
|
||
let freed = db.incremental_vacuum(pages)
|
||
.context("Fehler bei der Speicherbereinigung (Incremental Vacuum)")?;
|
||
|
||
ui::step(2, 2, "💾", "Führe finalen WAL-Checkpoint durch...");
|
||
db.checkpoint()
|
||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||
|
||
let freelist_after = db.freelist_count().unwrap_or(0);
|
||
|
||
println!();
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ ✔ Container erfolgreich kompaktiert! │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!();
|
||
println!(" • Freigegebene Seiten: {}", ui::green(&freed.to_string()));
|
||
println!(" • Verbleibende Freeseiten: {}", ui::dim(&freelist_after.to_string()));
|
||
println!();
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()> {
|
||
if !container_path.exists() {
|
||
bail!(
|
||
"Containerdatei '{}' existiert nicht.",
|
||
container_path.display()
|
||
);
|
||
}
|
||
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Master-Passwort ändern │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!(" Container: {}", container_path.display());
|
||
println!();
|
||
|
||
let db = Database::open(container_path)
|
||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||
|
||
let (dek, target_slot_id) = if let Some(phrase) = recovery_key {
|
||
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
||
let d = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||
(d, 0)
|
||
} else {
|
||
let old_password = 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.");
|
||
}
|
||
|
||
println!();
|
||
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
||
let meta = db
|
||
.read_meta()
|
||
.context("Konnte Container-Header nicht lesen")?;
|
||
|
||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab & prüfe Passwort...");
|
||
let mut found = None;
|
||
for slot in &meta.slots {
|
||
if let Ok(old_kek) = derive_kek(&old_password, &slot.kdf_salt, &slot.kdf_params) {
|
||
if let Ok(d) = unwrap_dek(
|
||
&old_kek,
|
||
&slot.wrapped_dek,
|
||
&slot.header_nonce,
|
||
&slot.header_tag,
|
||
) {
|
||
found = Some((d, slot.slot_id));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
found.ok_or_else(|| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?
|
||
};
|
||
|
||
println!();
|
||
let new_password = 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")?;
|
||
|
||
if new_password != confirm_password {
|
||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||
}
|
||
|
||
println!();
|
||
ui::step(3, 4, "🔒", "Generiere frisches Salt & leite neuen KEK ab...");
|
||
let new_salt = generate_salt();
|
||
let new_params = KdfParams::default();
|
||
let new_kek = derive_kek(&new_password, &new_salt, &new_params)
|
||
.context("KDF-Schlüsselableitung mit neuem Passwort fehlgeschlagen")?;
|
||
|
||
let (new_wrapped_dek, new_nonce, new_tag) =
|
||
wrap_dek(&new_kek, &dek).context("DEK-Wrapping mit neuem KEK fehlgeschlagen")?;
|
||
|
||
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
|
||
db.update_slot_keys(
|
||
target_slot_id,
|
||
&new_salt,
|
||
&new_params,
|
||
&new_wrapped_dek,
|
||
&new_nonce,
|
||
&new_tag,
|
||
)
|
||
.context("Fehler beim Aktualisieren der Container-Metadaten")?;
|
||
|
||
db.checkpoint()
|
||
.context("Fehler beim WAL-Checkpoint nach Passwortänderung")?;
|
||
|
||
println!();
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ ✔ Master-Passwort erfolgreich geändert! │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!();
|
||
println!(" • Container: {}", container_path.display());
|
||
println!(" • Vault Slot: {}", target_slot_id);
|
||
println!(" • KDF: Argon2id mit frischem Salt");
|
||
println!(" • Status: DEK sicher neu verpackt (alle Chunks intakt)");
|
||
println!();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_backup_header(container_path: &Path, output_path: Option<&Path>) -> Result<()> {
|
||
let out = match output_path {
|
||
Some(p) => p.to_path_buf(),
|
||
None => {
|
||
let file_name = container_path
|
||
.file_name()
|
||
.unwrap_or_default()
|
||
.to_string_lossy();
|
||
container_path.with_file_name(format!("{file_name}.hdr"))
|
||
}
|
||
};
|
||
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Container-Header sichern │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!(" Container: {}", container_path.display());
|
||
println!(" Backup-Ziel: {}", out.display());
|
||
println!();
|
||
|
||
export_header_backup(container_path, &out)?;
|
||
|
||
println!("{}", ui::green("✔ Header-Backup erfolgreich exportiert!"));
|
||
println!();
|
||
println!(" Bewahren Sie diese Sicherungsdatei an einem sicheren Ort auf.");
|
||
println!(" Sie enthält alle KDF-Parameter und den verschlüsselten Master-DEK.");
|
||
println!();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_restore_header(
|
||
container_path: &Path,
|
||
header_file: Option<&Path>,
|
||
recovery_key: Option<&str>,
|
||
) -> Result<()> {
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Container-Header wiederherstellen │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!(" Container: {}", container_path.display());
|
||
println!();
|
||
|
||
if let Some(hdr) = header_file {
|
||
println!(" Verwende Backup-Datei: {}", hdr.display());
|
||
ui::step(1, 2, "📦", "Lese und validiere Header-Backup...");
|
||
restore_header_backup(container_path, hdr)?;
|
||
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 {
|
||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel...");
|
||
let new_password = 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")?;
|
||
|
||
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!");
|
||
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!();
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
||
if !container_path.exists() {
|
||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||
}
|
||
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — 24-Wort Notfallschlüssel anzeigen │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!(" Container: {}", container_path.display());
|
||
println!();
|
||
|
||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||
.context("Fehler beim Einlesen des Passworts")?;
|
||
|
||
let db = Database::open(container_path)
|
||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||
|
||
let mut found = None;
|
||
for slot in &meta.slots {
|
||
if let Ok(kek) = derive_kek(&password, &slot.kdf_salt, &slot.kdf_params) {
|
||
if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
|
||
found = Some((dek, slot.slot_id));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
let (dek, slot_id) = found.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||
|
||
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(())
|
||
}
|
||
|
||
fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||
if !container_path.exists() {
|
||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||
}
|
||
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Container-Integritätsprüfung (FSCK) │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
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 dek = if !password.trim().is_empty() {
|
||
let db = Database::open(container_path).context("Konnte Container nicht öffnen")?;
|
||
let meta = db.read_meta().context("Konnte Header nicht lesen")?;
|
||
|
||
let mut found = None;
|
||
for slot in &meta.slots {
|
||
if let Ok(kek) = derive_kek(&password, &slot.kdf_salt, &slot.kdf_params) {
|
||
if let Ok(d) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
|
||
found = Some(d);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
match found {
|
||
Some(d) => {
|
||
println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"));
|
||
Some(d)
|
||
}
|
||
None => {
|
||
println!(" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.", ui::yellow("⚠️"));
|
||
None
|
||
}
|
||
}
|
||
} else {
|
||
println!(" {} Kein Passwort angegeben. Führe nur Strukturprüfung durch.", ui::dim("ℹ"));
|
||
None
|
||
};
|
||
|
||
let report = verify_container(container_path, dek.as_ref(), full)?;
|
||
ui::print_verification_report(&report);
|
||
|
||
if !report.is_healthy() {
|
||
std::process::exit(1);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn run() -> Result<()> {
|
||
let cli = Cli::parse();
|
||
|
||
match cli.command {
|
||
Commands::Init { path, with_hidden } => {
|
||
handle_init(&path, with_hidden)?;
|
||
}
|
||
Commands::Compact { path, pages } => {
|
||
handle_compact(&path, pages)?;
|
||
}
|
||
Commands::Mount {
|
||
path,
|
||
drive,
|
||
port,
|
||
recovery_key,
|
||
no_open,
|
||
no_tray,
|
||
idle_timeout,
|
||
no_screen_lock,
|
||
no_anti_leak,
|
||
} => {
|
||
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
|
||
);
|
||
auto_drive
|
||
}
|
||
};
|
||
let auth = if let Some(key) = recovery_key {
|
||
ContainerAuth::RecoveryKey(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")?;
|
||
ContainerAuth::Password(password)
|
||
};
|
||
|
||
let open_explorer = !no_open;
|
||
let enable_tray = !no_tray;
|
||
let lock_on_screen_lock = !no_screen_lock;
|
||
let anti_leak = !no_anti_leak;
|
||
mount_container(
|
||
&path,
|
||
drive_char,
|
||
port,
|
||
auth,
|
||
open_explorer,
|
||
enable_tray,
|
||
idle_timeout,
|
||
lock_on_screen_lock,
|
||
anti_leak,
|
||
)
|
||
.await?;
|
||
}
|
||
Commands::Unmount { drive } => {
|
||
let drive_char = parse_drive_letter(&drive)?;
|
||
let drive_str = format_drive(drive_char);
|
||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||
unmount_drive(drive_char)?;
|
||
println!("{}", ui::green("OK"));
|
||
println!("{} Laufwerk {} erfolgreich getrennt.", ui::green("✔"), drive_str);
|
||
}
|
||
Commands::Passwd { path, recovery_key } => {
|
||
handle_passwd(&path, recovery_key.as_deref())?;
|
||
}
|
||
Commands::BackupHeader { path, output } => {
|
||
handle_backup_header(&path, output.as_deref())?;
|
||
}
|
||
Commands::RestoreHeader {
|
||
path,
|
||
header_file,
|
||
recovery_key,
|
||
} => {
|
||
handle_restore_header(&path, header_file.as_deref(), recovery_key.as_deref())?;
|
||
}
|
||
Commands::RecoveryKey { path } => {
|
||
handle_recovery_key(&path)?;
|
||
}
|
||
Commands::Verify { path, full } => {
|
||
handle_verify(&path, full)?;
|
||
}
|
||
Commands::Register => {
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!();
|
||
sanctum::windows::register_explorer_integration()?;
|
||
println!(" {} .sanctum-Dateien wurden erfolgreich im Windows Explorer verknüpft!", ui::green("✔"));
|
||
println!(" • Doppelklick: Öffnet und bindet den Container direkt als Laufwerk ein");
|
||
println!(" • Rechtsklick: Bietet Optionen für Integritätsprüfung (FSCK) und Header-Backup");
|
||
println!();
|
||
}
|
||
Commands::Unregister => {
|
||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||
println!("│ Sanctum — Windows Explorer Integration entfernen │");
|
||
println!("└─────────────────────────────────────────────────────────────┘");
|
||
println!();
|
||
sanctum::windows::unregister_explorer_integration()?;
|
||
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
||
println!();
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
|
||
#[tokio::main]
|
||
async fn main() {
|
||
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren
|
||
ui::init_console();
|
||
|
||
// Tracing / Logging initialisieren (Standard-Filter: warn)
|
||
tracing_subscriber::fmt()
|
||
.with_env_filter(
|
||
EnvFilter::try_from_default_env()
|
||
.unwrap_or_else(|_| EnvFilter::new("sanctum=warn,dav_server=warn,hyper=warn")),
|
||
)
|
||
.init();
|
||
|
||
if let Err(err) = run().await {
|
||
eprintln!();
|
||
eprintln!("{} {:#}", ui::red("✖ Fehler:"), err);
|
||
std::process::exit(1);
|
||
}
|
||
}
|
||
|