release: v0.7.2 — Security Audit Remediation (SA-01 bis SA-07)
- 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:
+361
-110
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user