fix(crypto): K-03 — emergency card persistence warning and rekey command
This commit is contained in:
+90
@@ -134,6 +134,13 @@ enum Commands {
|
||||
recovery_key: Option<String>,
|
||||
},
|
||||
|
||||
/// Erneuert den internen Verschlüsselungsschlüssel (DEK), verschlüsselt alle Chunks um und generiert ein neues Notfallblatt (K-03)
|
||||
Rekey {
|
||||
/// Pfad zur .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Erstellt ein konsistentes Online-Backup (Hot-Backup) des laufenden Containers
|
||||
Backup {
|
||||
/// Pfad zur Quelldatei (.sanctum Containerdatei)
|
||||
@@ -879,6 +886,86 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
println!(" • KDF: Argon2id mit frischem Salt");
|
||||
println!(" • Status: DEK sicher neu verpackt (alle Chunks intakt)");
|
||||
println!();
|
||||
println!(" [!] HINWEIS: Das 24-Wörter-Notfallblatt bleibt weiterhin gültig!");
|
||||
println!(" Um den Container vollständig mit neuem Master-Schlüssel zu sichern,");
|
||||
println!(" nutzen Sie `sanctum rekey`.");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_rekey(container_path: &Path) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!(
|
||||
"Containerdatei '{}' existiert nicht.",
|
||||
container_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Master-Schlüssel erneuern (Rekeying) │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!();
|
||||
println!(" [!] WICHTIGER SICHERHEITSHINWEIS (K-03):");
|
||||
println!(" Beim Rekeying wird der interne Daten-Verschlüsselungsschlüssel (DEK)");
|
||||
println!(" neu generiert und alle Datenblöcke werden umverschlüsselt.");
|
||||
println!(" Das bisherige 24-Wörter-Notfallblatt wird damit UNWIDERRUFLICH UNGÜLTIG.");
|
||||
println!();
|
||||
|
||||
let password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort des Containers eingeben: ")
|
||||
.context("Fehler beim Einlesen des Master-Passworts")?,
|
||||
);
|
||||
|
||||
if password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
|
||||
println!();
|
||||
ui::step(
|
||||
1,
|
||||
3,
|
||||
"🔄",
|
||||
"Entschlüssele Datenblöcke & verschlüssele mit neuem DEK...",
|
||||
);
|
||||
let new_phrase = sanctum::recovery::rekey_container(container_path, &password)
|
||||
.context("Fehler beim Ausführen des Rekeyings")?;
|
||||
ui::step(
|
||||
2,
|
||||
3,
|
||||
"🔒",
|
||||
"Aktualisiere Container-Header & Metadaten-Authentifizierung...",
|
||||
);
|
||||
ui::step(3, 3, "🔑", "Generiere neues 24-Wort BIP-39 Notfallblatt...");
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Container erfolgreich rekeyed! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Status: Alle Chunks wurden unter einem frischen DEK neu verschlüsselt.");
|
||||
println!(" • Alt-Schlüssel:Bisheriges Notfallblatt ist ab sofort ungültig und entwertet.");
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ NEUES 24-WORT BIP-39 NOTFALLBLATT (NOTFALL-WIEDERHERSTELLUNG)│");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
let words: Vec<&str> = new_phrase.split_whitespace().collect();
|
||||
for (i, chunk) in words.chunks(4).enumerate() {
|
||||
let line = chunk
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, w)| format!("{:2}. {:<12}", i * 4 + j + 1, w))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
println!(" {}", line);
|
||||
}
|
||||
println!();
|
||||
println!(" [!] WICHTIG: Drucken Sie diese Wörter aus oder notieren Sie sie sicher.");
|
||||
println!(" Das alte Notfallblatt kann nicht mehr zur Wiederherstellung verwendet werden.");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1573,6 +1660,9 @@ async fn run() -> Result<()> {
|
||||
Commands::UpgradeFormat { path } => {
|
||||
handle_upgrade_format(&path)?;
|
||||
}
|
||||
Commands::Rekey { path } => {
|
||||
handle_rekey(&path)?;
|
||||
}
|
||||
Commands::Sync {
|
||||
path,
|
||||
source,
|
||||
|
||||
@@ -427,6 +427,73 @@ pub fn restore_header_from_recovery_key(
|
||||
restore_slot_from_recovery_key(container_path, recovery_key, new_password, 0, None)
|
||||
}
|
||||
|
||||
/// Führt ein vollständiges Rekeying eines Containers durch (K-03).
|
||||
/// Generiert einen neuen kryptografischen DEK, verschlüsselt alle Chunks um,
|
||||
/// verpackt den neuen DEK mit dem bestehenden Passwort und gibt das neue
|
||||
/// 24-Wort Notfallblatt (BIP-39 Mnemonic) zurück.
|
||||
/// Das alte Notfallblatt wird dadurch unwiderruflich ungültig.
|
||||
pub fn rekey_container(container_path: &Path, password: &str) -> Result<String> {
|
||||
if !container_path.exists() {
|
||||
bail!(
|
||||
"Containerdatei '{}' existiert nicht.",
|
||||
container_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
let meta = db
|
||||
.read_meta()
|
||||
.context("Konnte Container-Header nicht lesen")?;
|
||||
let keys = meta.authenticate(password).ok_or_else(|| {
|
||||
anyhow::anyhow!("Ungültiges Master-Passwort! Authentifizierung fehlgeschlagen.")
|
||||
})?;
|
||||
|
||||
let old_dek = keys.dek().clone();
|
||||
let slot_id = keys.slot_id();
|
||||
let version = keys.version();
|
||||
let carrier_dek = keys.carrier_dek();
|
||||
let carrier_node_id = keys.carrier_node_id();
|
||||
|
||||
// 1. Neuen DEK generieren
|
||||
let new_dek = crate::crypto::generate_dek();
|
||||
|
||||
// 2. Chunks des aktiven Vaults umverschlüsseln
|
||||
db.rekey_vault(slot_id, &old_dek, &new_dek, version)?;
|
||||
|
||||
// 3. Neuen KEK ableiten und DEK neu verpacken
|
||||
let new_salt = crate::crypto::generate_salt();
|
||||
let new_params = KdfParams::default();
|
||||
let new_kek = derive_kek(password, &new_salt, &new_params)?;
|
||||
|
||||
let (new_wrapped_dek, new_nonce, new_tag) = if slot_id == 1 {
|
||||
let c_dek = carrier_dek
|
||||
.ok_or_else(|| anyhow::anyhow!("DEK_0 (Träger-Schlüssel) für Slot 1 fehlt"))?;
|
||||
let c_nid = carrier_node_id.unwrap_or(0);
|
||||
wrap_slot1_payload(&new_kek, &new_dek, &c_dek, c_nid)?
|
||||
} else {
|
||||
let c_nid = carrier_node_id.unwrap_or(0);
|
||||
wrap_slot0_payload(&new_kek, &new_dek, c_nid)?
|
||||
};
|
||||
|
||||
db.update_slot_keys(
|
||||
slot_id,
|
||||
&new_salt,
|
||||
&new_params,
|
||||
&new_wrapped_dek,
|
||||
&new_nonce,
|
||||
&new_tag,
|
||||
)?;
|
||||
|
||||
// 4. Metadaten-MAC mit neuem DEK aktualisieren
|
||||
db.set_active_slot_and_dek(slot_id, new_dek.clone());
|
||||
db.update_metadata_mac()?;
|
||||
db.checkpoint()?;
|
||||
|
||||
// 5. Neues BIP-39 Notfallblatt generieren
|
||||
let new_phrase = crate::crypto::dek_to_mnemonic(&new_dek)?;
|
||||
Ok(new_phrase)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+128
-4
@@ -1128,6 +1128,126 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verschlüsselt alle Chunks eines Vaults mit einem neuen DEK um (K-03 Rekeying).
|
||||
pub fn rekey_vault(
|
||||
&self,
|
||||
slot_id: u32,
|
||||
old_dek: &[u8; 32],
|
||||
new_dek: &[u8; 32],
|
||||
version: u32,
|
||||
) -> Result<()> {
|
||||
let mut conn = self.conn.lock().unwrap();
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// Ermittle alle Node-IDs dieses Vaults
|
||||
let node_ids: Vec<i64> = if slot_id == 1 {
|
||||
let mut stmt = tx.prepare(
|
||||
"WITH RECURSIVE vault1(id) AS (
|
||||
SELECT 2
|
||||
UNION ALL
|
||||
SELECT n.id FROM nodes n JOIN vault1 v ON n.parent_id = v.id
|
||||
) SELECT id FROM vault1",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| r.get(0))?;
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
} else {
|
||||
let mut stmt = tx.prepare(
|
||||
"WITH RECURSIVE vault0(id) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT n.id FROM nodes n JOIN vault0 v ON n.parent_id = v.id
|
||||
) SELECT id FROM vault0",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| r.get(0))?;
|
||||
rows.filter_map(|r| r.ok()).collect()
|
||||
};
|
||||
|
||||
// Option B: Falls Slot 1, verschlüsselte Dateinamen unter parent_id = 2 umverschlüsseln
|
||||
if slot_id == 1 {
|
||||
let mut name_stmt =
|
||||
tx.prepare("SELECT id, parent_id, name FROM nodes WHERE parent_id = 2")?;
|
||||
let node_rows: Vec<(i64, i64, String)> = name_stmt
|
||||
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
drop(name_stmt);
|
||||
|
||||
for (id, parent_id, enc_name) in node_rows {
|
||||
if let Some(decrypted_name) =
|
||||
crate::crypto::decrypt_node_name(old_dek, parent_id, &enc_name)
|
||||
{
|
||||
let new_enc =
|
||||
crate::crypto::encrypt_node_name(new_dek, parent_id, &decrypted_name);
|
||||
tx.execute(
|
||||
"UPDATE nodes SET name = ?1 WHERE id = ?2",
|
||||
params![new_enc, id],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chunks der betroffenen Nodes umverschlüsseln
|
||||
for nid in node_ids {
|
||||
let mut chunk_stmt = tx.prepare(
|
||||
"SELECT chunk_index, generation, nonce, tag, ciphertext FROM chunks WHERE node_id = ?1",
|
||||
)?;
|
||||
let chunk_rows: Vec<(u32, u64, [u8; 12], [u8; 16], Vec<u8>)> = chunk_stmt
|
||||
.query_map(params![nid], |row| {
|
||||
let chunk_index: u32 = row.get(0)?;
|
||||
let generation: i64 = row.get(1).unwrap_or(0);
|
||||
let nonce_vec: Vec<u8> = row.get(2)?;
|
||||
let tag_vec: Vec<u8> = row.get(3)?;
|
||||
let ciphertext: Vec<u8> = row.get(4)?;
|
||||
let mut nonce = [0u8; 12];
|
||||
let mut tag = [0u8; 16];
|
||||
if nonce_vec.len() == 12 {
|
||||
nonce.copy_from_slice(&nonce_vec);
|
||||
}
|
||||
if tag_vec.len() == 16 {
|
||||
tag.copy_from_slice(&tag_vec);
|
||||
}
|
||||
Ok((chunk_index, generation as u64, nonce, tag, ciphertext))
|
||||
})?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
drop(chunk_stmt);
|
||||
|
||||
for (chunk_index, generation, nonce, tag, ct) in chunk_rows {
|
||||
let plaintext = crate::crypto::decrypt_chunk(
|
||||
old_dek,
|
||||
nid,
|
||||
chunk_index,
|
||||
&ct,
|
||||
&nonce,
|
||||
&tag,
|
||||
version,
|
||||
generation,
|
||||
)?;
|
||||
let (new_ct, new_nonce, new_tag) = crate::crypto::encrypt_chunk(
|
||||
new_dek,
|
||||
nid,
|
||||
chunk_index,
|
||||
&plaintext,
|
||||
version,
|
||||
generation,
|
||||
)?;
|
||||
tx.execute(
|
||||
"UPDATE chunks SET nonce = ?1, tag = ?2, ciphertext = ?3 WHERE node_id = ?4 AND chunk_index = ?5",
|
||||
params![
|
||||
new_nonce.as_slice(),
|
||||
new_tag.as_slice(),
|
||||
new_ct,
|
||||
nid,
|
||||
chunk_index,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Löst einen hierarchischen Pfad innerhalb eines bestimmten Vaults auf.
|
||||
pub fn resolve_path_in_vault(
|
||||
&self,
|
||||
@@ -1824,7 +1944,12 @@ impl Database {
|
||||
|
||||
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
|
||||
let mac_key = derive_metadata_mac_key(dek);
|
||||
Ok(verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac))
|
||||
Ok(verify_metadata_mac(
|
||||
&mac_key,
|
||||
gen,
|
||||
&canonical,
|
||||
&expected_mac,
|
||||
))
|
||||
}
|
||||
|
||||
/// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01 & K-02).
|
||||
@@ -1852,9 +1977,8 @@ impl Database {
|
||||
|
||||
// K-02: Alle bestehenden Chunks von alter 16-Byte-AAD auf Format V3 24-Byte-AAD (generation = 0) umverschlüsseln
|
||||
{
|
||||
let mut chunk_stmt = conn.prepare(
|
||||
"SELECT node_id, chunk_index, nonce, tag, ciphertext FROM chunks",
|
||||
)?;
|
||||
let mut chunk_stmt =
|
||||
conn.prepare("SELECT node_id, chunk_index, nonce, tag, ciphertext FROM chunks")?;
|
||||
let chunk_rows: Vec<(i64, u32, [u8; 12], [u8; 16], Vec<u8>)> = chunk_stmt
|
||||
.query_map([], |row| {
|
||||
let node_id: i64 = row.get(0)?;
|
||||
|
||||
Reference in New Issue
Block a user