feat(security): release v0.7.0 with comprehensive security hardening (S-01 to S-11)
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
Sanctum Release / Build & Release (Windows x86_64) (push) Canceled after 0s
This commit is contained in:
+144
-91
@@ -6,12 +6,14 @@ use tracing_subscriber::EnvFilter;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use sanctum::crypto::{
|
||||
dek_to_mnemonic, derive_kek, generate_dek, generate_salt, mnemonic_to_dek,
|
||||
wrap_slot0_payload, wrap_slot1_payload, KdfParams, FORMAT_VERSION,
|
||||
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,
|
||||
};
|
||||
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
|
||||
use sanctum::recovery::{
|
||||
export_header_backup, restore_header_backup, restore_slot_from_recovery_key,
|
||||
detect_recovery_key_slot, export_header_backup, restore_header_backup,
|
||||
restore_slot_from_recovery_key,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::ui;
|
||||
@@ -23,6 +25,10 @@ use sanctum::verify::verify_container;
|
||||
#[command(about = "Sanctum: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland", long_about = None)]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
/// Erlaubt Legacy-Namensentschlüsselung ohne deterministisches AAD (S-07)
|
||||
#[arg(long, global = true, default_value_t = false)]
|
||||
legacy_names: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
@@ -35,7 +41,7 @@ enum Commands {
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Erstellt zusätzlich einen plausibel abstreitbaren Hidden Vault (Plausible Deniability)
|
||||
/// Erstellt einen separaten, isolierten zweiten Tresor (Dual-Vault / Second Safe)
|
||||
#[arg(long, default_value_t = false)]
|
||||
with_hidden: bool,
|
||||
|
||||
@@ -267,6 +273,10 @@ enum Commands {
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -302,11 +312,51 @@ pub fn parse_size_string(s: &str) -> Result<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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") {
|
||||
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 PowerShell-Historie.", ui::dim("OpSec"));
|
||||
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() {
|
||||
@@ -333,15 +383,12 @@ fn handle_init(
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Zieldatei: {}", container_path.display());
|
||||
if with_hidden {
|
||||
println!(" Modus: Dual-Vault (Modell A: {})", ui::magenta("Alibi-Carrier"));
|
||||
println!(" Alibi: {} ({})", ui::cyan(carrier_name), ui::cyan(carrier_size_str));
|
||||
let lower = carrier_name.to_lowercase();
|
||||
if lower.ends_with(".iso") || lower.ends_with(".vhd") || lower.ends_with(".img") || lower.ends_with(".zip") {
|
||||
println!(
|
||||
" [{}] Hinweis zur Plausible Deniability: Dateitypen wie '.iso' oder '.zip' besitzen ein starres Format. Da Carrier-Blöcke verschlüsselt sind (Entropie ~8.0), empfehlen sich unformatierte Typen wie '.dat', '.bin' oder '.enc' für maximale forensische Unauffälligkeit.",
|
||||
ui::yellow("Tipp")
|
||||
);
|
||||
}
|
||||
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!();
|
||||
|
||||
@@ -356,9 +403,8 @@ fn handle_init(
|
||||
rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
if password_0.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
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")?,
|
||||
@@ -368,18 +414,15 @@ fn handle_init(
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(" ─── [2/2] Hidden Vault (Versteckter Speicher / Plausible Deniability) ───");
|
||||
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")?,
|
||||
);
|
||||
if password_1.trim().is_empty() {
|
||||
bail!("Das Master-Passwort für den Hidden-Vault darf nicht leer sein.");
|
||||
}
|
||||
if *password_1 == *password_0 {
|
||||
bail!("Das Hidden-Vault-Passwort darf nicht mit dem Standard-Passwort identisch sein!");
|
||||
}
|
||||
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")?,
|
||||
@@ -398,33 +441,36 @@ fn handle_init(
|
||||
let kdf_params_1 = KdfParams::default();
|
||||
let kek_1 = derive_kek(&password_1, &salt_1, &kdf_params_1)?;
|
||||
|
||||
ui::step(2, 4, "🎲", "Erzeuge getrennte DEKs für beide Vaults via CSPRNG...");
|
||||
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 für Modell A via AES-256-GCM...");
|
||||
ui::step(3, 4, "🔒", "Verschlüssele Slot-Payloads (Dual-Vault Key-Wrapping)...");
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_dek_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?;
|
||||
let (wrapped_dek_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id)?;
|
||||
let (wrapped_dek_0, header_nonce_0, header_tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?;
|
||||
|
||||
ui::step(4, 4, "📦", &format!("Initialisiere Alibi-Carrier '{}' ({}) & Container...", carrier_name, carrier_size_str));
|
||||
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,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
&header_nonce_0,
|
||||
&header_tag_0,
|
||||
Some((
|
||||
carrier_name,
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params_1,
|
||||
&wrapped_dek_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&header_nonce_1,
|
||||
&header_tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
@@ -437,16 +483,16 @@ fn handle_init(
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum Dual-Vault Container (Modell A) initialisiert! │");
|
||||
println!("│ ✔ Sanctum Dual-Vault Container initialisiert! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||
println!(" • Alibi-Datei: {} ({})", carrier_name, carrier_size_str);
|
||||
println!(" • Carrier-Datei:{} ({})", carrier_name, carrier_size_str);
|
||||
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
||||
println!(" • Steganografie:Hidden Vault verbirgt sich vollständig in der Trägerdatei");
|
||||
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 Containergröße — kein dynamisches Wachstum auf Festplatte");
|
||||
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())));
|
||||
@@ -467,10 +513,7 @@ fn handle_init(
|
||||
rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
if password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&password)?;
|
||||
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||
@@ -517,7 +560,7 @@ fn handle_init(
|
||||
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!(" • Deniability: Slot 1 mit CSPRNG-Zufallsrauschen gefüllt");
|
||||
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())));
|
||||
@@ -592,16 +635,26 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
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(phrase) = recovery_key {
|
||||
let actual_phrase = if phrase.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(phrase.to_string())
|
||||
};
|
||||
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!");
|
||||
(d, 0, None, None)
|
||||
|
||||
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: ")
|
||||
@@ -632,10 +685,7 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
rpassword::prompt_password("Neues Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des neuen Passworts")?,
|
||||
);
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das neue Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&new_password)?;
|
||||
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
@@ -654,7 +704,7 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
.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.unwrap_or_else(generate_dek);
|
||||
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")?
|
||||
@@ -810,12 +860,8 @@ fn handle_restore_header(
|
||||
println!();
|
||||
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
|
||||
} else {
|
||||
let key_str = if let Some(key) = recovery_key {
|
||||
if key.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(key.to_string())
|
||||
}
|
||||
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.");
|
||||
@@ -840,15 +886,21 @@ fn handle_restore_header(
|
||||
}
|
||||
};
|
||||
|
||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", slot);
|
||||
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")?,
|
||||
);
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&new_password)?;
|
||||
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
@@ -859,9 +911,24 @@ fn handle_restore_header(
|
||||
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, slot)?;
|
||||
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", 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!"));
|
||||
}
|
||||
@@ -962,18 +1029,8 @@ fn handle_sync(
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
let auth = if let Some(key_arg) = recovery_key {
|
||||
let phrase_str = if key_arg.trim().is_empty() {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ 🔑 Maskierte interaktive Notfallschlüssel-Eingabe │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
rpassword::prompt_password("24-Wort Notfall-Wiederherstellungsschlüssel: ")
|
||||
.context("Fehler beim Einlesen des Schlüssels")?
|
||||
} else {
|
||||
key_arg.to_string()
|
||||
};
|
||||
ContainerAuth::RecoveryKey(Zeroizing::new(phrase_str))
|
||||
let auth = if let Some(phrase_str) = resolve_recovery_key(recovery_key)? {
|
||||
ContainerAuth::RecoveryKey(phrase_str)
|
||||
} else {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Ordner- & Dateisynchronisation (Sync) │");
|
||||
@@ -999,11 +1056,7 @@ fn handle_sync(
|
||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||
let dek = mnemonic_to_dek(phrase)
|
||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
let is_hidden = {
|
||||
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||
!children.is_empty()
|
||||
};
|
||||
let v_id = if is_hidden { 1 } else { 0 };
|
||||
let v_id = detect_recovery_key_slot(&db, &dek, meta.version).unwrap_or(0);
|
||||
(dek, meta.version, v_id)
|
||||
}
|
||||
};
|
||||
@@ -1102,6 +1155,10 @@ fn handle_sync(
|
||||
async fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.legacy_names {
|
||||
set_allow_legacy_names(true);
|
||||
}
|
||||
|
||||
match cli.command {
|
||||
Commands::Init {
|
||||
path,
|
||||
@@ -1154,12 +1211,7 @@ async fn run() -> Result<()> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let auth = if let Some(key) = recovery_key {
|
||||
let actual_key = if key.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(key)
|
||||
};
|
||||
let auth = if let Some(actual_key) = resolve_recovery_key(recovery_key.as_deref())? {
|
||||
ContainerAuth::RecoveryKey(actual_key)
|
||||
} else {
|
||||
let prompt_text = format!(
|
||||
@@ -1266,12 +1318,13 @@ async fn run() -> Result<()> {
|
||||
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
||||
println!();
|
||||
}
|
||||
Commands::Upgrade { check, yes, force, url } => {
|
||||
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)?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user