- F-02: Require restore_nonce token and explicit confirmation (--rebuild-mac) for PendingRebuild - F-01: Extend canonical MAC transcript to include chunk generation tuples (node_id, chunk_index, generation) and support transparent legacy migration - F-03: Make V2-to-V3 container upgrade atomic with transactional rollback and dual-slot version update - Bump version to 0.9.4 and update changelog and security docs
1833 lines
72 KiB
Rust
1833 lines
72 KiB
Rust
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<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 unter Windows standardmäßig das nächste freie Laufwerk)
|
||
#[arg(short, long)]
|
||
drive: Option<String>,
|
||
|
||
/// 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<PathBuf>,
|
||
|
||
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
||
#[arg(long)]
|
||
port: Option<u16>,
|
||
|
||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage).
|
||
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel sicher und interaktiv
|
||
/// einzugeben (verhindert Klartext-Einträge in der PowerShell-Historie).
|
||
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||
recovery_key: Option<String>,
|
||
|
||
/// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer
|
||
#[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,
|
||
|
||
/// Pfad zu einer Textdatei mit zusätzlichen Anti-Leak Filterregeln (V-04)
|
||
#[arg(long, value_name = "FILE")]
|
||
anti_leak_list: Option<PathBuf>,
|
||
|
||
/// 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<String>,
|
||
},
|
||
|
||
/// 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: <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.
|
||
/// Tipp: Rufen Sie '--recovery-key' ohne Wert auf, um den Schlüssel interaktiv einzugeben.
|
||
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||
recovery_key: Option<String>,
|
||
|
||
/// Ziel-Slot für die Wiederherstellung (0 = Decoy/Standard, 1 = Hidden Vault; Standard: 0)
|
||
#[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<String>,
|
||
|
||
/// 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<String>,
|
||
},
|
||
|
||
/// 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<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())
|
||
}
|
||
|
||
pub fn parse_size_string(s: &str) -> Result<u64> {
|
||
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::<u64>() {
|
||
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<Option<Zeroizing<String>>> {
|
||
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<Zeroizing<String>> {
|
||
println!(
|
||
" [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.",
|
||
ui::cyan("ℹ")
|
||
);
|
||
println!(
|
||
" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der 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<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, 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::<Vec<_>>()
|
||
.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<String>,
|
||
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);
|
||
}
|
||
}
|