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, }, /// 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, /// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443) #[arg(long)] port: Option, /// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage) #[arg(long)] recovery_key: Option, /// Ö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, /// 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, }, /// Sichert den Container-Header in eine separate Backup-Datei (.sanctum.hdr) BackupHeader { /// Pfad zur .sanctum Containerdatei #[arg(short, long)] path: PathBuf, /// Optionaler Ausgabepfad (Standard: .hdr) #[arg(short, long)] output: Option, }, /// 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, /// 24-Wort BIP-39 Notfallschlüssel zur Rekonstruktion mit neuem Passwort #[arg(long)] recovery_key: Option, }, /// 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 { 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) -> 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 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); } }