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
+87
View File
@@ -129,6 +129,70 @@ pub fn unwrap_dek(
Ok(dek)
}
/// Erzeugt einen Dummy-Header-Slot mit kryptografisch sicherem Zufallsrauschen derselben Länge wie
/// ein echter KDF/DEK-Slot. Dadurch sind Standard-Container von Containern mit Hidden Vault
/// auf Bitebene und Entropieebene ununterscheidbar (Plausible Deniability).
pub fn generate_dummy_slot() -> (Vec<u8>, [u8; 12], [u8; 16], [u8; 16]) {
let mut wrapped_dek = vec![0u8; 32];
let mut nonce = [0u8; 12];
let mut tag = [0u8; 16];
let mut salt = [0u8; 16];
OsRng.fill_bytes(&mut wrapped_dek);
OsRng.fill_bytes(&mut nonce);
OsRng.fill_bytes(&mut tag);
OsRng.fill_bytes(&mut salt);
(wrapped_dek, nonce, tag, salt)
}
/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM.
/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind.
pub fn encrypt_node_name(dek: &[u8; 32], name: &str) -> String {
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let cipher = Aes256Gcm::new_from_slice(dek).expect("AES init");
let mut buffer = name.as_bytes().to_vec();
let tag = cipher
.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), b"SANCTUM_NODE_NAME", &mut buffer)
.expect("Name encryption");
format!(
"$h${}${}${}",
hex::encode(nonce_bytes),
hex::encode(tag.as_slice()),
hex::encode(&buffer)
)
}
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option<String> {
if let Some(rest) = stored.strip_prefix("$h$") {
let parts: Vec<&str> = rest.split('$').collect();
if parts.len() == 3 {
if let (Ok(nonce_bytes), Ok(tag_bytes), Ok(ct_bytes)) = (
hex::decode(parts[0]),
hex::decode(parts[1]),
hex::decode(parts[2]),
) {
if nonce_bytes.len() == 12 && tag_bytes.len() == 16 {
let cipher = Aes256Gcm::new_from_slice(dek).ok()?;
let mut buffer = ct_bytes;
if cipher
.decrypt_in_place_detached(
Nonce::from_slice(&nonce_bytes),
b"SANCTUM_NODE_NAME",
&mut buffer,
Tag::from_slice(&tag_bytes),
)
.is_ok()
{
return String::from_utf8(buffer).ok();
}
}
}
}
}
None
}
/// Kodiert den 32-Byte (256-Bit) DEK in eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase (englisch) mit 8-Bit Checksumme.
pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
let mnemonic = bip39::Mnemonic::from_entropy(dek)
@@ -412,5 +476,28 @@ mod tests {
let corrupted_phrase = words.join(" ");
assert!(mnemonic_to_dek(&corrupted_phrase).is_err(), "Checksum check must fail");
}
#[test]
fn test_hidden_node_name_encryption_and_dummy_slot() {
let dek = generate_dek();
let filename = "ultra_geheimes_dokument.pdf";
let encrypted = encrypt_node_name(&dek, filename);
assert!(encrypted.starts_with("$h$"));
assert!(!encrypted.contains(filename));
let decrypted = decrypt_node_name(&dek, &encrypted).expect("Decrypt name");
assert_eq!(decrypted, filename);
// Mit anderem DEK schlägt Entschlüsselung fehl
let other_dek = generate_dek();
assert!(decrypt_node_name(&other_dek, &encrypted).is_none());
// Dummy-Slot hat korrekte Längen
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
assert_eq!(dummy_dek.len(), 32);
assert_eq!(dummy_nonce.len(), 12);
assert_eq!(dummy_tag.len(), 16);
assert_eq!(dummy_salt.len(), 16);
}
}