feat(anti-forensics): implement incremental auto-vacuum, chunk shredding, and plausible deniability
This commit is contained in:
@@ -129,6 +129,70 @@ pub fn unwrap_dek(
|
|||||||
Ok(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.
|
/// 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> {
|
pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
|
||||||
let mnemonic = bip39::Mnemonic::from_entropy(dek)
|
let mnemonic = bip39::Mnemonic::from_entropy(dek)
|
||||||
@@ -412,5 +476,28 @@ mod tests {
|
|||||||
let corrupted_phrase = words.join(" ");
|
let corrupted_phrase = words.join(" ");
|
||||||
assert!(mnemonic_to_dek(&corrupted_phrase).is_err(), "Checksum check must fail");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+250
-60
@@ -33,6 +33,21 @@ enum Commands {
|
|||||||
/// Pfad zur zu erstellenden .sanctum Containerdatei
|
/// Pfad zur zu erstellenden .sanctum Containerdatei
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Erstellt zusätzlich einen plausibel abstreitbaren Hidden Vault (Plausible Deniability)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
with_hidden: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Kompaktiert den Container-Speicherplatz (Incremental Vacuum) und bereinigt ungenutzte Seiten
|
||||||
|
Compact {
|
||||||
|
/// Pfad zur .sanctum Containerdatei
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Maximale Anzahl an Seiten, die freigegeben werden sollen (Standard: alle freien Seiten)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pages: Option<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Bindet einen Sanctum-Container als Windows-Netzlaufwerk via WebDAV ein
|
/// Bindet einen Sanctum-Container als Windows-Netzlaufwerk via WebDAV ein
|
||||||
@@ -157,7 +172,7 @@ fn parse_drive_letter(s: &str) -> Result<char> {
|
|||||||
Ok(ch.to_ascii_uppercase())
|
Ok(ch.to_ascii_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_init(container_path: &Path) -> Result<()> {
|
fn handle_init(container_path: &Path, with_hidden: bool) -> Result<()> {
|
||||||
if container_path.exists() {
|
if container_path.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
"Zieldatei '{}' existiert bereits. Initialisierung abgebrochen, um Überschreiben zu verhindern.",
|
"Zieldatei '{}' existiert bereits. Initialisierung abgebrochen, um Überschreiben zu verhindern.",
|
||||||
@@ -169,65 +184,208 @@ fn handle_init(container_path: &Path) -> Result<()> {
|
|||||||
println!("│ Sanctum — Neuen verschlüsselten Container erstellen │");
|
println!("│ Sanctum — Neuen verschlüsselten Container erstellen │");
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" Zieldatei: {}", container_path.display());
|
println!(" Zieldatei: {}", container_path.display());
|
||||||
|
if with_hidden {
|
||||||
|
println!(" Modus: Dual-Vault (Standard + {})", ui::magenta("Hidden Vault"));
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
if with_hidden {
|
||||||
.context("Fehler beim Einlesen des Passworts")?;
|
println!(" ─── [1/2] Standard-Vault (Äußerer Container / Decoy) ───");
|
||||||
|
let password_0 = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||||
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
|
if password_0.trim().is_empty() {
|
||||||
|
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||||
|
}
|
||||||
|
let confirm_0 = rpassword::prompt_password("Master-Passwort für Standard-Vault bestätigen: ")
|
||||||
|
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||||
|
if password_0 != confirm_0 {
|
||||||
|
bail!("Die eingegebenen Passwörter für den Standard-Vault stimmen nicht überein!");
|
||||||
|
}
|
||||||
|
|
||||||
if password.trim().is_empty() {
|
println!();
|
||||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
println!(" ─── [2/2] Hidden Vault (Versteckter Speicher / Plausible Deniability) ───");
|
||||||
|
println!(" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG"));
|
||||||
|
let password_1 = rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
||||||
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
|
if password_1.trim().is_empty() {
|
||||||
|
bail!("Das Master-Passwort für den Hidden-Vault darf nicht leer sein.");
|
||||||
|
}
|
||||||
|
if password_1 == password_0 {
|
||||||
|
bail!("Das Hidden-Vault-Passwort darf nicht mit dem Standard-Passwort identisch sein!");
|
||||||
|
}
|
||||||
|
let confirm_1 = rpassword::prompt_password("Master-Passwort für Hidden-Vault bestätigen: ")
|
||||||
|
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||||
|
if password_1 != confirm_1 {
|
||||||
|
bail!("Die eingegebenen Passwörter für den Hidden-Vault stimmen nicht überein!");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
ui::step(1, 4, "🔑", "Leite KEKs für Standard- und Hidden-Vault via Argon2id ab...");
|
||||||
|
let salt_0 = generate_salt();
|
||||||
|
let kdf_params_0 = KdfParams::default();
|
||||||
|
let kek_0 = derive_kek(&password_0, &salt_0, &kdf_params_0)?;
|
||||||
|
|
||||||
|
let salt_1 = generate_salt();
|
||||||
|
let kdf_params_1 = KdfParams::default();
|
||||||
|
let kek_1 = derive_kek(&password_1, &salt_1, &kdf_params_1)?;
|
||||||
|
|
||||||
|
ui::step(2, 4, "🎲", "Erzeuge getrennte DEKs für beide Vaults via CSPRNG...");
|
||||||
|
let dek_0 = generate_dek();
|
||||||
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
|
ui::step(3, 4, "🔒", "Verschlüssele beide DEKs unabhängig via AES-256-GCM...");
|
||||||
|
let (wrapped_dek_0, nonce_0, tag_0) = wrap_dek(&kek_0, &dek_0)?;
|
||||||
|
let (wrapped_dek_1, nonce_1, tag_1) = wrap_dek(&kek_1, &dek_1)?;
|
||||||
|
|
||||||
|
ui::step(4, 4, "📦", "Initialisiere SQLite-Container mit Dual-Slot Header & WAL-Modus...");
|
||||||
|
let db = Database::open(container_path)
|
||||||
|
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||||
|
|
||||||
|
db.init_schema_with_hidden(
|
||||||
|
&salt_0,
|
||||||
|
&kdf_params_0,
|
||||||
|
&wrapped_dek_0,
|
||||||
|
&nonce_0,
|
||||||
|
&tag_0,
|
||||||
|
Some((&salt_1, &kdf_params_1, &wrapped_dek_1, &nonce_1, &tag_1)),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
db.checkpoint()?;
|
||||||
|
|
||||||
|
let phrase_0 = dek_to_mnemonic(&dek_0)?;
|
||||||
|
let phrase_1 = dek_to_mnemonic(&dek_1)?;
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ✔ Sanctum Dual-Vault Container erfolgreich initialisiert! │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!();
|
||||||
|
println!(" • Container: {}", container_path.display());
|
||||||
|
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||||
|
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
||||||
|
println!(" • Verschlüssel: AES-256-GCM + LZ4-Kompression + Dateinamens-Verschleierung");
|
||||||
|
println!(" • Deniability: Beide Slots besitzen identische Struktur und Bit-Entropie");
|
||||||
|
println!();
|
||||||
|
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||||
|
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||||||
|
println!(" (Die Eingabe des jeweiligen Passworts bindet automatisch den passenden Vault ein)");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||||||
|
println!(" │ NOTFALLSCHLÜSSEL — SLOT 0 (STANDARD / DECOY VAULT) │");
|
||||||
|
println!(" └─────────────────────────────────────────────────────────┘");
|
||||||
|
ui::print_recovery_phrase_card(&phrase_0);
|
||||||
|
|
||||||
|
println!(" ┌─────────────────────────────────────────────────────────┐");
|
||||||
|
println!(" │ NOTFALLSCHLÜSSEL — SLOT 1 (HIDDEN VAULT) │");
|
||||||
|
println!(" └─────────────────────────────────────────────────────────┘");
|
||||||
|
ui::print_recovery_phrase_card(&phrase_1);
|
||||||
|
} else {
|
||||||
|
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||||
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
|
|
||||||
|
if password.trim().is_empty() {
|
||||||
|
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirm_password = rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||||
|
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
||||||
|
|
||||||
|
if password != confirm_password {
|
||||||
|
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
||||||
|
let salt = generate_salt();
|
||||||
|
let kdf_params = KdfParams::default();
|
||||||
|
let kek = derive_kek(&password, &salt, &kdf_params)
|
||||||
|
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||||||
|
|
||||||
|
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
||||||
|
let dek = generate_dek();
|
||||||
|
|
||||||
|
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
||||||
|
let (wrapped_dek, header_nonce, header_tag) =
|
||||||
|
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
||||||
|
|
||||||
|
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
||||||
|
let db = Database::open(container_path)
|
||||||
|
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||||
|
|
||||||
|
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||||
|
.context("Fehler bei der Schema-Initialisierung")?;
|
||||||
|
|
||||||
|
db.checkpoint()
|
||||||
|
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||||||
|
|
||||||
|
let recovery_phrase =
|
||||||
|
dek_to_mnemonic(&dek).context("Fehler beim Erzeugen der Notfallphrase")?;
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!();
|
||||||
|
println!(" • Container: {}", container_path.display());
|
||||||
|
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||||
|
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||||
|
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
||||||
|
println!(" • Deniability: Slot 1 mit CSPRNG-Zufallsrauschen gefüllt");
|
||||||
|
println!();
|
||||||
|
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||||
|
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
ui::print_recovery_phrase_card(&recovery_phrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
let confirm_password = rpassword::prompt_password("Master-Passwort bestätigen: ")
|
Ok(())
|
||||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?;
|
}
|
||||||
|
|
||||||
if password != confirm_password {
|
fn handle_compact(container_path: &Path, pages: Option<usize>) -> Result<()> {
|
||||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
if !container_path.exists() {
|
||||||
|
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
|
println!("│ Sanctum — Storage-Kompaktierung (Anti-Forensics) │");
|
||||||
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
|
println!(" Container: {}", container_path.display());
|
||||||
println!();
|
println!();
|
||||||
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
|
||||||
let salt = generate_salt();
|
|
||||||
let kdf_params = KdfParams::default();
|
|
||||||
let kek = derive_kek(&password, &salt, &kdf_params)
|
|
||||||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
|
||||||
|
|
||||||
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
|
||||||
let dek = generate_dek();
|
|
||||||
|
|
||||||
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
|
||||||
let (wrapped_dek, header_nonce, header_tag) =
|
|
||||||
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
|
||||||
|
|
||||||
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
let freelist_before = db.freelist_count()
|
||||||
.context("Fehler bei der Schema-Initialisierung")?;
|
.context("Konnte Freelist-Größe nicht ermitteln")?;
|
||||||
|
|
||||||
|
println!(" • Freie Seiten in Freelist: {}", ui::cyan(&freelist_before.to_string()));
|
||||||
|
|
||||||
|
if freelist_before == 0 {
|
||||||
|
println!(" • Der Container ist bereits optimal kompaktiert. Keine Bereinigung nötig.");
|
||||||
|
println!();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::step(1, 2, "🧹", "Führe Incremental-Vacuum aus...");
|
||||||
|
let freed = db.incremental_vacuum(pages)
|
||||||
|
.context("Fehler bei der Speicherbereinigung (Incremental Vacuum)")?;
|
||||||
|
|
||||||
|
ui::step(2, 2, "💾", "Führe finalen WAL-Checkpoint durch...");
|
||||||
db.checkpoint()
|
db.checkpoint()
|
||||||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||||||
|
|
||||||
let recovery_phrase =
|
let freelist_after = db.freelist_count().unwrap_or(0);
|
||||||
dek_to_mnemonic(&dek).context("Fehler beim Erzeugen der Notfallphrase")?;
|
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │");
|
println!("│ ✔ Container erfolgreich kompaktiert! │");
|
||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Freigegebene Seiten: {}", ui::green(&freed.to_string()));
|
||||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
println!(" • Verbleibende Freeseiten: {}", ui::dim(&freelist_after.to_string()));
|
||||||
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
|
||||||
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
|
||||||
println!();
|
println!();
|
||||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
|
||||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\" --drive S", container_path.display())));
|
|
||||||
println!();
|
|
||||||
|
|
||||||
ui::print_recovery_phrase_card(&recovery_phrase);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,11 +406,11 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
let dek = if let Some(phrase) = recovery_key {
|
let (dek, target_slot_id) = if let Some(phrase) = recovery_key {
|
||||||
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
||||||
let d = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
let d = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||||||
d
|
(d, 0)
|
||||||
} else {
|
} else {
|
||||||
let old_password = rpassword::prompt_password("Aktuelles Master-Passwort eingeben: ")
|
let old_password = rpassword::prompt_password("Aktuelles Master-Passwort eingeben: ")
|
||||||
.context("Fehler beim Einlesen des aktuellen Passworts")?;
|
.context("Fehler beim Einlesen des aktuellen Passworts")?;
|
||||||
@@ -268,16 +426,22 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
.context("Konnte Container-Header nicht lesen")?;
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab & prüfe Passwort...");
|
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab & prüfe Passwort...");
|
||||||
let old_kek = derive_kek(&old_password, &meta.kdf_salt, &meta.kdf_params)
|
let mut found = None;
|
||||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
for slot in &meta.slots {
|
||||||
|
if let Ok(old_kek) = derive_kek(&old_password, &slot.kdf_salt, &slot.kdf_params) {
|
||||||
|
if let Ok(d) = unwrap_dek(
|
||||||
|
&old_kek,
|
||||||
|
&slot.wrapped_dek,
|
||||||
|
&slot.header_nonce,
|
||||||
|
&slot.header_tag,
|
||||||
|
) {
|
||||||
|
found = Some((d, slot.slot_id));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unwrap_dek(
|
found.ok_or_else(|| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?
|
||||||
&old_kek,
|
|
||||||
&meta.wrapped_dek,
|
|
||||||
&meta.header_nonce,
|
|
||||||
&meta.header_tag,
|
|
||||||
)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Ungültiges aktuelles Master-Passwort! Authentifizierung fehlgeschlagen."))?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
@@ -306,7 +470,8 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
wrap_dek(&new_kek, &dek).context("DEK-Wrapping mit neuem KEK fehlgeschlagen")?;
|
wrap_dek(&new_kek, &dek).context("DEK-Wrapping mit neuem KEK fehlgeschlagen")?;
|
||||||
|
|
||||||
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
|
ui::step(4, 4, "💾", "Aktualisiere Container-Header & führe Checkpoint aus...");
|
||||||
db.update_meta_keys(
|
db.update_slot_keys(
|
||||||
|
target_slot_id,
|
||||||
&new_salt,
|
&new_salt,
|
||||||
&new_params,
|
&new_params,
|
||||||
&new_wrapped_dek,
|
&new_wrapped_dek,
|
||||||
@@ -324,6 +489,7 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
|||||||
println!("└─────────────────────────────────────────────────────────────┘");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
|
println!(" • Vault Slot: {}", target_slot_id);
|
||||||
println!(" • KDF: Argon2id mit frischem Salt");
|
println!(" • KDF: Argon2id mit frischem Salt");
|
||||||
println!(" • Status: DEK sicher neu verpackt (alle Chunks intakt)");
|
println!(" • Status: DEK sicher neu verpackt (alle Chunks intakt)");
|
||||||
println!();
|
println!();
|
||||||
@@ -425,12 +591,23 @@ fn handle_recovery_key(container_path: &Path) -> Result<()> {
|
|||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||||
let kek = derive_kek(&password, &meta.kdf_salt, &meta.kdf_params)
|
|
||||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
let mut found = None;
|
||||||
let dek = unwrap_dek(&kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag)
|
for slot in &meta.slots {
|
||||||
.map_err(|_| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
if let Ok(kek) = derive_kek(&password, &slot.kdf_salt, &slot.kdf_params) {
|
||||||
|
if let Ok(dek) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
|
||||||
|
found = Some((dek, slot.slot_id));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (dek, slot_id) = found.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||||
|
|
||||||
let phrase = dek_to_mnemonic(&dek)?;
|
let phrase = dek_to_mnemonic(&dek)?;
|
||||||
|
if slot_id == 1 {
|
||||||
|
println!(" • Vault: {}", ui::magenta("Hidden Vault (Slot 1)"));
|
||||||
|
}
|
||||||
ui::print_recovery_phrase_card(&phrase);
|
ui::print_recovery_phrase_card(&phrase);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -453,13 +630,23 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
|||||||
let dek = if !password.trim().is_empty() {
|
let dek = if !password.trim().is_empty() {
|
||||||
let db = Database::open(container_path).context("Konnte Container nicht öffnen")?;
|
let db = Database::open(container_path).context("Konnte Container nicht öffnen")?;
|
||||||
let meta = db.read_meta().context("Konnte Header nicht lesen")?;
|
let meta = db.read_meta().context("Konnte Header nicht lesen")?;
|
||||||
let kek = derive_kek(&password, &meta.kdf_salt, &meta.kdf_params)?;
|
|
||||||
match unwrap_dek(&kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag) {
|
let mut found = None;
|
||||||
Ok(d) => {
|
for slot in &meta.slots {
|
||||||
|
if let Ok(kek) = derive_kek(&password, &slot.kdf_salt, &slot.kdf_params) {
|
||||||
|
if let Ok(d) = unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag) {
|
||||||
|
found = Some(d);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match found {
|
||||||
|
Some(d) => {
|
||||||
println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"));
|
println!(" {} Master-Passwort verifiziert. Führe kryptografische AEAD-Vollprüfung durch...", ui::green("✔"));
|
||||||
Some(d)
|
Some(d)
|
||||||
}
|
}
|
||||||
Err(_) => {
|
None => {
|
||||||
println!(" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.", ui::yellow("⚠️"));
|
println!(" {} Passwort falsch! Führe nur SQLite- und Strukturprüfung durch.", ui::yellow("⚠️"));
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -483,8 +670,11 @@ async fn run() -> Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Init { path } => {
|
Commands::Init { path, with_hidden } => {
|
||||||
handle_init(&path)?;
|
handle_init(&path, with_hidden)?;
|
||||||
|
}
|
||||||
|
Commands::Compact { path, pages } => {
|
||||||
|
handle_compact(&path, pages)?;
|
||||||
}
|
}
|
||||||
Commands::Mount {
|
Commands::Mount {
|
||||||
path,
|
path,
|
||||||
|
|||||||
+53
-15
@@ -107,33 +107,56 @@ pub async fn mount_container(
|
|||||||
.read_meta()
|
.read_meta()
|
||||||
.context("Konnte Container-Header nicht lesen")?;
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
let (dek, version) = match auth {
|
let (dek, version, vault_id) = match auth {
|
||||||
ContainerAuth::Password(ref password) => {
|
ContainerAuth::Password(ref password) => {
|
||||||
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
|
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
|
||||||
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
let mut unwrapped = None;
|
||||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
|
||||||
|
|
||||||
ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM...");
|
for slot in &meta.slots {
|
||||||
let dek = unwrap_dek(
|
if let Ok(kek) = derive_kek(password, &slot.kdf_salt, &slot.kdf_params) {
|
||||||
&kek,
|
if let Ok(dek) = unwrap_dek(
|
||||||
&meta.wrapped_dek,
|
&kek,
|
||||||
&meta.header_nonce,
|
&slot.wrapped_dek,
|
||||||
&meta.header_tag,
|
&slot.header_nonce,
|
||||||
)
|
&slot.header_tag,
|
||||||
.context("Ungültiges Master-Passwort oder Container beschädigt")?;
|
) {
|
||||||
(dek, meta.version)
|
unwrapped = Some((dek, slot.version, slot.slot_id));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match unwrapped {
|
||||||
|
Some((dek, ver, slot_id)) => {
|
||||||
|
ui::step(3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!");
|
||||||
|
(dek, ver, slot_id)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bail!("Ungültiges Master-Passwort oder Container beschädigt");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||||
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel...");
|
||||||
let dek = mnemonic_to_dek(phrase)
|
let dek = mnemonic_to_dek(phrase)
|
||||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||||
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
||||||
(dek, meta.version)
|
|
||||||
|
let vault_id = if db.has_hidden_vault().unwrap_or(false) {
|
||||||
|
let is_hidden = {
|
||||||
|
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||||
|
!children.is_empty()
|
||||||
|
};
|
||||||
|
if is_hidden { 1 } else { 0 }
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
(dek, meta.version, vault_id)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield)
|
// WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Vault-Routing)
|
||||||
let fs = SanctumFs::with_options(db.clone(), dek, version, anti_leak);
|
let fs = SanctumFs::with_vault(db.clone(), dek, version, anti_leak, vault_id);
|
||||||
let last_activity = fs.last_activity();
|
let last_activity = fs.last_activity();
|
||||||
let dav_server = DavHandler::builder()
|
let dav_server = DavHandler::builder()
|
||||||
.filesystem(Box::new(fs))
|
.filesystem(Box::new(fs))
|
||||||
@@ -300,6 +323,9 @@ pub async fn mount_container(
|
|||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
||||||
|
if vault_id == 1 {
|
||||||
|
println!(" • Vault: {} (Verschlüsselte Dateinamen)", ui::magenta("Hidden Vault (Slot 1)"));
|
||||||
|
}
|
||||||
if let Some(secs) = idle_timeout {
|
if let Some(secs) = idle_timeout {
|
||||||
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
println!(" • Auto-Lock: Inaktivität nach {}s", secs);
|
||||||
}
|
}
|
||||||
@@ -350,6 +376,18 @@ pub async fn mount_container(
|
|||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
|
|
||||||
|
// Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren
|
||||||
|
if let Ok(freelist) = db.freelist_count() {
|
||||||
|
if freelist > 0 {
|
||||||
|
print!(" {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist);
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
match db.incremental_vacuum(None) {
|
||||||
|
Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed),
|
||||||
|
Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SQLite WAL Checkpoint erzwingen
|
// SQLite WAL Checkpoint erzwingen
|
||||||
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
|||||||
+23
-1
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::crypto::{
|
use crate::crypto::{
|
||||||
derive_kek, mnemonic_to_dek, wrap_dek, KdfParams, FORMAT_VERSION,
|
derive_kek, mnemonic_to_dek, wrap_dek, KdfParams, FORMAT_VERSION,
|
||||||
};
|
};
|
||||||
use crate::storage::{ContainerMeta, Database};
|
use crate::storage::{ContainerMeta, Database, SlotMeta};
|
||||||
|
|
||||||
pub const HEADER_BACKUP_MAGIC: &str = "SANCTUM_HEADER_BACKUP";
|
pub const HEADER_BACKUP_MAGIC: &str = "SANCTUM_HEADER_BACKUP";
|
||||||
pub const CURRENT_BACKUP_VERSION: u32 = 1;
|
pub const CURRENT_BACKUP_VERSION: u32 = 1;
|
||||||
@@ -79,6 +79,16 @@ impl HeaderBackup {
|
|||||||
let mut header_tag = [0u8; 16];
|
let mut header_tag = [0u8; 16];
|
||||||
header_tag.copy_from_slice(&tag_bytes);
|
header_tag.copy_from_slice(&tag_bytes);
|
||||||
|
|
||||||
|
let slot0 = SlotMeta {
|
||||||
|
slot_id: 0,
|
||||||
|
version: self.container_format_version,
|
||||||
|
kdf_salt,
|
||||||
|
kdf_params: self.kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce,
|
||||||
|
header_tag,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(ContainerMeta {
|
Ok(ContainerMeta {
|
||||||
version: self.container_format_version,
|
version: self.container_format_version,
|
||||||
kdf_salt,
|
kdf_salt,
|
||||||
@@ -86,6 +96,7 @@ impl HeaderBackup {
|
|||||||
wrapped_dek,
|
wrapped_dek,
|
||||||
header_nonce,
|
header_nonce,
|
||||||
header_tag,
|
header_tag,
|
||||||
|
slots: vec![slot0],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +177,16 @@ pub fn restore_header_from_recovery_key(
|
|||||||
let (wrapped_dek, header_nonce, header_tag) =
|
let (wrapped_dek, header_nonce, header_tag) =
|
||||||
wrap_dek(&kek, &dek).context("Verschlüsseln des DEK fehlgeschlagen")?;
|
wrap_dek(&kek, &dek).context("Verschlüsseln des DEK fehlgeschlagen")?;
|
||||||
|
|
||||||
|
let slot0 = SlotMeta {
|
||||||
|
slot_id: 0,
|
||||||
|
version: FORMAT_VERSION,
|
||||||
|
kdf_salt: salt,
|
||||||
|
kdf_params: kdf_params.clone(),
|
||||||
|
wrapped_dek: wrapped_dek.clone(),
|
||||||
|
header_nonce,
|
||||||
|
header_tag,
|
||||||
|
};
|
||||||
|
|
||||||
let meta = ContainerMeta {
|
let meta = ContainerMeta {
|
||||||
version: FORMAT_VERSION,
|
version: FORMAT_VERSION,
|
||||||
kdf_salt: salt,
|
kdf_salt: salt,
|
||||||
@@ -173,6 +194,7 @@ pub fn restore_header_from_recovery_key(
|
|||||||
wrapped_dek,
|
wrapped_dek,
|
||||||
header_nonce,
|
header_nonce,
|
||||||
header_tag,
|
header_tag,
|
||||||
|
slots: vec![slot0],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. In Container schreiben
|
// 4. In Container schreiben
|
||||||
|
|||||||
+717
-127
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,14 @@ pub fn red(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn magenta(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[1;35m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
||||||
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
||||||
let tag = cyan(&format!("[{}/{}]", num, total));
|
let tag = cyan(&format!("[{}/{}]", num, total));
|
||||||
|
|||||||
+64
-67
@@ -388,12 +388,13 @@ pub struct SanctumFs {
|
|||||||
dek: Arc<Zeroizing<[u8; 32]>>,
|
dek: Arc<Zeroizing<[u8; 32]>>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
|
vault_id: u32,
|
||||||
last_activity: Arc<AtomicU64>,
|
last_activity: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SanctumFs {
|
impl SanctumFs {
|
||||||
pub fn new(db: Database, dek: Zeroizing<[u8; 32]>, format_version: u32) -> Self {
|
pub fn new(db: Database, dek: Zeroizing<[u8; 32]>, format_version: u32) -> Self {
|
||||||
Self::with_options(db, dek, format_version, true)
|
Self::with_vault(db, dek, format_version, true, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_options(
|
pub fn with_options(
|
||||||
@@ -401,6 +402,16 @@ impl SanctumFs {
|
|||||||
dek: Zeroizing<[u8; 32]>,
|
dek: Zeroizing<[u8; 32]>,
|
||||||
format_version: u32,
|
format_version: u32,
|
||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_vault(db, dek, format_version, anti_leak, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_vault(
|
||||||
|
db: Database,
|
||||||
|
dek: Zeroizing<[u8; 32]>,
|
||||||
|
format_version: u32,
|
||||||
|
anti_leak: bool,
|
||||||
|
vault_id: u32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -411,10 +422,15 @@ impl SanctumFs {
|
|||||||
dek: Arc::new(dek),
|
dek: Arc::new(dek),
|
||||||
format_version,
|
format_version,
|
||||||
anti_leak,
|
anti_leak,
|
||||||
|
vault_id,
|
||||||
last_activity: Arc::new(AtomicU64::new(now)),
|
last_activity: Arc::new(AtomicU64::new(now)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn vault_id(&self) -> u32 {
|
||||||
|
self.vault_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
pub fn last_activity(&self) -> Arc<AtomicU64> {
|
||||||
self.last_activity.clone()
|
self.last_activity.clone()
|
||||||
}
|
}
|
||||||
@@ -442,6 +458,33 @@ impl SanctumFs {
|
|||||||
None => ("", trimmed),
|
None => ("", trimmed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_path(&self, path: &str) -> Result<Option<NodeRecord>, FsError> {
|
||||||
|
self.db
|
||||||
|
.resolve_path_in_vault(path, self.vault_id, &self.dek)
|
||||||
|
.map_err(|_| FsError::GeneralFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_children(&self, parent_id: i64) -> Result<Vec<NodeRecord>, FsError> {
|
||||||
|
self.db
|
||||||
|
.list_children_in_vault(parent_id, self.vault_id, &self.dek)
|
||||||
|
.map_err(|_| FsError::GeneralFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_node(&self, parent_id: i64, name: &str, is_dir: bool) -> Result<NodeRecord, FsError> {
|
||||||
|
self.db
|
||||||
|
.create_node_in_vault(self.vault_id, parent_id, name, is_dir, &self.dek)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Fehler beim Erstellen des Knotens '{}': {e}", name);
|
||||||
|
FsError::GeneralFailure
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_node(&self, id: i64, new_parent_id: i64, new_name: &str) -> Result<(), FsError> {
|
||||||
|
self.db
|
||||||
|
.rename_node_in_vault(id, new_parent_id, new_name, self.vault_id, &self.dek)
|
||||||
|
.map_err(|_| FsError::GeneralFailure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DavFileSystem for SanctumFs {
|
impl DavFileSystem for SanctumFs {
|
||||||
@@ -473,10 +516,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
self.touch();
|
self.touch();
|
||||||
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
||||||
|
|
||||||
let existing_node = self
|
let existing_node = self.resolve_path(&path_str)?;
|
||||||
.db
|
|
||||||
.resolve_path(&path_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
let node = match existing_node {
|
let node = match existing_node {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
@@ -511,21 +551,14 @@ impl DavFileSystem for SanctumFs {
|
|||||||
None => {
|
None => {
|
||||||
if options.create || options.create_new {
|
if options.create || options.create_new {
|
||||||
let parent = self
|
let parent = self
|
||||||
.db
|
.resolve_path(parent_path)?
|
||||||
.resolve_path(parent_path)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if !parent.is_dir {
|
if !parent.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db
|
self.create_node(parent.id, file_name, false)?
|
||||||
.create_node(parent.id, file_name, false)
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("Fehler beim Erstellen der Datei: {e}");
|
|
||||||
FsError::GeneralFailure
|
|
||||||
})?
|
|
||||||
} else {
|
} else {
|
||||||
return Err(FsError::NotFound);
|
return Err(FsError::NotFound);
|
||||||
}
|
}
|
||||||
@@ -552,19 +585,14 @@ impl DavFileSystem for SanctumFs {
|
|||||||
self.touch();
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.resolve_path(&path_str)?
|
||||||
.resolve_path(&path_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if !node.is_dir {
|
if !node.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
let children = self
|
let children = self.list_children(node.id)?;
|
||||||
.db
|
|
||||||
.list_children(node.id)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = children
|
let entries: Vec<Result<Box<dyn DavDirEntry>, FsError>> = children
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -593,9 +621,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
self.touch();
|
self.touch();
|
||||||
}
|
}
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.resolve_path(&path_str)?
|
||||||
.resolve_path(&path_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
let meta = SanctumMetaData {
|
let meta = SanctumMetaData {
|
||||||
@@ -623,28 +649,19 @@ impl DavFileSystem for SanctumFs {
|
|||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if self.resolve_path(&path_str)?.is_some() {
|
||||||
.db
|
|
||||||
.resolve_path(&path_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return Err(FsError::Exists);
|
return Err(FsError::Exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parent = self
|
let parent = self
|
||||||
.db
|
.resolve_path(parent_path)?
|
||||||
.resolve_path(parent_path)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if !parent.is_dir {
|
if !parent.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db
|
self.create_node(parent.id, dir_name, true)?;
|
||||||
.create_node(parent.id, dir_name, true)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -655,15 +672,14 @@ impl DavFileSystem for SanctumFs {
|
|||||||
self.touch();
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.resolve_path(&path_str)?
|
||||||
.resolve_path(&path_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if !node.is_dir {
|
if !node.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
if node.id == 1 {
|
let root_id = Database::get_root_node_id_for_vault(self.vault_id);
|
||||||
|
if node.id == root_id {
|
||||||
// Root-Verzeichnis darf nicht gelöscht werden
|
// Root-Verzeichnis darf nicht gelöscht werden
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
@@ -681,9 +697,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
self.touch();
|
self.touch();
|
||||||
let path_str = Self::path_to_str(path);
|
let path_str = Self::path_to_str(path);
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.resolve_path(&path_str)?
|
||||||
.resolve_path(&path_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if node.is_dir {
|
if node.is_dir {
|
||||||
@@ -709,9 +723,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
let to_str = Self::path_to_str(to);
|
let to_str = Self::path_to_str(to);
|
||||||
|
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.resolve_path(&from_str)?
|
||||||
.resolve_path(&from_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
let (to_parent_path, to_name) = self.split_parent_and_name(&to_str);
|
||||||
@@ -721,9 +733,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let to_parent = self
|
let to_parent = self
|
||||||
.db
|
.resolve_path(to_parent_path)?
|
||||||
.resolve_path(to_parent_path)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if !to_parent.is_dir {
|
if !to_parent.is_dir {
|
||||||
@@ -731,11 +741,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Falls Zieldatei bereits existiert und Datei ist: überschreiben / löschen
|
// Falls Zieldatei bereits existiert und Datei ist: überschreiben / löschen
|
||||||
if let Some(dest) = self
|
if let Some(dest) = self.resolve_path(&to_str)? {
|
||||||
.db
|
|
||||||
.resolve_path(&to_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
{
|
|
||||||
if dest.is_dir {
|
if dest.is_dir {
|
||||||
return Err(FsError::Forbidden);
|
return Err(FsError::Forbidden);
|
||||||
}
|
}
|
||||||
@@ -744,9 +750,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
.map_err(|_| FsError::GeneralFailure)?;
|
.map_err(|_| FsError::GeneralFailure)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.db
|
self.rename_node(node.id, to_parent.id, to_name)?;
|
||||||
.rename_node(node.id, to_parent.id, to_name)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -763,9 +767,7 @@ impl DavFileSystem for SanctumFs {
|
|||||||
let to_str = Self::path_to_str(to);
|
let to_str = Self::path_to_str(to);
|
||||||
|
|
||||||
let node = self
|
let node = self
|
||||||
.db
|
.resolve_path(&from_str)?
|
||||||
.resolve_path(&from_str)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
if node.is_dir {
|
if node.is_dir {
|
||||||
@@ -779,15 +781,10 @@ impl DavFileSystem for SanctumFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let to_parent = self
|
let to_parent = self
|
||||||
.db
|
.resolve_path(to_parent_path)?
|
||||||
.resolve_path(to_parent_path)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?
|
|
||||||
.ok_or(FsError::NotFound)?;
|
.ok_or(FsError::NotFound)?;
|
||||||
|
|
||||||
let dest_node = self
|
let dest_node = self.create_node(to_parent.id, to_name, false)?;
|
||||||
.db
|
|
||||||
.create_node(to_parent.id, to_name, false)
|
|
||||||
.map_err(|_| FsError::GeneralFailure)?;
|
|
||||||
|
|
||||||
// Kopiere alle Chunks und re-verschlüssele mit neuer node_id (wegen AAD-Bindung!)
|
// Kopiere alle Chunks und re-verschlüssele mit neuer node_id (wegen AAD-Bindung!)
|
||||||
let total_chunks = if node.size == 0 {
|
let total_chunks = if node.size == 0 {
|
||||||
|
|||||||
@@ -609,5 +609,189 @@ async fn test_anti_leak_and_inactivity_shield() {
|
|||||||
let _ = std::fs::remove_file(&container_path);
|
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