- SA-01: Container-DoS / KDF-Amplification Schutz mit Pre-KDF Validierung, max 2 Slots (nur 0 und 1), Slot 0 Pflicht und strikten BLOB-Laengen - SA-02: Release-Signierung in CI entkoppelt (getrennte build und sign-and-release Jobs, Secret-Isolation) - SA-03: Pinned Download-Integritaet fuer minisign.exe in CI via SHA-256 - SA-04: Immutable Action-Pinning (@sha) und Toolchain-Pinning (1.85.0) in CI - SA-05: Session-Token vollstaendig aus URIs verbannt (403 Forbidden bei Vorkommen im Pfad/Query) - SA-06: Constant-Time Token- und Auth-Vergleiche via subtle::ConstantTimeEq - SA-07: Dokumentations-Klarstellung bzgl. logischem Shredding vs. physischer SSD/FTL/CoW-Persistenz
680 lines
23 KiB
Rust
680 lines
23 KiB
Rust
use sanctum::crypto::{
|
|
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
|
MIN_TIME_COST,
|
|
};
|
|
use sanctum::storage::Database;
|
|
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());
|
|
}
|