feat(security): implement Phase 2 Modell A (Steganografischer Alibi-Carrier für Plausible Deniability)
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bytes::Bytes;
|
||||
use dav_server::davpath::DavPath;
|
||||
use dav_server::fs::{DavFileSystem, FsError, OpenOptions, ReadDirMeta};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, CHUNK_SIZE,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::verify::verify_container;
|
||||
use sanctum::vfs::SanctumFs;
|
||||
|
||||
fn temp_db_path(prefix: &str) -> PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
let id: u64 = OsRng.next_u64();
|
||||
path.push(format!("sanctum_test_{}_{}.sanctum", prefix, id));
|
||||
path
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_model_a_carrier_filesystem_and_accounting_attack_resistance() {
|
||||
let path = temp_db_path("carrier_accounting");
|
||||
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB (10 Blöcke à 1 MB)
|
||||
let carrier_name = "backup_image.iso";
|
||||
|
||||
let pass_decoy = "DecoyPassword2026!";
|
||||
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let salt_0 = generate_salt();
|
||||
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_0, nonce_0, tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
let (wrapped_1, nonce_1, tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
|
||||
let db = Database::open(&path).expect("Open database");
|
||||
|
||||
let created_cid = db
|
||||
.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params,
|
||||
&wrapped_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((
|
||||
carrier_name,
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params,
|
||||
&wrapped_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
)
|
||||
.expect("Init carrier schema");
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
assert_eq!(created_cid, Some(carrier_node_id));
|
||||
|
||||
// 1. Authentifizierung beider Passwörter
|
||||
let meta = db.read_meta().unwrap();
|
||||
let auth_decoy = meta.authenticate(pass_decoy).expect("Auth decoy");
|
||||
assert_eq!(auth_decoy.slot_id(), 0);
|
||||
assert_eq!(**auth_decoy.dek(), *dek_0);
|
||||
assert_eq!(auth_decoy.carrier_node_id(), Some(carrier_node_id));
|
||||
|
||||
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||
assert_eq!(auth_hidden.slot_id(), 1);
|
||||
assert_eq!(**auth_hidden.dek(), *dek_1);
|
||||
assert_eq!(*auth_hidden.carrier_dek().unwrap(), *dek_0);
|
||||
assert_eq!(auth_hidden.carrier_node_id(), Some(carrier_node_id));
|
||||
|
||||
// 2. Decoy Mount: Schutz der Alibi-Datei (backup_image.iso)
|
||||
let decoy_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_decoy.dek().clone(),
|
||||
auth_decoy.carrier_dek(),
|
||||
auth_decoy.carrier_node_id(),
|
||||
auth_decoy.version(),
|
||||
true,
|
||||
0,
|
||||
);
|
||||
|
||||
let carrier_path = DavPath::new("/backup_image.iso").unwrap();
|
||||
|
||||
// Metadaten der Alibi-Datei im Decoy prüfen
|
||||
let carrier_meta = decoy_fs.metadata(&carrier_path).await.expect("Carrier meta");
|
||||
assert_eq!(carrier_meta.len(), carrier_size_bytes);
|
||||
assert!(!carrier_meta.is_dir());
|
||||
|
||||
// Alibi-Datei darf im Decoy-Mount NICHT zum Schreiben geöffnet werden
|
||||
let write_opts = OpenOptions {
|
||||
write: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
matches!(decoy_fs.open(&carrier_path, write_opts).await, Err(FsError::Forbidden)),
|
||||
"Alibi-Datei darf nicht zum Schreiben geöffnet werden"
|
||||
);
|
||||
|
||||
// Alibi-Datei darf im Decoy-Mount NICHT gelöscht werden
|
||||
assert!(
|
||||
matches!(decoy_fs.remove_file(&carrier_path).await, Err(FsError::Forbidden)),
|
||||
"Alibi-Datei darf nicht gelöscht werden"
|
||||
);
|
||||
|
||||
// Alibi-Datei darf im Decoy-Mount NICHT umbenannt werden
|
||||
let new_name = DavPath::new("/renamed.iso").unwrap();
|
||||
assert!(
|
||||
matches!(decoy_fs.rename(&carrier_path, &new_name).await, Err(FsError::Forbidden)),
|
||||
"Alibi-Datei darf nicht umbenannt werden"
|
||||
);
|
||||
|
||||
// Alibi-Datei KANN im Decoy-Mount gelesen werden
|
||||
let read_opts = OpenOptions {
|
||||
read: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut file_reader = decoy_fs.open(&carrier_path, read_opts).await.expect("Open read");
|
||||
let first_mb = file_reader.read_bytes(CHUNK_SIZE).await.expect("Read first chunk");
|
||||
assert_eq!(first_mb.len(), CHUNK_SIZE);
|
||||
|
||||
// 3. Hidden Mount: Dateisystem-Operationen innerhalb des Alibi-Carriers
|
||||
let hidden_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_hidden.dek().clone(),
|
||||
auth_hidden.carrier_dek(),
|
||||
auth_hidden.carrier_node_id(),
|
||||
auth_hidden.version(),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
// Wurzelverzeichnis des Hidden Vault auflisten (anfangs leer)
|
||||
let root_path = DavPath::new("/").unwrap();
|
||||
let mut stream = hidden_fs
|
||||
.read_dir(&root_path, ReadDirMeta::None)
|
||||
.await
|
||||
.expect("Read dir root");
|
||||
use futures_util::StreamExt;
|
||||
let mut entries = Vec::new();
|
||||
while let Some(item) = stream.next().await {
|
||||
entries.push(item.unwrap().name());
|
||||
}
|
||||
assert!(entries.is_empty(), "Hidden Vault Wurzelverzeichnis muss anfangs leer sein");
|
||||
|
||||
// Ordner erstellen
|
||||
let secret_dir = DavPath::new("/Classified").unwrap();
|
||||
hidden_fs.create_dir(&secret_dir).await.expect("Create Classified dir");
|
||||
|
||||
// Datei im Ordner anlegen und schreiben
|
||||
let secret_file_path = DavPath::new("/Classified/passwords.txt").unwrap();
|
||||
let create_opts = OpenOptions {
|
||||
create: true,
|
||||
write: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut secret_file = hidden_fs
|
||||
.open(&secret_file_path, create_opts)
|
||||
.await
|
||||
.expect("Create secret file");
|
||||
|
||||
let secret_content = b"TopSecretCredentials_2026_SanctumCoreSecureVault";
|
||||
secret_file
|
||||
.write_bytes(Bytes::from_static(secret_content))
|
||||
.await
|
||||
.expect("Write secret content");
|
||||
secret_file.flush().await.expect("Flush secret file");
|
||||
drop(secret_file);
|
||||
|
||||
// Datei lesen und verifizieren
|
||||
let read_opts = OpenOptions {
|
||||
read: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut read_handle = hidden_fs
|
||||
.open(&secret_file_path, read_opts)
|
||||
.await
|
||||
.expect("Open secret file for read");
|
||||
let read_data = read_handle.read_bytes(1024).await.expect("Read secret bytes");
|
||||
assert_eq!(&read_data[..], secret_content);
|
||||
drop(read_handle);
|
||||
|
||||
// Größere Binärdatei schreiben (über 2 MB = 2 Blöcke)
|
||||
let big_file_path = DavPath::new("/Classified/payload.bin").unwrap();
|
||||
let mut big_file = hidden_fs
|
||||
.open(&big_file_path, OpenOptions { create: true, write: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Create big file");
|
||||
|
||||
let payload_size = 2 * 1024 * 1024 + 12345; // 2 MB + 12.345 Bytes
|
||||
let mut payload = vec![0u8; payload_size];
|
||||
OsRng.fill_bytes(&mut payload);
|
||||
|
||||
big_file.write_bytes(Bytes::copy_from_slice(&payload)).await.expect("Write big payload");
|
||||
big_file.flush().await.expect("Flush big file");
|
||||
drop(big_file);
|
||||
|
||||
// Datei zurücklesen und Bit-für-Bit verifizieren
|
||||
let mut read_big = hidden_fs
|
||||
.open(&big_file_path, OpenOptions { read: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open big file");
|
||||
let read_big_bytes = read_big.read_bytes(payload_size + 100).await.expect("Read big file bytes");
|
||||
assert_eq!(read_big_bytes.len(), payload_size);
|
||||
assert_eq!(&read_big_bytes[..], &payload[..]);
|
||||
drop(read_big);
|
||||
|
||||
// Datei umbenennen
|
||||
let renamed_path = DavPath::new("/Classified/renamed_payload.bin").unwrap();
|
||||
hidden_fs.rename(&big_file_path, &renamed_path).await.expect("Rename file");
|
||||
assert!(hidden_fs.metadata(&big_file_path).await.is_err());
|
||||
assert!(hidden_fs.metadata(&renamed_path).await.is_ok());
|
||||
|
||||
// Datei löschen (Blöcke werden geshreddert und freigegeben)
|
||||
hidden_fs.remove_file(&renamed_path).await.expect("Remove file");
|
||||
assert!(hidden_fs.metadata(&renamed_path).await.is_err());
|
||||
|
||||
// Checkpoint SQLite
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// 4. CHUNKS-ACCOUNTING-ANGRIFF & INTEGRITÄTSPRÜFUNG
|
||||
// Ein Angreifer besitzt nur das Decoy-Passwort (dek_0).
|
||||
// Er führt eine 100%-ige kryptografische AEAD-Prüfung aller Chunks in der SQLite-Datenbank durch.
|
||||
// ALLE Chunks müssen fehlerfrei unter DEK_0 entschlüsseln!
|
||||
let report = verify_container(&path, Some(&dek_0), true).expect("Verify with DEK_0");
|
||||
assert!(
|
||||
report.is_healthy(),
|
||||
"Container muss für einen Angreifer mit DEK_0 100% gesund und fehlerfrei sein! Fehler: {:?}",
|
||||
report.errors
|
||||
);
|
||||
assert_eq!(
|
||||
report.corrupted_chunks, 0,
|
||||
"Chunks-Accounting: Es darf exakt 0 korrupte Chunks unter DEK_0 geben!"
|
||||
);
|
||||
assert_eq!(
|
||||
report.orphan_nodes, 0,
|
||||
"Es darf keine verwaisten Knoten geben!"
|
||||
);
|
||||
|
||||
// Aufräumen
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_model_a_container_file_size_invariance() {
|
||||
let path = temp_db_path("carrier_size_invariance");
|
||||
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
let pass_decoy = "DecoyPass2026!";
|
||||
let pass_hidden = "HiddenPass2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
let salt_0 = generate_salt();
|
||||
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
|
||||
let salt_1 = generate_salt();
|
||||
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_0, nonce_0, tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||
let (wrapped_1, nonce_1, tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||
|
||||
let db = Database::open(&path).expect("Open database");
|
||||
|
||||
db.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params,
|
||||
&wrapped_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
Some((
|
||||
"virtual_disk.vhdx",
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params,
|
||||
&wrapped_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
)
|
||||
.expect("Init carrier schema");
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Initiale Dateigröße messen
|
||||
let initial_file_size = std::fs::metadata(&path).unwrap().len();
|
||||
assert!(initial_file_size >= carrier_size_bytes, "Containergröße muss mindestens 10 MB betragen");
|
||||
|
||||
// Hidden Mount öffnen und 4 MB geheime Daten schreiben
|
||||
let meta = db.read_meta().unwrap();
|
||||
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||
|
||||
let hidden_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_hidden.dek().clone(),
|
||||
auth_hidden.carrier_dek(),
|
||||
auth_hidden.carrier_node_id(),
|
||||
auth_hidden.version(),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
let test_file = DavPath::new("/large_confidential.pdf").unwrap();
|
||||
let mut handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { create: true, write: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file");
|
||||
|
||||
let mut random_data = vec![0u8; 4 * 1024 * 1024]; // 4 MB
|
||||
OsRng.fill_bytes(&mut random_data);
|
||||
handle.write_bytes(Bytes::copy_from_slice(&random_data)).await.expect("Write 4MB");
|
||||
handle.flush().await.expect("Flush 4MB");
|
||||
drop(handle);
|
||||
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Dateigröße nach dem Schreiben von 4 MB im Hidden Vault messen
|
||||
let size_after_hidden_writes = std::fs::metadata(&path).unwrap().len();
|
||||
|
||||
// Die Dateigröße auf der Festplatte DARF NICHT WACHSEN!
|
||||
// Alle Chunks wurden in vorallokierte Carrier-Blöcke überschrieben.
|
||||
assert_eq!(
|
||||
initial_file_size, size_after_hidden_writes,
|
||||
"Dateigröße auf der Festplatte darf sich beim Schreiben in den Hidden Vault NICHT verändern! Vorher: {}, Nachher: {}",
|
||||
initial_file_size, size_after_hidden_writes
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
Reference in New Issue
Block a user