feat(anti-forensics): implement incremental auto-vacuum, chunk shredding, and plausible deniability

This commit is contained in:
2026-09-08 10:46:24 +02:00
parent 2cb065c8c0
commit 1e6854e9f5
8 changed files with 1386 additions and 270 deletions
+184
View File
@@ -609,5 +609,189 @@ async fn test_anti_leak_and_inactivity_shield() {
let _ = std::fs::remove_file(&container_path);
}
#[tokio::test]
async fn test_hidden_vault_and_storage_compaction_integration() {
let temp_dir = std::env::temp_dir();
let container_path: PathBuf = temp_dir.join(format!("test_hidden_{}.sanctum", std::process::id()));
if container_path.exists() {
let _ = std::fs::remove_file(&container_path);
}
let password_decoy = "DecoyOuterPassword2026!";
let password_hidden = "TopSecretHiddenPassword2026!";
// 1. Dual-Vault Initialisierung (Slot 0 = Decoy, Slot 1 = Hidden Vault)
let salt0 = generate_salt();
let salt1 = generate_salt();
let kdf_params = KdfParams {
memory_cost: 1024,
time_cost: 1,
parallelism: 1,
};
let kek0 = derive_kek(password_decoy, &salt0, &kdf_params).expect("KEK 0");
let kek1 = derive_kek(password_hidden, &salt1, &kdf_params).expect("KEK 1");
let dek0 = generate_dek();
let dek1 = generate_dek();
let (wrapped_dek0, nonce0, tag0) = wrap_dek(&kek0, &dek0).expect("Wrap DEK 0");
let (wrapped_dek1, nonce1, tag1) = wrap_dek(&kek1, &dek1).expect("Wrap DEK 1");
let db = Database::open(&container_path).expect("Open container");
db.init_schema_with_hidden(
&salt0,
&kdf_params,
&wrapped_dek0,
&nonce0,
&tag0,
Some((&salt1, &kdf_params, &wrapped_dek1, &nonce1, &tag1)),
)
.expect("Init schema with hidden");
db.checkpoint().expect("Checkpoint init");
// 2. Multi-Slot Authentifizierungsprüfung
let meta = db.read_meta().expect("Read meta");
assert_eq!(meta.slots.len(), 2, "Es müssen 2 Slots initialisiert sein");
// Decoy Passwort entsperrt Slot 0
let mut auth_decoy = None;
for slot in &meta.slots {
if let Ok(kek) = derive_kek(password_decoy, &slot.kdf_salt, &slot.kdf_params) {
if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
auth_decoy = Some((dek, slot.slot_id));
break;
}
}
}
let (unwrapped_dek0, slot_id0) = auth_decoy.expect("Decoy password must unwrap");
assert_eq!(slot_id0, 0);
assert_eq!(*unwrapped_dek0, *dek0);
// Hidden Passwort entsperrt Slot 1
let mut auth_hidden = None;
for slot in &meta.slots {
if let Ok(kek) = derive_kek(password_hidden, &slot.kdf_salt, &slot.kdf_params) {
if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
auth_hidden = Some((dek, slot.slot_id));
break;
}
}
}
let (unwrapped_dek1, slot_id1) = auth_hidden.expect("Hidden password must unwrap");
assert_eq!(slot_id1, 1);
assert_eq!(*unwrapped_dek1, *dek1);
// 3. VFS Operationen im Decoy-Vault (Slot 0)
let fs_decoy = SanctumFs::with_vault(db.clone(), unwrapped_dek0, meta.version, true, 0);
let decoy_file_path = DavPath::new("/harmless_recipe.txt").unwrap();
let mut opts_write = OpenOptions::default();
opts_write.write = true;
opts_write.create_new = true;
let mut df = fs_decoy.open(&decoy_file_path, opts_write).await.expect("Open decoy file");
df.write_bytes(Bytes::from_static(b"Apples, Flour, Sugar, Butter"))
.await
.expect("Write decoy");
df.flush().await.expect("Flush decoy");
drop(df);
// 4. VFS Operationen im Hidden-Vault (Slot 1)
let fs_hidden = SanctumFs::with_vault(db.clone(), unwrapped_dek1, meta.version, true, 1);
let hidden_file_path = DavPath::new("/classified_report.pdf").unwrap();
let mut opts_write2 = OpenOptions::default();
opts_write2.write = true;
opts_write2.create_new = true;
let mut hf = fs_hidden.open(&hidden_file_path, opts_write2).await.expect("Open hidden file");
hf.write_bytes(Bytes::from_static(b"TOP SECRET INTELLIGENCE DATA"))
.await
.expect("Write hidden");
hf.flush().await.expect("Flush hidden");
drop(hf);
// 5. Strikte Isolierung verifizieren: Decoy-Vault sieht NICHTS vom Hidden-Vault
let mut stream_decoy = fs_decoy
.read_dir(&DavPath::new("/").unwrap(), ReadDirMeta::None)
.await
.expect("read_dir decoy");
let mut decoy_entries = Vec::new();
while let Some(Ok(entry)) = stream_decoy.next().await {
decoy_entries.push(String::from_utf8_lossy(&entry.name()).to_string());
}
assert_eq!(decoy_entries, vec!["harmless_recipe.txt"]);
assert!(fs_decoy.metadata(&hidden_file_path).await.is_err(), "Decoy darf classified_report nicht sehen");
// Hidden-Vault sieht ebenfalls nur seine eigenen Dateien
let mut stream_hidden = fs_hidden
.read_dir(&DavPath::new("/").unwrap(), ReadDirMeta::None)
.await
.expect("read_dir hidden");
let mut hidden_entries = Vec::new();
while let Some(Ok(entry)) = stream_hidden.next().await {
hidden_entries.push(String::from_utf8_lossy(&entry.name()).to_string());
}
assert_eq!(hidden_entries, vec!["classified_report.pdf"]);
assert!(fs_hidden.metadata(&decoy_file_path).await.is_err(), "Hidden darf harmless_recipe nicht auflösen");
// 6. Forensische Dateiprüfung (Anti-Forensics / Plausible Deniability)
db.checkpoint().expect("Checkpoint before raw inspection");
let raw_bytes = std::fs::read(&container_path).expect("Read container raw bytes");
// Der Dateiname "classified_report.pdf" darf NIRGENDS im Rohformat im Container stehen
let needle_filename = b"classified_report";
assert!(
!raw_bytes.windows(needle_filename.len()).any(|w| w == needle_filename),
"Forensischer Leak: Dateiname des Hidden Vaults taucht als Klartext in der Datei auf!"
);
let needle_payload = b"TOP SECRET INTELLIGENCE DATA";
assert!(
!raw_bytes.windows(needle_payload.len()).any(|w| w == needle_payload),
"Forensischer Leak: Nutzlast des Hidden Vaults taucht als Klartext auf!"
);
// 7. Storage Compaction (Incremental Vacuum & Chunk Shredding)
// Große temporäre Datei im Hidden Vault anlegen
let temp_large_path = DavPath::new("/large_dump.dat").unwrap();
let mut opts_dump = OpenOptions::default();
opts_dump.write = true;
opts_dump.create_new = true;
let mut dump_file = fs_hidden.open(&temp_large_path, opts_dump).await.expect("Open dump");
let mut large_buffer = vec![0u8; 512 * 1024]; // 512 KB echte Zufallsdaten (Shannon-Entropie 8.0)
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut large_buffer);
dump_file.write_bytes(Bytes::copy_from_slice(&large_buffer)).await.expect("Write dump");
dump_file.flush().await.expect("Flush dump");
drop(dump_file);
db.checkpoint().expect("Checkpoint after write");
// Datei löschen (löst automatisches Chunk-Shredding aus)
fs_hidden.remove_file(&temp_large_path).await.expect("Remove dump");
db.checkpoint().expect("Checkpoint after remove");
let free_pages = db.freelist_count().expect("freelist count");
assert!(free_pages > 0, "Nach dem Löschen müssen freie Seiten in der Freelist existieren");
// Incremental Vacuum ausführen
let reclaimed = db.incremental_vacuum(None).expect("incremental vacuum");
assert!(reclaimed > 0, "Seiten müssen an das Dateisystem zurückgegeben werden");
assert_eq!(db.freelist_count().unwrap(), 0, "Freelist muss jetzt 0 sein");
// Verifiziere, dass verbleibende Dateien in beiden Vaults intakt lesbar sind
let mut r_decoy = fs_decoy.open(&decoy_file_path, OpenOptions::default()).await.expect("open decoy");
let d_data = r_decoy.read_bytes(30).await.expect("read decoy");
assert_eq!(&d_data[..], b"Apples, Flour, Sugar, Butter");
let mut r_hidden = fs_hidden.open(&hidden_file_path, OpenOptions::default()).await.expect("open hidden");
let h_data = r_hidden.read_bytes(30).await.expect("read hidden");
assert_eq!(&h_data[..], b"TOP SECRET INTELLIGENCE DATA");
// Aufräumen
let _ = std::fs::remove_file(&container_path);
}