feat(anti-forensics): implement incremental auto-vacuum, chunk shredding, and plausible deniability
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user