Files
sanctum/tests/untrusted_container_test.rs
T

872 lines
30 KiB
Rust

use sanctum::crypto::{
derive_kek, generate_dek, generate_salt, wrap_dek, wrap_slot0_payload, KdfParams,
MIN_MEMORY_COST_KIB, MIN_TIME_COST,
};
use sanctum::storage::Database;
use sanctum::verify::verify_container;
use std::path::PathBuf;
#[test]
fn test_reject_arbitrary_sqlite_database() {
let temp_dir = std::env::temp_dir();
let db_path: PathBuf = temp_dir.join(format!("test_foreign_sqlite_{}.db", std::process::id()));
if db_path.exists() {
let _ = std::fs::remove_file(&db_path);
}
// Erstelle eine fremde SQLite-Datenbank mit beliebigen Daten
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", [])
.unwrap();
conn.execute("INSERT INTO users (name) VALUES ('Alice')", [])
.unwrap();
}
// Sanctum Database::open muss die Datei sofort fail-closed ablehnen
let res = Database::open(&db_path);
match res {
Err(err) => {
let msg = err.to_string();
assert!(
msg.contains("kein gültiger Sanctum-Container"),
"Fehler muss ungültigen Container abweisen: {msg}"
);
}
Ok(_) => panic!("Fremde SQLite-DB ohne meta-Tabelle muss abgelehnt werden"),
}
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_sqlite_database_with_invalid_magic() {
let temp_dir = std::env::temp_dir();
let db_path: PathBuf = temp_dir.join(format!("test_invalid_magic_{}.db", std::process::id()));
if db_path.exists() {
let _ = std::fs::remove_file(&db_path);
}
// Erstelle SQLite DB mit 'meta' Tabelle, aber falscher Magic
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER PRIMARY KEY,
magic BLOB NOT NULL,
version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL,
kdf_params TEXT NOT NULL,
wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL,
header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
VALUES (0, ?1, 2, ?2, '{}', ?3, ?4, ?5)",
rusqlite::params![
b"EVIL_MAGIC",
&[0u8; 16][..],
&[0u8; 40][..],
&[0u8; 12][..],
&[0u8; 16][..]
],
)
.unwrap();
}
let res = Database::open(&db_path);
match res {
Err(err) => {
let msg = err.to_string();
assert!(
msg.contains("kein gültiger Sanctum-Container"),
"Fehler muss ungültigen Magic-Header monieren: {msg}"
);
}
Ok(_) => panic!("Container mit ungültigen Magic Bytes muss abgelehnt werden"),
}
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_out_of_bounds_kdf_params() {
let temp_dir = std::env::temp_dir();
let db_path: PathBuf = temp_dir.join(format!("test_oob_kdf_{}.sanctum", std::process::id()));
if db_path.exists() {
let _ = std::fs::remove_file(&db_path);
}
// 1. Initialisiere gültigen Container
let salt = generate_salt();
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let kek = derive_kek("ValidPassword2026!", &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
let db = Database::open(&db_path).unwrap();
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
.unwrap();
db.checkpoint().unwrap();
drop(db);
// 2. Manipuliere KDF-Parameter in der meta-Tabelle auf gefährliche Werte (DoS: 16 GiB Memory)
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE meta SET kdf_params = '{\"memory_cost\": 16777216, \"time_cost\": 3, \"parallelism\": 4}' WHERE slot_id = 0",
[],
)
.unwrap();
}
// 3. Öffnen und read_slots() aufrufen: muss mit Kdf-Param-Fehler abbrechen
let db_reopened = Database::open(&db_path).unwrap();
let slots_res = db_reopened.read_slots();
match slots_res {
Err(err) => {
let msg = err.to_string();
assert!(
msg.contains("KDF-Parameter"),
"Fehler muss KDF-Parameter monieren: {msg}"
);
}
Ok(_) => panic!("Überhöhte KDF-Parameter müssen fail-closed abgewehrt werden"),
}
// 4. Manipuliere KDF-Parameter auf zu schwache Werte (< MIN_MEMORY_COST_KIB)
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE meta SET kdf_params = '{\"memory_cost\": 1024, \"time_cost\": 3, \"parallelism\": 4}' WHERE slot_id = 0",
[],
)
.unwrap();
}
let slots_res2 = db_reopened.read_slots();
match slots_res2 {
Err(err) => {
let msg = err.to_string();
assert!(
msg.contains("KDF-Parameter"),
"Fehler muss KDF-Parameter monieren: {msg}"
);
}
Ok(_) => panic!("Zu schwache KDF-Parameter müssen fail-closed abgewehrt werden"),
}
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_corrupted_slot_lengths() {
let temp_dir = std::env::temp_dir();
let db_path: PathBuf =
temp_dir.join(format!("test_slot_lengths_{}.sanctum", std::process::id()));
if db_path.exists() {
let _ = std::fs::remove_file(&db_path);
}
let salt = generate_salt();
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let kek = derive_kek("ValidPassword2026!", &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
let db = Database::open(&db_path).unwrap();
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
.unwrap();
db.checkpoint().unwrap();
drop(db);
// Manipuliere Salt-Länge auf 8 Byte statt 16 Byte
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE meta SET kdf_salt = ?1 WHERE slot_id = 0",
[&[0u8; 8][..]],
)
.unwrap();
}
let db_reopened = Database::open(&db_path).unwrap();
let res = db_reopened.read_slots();
match res {
Err(err) => {
let msg = err.to_string();
assert!(
msg.contains("Salt-Länge"),
"Fehler muss fehlerhafte Salt-Länge monieren: {msg}"
);
}
Ok(_) => panic!("Ungültige Salt-Länge muss abgewiesen werden"),
}
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_slot_amplification_and_excessive_slots_dos() {
let temp_dir = std::env::temp_dir();
let salt = generate_salt();
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let kek = derive_kek("TestPassword2026!", &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
let params_json = serde_json::to_string(&kdf_params).unwrap();
// 1. Fall: 0 Slots (leere meta-Tabelle)
{
let db_path = temp_dir.join(format!("test_0_slots_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER PRIMARY KEY, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
conn.execute("CREATE TABLE nodes (id INTEGER PRIMARY KEY);", [])
.unwrap();
conn.execute("CREATE TABLE chunks (node_id INTEGER);", [])
.unwrap();
}
let db = Database::open(&db_path).unwrap();
let res = db.read_slots();
assert!(res.is_err());
let err_msg = res.err().unwrap().to_string();
assert!(
err_msg.contains("leer oder beschädigt"),
"Fehlermeldung: {err_msg}"
);
let _ = std::fs::remove_file(&db_path);
}
// 2. Fall: Genau 1 Slot (Slot 0 Decoy/Standard) -> MUSS erfolgreich sein
{
let db_path = temp_dir.join(format!("test_1_slot_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
let db = Database::open(&db_path).unwrap();
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
.unwrap();
// Lösche Dummy-Slot 1, um reinen 1-Slot Container zu simulieren
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute("DELETE FROM meta WHERE slot_id = 1", [])
.unwrap();
}
let db_reopened = Database::open(&db_path).unwrap();
let slots = db_reopened
.read_slots()
.expect("1 gültiger Slot (Slot 0) muss erfolgreich gelesen werden");
assert_eq!(slots.len(), 1);
assert_eq!(slots[0].slot_id, 0);
let _ = std::fs::remove_file(&db_path);
}
// 3. Fall: Genau 2 Slots (Slot 0 & Slot 1) -> MUSS erfolgreich sein
{
let db_path = temp_dir.join(format!("test_2_slots_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
let db = Database::open(&db_path).unwrap();
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
.unwrap();
let slots = db
.read_slots()
.expect("2 gültige Slots müssen erfolgreich gelesen werden");
assert_eq!(slots.len(), 2);
assert_eq!(slots[0].slot_id, 0);
assert_eq!(slots[1].slot_id, 1);
let _ = std::fs::remove_file(&db_path);
}
// 4. Fall: 3 Slots (Überschreitung des Limits von 2) -> MUSS fail-closed abgewehrt werden
{
let db_path = temp_dir.join(format!("test_3_slots_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
for i in 0..3 {
conn.execute(
"INSERT INTO meta VALUES (?1, ?2, 2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
i,
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
],
)
.unwrap();
}
}
let open_res = Database::open(&db_path);
assert!(
open_res.is_err(),
"Database::open muss 3 Slots sofort abweisen"
);
let err_msg = open_res.err().unwrap().to_string();
assert!(
err_msg.contains("Ungültige Slot-Anzahl"),
"Fehlermeldung: {err_msg}"
);
let _ = std::fs::remove_file(&db_path);
}
// 5. Fall: 100 Slots (KDF-Amplification DoS Angriff) -> MUSS sofort ohne KDF abgewehrt werden
{
let db_path = temp_dir.join(format!("test_100_slots_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
for i in 0..100 {
conn.execute(
"INSERT INTO meta VALUES (?1, ?2, 2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
i,
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
],
)
.unwrap();
}
}
let open_res = Database::open(&db_path);
assert!(
open_res.is_err(),
"100 Slots müssen sofort abgewiesen werden"
);
let _ = std::fs::remove_file(&db_path);
}
// 6. Fall: 1000 Slots (Massiver DoS-Angriff) -> MUSS sofort abgewehrt werden
{
let db_path = temp_dir.join(format!("test_1000_slots_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
let mut stmt = conn
.prepare("INSERT INTO meta VALUES (?1, ?2, 2, ?3, ?4, ?5, ?6, ?7)")
.unwrap();
for i in 0..1000 {
stmt.execute(rusqlite::params![
i,
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
])
.unwrap();
}
}
let open_res = Database::open(&db_path);
assert!(
open_res.is_err(),
"1000 Slots müssen sofort abgewiesen werden"
);
let err_msg = open_res.err().unwrap().to_string();
assert!(
err_msg.contains("Ungültige Slot-Anzahl"),
"Fehlermeldung: {err_msg}"
);
let _ = std::fs::remove_file(&db_path);
}
}
#[test]
fn test_reject_duplicate_and_invalid_slot_ids() {
let temp_dir = std::env::temp_dir();
let salt = generate_salt();
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let kek = derive_kek("TestPassword2026!", &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
let params_json = serde_json::to_string(&kdf_params).unwrap();
// 1. Fall: Doppelte Slot-ID (zwei Slots mit slot_id = 0)
{
let db_path = temp_dir.join(format!("test_dup_slot_0_{}.sanctum", std::process::id()));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
conn.execute(
"INSERT INTO meta VALUES (0, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
],
)
.unwrap();
conn.execute(
"INSERT INTO meta VALUES (0, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
],
)
.unwrap();
}
let db = Database::open(&db_path).unwrap();
let res = db.read_slots();
assert!(res.is_err(), "Doppelte Slot-ID 0 muss abgewiesen werden");
let err_msg = res.err().unwrap().to_string();
assert!(
err_msg.contains("Doppelte Slot-ID 0"),
"Fehlermeldung: {err_msg}"
);
let _ = std::fs::remove_file(&db_path);
}
// 2. Fall: Ungültige Slot-ID (z. B. slot_id = 5 statt 0 oder 1)
{
let db_path = temp_dir.join(format!(
"test_invalid_slot_id_{}.sanctum",
std::process::id()
));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
conn.execute(
"INSERT INTO meta VALUES (5, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
],
)
.unwrap();
}
let db = Database::open(&db_path).unwrap();
let res = db.read_slots();
assert!(res.is_err(), "Ungültige Slot-ID 5 muss abgewiesen werden");
let err_msg = res.err().unwrap().to_string();
assert!(
err_msg.contains("Ungültige Slot-ID 5"),
"Fehlermeldung: {err_msg}"
);
let _ = std::fs::remove_file(&db_path);
}
// 3. Fall: Fehlender Slot 0 (nur Slot 1 vorhanden)
{
let db_path = temp_dir.join(format!(
"test_missing_slot_0_{}.sanctum",
std::process::id()
));
let _ = std::fs::remove_file(&db_path);
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE meta (
slot_id INTEGER, magic BLOB NOT NULL, version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL, kdf_params TEXT NOT NULL, wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL, header_tag BLOB NOT NULL
);",
[],
)
.unwrap();
conn.execute(
"INSERT INTO meta VALUES (1, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
b"SANCTUM\0",
&salt[..],
&params_json,
&wrapped_dek[..],
&nonce[..],
&tag[..]
],
)
.unwrap();
}
let db = Database::open(&db_path).unwrap();
let res = db.read_slots();
assert!(res.is_err(), "Fehlender Slot 0 muss abgewiesen werden");
let err_msg = res.err().unwrap().to_string();
assert!(
err_msg.contains("Slot 0 (Standard/Decoy Vault) fehlt"),
"Fehlermeldung: {err_msg}"
);
let _ = std::fs::remove_file(&db_path);
}
}
#[test]
fn test_container_meta_authenticate_defense_in_depth() {
use sanctum::storage::{ContainerMeta, SlotMeta};
let salt = generate_salt();
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let kek = derive_kek("TestPassword2026!", &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
let valid_slot_0 = SlotMeta {
slot_id: 0,
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
};
let valid_slot_1 = SlotMeta {
slot_id: 1,
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
};
// A. Leere Slots -> sofort None
let meta_empty = ContainerMeta {
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
slots: vec![],
};
assert!(meta_empty.authenticate("TestPassword2026!").is_none());
// B. Mehr als 2 Slots -> sofort None (keine KDF-Berechnung!)
let meta_3_slots = ContainerMeta {
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
slots: vec![
valid_slot_0.clone(),
valid_slot_1.clone(),
valid_slot_0.clone(),
],
};
assert!(meta_3_slots.authenticate("TestPassword2026!").is_none());
// C. Fehlender Slot 0 (nur Slot 1) -> sofort None
let meta_no_slot0 = ContainerMeta {
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
slots: vec![valid_slot_1.clone()],
};
assert!(meta_no_slot0.authenticate("TestPassword2026!").is_none());
// D. Ungültige Slot-ID (> 1) -> sofort None
let mut invalid_slot = valid_slot_0.clone();
invalid_slot.slot_id = 99;
let meta_invalid_id = ContainerMeta {
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
slots: vec![valid_slot_0.clone(), invalid_slot],
};
assert!(meta_invalid_id.authenticate("TestPassword2026!").is_none());
// E. Korrekter 1-Slot Container -> Erfolgreiche Authentifizierung
let meta_valid_1 = ContainerMeta {
version: 2,
kdf_salt: salt,
kdf_params: kdf_params.clone(),
wrapped_dek: wrapped_dek.clone(),
header_nonce: nonce,
header_tag: tag,
slots: vec![valid_slot_0.clone()],
};
assert!(meta_valid_1.authenticate("TestPassword2026!").is_some());
}
#[test]
fn test_k01_metadata_tampering_detected_by_verify() {
let temp_dir = std::env::temp_dir();
let db_path: PathBuf = temp_dir.join(format!("test_k01_meta_{}.sanctum", std::process::id()));
if db_path.exists() {
let _ = std::fs::remove_file(&db_path);
}
let password = "SecretMasterPassword2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let salt = generate_salt();
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_slot0_payload(&kek, &dek, 0).unwrap();
let db = Database::open(&db_path).unwrap();
db.set_active_dek(dek.clone());
db.init_schema_with_carrier(&salt, &kdf_params, &wrapped_dek, &nonce, &tag, None)
.unwrap();
// Knoten anlegen
let node = db
.create_node(1, "secret_financials.xlsx", false)
.unwrap();
db.update_node_size_and_time(node.id, 50000, 1000).unwrap();
db.checkpoint().unwrap();
// 1. Initialer Zustand: Verifikation muss erfolgreich sein
let report = verify_container(&db_path, Some(&dek), true).unwrap();
assert!(
report.is_healthy(),
"Initialer V3-Container muss gesund sein, aber Fehler: {:?}",
report.errors
);
// 2. Angreifer manipuliert Dateigröße direkt in SQLite (ohne MAC aktualisieren zu können)
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE nodes SET size = 999999 WHERE name = 'secret_financials.xlsx';",
[],
)
.unwrap();
}
// Verifikation MUSS fehlschlagen und den K-01 Metadatenfehler melden!
let tampered_report = verify_container(&db_path, Some(&dek), true).unwrap();
assert!(
!tampered_report.is_healthy(),
"Container mit manipulierter Knotengröße darf nicht als gesund eingestuft werden!"
);
assert!(
tampered_report
.errors
.iter()
.any(|e| e.contains("K-01") || e.contains("Metadaten-MAC")),
"Fehlermeldung zu Metadaten-MAC erwartet, erhalten: {:?}",
tampered_report.errors
);
// 3. Angreifer manipuliert Dateinamen direkt in SQLite
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE nodes SET name = 'backdoor.exe' WHERE id = ?1;",
[node.id],
)
.unwrap();
}
let tampered_name_report = verify_container(&db_path, Some(&dek), true).unwrap();
assert!(
!tampered_name_report.is_healthy(),
"Container mit manipuliertem Knotennamen darf nicht als gesund eingestuft werden!"
);
assert!(
tampered_name_report
.errors
.iter()
.any(|e| e.contains("K-01") || e.contains("Metadaten-MAC")),
"Fehlermeldung zu Metadaten-MAC erwartet, erhalten: {:?}",
tampered_name_report.errors
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_k01_upgrade_format_v2_to_v3() {
let temp_dir = std::env::temp_dir();
let db_path: PathBuf = temp_dir.join(format!("test_k01_upgrade_{}.sanctum", std::process::id()));
if db_path.exists() {
let _ = std::fs::remove_file(&db_path);
}
let password = "UpgradeMasterPassword2026!";
let kdf_params = KdfParams {
memory_cost: MIN_MEMORY_COST_KIB,
time_cost: MIN_TIME_COST,
parallelism: 1,
};
let salt = generate_salt();
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
let dek = generate_dek();
let (wrapped_dek, nonce, tag) = wrap_slot0_payload(&kek, &dek, 0).unwrap();
// Erzeuge bewusst einen V2-Container ohne metadata_mac
{
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE meta (
slot_id INTEGER NOT NULL PRIMARY KEY,
magic BLOB NOT NULL,
version INTEGER NOT NULL,
kdf_salt BLOB NOT NULL,
kdf_params TEXT NOT NULL,
wrapped_dek BLOB NOT NULL,
header_nonce BLOB NOT NULL,
header_tag BLOB NOT NULL
);
CREATE TABLE nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER,
name TEXT NOT NULL,
is_dir INTEGER NOT NULL,
size INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
modified_at INTEGER NOT NULL,
is_carrier INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE chunks (
node_id INTEGER NOT NULL,
chunk_index INTEGER NOT NULL,
nonce BLOB NOT NULL,
tag BLOB NOT NULL,
ciphertext BLOB NOT NULL,
PRIMARY KEY (node_id, chunk_index)
);",
)
.unwrap();
let params_json = serde_json::to_string(&kdf_params).unwrap();
conn.execute(
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
VALUES (0, ?1, 2, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
sanctum::crypto::MAGIC_BYTES.as_slice(),
salt.as_slice(),
params_json,
wrapped_dek,
nonce.as_slice(),
tag.as_slice(),
],
)
.unwrap();
conn.execute(
"INSERT INTO nodes (id, parent_id, name, is_dir, size, created_at, modified_at)
VALUES (1, NULL, '', 1, 0, 100, 100), (2, NULL, '', 1, 0, 100, 100);",
[],
)
.unwrap();
}
let db = Database::open(&db_path).unwrap();
let meta_before = db.read_meta().unwrap();
assert_eq!(meta_before.version, 2);
// Upgrade auf Format V3 durchführen
db.upgrade_to_v3(&dek).unwrap();
db.checkpoint().unwrap();
let meta_after = db.read_meta().unwrap();
assert_eq!(meta_after.version, 3);
// V3-Container muss nach dem Upgrade integer und gesund sein
let report = verify_container(&db_path, Some(&dek), true).unwrap();
assert!(
report.is_healthy(),
"Container nach upgrade_to_v3 muss gesund sein: {:?}",
report.errors
);
let _ = std::fs::remove_file(&db_path);
}