feat(cli): add passwd command for instant master password changes

This commit is contained in:
2026-09-07 17:29:50 +02:00
parent 58336fc059
commit a9c3dd25a3
5 changed files with 242 additions and 1 deletions
+111 -1
View File
@@ -4,7 +4,9 @@ use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams};
use sanctum::crypto::{
derive_kek, generate_dek, generate_salt, unwrap_dek, wrap_dek, KdfParams,
};
use sanctum::mount::{format_drive, mount_container, unmount_drive};
use sanctum::storage::Database;
use sanctum::ui;
@@ -49,6 +51,13 @@ enum Commands {
#[arg(short, long)]
drive: String,
},
/// Ändert das Master-Passwort eines bestehenden Sanctum-Containers
Passwd {
/// Pfad zur .sanctum Containerdatei
#[arg(short, long)]
path: PathBuf,
},
}
fn parse_drive_letter(s: &str) -> Result<char> {
@@ -134,6 +143,103 @@ fn handle_init(container_path: &Path) -> Result<()> {
Ok(())
}
fn handle_passwd(container_path: &Path) -> 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 old_password = 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 db = Database::open(container_path)
.context("Konnte Container-Datenbank nicht öffnen")?;
let meta = db
.read_meta()
.context("Konnte Container-Header nicht lesen")?;
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab & prüfe Passwort...");
let old_kek = derive_kek(&old_password, &meta.kdf_salt, &meta.kdf_params)
.context("Schlüsselableitung fehlgeschlagen")?;
let dek = unwrap_dek(
&old_kek,
&meta.wrapped_dek,
&meta.header_nonce,
&meta.header_tag,
)
.map_err(|_| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?;
println!();
let new_password = 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.");
}
let confirm_password = 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!");
}
if old_password == new_password {
bail!("Das neue Master-Passwort muss sich vom aktuellen Passwort unterscheiden.");
}
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) =
wrap_dek(&new_kek, &dek).context("DEK-Wrapping mit neuem KEK fehlgeschlagen")?;
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
db.update_meta_keys(
&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!(" • KDF: Argon2id mit frischem Salt");
println!(" • Status: DEK sicher neu verpackt (alle Chunks intakt)");
println!();
Ok(())
}
async fn run() -> Result<()> {
let cli = Cli::parse();
@@ -161,11 +267,15 @@ async fn run() -> Result<()> {
println!("{}", ui::green("OK"));
println!("{} Laufwerk {} erfolgreich getrennt.", ui::green(""), drive_str);
}
Commands::Passwd { path } => {
handle_passwd(&path)?;
}
}
Ok(())
}
#[tokio::main]
async fn main() {
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren