release: v0.7.2 — Security Audit Remediation (SA-01 bis SA-07)
Sanctum Release / Build & Test (Windows x86_64) (push) Canceled after 0s
Sanctum Release / Sign & Release (push) Canceled after 0s

- SA-01: Container-DoS / KDF-Amplification Schutz mit Pre-KDF Validierung, max 2 Slots (nur 0 und 1), Slot 0 Pflicht und strikten BLOB-Laengen
- SA-02: Release-Signierung in CI entkoppelt (getrennte build und sign-and-release Jobs, Secret-Isolation)
- SA-03: Pinned Download-Integritaet fuer minisign.exe in CI via SHA-256
- SA-04: Immutable Action-Pinning (@sha) und Toolchain-Pinning (1.85.0) in CI
- SA-05: Session-Token vollstaendig aus URIs verbannt (403 Forbidden bei Vorkommen im Pfad/Query)
- SA-06: Constant-Time Token- und Auth-Vergleiche via subtle::ConstantTimeEq
- SA-07: Dokumentations-Klarstellung bzgl. logischem Shredding vs. physischer SSD/FTL/CoW-Persistenz
This commit is contained in:
2026-09-18 23:40:35 +02:00
parent 1cdb30147b
commit fba7f305e3
28 changed files with 3048 additions and 968 deletions
+361 -110
View File
@@ -295,18 +295,30 @@ fn parse_drive_letter(s: &str) -> Result<char> {
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")?;
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")?;
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")?;
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);
bail!(
"Ungültiges Größenformat: '{}'. Erwartet z. B. '500MB', '1GB', '2GB'",
s
);
}
}
@@ -319,9 +331,14 @@ pub fn parse_size_string(s: &str) -> Result<u64> {
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(""));
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")?;
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.");
@@ -342,7 +359,10 @@ fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>>
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(""));
println!(
" [{}] Verwende Notfallschlüssel aus Umgebungsvariable 'SANCTUM_RECOVERY_KEY'.",
ui::cyan("")
);
Ok(Some(Zeroizing::new(trimmed.to_string())))
} else {
Ok(None)
@@ -355,8 +375,14 @@ fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>>
/// 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"));
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() {
@@ -383,8 +409,15 @@ fn handle_init(
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!(
" 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")
@@ -415,7 +448,10 @@ fn handle_init(
println!();
println!(" ─── [2/2] Hidden Vault (Zweiter isolierter Tresor / Dual-Vault) ───");
println!(" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG"));
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")?,
@@ -432,7 +468,12 @@ fn handle_init(
}
println!();
ui::step(1, 4, "🔑", "Leite KEKs für Standard- und Hidden-Vault via Argon2id ab...");
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)?;
@@ -445,14 +486,24 @@ fn handle_init(
let dek_0 = generate_dek();
let dek_1 = generate_dek();
ui::step(3, 4, "🔒", "Verschlüssele Slot-Payloads (Dual-Vault Key-Wrapping)...");
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")?;
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)?;
@@ -487,7 +538,10 @@ fn handle_init(
println!("└─────────────────────────────────────────────────────────────┘");
println!();
println!(" • Container: {}", container_path.display());
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
println!(
" • Format: Version {} (Magic: SANCTUM\\0)",
FORMAT_VERSION
);
println!(" • Carrier-Datei:{} ({})", carrier_name, carrier_size_str);
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
println!(" • Kapselung: Hidden Vault liegt in Carrier-Datei im Decoy-Vault");
@@ -495,8 +549,16 @@ fn handle_init(
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!(
" {}",
ui::cyan(&format!(
"sanctum mount --path \"{}\"",
container_path.display()
))
);
println!(
" (Die Eingabe des jeweiligen Passworts bindet automatisch den passenden Vault ein)"
);
println!();
println!(" ┌─────────────────────────────────────────────────────────┐");
@@ -525,25 +587,47 @@ fn handle_init(
}
println!();
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
ui::step(
1,
4,
"🔑",
"Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...",
);
let salt = generate_salt();
let kdf_params = KdfParams::default();
let kek = derive_kek(&password, &salt, &kdf_params)
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
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")?;
ui::step(
4,
4,
"📦",
"Initialisiere SQLite-Containerstruktur & WAL-Modus...",
);
let db =
Database::open(container_path).context("Konnte SQLite-Containerdatei nicht anlegen")?;
db.init_schema_with_carrier(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag, None)
.context("Fehler bei der Schema-Initialisierung")?;
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")?;
@@ -557,13 +641,22 @@ fn handle_init(
println!("└─────────────────────────────────────────────────────────────┘");
println!();
println!(" • Container: {}", container_path.display());
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
println!(
" • Format: Version {} (Magic: SANCTUM\\0)",
FORMAT_VERSION
);
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
println!(" • 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::cyan(&format!(
"sanctum mount --path \"{}\"",
container_path.display()
))
);
println!();
ui::print_recovery_phrase_card(&recovery_phrase);
@@ -574,7 +667,10 @@ fn handle_init(
fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
if !container_path.exists() {
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
bail!(
"Containerdatei '{}' existiert nicht.",
container_path.display()
);
}
println!("┌─────────────────────────────────────────────────────────────┐");
@@ -583,13 +679,16 @@ fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
println!(" Container: {}", container_path.display());
println!();
let db = Database::open(container_path)
.context("Konnte Container-Datenbank nicht öffnen")?;
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
let freelist_before = db.freelist_count()
let freelist_before = db
.freelist_count()
.context("Konnte Freelist-Größe nicht ermitteln")?;
println!(" • Freie Seiten in Freelist: {}", ui::cyan(&freelist_before.to_string()));
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.");
@@ -598,7 +697,8 @@ fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
}
ui::step(1, 2, "🧹", "Führe Incremental-Vacuum aus...");
let freed = db.incremental_vacuum(pages)
let freed = db
.incremental_vacuum(pages)
.context("Fehler bei der Speicherbereinigung (Incremental Vacuum)")?;
ui::step(2, 2, "💾", "Führe finalen WAL-Checkpoint durch...");
@@ -612,8 +712,14 @@ fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
println!("│ ✔ Container erfolgreich kompaktiert! │");
println!("└─────────────────────────────────────────────────────────────┘");
println!();
println!(" • Freigegebene Seiten: {}", ui::green(&freed.to_string()));
println!(" • Verbleibende Freeseiten: {}", ui::dim(&freelist_after.to_string()));
println!(
" • Freigegebene Seiten: {}",
ui::green(&freed.to_string())
);
println!(
" • Verbleibende Freeseiten: {}",
ui::dim(&freelist_after.to_string())
);
println!();
Ok(())
}
@@ -632,24 +738,29 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
println!(" Container: {}", container_path.display());
println!();
let db = Database::open(container_path)
.context("Konnte Container-Datenbank nicht öffnen")?;
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)? {
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 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")?
.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 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 {
@@ -671,10 +782,17 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
.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."))?;
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())
@@ -697,14 +815,20 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
}
println!();
ui::step(3, 4, "🔒", "Generiere frisches Salt & leite neuen KEK ab...");
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_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")?
@@ -714,7 +838,12 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
.context("Slot-0 Wrapping mit neuem KEK fehlgeschlagen")?
};
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
ui::step(
4,
4,
"💾",
"Aktualisiere Container-Header & führe Checkpoint aus...",
);
db.update_slot_keys(
target_slot_id,
&new_salt,
@@ -774,10 +903,16 @@ fn handle_backup_header(container_path: &Path, output_path: Option<&Path>) -> Re
fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
if !container_path.exists() {
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
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());
bail!(
"Zieldatei '{}' existiert bereits. Bitte wählen Sie einen anderen Pfad.",
output_path.display()
);
}
println!("┌─────────────────────────────────────────────────────────────┐");
@@ -787,11 +922,20 @@ fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
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(
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...");
ui::step(
2,
2,
"💾",
"Kopiere Datenbankseiten konsistent via SQLite Backup API...",
);
db.online_backup(output_path)
.context("Fehler beim Erstellen des Online-Backups")?;
@@ -801,7 +945,9 @@ fn handle_backup(container_path: &Path, output_path: &Path) -> Result<()> {
println!("└─────────────────────────────────────────────────────────────┘");
println!();
println!(" • Backup-Datei: {}", output_path.display());
println!(" • Status: Konsistent & verifiziert (kann im laufenden Betrieb gesichert werden)");
println!(
" • Status: Konsistent & verifiziert (kann im laufenden Betrieb gesichert werden)"
);
println!();
Ok(())
@@ -826,7 +972,12 @@ fn handle_restore(backup_path: &Path, output_path: &Path) -> Result<()> {
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!");
ui::step(
2,
2,
"🔍",
"B-Tree Integritätsprüfung (PRAGMA quick_check) erfolgreich!",
);
println!();
println!("┌─────────────────────────────────────────────────────────────┐");
@@ -856,9 +1007,17 @@ fn handle_restore_header(
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!");
ui::step(
2,
2,
"💾",
"Header in Container-Datenbank zurückgeschrieben!",
);
println!();
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
println!(
"{}",
ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!")
);
} else {
let key_str = if let Some(key) = resolve_recovery_key(recovery_key)? {
key
@@ -887,7 +1046,8 @@ fn handle_restore_header(
};
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 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)
@@ -895,7 +1055,10 @@ fn handle_restore_header(
slot
};
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", effective_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")?,
@@ -916,10 +1079,14 @@ fn handle_restore_header(
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")?
.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."))?;
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
@@ -927,10 +1094,27 @@ fn handle_restore_header(
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));
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!(
"{}",
ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!")
);
}
println!();
@@ -939,7 +1123,10 @@ fn handle_restore_header(
fn handle_recovery_key(container_path: &Path) -> Result<()> {
if !container_path.exists() {
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
bail!(
"Containerdatei '{}' existiert nicht.",
container_path.display()
);
}
println!("┌─────────────────────────────────────────────────────────────┐");
@@ -953,9 +1140,10 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
.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 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)
@@ -970,7 +1158,10 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
if !container_path.exists() {
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
bail!(
"Containerdatei '{}' existiert nicht.",
container_path.display()
);
}
println!("┌─────────────────────────────────────────────────────────────┐");
@@ -994,12 +1185,18 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
Some(keys.dek().clone())
}
None => {
println!(" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.", ui::yellow("⚠️"));
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(""));
println!(
" {} Kein Passwort angegeben. Führe nur Strukturprüfung durch.",
ui::dim("")
);
None
};
@@ -1026,7 +1223,10 @@ fn handle_sync(
recovery_key: Option<&str>,
) -> Result<()> {
if !container_path.exists() {
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
bail!(
"Containerdatei '{}' existiert nicht.",
container_path.display()
);
}
let auth = if let Some(phrase_str) = resolve_recovery_key(recovery_key)? {
@@ -1042,9 +1242,10 @@ fn handle_sync(
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 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) => {
@@ -1054,8 +1255,7 @@ fn handle_sync(
(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 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)
}
@@ -1085,34 +1285,35 @@ fn handle_sync(
println!();
if dry_run {
println!(" {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!", ui::yellow("[!]"));
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"));
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!(
" • 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,
)?;
let stats = sanctum::sync::run_sync(&db, vault_id, &dek, version, source, target, &options)?;
if !dry_run {
let _ = db.checkpoint();
@@ -1124,27 +1325,59 @@ fn handle_sync(
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);
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 delete {
println!(" • Zu löschen: {} Dateien/Ordner", stats.files_deleted);
println!(
" • Zu löschen: {} Dateien/Ordner",
stats.files_deleted
);
}
println!(" • Hinweis: {} Es wurden keine Änderungen vorgenommen.", ui::yellow(""));
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);
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 delete {
println!(" • Gelöscht: {} verwaiste Dateien/Ordner", stats.files_deleted);
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!(
" • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)",
secs, speed
);
}
println!();
}
@@ -1247,11 +1480,19 @@ async fn run() -> Result<()> {
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);
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);
println!(
"{} Laufwerk {} erfolgreich getrennt.",
ui::green(""),
drive_str
);
}
Commands::Passwd { path, recovery_key } => {
handle_passwd(&path, recovery_key.as_deref())?;
@@ -1304,7 +1545,10 @@ async fn run() -> Result<()> {
println!("└─────────────────────────────────────────────────────────────┘");
println!();
sanctum::windows::register_explorer_integration()?;
println!(" {} .sanctum-Dateien wurden erfolgreich im Windows Explorer verknüpft!", ui::green(""));
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!();
@@ -1315,10 +1559,19 @@ async fn run() -> Result<()> {
println!("└─────────────────────────────────────────────────────────────┘");
println!();
sanctum::windows::unregister_explorer_integration()?;
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green(""));
println!(
" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.",
ui::green("")
);
println!();
}
Commands::Upgrade { check, yes, force, url, insecure_url } => {
Commands::Upgrade {
check,
yes,
force,
url,
insecure_url,
} => {
let options = sanctum::upgrade::UpgradeOptions {
check_only: check,
yes,
@@ -1333,7 +1586,6 @@ async fn run() -> Result<()> {
Ok(())
}
#[tokio::main]
async fn main() {
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren
@@ -1353,4 +1605,3 @@ async fn main() {
std::process::exit(1);
}
}