feat(security): release v0.7.0 with comprehensive security hardening (S-01 to S-11)
Sanctum Release / Build & Release (Windows x86_64) (push) Waiting to run
Sanctum Release / Build & Release (Windows x86_64) (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
use std::path::PathBuf;
|
||||
use sanctum::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, MIN_MEMORY_COST_KIB,
|
||||
MIN_TIME_COST,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
|
||||
#[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);
|
||||
}
|
||||
Reference in New Issue
Block a user