use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use tracing_subscriber::EnvFilter; use zeroize::Zeroizing; use sanctum::crypto::{ check_password_prefix_collision, dek_to_mnemonic, derive_kek, generate_dek, generate_salt, mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload, wrap_slot1_payload, KdfParams, FORMAT_VERSION, FORMAT_VERSION_V3, }; use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth}; use sanctum::recovery::{ detect_recovery_key_slot, export_header_backup, restore_header_backup, restore_slot_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 { /// SICHERHEITSHINWEIS: Unsicher! Deaktiviert die kryptografische Bindung (AAD) von Dateinamen (Gefahr von Swap-Attacks/Manipulation). Nur zur temporären Migration alter Container verwenden! #[arg(long, global = true, default_value_t = false)] legacy_names: bool, #[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 einen separaten, isolierten zweiten Tresor (Dual-Vault / Second Safe) #[arg(long, default_value_t = false)] with_hidden: bool, /// Dateiname der Alibi-Trägerdatei im Standard-Tresor (Standard: system_backup.dat). /// Hinweis: Die Datei dient als Träger für den zweiten Tresor und ist forensisch erkennbar. #[arg(long, default_value = "system_backup.dat")] carrier_name: String, /// Größe der Trägerdatei (z. B. 100MB, 500MB, 1GB, 2GB; Standard: 1GB) #[arg(long, default_value = "1GB")] carrier_size: String, }, /// 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 unter Windows standardmäßig das nächste freie Laufwerk) #[arg(short, long)] drive: Option, /// Optionaler lokaler Mount-Pfad unter Linux/macOS (z. B. '/mnt/sanctum' oder '~/vault') #[arg(short = 'm', long = "mount-point", value_name = "DIR")] mount_point: Option, /// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443) #[arg(long)] port: Option, /// 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, /// Ö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, /// Pfad zu einer Textdatei mit zusätzlichen Anti-Leak Filterregeln (V-04) #[arg(long, value_name = "FILE")] anti_leak_list: Option, /// Aktiviert den lautlosen Stealth-Modus (keine Banner, Pfade, URLs oder Fortschrittsausgaben) #[arg(short, long, default_value_t = false)] stealth: bool, /// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02) #[arg(long, default_value_t = false)] rebuild_mac: 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). /// 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, }, /// Erneuert den internen Verschlüsselungsschlüssel (DEK), verschlüsselt alle Chunks um und generiert ein neues Notfallblatt (K-03) Rekey { /// Pfad zur .sanctum Containerdatei #[arg(short, long)] path: PathBuf, }, /// 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 #[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. /// 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, /// 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 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, }, /// Synchronisiert Dateien und Ordner zwischen Host und Container (rsync-artig, ohne Größenbeschränkung) Sync { /// Pfad zur .sanctum Containerdatei #[arg(short, long)] path: PathBuf, /// Quellpfad (lokales Verzeichnis/Datei oder Vault-Pfad bei --pull) source: String, /// Zielpfad (Vault-Pfad wie z. B. '/Downloads' oder lokaler Pfad bei --pull; Standard: '/') #[arg(default_value = "/")] target: String, /// Kehrt die Richtung um: Lädt Daten aus dem Tresor auf die lokale Festplatte (Pull) #[arg(long, default_value_t = false)] pull: bool, /// Löscht Dateien im Ziel, die an der Quelle nicht mehr existieren (Spiegelung) #[arg(long, default_value_t = false)] delete: bool, /// Löscht auch ausgeschlossene Dateien im Ziel bei aktiver --delete Spiegelung #[arg(long, default_value_t = false)] delete_excluded: bool, /// Führt eine Simulation aus: Zeigt Änderungen an, ohne Dateien zu schreiben oder zu löschen #[arg(short = 'n', long, default_value_t = false)] dry_run: bool, /// Vergleicht Dateien anhand kryptografischer Hashes statt nur Zeitstempel und Dateigröße #[arg(short = 'c', long, default_value_t = false)] checksum: bool, /// Schließt Dateien oder Ordner anhand von Mustern aus (z. B. '*.tmp', '*.crdownload') #[arg(long)] exclude: Vec, /// Unterdrückt Pro-Datei-Ausgaben #[arg(short, long, default_value_t = false)] quiet: bool, /// Erzwingt die Synchronisation auch bei erkanntem Advisory Lock eines anderen Prozesses (S-06) #[arg(short = 'f', long, default_value_t = false)] force: bool, /// Erstellt Sicherungskopien (.bak) überschriebener lokaler Dateien beim Pull (S-09) #[arg(short = 'b', long, default_value_t = false)] backup: bool, /// Verhindert das Überschreiben neuerer lokaler Zieldateien beim Pull (S-09) #[arg(short = 'u', long, default_value_t = false)] update: bool, /// Optionaler 24-Wort Notfallschlüssel (umgeht Passwortabfrage) #[arg(long, num_args = 0..=1, default_missing_value = "")] recovery_key: Option, }, /// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte) Register, /// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry Unregister, /// Aktualisiert Sanctum auf die neueste Version (In-Place Self-Upgrade) #[command(alias = "update")] Upgrade { /// Prüft nur auf verfügbare Aktualisierungen, ohne Dateien herunterzuladen #[arg(short = 'c', long, default_value_t = false)] check: bool, /// Führt die Aktualisierung ohne interaktive Bestätigung durch #[arg(short = 'y', long, default_value_t = false)] yes: bool, /// Erzwingt die Aktualisierung (auch bei gleicher Version oder erkannter Paketmanager-Verwaltung) #[arg(short = 'f', long, default_value_t = false)] force: bool, /// Benutzerdefinierte Gitea-Basis-URL (Standard: https://gitea.pansi.eu) #[arg(long, default_value = "https://gitea.pansi.eu")] url: String, /// Erlaubt abweichende Update-Server-URLs (erfordert interaktive Bestätigung) #[arg(long, default_value_t = false)] insecure_url: bool, }, /// Migriert einen bestehenden Container auf Format V3 (Metadaten-Authentifizierung & Replay-Schutz) UpgradeFormat { /// Pfad zur .sanctum Containerdatei #[arg(short, long)] path: PathBuf, }, } 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()) } pub fn parse_size_string(s: &str) -> Result { let trimmed = s.trim().to_uppercase(); if let Some(num_str) = trimmed.strip_suffix("GB") { let n: u64 = num_str .trim() .parse() .context("Ungültige Gigabyte-Angabe")?; Ok(n * 1024 * 1024 * 1024) } else if let Some(num_str) = trimmed.strip_suffix("MB") { let n: u64 = num_str .trim() .parse() .context("Ungültige Megabyte-Angabe")?; Ok(n * 1024 * 1024) } else if let Some(num_str) = trimmed.strip_suffix("KB") { let n: u64 = num_str .trim() .parse() .context("Ungültige Kilobyte-Angabe")?; Ok(n * 1024) } else if let Ok(n) = trimmed.parse::() { Ok(n) } else { bail!( "Ungültiges Größenformat: '{}'. Erwartet z. B. '500MB', '1GB', '2GB'", s ); } } /// Liest den 24-Wort BIP-39 Notfallschlüssel sicher ein. /// Unterstützt: /// 1. Interaktiv maskiert (wenn kein Wert oder leere Eingabe übergeben) /// 2. Stdin (wenn "-" übergeben) /// 3. Umgebungsvariable 'SANCTUM_RECOVERY_KEY' (wenn kein CLI-Argument übergeben) /// 4. Veraltetes CLI-Argument mit Sicherheitswarnung (S-09) fn resolve_recovery_key(arg: Option<&str>) -> Result>> { if let Some(key) = arg { if key == "-" { println!( " [{}] Lese 24-Wort Notfallschlüssel von der Standardeingabe (stdin)...", ui::cyan("ℹ") ); let mut line = String::new(); std::io::stdin() .read_line(&mut line) .context("Fehler beim Einlesen von stdin")?; let trimmed = line.trim(); if trimmed.is_empty() { bail!("Der Notfallschlüssel von stdin darf nicht leer sein."); } Ok(Some(Zeroizing::new(trimmed.to_string()))) } else if key.trim().is_empty() { Ok(Some(prompt_recovery_key_interactive()?)) } else { eprintln!( "{} WARNUNG: Die Angabe des Notfallschlüssels als CLI-Argument ist unsicher und veraltet!", ui::yellow("[SICHERHEITSHINWEIS]") ); eprintln!(" Verwenden Sie '--recovery-key' ohne Wert (interaktiv), '--recovery-key -' (stdin) oder die Umgebungsvariable 'SANCTUM_RECOVERY_KEY'."); Ok(Some(Zeroizing::new(key.to_string()))) } } else if let Ok(env_key) = std::env::var("SANCTUM_RECOVERY_KEY") { // R-06: Umgebungsvariable nach dem Einlesen sofort aus dem Speicher des Prozesses entfernen std::env::remove_var("SANCTUM_RECOVERY_KEY"); let trimmed = env_key.trim(); if !trimmed.is_empty() { println!( " [{}] Verwende Notfallschlüssel aus Umgebungsvariable 'SANCTUM_RECOVERY_KEY'.", ui::cyan("ℹ") ); Ok(Some(Zeroizing::new(trimmed.to_string()))) } else { Ok(None) } } else { Ok(None) } } /// 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> { println!( " [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.", ui::cyan("ℹ") ); println!( " [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der Shell-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, carrier_name: &str, carrier_size_str: &str, ) -> 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 (Modell A: {})", ui::magenta("Carrier-Datei") ); println!( " Carrier: {} ({})", ui::cyan(carrier_name), ui::cyan(carrier_size_str) ); println!( " [{}] Hinweis zum Dual-Vault: Modell A kapselt den Hidden Vault in einer Datei im Decoy-Vault. Dies schützt vor neugierigen Blicken, widersteht jedoch keinem forensischen Gutachten, da Carrier-Entropie und Schreibmuster analysierbar sind.", ui::yellow("Info") ); } println!(); if with_hidden { let carrier_size_bytes = parse_size_string(carrier_size_str)?; if carrier_size_bytes < 2 * 1024 * 1024 { bail!("Trägerdateigröße muss mindestens 2 MB betragen (Block 0 Manifest + mindestens 1 Datenblock)"); } println!(" ─── [1/2] Standard-Vault (Äußerer Container / Decoy) ───"); let password_0 = Zeroizing::new( rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ") .context("Fehler beim Einlesen des Passworts")?, ); validate_password(&password_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 (Zweiter isolierter Tresor / Dual-Vault) ───"); println!( " [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG") ); let password_1 = Zeroizing::new( rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ") .context("Fehler beim Einlesen des Passworts")?, ); validate_password(&password_1)?; check_password_prefix_collision(&password_0, &password_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!"); } 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 unabhängige DEKs (DEK_0 & DEK_1)..."); let dek_0 = generate_dek(); let dek_1 = generate_dek(); ui::step( 3, 4, "🔒", "Verschlüssele Slot-Payloads (Dual-Vault Key-Wrapping)...", ); let carrier_node_id = 3i64; let (wrapped_dek_0, header_nonce_0, header_tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?; ui::step( 4, 4, "📦", "Erzeuge SQLite-Container & alloziiere Alibi-Carrier-Datei...", ); let db = Database::open(container_path).context("Konnte SQLite-Containerdatei nicht anlegen")?; let (wrapped_dek_1, header_nonce_1, header_tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id)?; db.init_schema_with_carrier( &salt_0, &kdf_params_0, &wrapped_dek_0, &header_nonce_0, &header_tag_0, Some(( carrier_name, carrier_size_bytes, &salt_1, &kdf_params_1, &wrapped_dek_1, &header_nonce_1, &header_tag_1, &dek_0, &dek_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 initialisiert! │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(); println!(" • Container: {}", container_path.display()); println!( " • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION ); println!(" • Carrier-Datei:{} ({})", carrier_name, carrier_size_str); println!(" • KDF: Argon2id pro Slot (M=256MB, T=4, P=4)"); println!(" • Kapselung: Hidden Vault liegt in Carrier-Datei im Decoy-Vault"); println!(" • Accounting: 100% aller Chunks authentifizieren fehlerfrei unter DEK_0"); println!(" • Dateigröße: Feste Trägergröße zur Vermeidung von Größenveränderungen"); 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 = Zeroizing::new( rpassword::prompt_password("Master-Passwort eingeben: ") .context("Fehler beim Einlesen des Passworts")?, ); validate_password(&password)?; let confirm_password = Zeroizing::new( 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=256MB, T=4, 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_slot0_payload(&kek, &dek, 0).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.set_active_dek(dek.clone()); db.init_schema_with_carrier( &salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag, None, ) .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=256MB, T=4, P=4)"); println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)"); println!(" • Dual-Vault: Slot 1 mit Zufallsrauschen initialisiert (inaktiv)"); 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, carrier_dek, carrier_node_id) = if let Some(actual_phrase) = resolve_recovery_key(recovery_key)? { ui::step(1, 3, "🔑", "Lese DEK aus 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!"); let meta = db .read_meta() .context("Konnte Container-Header nicht lesen")?; let detected_slot = detect_recovery_key_slot(&db, &d, meta.version).unwrap_or(0); if detected_slot == 1 { println!(" Notfallschlüssel gehört zu Slot 1 (Hidden Vault)."); println!(" Zur Neuverpackung von Slot 1 wird das Passwort des Decoy-Tresors (Slot 0) benötigt."); let decoy_pw = Zeroizing::new( rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ") .context("Fehler beim Einlesen des Decoy-Passworts")?, ); let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| { anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen.") })?; let c_dek = decoy_auth.dek().clone(); (d, 1, Some(c_dek), db.find_carrier_node_id()?) } else { (d, 0, None, db.find_carrier_node_id()?) } } else { 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."); } 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 (konstante Zeit)...", ); let keys = meta.authenticate(&old_password).ok_or_else(|| { anyhow::anyhow!( "Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen." ) })?; let d = keys.dek().clone(); let slot_id = keys.slot_id(); (d, slot_id, keys.carrier_dek(), keys.carrier_node_id()) }; println!(); let new_password = Zeroizing::new( rpassword::prompt_password("Neues Master-Passwort eingeben: ") .context("Fehler beim Einlesen des neuen Passworts")?, ); validate_password(&new_password)?; 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 { 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) = if target_slot_id == 1 { let c_dek = carrier_dek .ok_or_else(|| anyhow::anyhow!("DEK_0 (Träger-Schlüssel) für Slot 1 fehlt"))?; let c_nid = carrier_node_id.unwrap_or(0); wrap_slot1_payload(&new_kek, &dek, &c_dek, c_nid) .context("Slot-1 Wrapping mit neuem KEK fehlgeschlagen")? } else { let c_nid = carrier_node_id.unwrap_or(0); wrap_slot0_payload(&new_kek, &dek, c_nid) .context("Slot-0 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!(); println!(" [!] HINWEIS: Das 24-Wörter-Notfallblatt bleibt weiterhin gültig!"); println!(" Um den Container vollständig mit neuem Master-Schlüssel zu sichern,"); println!(" nutzen Sie `sanctum rekey`."); println!(); Ok(()) } fn handle_rekey(container_path: &Path) -> Result<()> { if !container_path.exists() { bail!( "Containerdatei '{}' existiert nicht.", container_path.display() ); } println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ Sanctum — Master-Schlüssel erneuern (Rekeying) │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(" Container: {}", container_path.display()); println!(); println!(" [!] WICHTIGER SICHERHEITSHINWEIS (K-03):"); println!(" Beim Rekeying wird der interne Daten-Verschlüsselungsschlüssel (DEK)"); println!(" neu generiert und alle Datenblöcke werden umverschlüsselt."); println!(" Das bisherige 24-Wörter-Notfallblatt wird damit UNWIDERRUFLICH UNGÜLTIG."); println!(); let password = Zeroizing::new( rpassword::prompt_password("Master-Passwort des Containers eingeben: ") .context("Fehler beim Einlesen des Master-Passworts")?, ); if password.trim().is_empty() { bail!("Das Master-Passwort darf nicht leer sein."); } println!(); ui::step( 1, 3, "🔄", "Entschlüssele Datenblöcke & verschlüssele mit neuem DEK...", ); let new_phrase = sanctum::recovery::rekey_container(container_path, &password) .context("Fehler beim Ausführen des Rekeyings")?; ui::step( 2, 3, "🔒", "Aktualisiere Container-Header & Metadaten-Authentifizierung...", ); ui::step(3, 3, "🔑", "Generiere neues 24-Wort BIP-39 Notfallblatt..."); println!(); println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ ✔ Container erfolgreich rekeyed! │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(); println!(" • Container: {}", container_path.display()); println!(" • Status: Alle Chunks wurden unter einem frischen DEK neu verschlüsselt."); println!(" • Alt-Schlüssel:Bisheriges Notfallblatt ist ab sofort ungültig und entwertet."); println!(); println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ NEUES 24-WORT BIP-39 NOTFALLBLATT (NOTFALL-WIEDERHERSTELLUNG)│"); println!("└─────────────────────────────────────────────────────────────┘"); println!(); let words: Vec<&str> = new_phrase.split_whitespace().collect(); for (i, chunk) in words.chunks(4).enumerate() { let line = chunk .iter() .enumerate() .map(|(j, w)| format!("{:2}. {:<12}", i * 4 + j + 1, w)) .collect::>() .join(" "); println!(" {}", line); } println!(); println!(" [!] WICHTIG: Drucken Sie diese Wörter aus oder notieren Sie sie sicher."); println!(" Das alte Notfallblatt kann nicht mehr zur Wiederherstellung verwendet werden."); 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_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 │"); 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 { let key_str = if let Some(key) = resolve_recovery_key(recovery_key)? { key } 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."), } }; let dek = mnemonic_to_dek(&key_str).context("Ungültiger 24-Wort Notfallschlüssel")?; let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?; let effective_slot = if slot == 0 { detect_recovery_key_slot(&db, &dek, FORMAT_VERSION).unwrap_or(0) } else { slot }; println!( " Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", effective_slot ); let new_password = Zeroizing::new( rpassword::prompt_password("Neues Master-Passwort festlegen: ") .context("Fehler beim Einlesen des Passworts")?, ); validate_password(&new_password)?; 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 { bail!("Die eingegebenen Passwörter stimmen nicht überein!"); } let dek_0_holder: Option<[u8; 32]> = if effective_slot == 1 { println!(" Wiederherstellung von Slot 1 (Hidden Vault):"); println!(" Zur mathematischen Rekonstruktion wird das Passwort des Decoy-Tresors (Slot 0) benötigt."); let decoy_pw = Zeroizing::new( rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ") .context("Fehler beim Einlesen des Decoy-Passworts")?, ); let meta = db .read_meta() .context("Konnte Container-Header nicht lesen")?; let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| { anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen.") })?; Some(**decoy_auth.dek()) } else { None }; let dek_0_opt = dek_0_holder.as_ref(); ui::step(1, 2, "🔑", "Dekodiere DEK & leite neuen KEK ab..."); restore_slot_from_recovery_key( container_path, &key_str, &new_password, effective_slot, dek_0_opt, )?; ui::step( 2, 2, "💾", &format!( "Header für Slot {} mit neuem Passwort neu synthetisiert!", effective_slot ), ); println!(); println!( "{}", ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!") ); } 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 = 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")?; let meta = db .read_meta() .context("Konnte Container-Header nicht lesen")?; let keys = meta .authenticate(&password) .ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?; let dek = keys.dek().clone(); let phrase = dek_to_mnemonic(&dek)?; 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 = 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")?; let meta = db.read_meta().context("Konnte Header nicht lesen")?; match meta.authenticate(&password) { Some(keys) => { println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔")); Some(keys.dek().clone()) } 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(()) } fn handle_upgrade_format(container_path: &Path) -> Result<()> { if !container_path.exists() { bail!( "Containerdatei '{}' existiert nicht.", container_path.display() ); } println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ Sanctum — Format V3 Upgrade │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(" Container: {}", container_path.display()); println!(); let password = rpassword::prompt_password("Master-Passwort: ") .context("Fehler beim Einlesen des Passworts")?; let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?; let keys = match db.authenticate_password(&password)? { Some(k) => k, None => { bail!("Authentifizierung fehlgeschlagen: Falsches Passwort"); } }; if keys.version() >= FORMAT_VERSION_V3 { println!( " • Container ist bereits Format V{} (keine Migration erforderlich).", keys.version() ); return Ok(()); } db.upgrade_to_v3(keys.dek())?; db.checkpoint()?; println!(" ✔ Container erfolgreich auf Format V3 aktualisiert!"); println!(" • Metadaten-Authentifizierung (HMAC-SHA256) aktiviert."); println!(" • Replay-Schutz für Chunks (24-Byte AAD mit Generation) aktiviert."); Ok(()) } fn handle_sync( container_path: &Path, source: &str, target: &str, pull: bool, delete: bool, delete_excluded: bool, dry_run: bool, checksum: bool, exclude: Vec, quiet: bool, force: bool, backup: bool, update: bool, recovery_key: Option<&str>, ) -> Result<()> { if !container_path.exists() { bail!( "Containerdatei '{}' existiert nicht.", container_path.display() ); } let auth = if let Some(phrase_str) = resolve_recovery_key(recovery_key)? { ContainerAuth::RecoveryKey(phrase_str) } else { println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ Sanctum — Ordner- & Dateisynchronisation (Sync) │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(" Container: {}", container_path.display()); println!(); let pass = rpassword::prompt_password("Master-Passwort: ") .context("Fehler beim Einlesen des Passworts")?; ContainerAuth::Password(Zeroizing::new(pass)) }; let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?; let meta = db .read_meta() .context("Konnte Container-Header nicht lesen")?; let (dek, version, vault_id) = match auth { ContainerAuth::Password(ref password) => { let keys = meta .authenticate(password) .ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?; (keys.dek().clone(), meta.version, keys.slot_id()) } ContainerAuth::RecoveryKey(ref phrase) => { let dek = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?; let v_id = detect_recovery_key_slot(&db, &dek, meta.version).unwrap_or(0); (dek, meta.version, v_id) } }; // Richtung ermitteln let direction = if pull || source.starts_with('/') { sanctum::sync::SyncDirection::Pull } else { sanctum::sync::SyncDirection::Push }; let options = sanctum::sync::SyncOptions { direction, delete, delete_excluded, dry_run, checksum, exclude_patterns: exclude, quiet, force, backup, update, }; if !quiet { let dir_str = match direction { sanctum::sync::SyncDirection::Push => "Push (Host -> Container)", sanctum::sync::SyncDirection::Pull => "Pull (Container -> Host)", }; println!(); if dry_run { println!( " {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!", ui::yellow("[!]") ); } println!(" • Richtung: {}", ui::cyan(dir_str)); println!(" • Quelle: {}", source); println!(" • Ziel: {}", target); if delete { println!( " • Spiegelung: {} (Dateien im Ziel ohne Entsprechung werden gelöscht)", ui::red("--delete aktiv") ); } if checksum { println!(" • Modus: Kryptografischer Inhaltsabgleich (--checksum aktiv)"); } if !options.exclude_patterns.is_empty() { println!( " • Filter: {} Ausschlussmuster", options.exclude_patterns.len() ); } println!(); println!(" {} Starte Synchronisation ...", ui::dim("[-]")); println!(); } let stats = sanctum::sync::run_sync(&db, vault_id, &dek, version, source, target, &options)?; if !dry_run { let _ = db.checkpoint(); } if !quiet { println!(); if dry_run { println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ ℹ Dry-Run Simulation erfolgreich abgeschlossen │"); println!("└─────────────────────────────────────────────────────────────┘"); println!( " • Gescannt: {} Dateien/Objekte", stats.files_scanned ); println!( " • Zu übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred) ); println!( " • Bereits aktuell: {} Dateien (übersprungen)", stats.files_skipped ); if stats.files_skipped_symlinks > 0 { println!( " • Symlinks: {} übersprungen", stats.files_skipped_symlinks ); } if stats.files_skipped_invalid > 0 { println!( " • Ungültige Namen: {} übersprungen", stats.files_skipped_invalid ); } if stats.files_backed_up > 0 { println!( " • Backups: {} Sicherheitskopien (.bak)", stats.files_backed_up ); } if delete { println!( " • Zu löschen: {} Dateien/Ordner", stats.files_deleted ); } println!( " • Hinweis: {} Es wurden keine Änderungen vorgenommen.", ui::yellow("✔") ); } else { println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ ✔ Synchronisation erfolgreich abgeschlossen │"); println!("└─────────────────────────────────────────────────────────────┘"); println!( " • Gescannt: {} Dateien/Objekte", stats.files_scanned ); println!( " • Übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred) ); println!( " • Übersprungen: {} Dateien (bereits aktuell)", stats.files_skipped ); if stats.files_skipped_symlinks > 0 { println!( " • Symlinks: {} übersprungen", stats.files_skipped_symlinks ); } if stats.files_skipped_invalid > 0 { println!( " • Ungültige Namen: {} übersprungen", stats.files_skipped_invalid ); } if stats.files_backed_up > 0 { println!( " • Backups: {} Sicherheitskopien (.bak)", stats.files_backed_up ); } if delete { println!( " • Gelöscht: {} verwaiste Dateien/Ordner", stats.files_deleted ); } let secs = stats.elapsed.as_secs_f64(); let mb = (stats.bytes_transferred as f64) / (1024.0 * 1024.0); let speed = if secs > 0.05 { mb / secs } else { 0.0 }; println!( " • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)", secs, speed ); } println!(); } Ok(()) } async fn run() -> Result<()> { let cli = Cli::parse(); if cli.legacy_names { set_allow_legacy_names(true); eprintln!("WARNUNG: Das Flag '--legacy-names' ist aktiv. Die kryptografische Authentifizierung von Dateinamen ist geschwächt (Gefahr von Swap-Attacks). Verwenden Sie dieses Flag nur zur Datenmigration!"); tracing::warn!("Flag --legacy-names ist aktiv. Authenticated Associated Data (AAD) fuer Dateinamen ist abgeschwaecht."); } match cli.command { Commands::Init { path, with_hidden, carrier_name, carrier_size, } => { handle_init(&path, with_hidden, &carrier_name, &carrier_size)?; } 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, mount_point, port, recovery_key, no_open, no_tray, idle_timeout, no_screen_lock, no_anti_leak, anti_leak_list, stealth, rebuild_mac, } => { let drive_char = match drive { Some(ref d) => parse_drive_letter(d)?, None => { #[cfg(windows)] { let auto_drive = sanctum::windows::find_next_available_drive()?; if !stealth { println!( " {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:", ui::cyan("ℹ"), auto_drive ); } auto_drive } #[cfg(not(windows))] { 'Z' } } }; let auth = if let Some(actual_key) = resolve_recovery_key(recovery_key.as_deref())? { ContainerAuth::RecoveryKey(actual_key) } else { let prompt_text = format!( "Master-Passwort für Container '{}' eingeben: ", path.display() ); let password = Zeroizing::new( rpassword::prompt_password(prompt_text) .context("Fehler beim Einlesen des Passworts")?, ); ContainerAuth::Password(password) }; 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( &path, drive_char, mount_point.as_deref(), port, auth, open_explorer, enable_tray, idle_timeout, lock_on_screen_lock, anti_leak, anti_leak_list.as_deref(), stealth, rebuild_mac, ) .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, slot, } => { handle_restore_header(&path, header_file.as_deref(), recovery_key.as_deref(), slot)?; } Commands::RecoveryKey { path } => { handle_recovery_key(&path)?; } Commands::Verify { path, full } => { handle_verify(&path, full)?; } Commands::UpgradeFormat { path } => { handle_upgrade_format(&path)?; } Commands::Rekey { path } => { handle_rekey(&path)?; } Commands::Sync { path, source, target, pull, delete, delete_excluded, dry_run, checksum, exclude, quiet, force, backup, update, recovery_key, } => { handle_sync( &path, &source, &target, pull, delete, delete_excluded, dry_run, checksum, exclude, quiet, force, backup, update, recovery_key.as_deref(), )?; } 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!(); } Commands::Upgrade { check, yes, force, url, insecure_url, } => { let options = sanctum::upgrade::UpgradeOptions { check_only: check, yes, force, base_url: url, insecure_url, }; sanctum::upgrade::run_upgrade(&options)?; } } 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); } }