Files
sanctum/src/main.rs
T
harald 510cb9b64e fix(format-v3): harden system binding against replay and bypass attacks (F-01, F-02, F-03)
- 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
2026-09-22 16:48:12 +02:00

1833 lines
72 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}