Files
sanctum/tests/carrier_model_a_test.rs
T

777 lines
23 KiB
Rust

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, MIN_MEMORY_COST_KIB, MIN_TIME_COST,
};
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 = "system_backup.dat";
let pass_decoy = "DecoyPassword2026!";
let pass_hidden = "SuperSecretHiddenPassword2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
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 (system_backup.dat)
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("/system_backup.dat").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: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
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);
}
#[tokio::test]
async fn test_carrier_file_drop_and_append_mode() {
let path = temp_db_path("carrier_drop_append");
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
let carrier_name = "test_carrier.iso";
let pass_decoy = "DecoyPassword2026!";
let pass_hidden = "HiddenSecretPassword2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
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((
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();
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,
);
// 1. TEST CarrierFile::drop: Write bytes OHNE expliziten flush(), dann drop(handle)
let test_file = DavPath::new("/drop_flush_test.txt").unwrap();
let mut write_handle = hidden_fs
.open(
&test_file,
OpenOptions {
create: true,
write: true,
..Default::default()
},
)
.await
.expect("Open file for write");
let initial_data = b"Hello from unflushed write!";
write_handle
.write_bytes(Bytes::from_static(initial_data))
.await
.expect("Write initial data");
// WICHTIG: KEIN write_handle.flush()! Nur drop:
drop(write_handle);
// Jetzt Datei wieder lesend öffnen und prüfen, ob Daten durch Drop persistiert wurden
let mut read_handle = hidden_fs
.open(
&test_file,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open file for read");
let read_back = read_handle.read_bytes(100).await.expect("Read data back");
assert_eq!(
&read_back[..],
initial_data,
"Drop muss ungeflushte Datenblöcke und Inode automatisch sichern"
);
drop(read_handle);
// 2. TEST O_APPEND: Im Append-Modus öffnen und weitere Daten anhängen
let append_data = b" - Appended data at EOF!";
let mut append_handle = hidden_fs
.open(
&test_file,
OpenOptions {
write: true,
append: true,
..Default::default()
},
)
.await
.expect("Open file for append");
append_handle
.write_bytes(Bytes::from_static(append_data))
.await
.expect("Write appended data");
drop(append_handle); // Drop sichert auch hier
// Prüfe den vollständigen Dateiinhalt nach Append
let mut read_handle_2 = hidden_fs
.open(
&test_file,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open file for read after append");
let full_content = read_handle_2
.read_bytes(200)
.await
.expect("Read full content");
let mut expected = Vec::new();
expected.extend_from_slice(initial_data);
expected.extend_from_slice(append_data);
assert_eq!(
&full_content[..],
&expected[..],
"O_APPEND muss Daten am Dateiende anhängen"
);
drop(read_handle_2);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn test_v01_carrier_fs_copy_delegation_and_functionality() {
let path = temp_db_path("v01_copy");
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
let carrier_name = "carrier_for_copy.bin";
let pass_decoy = "DecoyPass2026!";
let pass_hidden = "HiddenPass2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
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((
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();
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,
);
// Ordner anlegen
let folder = DavPath::new("/work").unwrap();
hidden_fs.create_dir(&folder).await.expect("Create folder");
// Datei anlegen und Inhalt schreiben
let src_path = DavPath::new("/work/original.txt").unwrap();
let test_data = b"V01 CarrierFs Copy Functionality Test Payload 2026";
let mut file = hidden_fs
.open(
&src_path,
OpenOptions {
create: true,
write: true,
..Default::default()
},
)
.await
.expect("Create source file");
file.write_bytes(Bytes::from_static(test_data))
.await
.expect("Write source data");
file.flush().await.expect("Flush source file");
drop(file);
// V-01: copy von /work/original.txt nach /work/copy.txt aufrufen
let dest_path = DavPath::new("/work/copy.txt").unwrap();
hidden_fs
.copy(&src_path, &dest_path)
.await
.expect("V-01: Copy must succeed on CarrierFs");
// Metadaten der Kopie prüfen
let copy_meta = hidden_fs
.metadata(&dest_path)
.await
.expect("Metadata of copied file");
assert_eq!(copy_meta.len(), test_data.len() as u64);
assert!(!copy_meta.is_dir());
// Inhalt der Kopie lesen und verifizieren
let mut read_copy = hidden_fs
.open(
&dest_path,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open copied file");
let copy_bytes = read_copy.read_bytes(1024).await.expect("Read copy bytes");
assert_eq!(&copy_bytes[..], test_data);
drop(read_copy);
// Quelldatei löschen: Die Kopie muss unabhängig erhalten bleiben!
hidden_fs
.remove_file(&src_path)
.await
.expect("Delete source file");
assert!(hidden_fs.metadata(&src_path).await.is_err());
let mut read_copy_again = hidden_fs
.open(
&dest_path,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open copied file after source delete");
let copy_bytes_2 = read_copy_again
.read_bytes(1024)
.await
.expect("Read copy bytes after source delete");
assert_eq!(&copy_bytes_2[..], test_data);
drop(read_copy_again);
// Overwrite-Copy testen
let src2_path = DavPath::new("/work/source2.txt").unwrap();
let test_data_2 = b"New Overwrite Payload Content";
let mut file2 = hidden_fs
.open(
&src2_path,
OpenOptions {
create: true,
write: true,
..Default::default()
},
)
.await
.expect("Create source2");
file2
.write_bytes(Bytes::from_static(test_data_2))
.await
.expect("Write source2");
file2.flush().await.expect("Flush source2");
drop(file2);
hidden_fs
.copy(&src2_path, &dest_path)
.await
.expect("Overwrite copy must succeed");
let mut read_overwritten = hidden_fs
.open(
&dest_path,
OpenOptions {
read: true,
..Default::default()
},
)
.await
.expect("Open overwritten copy");
let overwritten_bytes = read_overwritten
.read_bytes(1024)
.await
.expect("Read overwritten bytes");
assert_eq!(&overwritten_bytes[..], test_data_2);
drop(read_overwritten);
// Checkpoint & Verify
db.checkpoint().unwrap();
let report = verify_container(&path, Some(&dek_0), true).expect("Verify with DEK_0");
assert!(
report.is_healthy(),
"Container muss integer und gesund bleiben: {:?}",
report.errors
);
let _ = std::fs::remove_file(&path);
}