feat(security): implement Phase 1 of Plausible Deniability hardening

This commit is contained in:
2026-09-09 19:55:34 +02:00
parent ea571d245e
commit b8e4dcb614
7 changed files with 518 additions and 181 deletions
+63 -5
View File
@@ -110,7 +110,7 @@ pub fn verify_container(
node_map.insert(node.id, node.clone());
}
// Root-Knoten prüfen (id = 1)
// Root-Knoten prüfen (id = 1 und optional id = 2 für Hidden Vault)
match node_map.get(&1) {
Some(root) => {
if !root.is_dir {
@@ -125,9 +125,18 @@ pub fn verify_container(
}
}
if let Some(root2) = node_map.get(&2) {
if !root2.is_dir {
report.errors.push("Root-Knoten (id=2) ist nicht als Verzeichnis markiert!".to_string());
}
if root2.parent_id.is_some() {
report.errors.push("Root-Knoten (id=2) darf keinen Parent haben!".to_string());
}
}
// Alle anderen Knoten prüfen: Existenz des Parents, keine Zyklen
for node in &all_nodes {
if node.id == 1 {
if node.id == 1 || node.id == 2 {
continue;
}
@@ -174,6 +183,54 @@ pub fn verify_container(
}
}
// Ermittle die Abstammung aller Knoten zu Root 1 (Vault 0) bzw. Root 2 (Vault 1)
let mut vault0_nodes = HashSet::new();
let mut vault1_nodes = HashSet::new();
vault0_nodes.insert(1i64);
if node_map.contains_key(&2) {
vault1_nodes.insert(2i64);
}
let mut changed = true;
while changed {
changed = false;
for node in &all_nodes {
if let Some(pid) = node.parent_id {
if vault0_nodes.contains(&pid) && !vault0_nodes.contains(&node.id) {
vault0_nodes.insert(node.id);
changed = true;
} else if vault1_nodes.contains(&pid) && !vault1_nodes.contains(&node.id) {
vault1_nodes.insert(node.id);
changed = true;
}
}
}
}
// Falls ein DEK übergeben wurde: Bestimme, zu welchem Vault er gehört
let active_vault_nodes = if let Some(active_dek) = dek {
let is_vault1 = {
let mut found_v1 = false;
for node in &all_nodes {
if node.parent_id == Some(2) {
if crate::crypto::decrypt_node_name(active_dek, &node.name).is_some() {
found_v1 = true;
break;
}
}
}
found_v1
};
if is_vault1 {
Some(&vault1_nodes)
} else {
Some(&vault0_nodes)
}
} else {
None
};
// 4. Kryptografische Chunk- & AEAD-Authentifizierungsprüfung
let chunk_headers = db.list_all_chunk_headers()
.context("Fehler beim Abrufen der Chunk-Liste")?;
@@ -187,8 +244,9 @@ pub fn verify_container(
));
}
if let Some(active_dek) = dek {
if full_chunks {
if let (Some(active_dek), Some(target_nodes)) = (dek, active_vault_nodes) {
// Nur Chunks verifizieren, die zum verifizierten Tresor gehören (kein Falschalarm für Hidden Vault)
if target_nodes.contains(&node_id) && full_chunks {
match db.read_chunk(node_id, chunk_index) {
Ok(Some(record)) => {
match decrypt_chunk(
@@ -283,7 +341,7 @@ mod tests {
let report = verify_container(&container_path, Some(&dek), true).expect("Verify container");
assert!(report.is_healthy(), "Container must be healthy, report: {:?}", report);
assert_eq!(report.total_files, 1);
assert_eq!(report.total_dirs, 2); // Root + photos
assert_eq!(report.total_dirs, 3); // Root 1 + Root 2 (Plausible Deniability) + photos
assert_eq!(report.total_chunks, 2);
assert_eq!(report.corrupted_chunks, 0);
assert_eq!(report.orphan_nodes, 0);