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[..], ¶ms_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[..], ¶ms_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[..], ¶ms_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[..], ¶ms_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[..], ¶ms_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[..], ¶ms_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[..], ¶ms_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); }