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:
+138
-41
@@ -40,12 +40,74 @@ impl Default for KdfParams {
|
||||
}
|
||||
}
|
||||
|
||||
pub const MIN_MEMORY_COST_KIB: u32 = 19_456;
|
||||
pub const MAX_MEMORY_COST_KIB: u32 = 262_144;
|
||||
pub const MIN_TIME_COST: u32 = 2;
|
||||
pub const MAX_TIME_COST: u32 = 8;
|
||||
pub const MIN_PARALLELISM: u32 = 1;
|
||||
pub const MAX_PARALLELISM: u32 = 8;
|
||||
pub const MIN_PASSWORD_LENGTH: usize = 12;
|
||||
|
||||
/// Validiert KDF-Parameter gegen DoS-Angriffe (z. B. Memory-Bombing mit 4 GB) und unsichere Parameter (S-04).
|
||||
pub fn validate_kdf_params(params: &KdfParams) -> Result<()> {
|
||||
if !(MIN_MEMORY_COST_KIB..=MAX_MEMORY_COST_KIB).contains(¶ms.memory_cost) {
|
||||
bail!(
|
||||
"KDF-Parameter ungültig: memory_cost = {} KiB (erlaubter Bereich: {}..={} KiB)",
|
||||
params.memory_cost,
|
||||
MIN_MEMORY_COST_KIB,
|
||||
MAX_MEMORY_COST_KIB
|
||||
);
|
||||
}
|
||||
if !(MIN_TIME_COST..=MAX_TIME_COST).contains(¶ms.time_cost) {
|
||||
bail!(
|
||||
"KDF-Parameter ungültig: time_cost = {} (erlaubter Bereich: {}..={})",
|
||||
params.time_cost,
|
||||
MIN_TIME_COST,
|
||||
MAX_TIME_COST
|
||||
);
|
||||
}
|
||||
if !(MIN_PARALLELISM..=MAX_PARALLELISM).contains(¶ms.parallelism) {
|
||||
bail!(
|
||||
"KDF-Parameter ungültig: parallelism = {} (erlaubter Bereich: {}..={})",
|
||||
params.parallelism,
|
||||
MIN_PARALLELISM,
|
||||
MAX_PARALLELISM
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validiert, dass ein Master-Passwort die geforderte Mindestlänge (>= 12 Zeichen) aufweist (S-10).
|
||||
pub fn validate_password(password: &str) -> Result<()> {
|
||||
if password.chars().count() < MIN_PASSWORD_LENGTH {
|
||||
bail!(
|
||||
"Das Master-Passwort muss mindestens {} Zeichen lang sein (eingegeben: {} Zeichen).",
|
||||
MIN_PASSWORD_LENGTH,
|
||||
password.chars().count()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft, ob bei Dual-Vault ein Passwort das Präfix des anderen ist oder identisch ist.
|
||||
pub fn check_password_prefix_collision(pass0: &str, pass1: &str) -> Result<()> {
|
||||
if pass0 == pass1 {
|
||||
bail!("Die Passwörter für Standard-Vault und Hidden-Vault sind identisch!");
|
||||
} else if pass0.starts_with(pass1) || pass1.starts_with(pass0) {
|
||||
bail!("Ein Passwort ist ein Präfix des anderen! Dies schwächt die Sicherheit.");
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Leitet aus dem Master-Passwort und dem Salt einen 256-Bit Key Encryption Key (KEK) via Argon2id ab.
|
||||
pub fn derive_kek(
|
||||
password: &str,
|
||||
salt: &[u8],
|
||||
params: &KdfParams,
|
||||
) -> Result<Zeroizing<[u8; 32]>> {
|
||||
validate_kdf_params(params)?;
|
||||
|
||||
let argon2_params = Params::new(
|
||||
params.memory_cost,
|
||||
params.time_cost,
|
||||
@@ -56,10 +118,13 @@ pub fn derive_kek(
|
||||
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||
let mut kek = Zeroizing::new([0u8; 32]);
|
||||
let _ = crate::windows::lock_memory(kek.as_ptr(), 32);
|
||||
|
||||
argon2
|
||||
.hash_password_into(password.as_bytes(), salt, &mut *kek)
|
||||
.map_err(|e| anyhow::anyhow!("Argon2id KDF-Berechnung fehlgeschlagen: {e}"))?;
|
||||
let res = argon2.hash_password_into(password.as_bytes(), salt, &mut *kek);
|
||||
if let Err(e) = res {
|
||||
let _ = crate::windows::unlock_memory(kek.as_ptr(), 32);
|
||||
bail!("Argon2id KDF-Berechnung fehlgeschlagen: {e}");
|
||||
}
|
||||
|
||||
Ok(kek)
|
||||
}
|
||||
@@ -67,6 +132,7 @@ pub fn derive_kek(
|
||||
/// Generiert einen kryptografisch sicheren 256-Bit Data Encryption Key (DEK).
|
||||
pub fn generate_dek() -> Zeroizing<[u8; 32]> {
|
||||
let mut dek = Zeroizing::new([0u8; 32]);
|
||||
let _ = crate::windows::lock_memory(dek.as_ptr(), 32);
|
||||
OsRng.fill_bytes(&mut *dek);
|
||||
dek
|
||||
}
|
||||
@@ -200,6 +266,17 @@ pub fn build_name_aad(parent_id: i64) -> [u8; 16] {
|
||||
aad
|
||||
}
|
||||
|
||||
static ALLOW_LEGACY_NAMES: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// Aktiviert oder deaktiviert den veralteten AAD-Fallback für Dateinamen (S-10).
|
||||
pub fn set_allow_legacy_names(allow: bool) {
|
||||
ALLOW_LEGACY_NAMES.store(allow, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn allows_legacy_names() -> bool {
|
||||
ALLOW_LEGACY_NAMES.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM und bindet die parent_id als AAD ein.
|
||||
/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind
|
||||
/// und verhindert, dass verschlüsselte Knoten zwischen Ordnern verschoben oder vertauscht werden können.
|
||||
@@ -207,12 +284,16 @@ pub fn build_name_aad(parent_id: i64) -> [u8; 16] {
|
||||
pub fn encrypt_node_name(dek: &[u8; 32], parent_id: i64, name: &str) -> String {
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let cipher = Aes256Gcm::new_from_slice(dek).expect("AES init");
|
||||
let cipher = match Aes256Gcm::new_from_slice(dek) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
let mut buffer = name.as_bytes().to_vec();
|
||||
let aad = build_name_aad(parent_id);
|
||||
let tag = cipher
|
||||
.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), &aad, &mut buffer)
|
||||
.expect("Name encryption");
|
||||
let tag = match cipher.encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), &aad, &mut buffer) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
let mut combined = Vec::with_capacity(12 + 16 + buffer.len());
|
||||
combined.extend_from_slice(&nonce_bytes);
|
||||
combined.extend_from_slice(tag.as_slice());
|
||||
@@ -221,9 +302,9 @@ pub fn encrypt_node_name(dek: &[u8; 32], parent_id: i64, name: &str) -> String {
|
||||
}
|
||||
|
||||
/// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM.
|
||||
/// Prüft primär die kryptografische Bindung an parent_id; bietet transparenten Fallback
|
||||
/// auf die statische AAD für ältere Container (Abwärtskompatibilität).
|
||||
pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option<String> {
|
||||
/// Prüft primär die kryptografische Bindung an parent_id; bietet optional Fallback
|
||||
/// auf die statische AAD für ältere Container, wenn `allow_legacy` aktiv ist (S-10).
|
||||
pub fn decrypt_node_name_ext(dek: &[u8; 32], parent_id: i64, stored: &str, allow_legacy: bool) -> Option<String> {
|
||||
// Abwärtskompatibilität für alte v0.2.0 $h$<nonce>$<tag>$<ct> Namen
|
||||
if let Some(rest) = stored.strip_prefix("$h$") {
|
||||
let parts: Vec<&str> = rest.split('$').collect();
|
||||
@@ -250,18 +331,20 @@ pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option
|
||||
return String::from_utf8(buffer).ok();
|
||||
}
|
||||
|
||||
// 2. Fallback: Statische AAD für echte Legacy-Dateinamen
|
||||
let mut buffer_legacy = ct_bytes;
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(&nonce_bytes),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer_legacy,
|
||||
Tag::from_slice(&tag_bytes),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer_legacy).ok();
|
||||
// 2. Fallback: Statische AAD nur wenn explizit aktiviert (S-10)
|
||||
if allow_legacy {
|
||||
let mut buffer_legacy = ct_bytes;
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(&nonce_bytes),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer_legacy,
|
||||
Tag::from_slice(&tag_bytes),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer_legacy).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,18 +376,20 @@ pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option
|
||||
return String::from_utf8(buffer).ok();
|
||||
}
|
||||
|
||||
// 2. Fallback: Alte statische AAD für bestehende Container
|
||||
let mut buffer_legacy = ct.to_vec();
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(nonce),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer_legacy,
|
||||
Tag::from_slice(tag),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer_legacy).ok();
|
||||
// 2. Fallback: Alte statische AAD nur wenn explizit aktiviert (S-10)
|
||||
if allow_legacy {
|
||||
let mut buffer_legacy = ct.to_vec();
|
||||
if cipher
|
||||
.decrypt_in_place_detached(
|
||||
Nonce::from_slice(nonce),
|
||||
b"SANCTUM_NODE_NAME",
|
||||
&mut buffer_legacy,
|
||||
Tag::from_slice(tag),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return String::from_utf8(buffer_legacy).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,6 +398,10 @@ pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option
|
||||
None
|
||||
}
|
||||
|
||||
pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option<String> {
|
||||
decrypt_node_name_ext(dek, parent_id, stored, allows_legacy_names())
|
||||
}
|
||||
|
||||
/// Kodiert den 32-Byte (256-Bit) DEK in eine 24-Wort BIP-39 Notfall-Wiederherstellungsphrase (englisch) mit 8-Bit Checksumme.
|
||||
pub fn dek_to_mnemonic(dek: &[u8; 32]) -> Result<String> {
|
||||
let mnemonic = bip39::Mnemonic::from_entropy(dek)
|
||||
@@ -545,7 +634,9 @@ pub fn decrypt_chunk(
|
||||
if payload.len() < 4 {
|
||||
bail!("LZ4-Chunk beschädigt: Payload zu kurz für Längen-Präfix");
|
||||
}
|
||||
let uncompressed_size = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
|
||||
let mut size_bytes = [0u8; 4];
|
||||
size_bytes.copy_from_slice(&payload[0..4]);
|
||||
let uncompressed_size = u32::from_le_bytes(size_bytes) as usize;
|
||||
if uncompressed_size > CHUNK_SIZE {
|
||||
bail!(
|
||||
"LZ4-Dekomprimierungsfehler: Decompression-Bomb Schutz ausgelöst (angeforderte Größe {} Bytes > Limit {} Bytes)",
|
||||
@@ -574,8 +665,8 @@ mod tests {
|
||||
let password = "SuperSecretMasterPassword123!";
|
||||
let salt = generate_salt();
|
||||
let params = KdfParams {
|
||||
memory_cost: 1024, // Schneller für Unit-Test
|
||||
time_cost: 1,
|
||||
memory_cost: MIN_MEMORY_COST_KIB,
|
||||
time_cost: MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
|
||||
@@ -787,8 +878,14 @@ mod tests {
|
||||
hex::encode(static_tag),
|
||||
hex::encode(&static_buf)
|
||||
);
|
||||
let decrypted_static = decrypt_node_name(&dek, parent_id, &legacy_static_format).expect("Decrypt legacy static AAD name");
|
||||
// Standardmäßig muss Legacy-AAD abgewiesen werden (S-07)
|
||||
assert!(decrypt_node_name(&dek, parent_id, &legacy_static_format).is_none());
|
||||
|
||||
// Mit aktiviertem Legacy-Flag darf es entschlüsselt werden
|
||||
set_allow_legacy_names(true);
|
||||
let decrypted_static = decrypt_node_name(&dek, parent_id, &legacy_static_format).expect("Decrypt legacy static AAD name with flag");
|
||||
assert_eq!(decrypted_static, filename);
|
||||
set_allow_legacy_names(false);
|
||||
|
||||
// Mit anderem DEK schlägt Entschlüsselung fehl
|
||||
let other_dek = generate_dek();
|
||||
@@ -807,7 +904,6 @@ mod tests {
|
||||
let dek = generate_dek();
|
||||
let enc_folder_a = encrypt_node_name(&dek, 10, "secrets.txt");
|
||||
let enc_folder_b = encrypt_node_name(&dek, 20, "passwords.txt");
|
||||
|
||||
// Gültige parent_ids entschlüsseln erfolgreich
|
||||
assert_eq!(decrypt_node_name(&dek, 10, &enc_folder_a).unwrap(), "secrets.txt");
|
||||
assert_eq!(decrypt_node_name(&dek, 20, &enc_folder_b).unwrap(), "passwords.txt");
|
||||
@@ -819,8 +915,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_model_a_slot_payloads() {
|
||||
let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap();
|
||||
let kek_1 = derive_kek("HiddenPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap();
|
||||
let test_kdf = KdfParams { memory_cost: MIN_MEMORY_COST_KIB, time_cost: MIN_TIME_COST, parallelism: 1 };
|
||||
let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &test_kdf).unwrap();
|
||||
let kek_1 = derive_kek("HiddenPass123!", &generate_salt(), &test_kdf).unwrap();
|
||||
let dek_0 = generate_dek();
|
||||
let dek_1 = generate_dek();
|
||||
let carrier_node_id = 42i64;
|
||||
|
||||
+144
-91
@@ -6,12 +6,14 @@ use tracing_subscriber::EnvFilter;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use sanctum::crypto::{
|
||||
dek_to_mnemonic, derive_kek, generate_dek, generate_salt, mnemonic_to_dek,
|
||||
wrap_slot0_payload, wrap_slot1_payload, KdfParams, FORMAT_VERSION,
|
||||
check_password_prefix_collision, dek_to_mnemonic, derive_kek, generate_dek, generate_salt,
|
||||
mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, FORMAT_VERSION,
|
||||
};
|
||||
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
|
||||
use sanctum::recovery::{
|
||||
export_header_backup, restore_header_backup, restore_slot_from_recovery_key,
|
||||
detect_recovery_key_slot, export_header_backup, restore_header_backup,
|
||||
restore_slot_from_recovery_key,
|
||||
};
|
||||
use sanctum::storage::Database;
|
||||
use sanctum::ui;
|
||||
@@ -23,6 +25,10 @@ use sanctum::verify::verify_container;
|
||||
#[command(about = "Sanctum: Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland", long_about = None)]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
/// Erlaubt Legacy-Namensentschlüsselung ohne deterministisches AAD (S-07)
|
||||
#[arg(long, global = true, default_value_t = false)]
|
||||
legacy_names: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
@@ -35,7 +41,7 @@ enum Commands {
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Erstellt zusätzlich einen plausibel abstreitbaren Hidden Vault (Plausible Deniability)
|
||||
/// Erstellt einen separaten, isolierten zweiten Tresor (Dual-Vault / Second Safe)
|
||||
#[arg(long, default_value_t = false)]
|
||||
with_hidden: bool,
|
||||
|
||||
@@ -267,6 +273,10 @@ enum Commands {
|
||||
/// Benutzerdefinierte Gitea-Basis-URL (Standard: https://gitea.pansi.eu)
|
||||
#[arg(long, default_value = "https://gitea.pansi.eu")]
|
||||
url: String,
|
||||
|
||||
/// Erlaubt abweichende Update-Server-URLs (erfordert interaktive Bestätigung)
|
||||
#[arg(long, default_value_t = false)]
|
||||
insecure_url: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -302,11 +312,51 @@ pub fn parse_size_string(s: &str) -> Result<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest den 24-Wort BIP-39 Notfallschlüssel sicher ein.
|
||||
/// Unterstützt:
|
||||
/// 1. Interaktiv maskiert (wenn kein Wert oder leere Eingabe übergeben)
|
||||
/// 2. Stdin (wenn "-" übergeben)
|
||||
/// 3. Umgebungsvariable 'SANCTUM_RECOVERY_KEY' (wenn kein CLI-Argument übergeben)
|
||||
/// 4. Veraltetes CLI-Argument mit Sicherheitswarnung (S-09)
|
||||
fn resolve_recovery_key(arg: Option<&str>) -> Result<Option<Zeroizing<String>>> {
|
||||
if let Some(key) = arg {
|
||||
if key == "-" {
|
||||
println!(" [{}] Lese 24-Wort Notfallschlüssel von der Standardeingabe (stdin)...", ui::cyan("ℹ"));
|
||||
let mut line = String::new();
|
||||
std::io::stdin().read_line(&mut line).context("Fehler beim Einlesen von stdin")?;
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
bail!("Der Notfallschlüssel von stdin darf nicht leer sein.");
|
||||
}
|
||||
Ok(Some(Zeroizing::new(trimmed.to_string())))
|
||||
} else if key.trim().is_empty() {
|
||||
Ok(Some(prompt_recovery_key_interactive()?))
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} WARNUNG: Die Angabe des Notfallschlüssels als CLI-Argument ist unsicher und veraltet!",
|
||||
ui::yellow("[SICHERHEITSHINWEIS]")
|
||||
);
|
||||
eprintln!(" Verwenden Sie '--recovery-key' ohne Wert (interaktiv), '--recovery-key -' (stdin) oder die Umgebungsvariable 'SANCTUM_RECOVERY_KEY'.");
|
||||
Ok(Some(Zeroizing::new(key.to_string())))
|
||||
}
|
||||
} else if let Ok(env_key) = std::env::var("SANCTUM_RECOVERY_KEY") {
|
||||
let trimmed = env_key.trim();
|
||||
if !trimmed.is_empty() {
|
||||
println!(" [{}] Verwende Notfallschlüssel aus Umgebungsvariable 'SANCTUM_RECOVERY_KEY'.", ui::cyan("ℹ"));
|
||||
Ok(Some(Zeroizing::new(trimmed.to_string())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest den 24-Wort BIP-39 Notfallschlüssel interaktiv und maskiert ein,
|
||||
/// ohne dass Eingaben in der PowerShell-Historie (ConsoleHost_history.txt) landen.
|
||||
fn prompt_recovery_key_interactive() -> Result<Zeroizing<String>> {
|
||||
println!(" [{}] Geben Sie die 24 Notfall-Wörter durch Leerzeichen getrennt ein.", ui::cyan("ℹ"));
|
||||
println!(" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der PowerShell-Historie.", ui::dim("OpSec"));
|
||||
println!(" [{}] Die Eingabe erfolgt geschützt ohne Aufzeichnung in der Shell-Historie.", ui::dim("OpSec"));
|
||||
let phrase = rpassword::prompt_password("24-Wort Notfallschlüssel: ")
|
||||
.context("Fehler beim Einlesen des Notfallschlüssels")?;
|
||||
if phrase.trim().is_empty() {
|
||||
@@ -333,15 +383,12 @@ fn handle_init(
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Zieldatei: {}", container_path.display());
|
||||
if with_hidden {
|
||||
println!(" Modus: Dual-Vault (Modell A: {})", ui::magenta("Alibi-Carrier"));
|
||||
println!(" Alibi: {} ({})", ui::cyan(carrier_name), ui::cyan(carrier_size_str));
|
||||
let lower = carrier_name.to_lowercase();
|
||||
if lower.ends_with(".iso") || lower.ends_with(".vhd") || lower.ends_with(".img") || lower.ends_with(".zip") {
|
||||
println!(
|
||||
" [{}] Hinweis zur Plausible Deniability: Dateitypen wie '.iso' oder '.zip' besitzen ein starres Format. Da Carrier-Blöcke verschlüsselt sind (Entropie ~8.0), empfehlen sich unformatierte Typen wie '.dat', '.bin' oder '.enc' für maximale forensische Unauffälligkeit.",
|
||||
ui::yellow("Tipp")
|
||||
);
|
||||
}
|
||||
println!(" Modus: Dual-Vault (Modell A: {})", ui::magenta("Carrier-Datei"));
|
||||
println!(" Carrier: {} ({})", ui::cyan(carrier_name), ui::cyan(carrier_size_str));
|
||||
println!(
|
||||
" [{}] Hinweis zum Dual-Vault: Modell A kapselt den Hidden Vault in einer Datei im Decoy-Vault. Dies schützt vor neugierigen Blicken, widersteht jedoch keinem forensischen Gutachten, da Carrier-Entropie und Schreibmuster analysierbar sind.",
|
||||
ui::yellow("Info")
|
||||
);
|
||||
}
|
||||
println!();
|
||||
|
||||
@@ -356,9 +403,8 @@ fn handle_init(
|
||||
rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
if password_0.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&password_0)?;
|
||||
|
||||
let confirm_0 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Standard-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
@@ -368,18 +414,15 @@ fn handle_init(
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(" ─── [2/2] Hidden Vault (Versteckter Speicher / Plausible Deniability) ───");
|
||||
println!(" ─── [2/2] Hidden Vault (Zweiter isolierter Tresor / Dual-Vault) ───");
|
||||
println!(" [{}] Verwenden Sie ein völlig eigenständiges, separates Passwort!", ui::yellow("WICHTIG"));
|
||||
let password_1 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Hidden-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
if password_1.trim().is_empty() {
|
||||
bail!("Das Master-Passwort für den Hidden-Vault darf nicht leer sein.");
|
||||
}
|
||||
if *password_1 == *password_0 {
|
||||
bail!("Das Hidden-Vault-Passwort darf nicht mit dem Standard-Passwort identisch sein!");
|
||||
}
|
||||
validate_password(&password_1)?;
|
||||
check_password_prefix_collision(&password_0, &password_1)?;
|
||||
|
||||
let confirm_1 = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort für Hidden-Vault bestätigen: ")
|
||||
.context("Fehler beim Einlesen der Passwort-Bestätigung")?,
|
||||
@@ -398,33 +441,36 @@ fn handle_init(
|
||||
let kdf_params_1 = KdfParams::default();
|
||||
let kek_1 = derive_kek(&password_1, &salt_1, &kdf_params_1)?;
|
||||
|
||||
ui::step(2, 4, "🎲", "Erzeuge getrennte DEKs für beide Vaults via CSPRNG...");
|
||||
ui::step(2, 4, "🎲", "Erzeuge unabhängige DEKs (DEK_0 & DEK_1)...");
|
||||
let dek_0 = generate_dek();
|
||||
let dek_1 = generate_dek();
|
||||
|
||||
ui::step(3, 4, "🔒", "Verschlüssele Slot-Payloads für Modell A via AES-256-GCM...");
|
||||
ui::step(3, 4, "🔒", "Verschlüssele Slot-Payloads (Dual-Vault Key-Wrapping)...");
|
||||
let carrier_node_id = 3i64;
|
||||
let (wrapped_dek_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?;
|
||||
let (wrapped_dek_1, nonce_1, tag_1) = wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id)?;
|
||||
let (wrapped_dek_0, header_nonce_0, header_tag_0) =
|
||||
wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id)?;
|
||||
|
||||
ui::step(4, 4, "📦", &format!("Initialisiere Alibi-Carrier '{}' ({}) & Container...", carrier_name, carrier_size_str));
|
||||
ui::step(4, 4, "📦", "Erzeuge SQLite-Container & alloziiere Alibi-Carrier-Datei...");
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||
|
||||
let (wrapped_dek_1, header_nonce_1, header_tag_1) =
|
||||
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id)?;
|
||||
|
||||
db.init_schema_with_carrier(
|
||||
&salt_0,
|
||||
&kdf_params_0,
|
||||
&wrapped_dek_0,
|
||||
&nonce_0,
|
||||
&tag_0,
|
||||
&header_nonce_0,
|
||||
&header_tag_0,
|
||||
Some((
|
||||
carrier_name,
|
||||
carrier_size_bytes,
|
||||
&salt_1,
|
||||
&kdf_params_1,
|
||||
&wrapped_dek_1,
|
||||
&nonce_1,
|
||||
&tag_1,
|
||||
&header_nonce_1,
|
||||
&header_tag_1,
|
||||
&dek_0,
|
||||
&dek_1,
|
||||
)),
|
||||
@@ -437,16 +483,16 @@ fn handle_init(
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum Dual-Vault Container (Modell A) initialisiert! │");
|
||||
println!("│ ✔ Sanctum Dual-Vault Container initialisiert! │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||
println!(" • Alibi-Datei: {} ({})", carrier_name, carrier_size_str);
|
||||
println!(" • Carrier-Datei:{} ({})", carrier_name, carrier_size_str);
|
||||
println!(" • KDF: Argon2id pro Slot (M=64MB, T=3, P=4)");
|
||||
println!(" • Steganografie:Hidden Vault verbirgt sich vollständig in der Trägerdatei");
|
||||
println!(" • Kapselung: Hidden Vault liegt in Carrier-Datei im Decoy-Vault");
|
||||
println!(" • Accounting: 100% aller Chunks authentifizieren fehlerfrei unter DEK_0");
|
||||
println!(" • Dateigröße: Feste Containergröße — kein dynamisches Wachstum auf Festplatte");
|
||||
println!(" • Dateigröße: Feste Trägergröße zur Vermeidung von Größenveränderungen");
|
||||
println!();
|
||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||||
@@ -467,10 +513,7 @@ fn handle_init(
|
||||
rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
if password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&password)?;
|
||||
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Master-Passwort bestätigen: ")
|
||||
@@ -517,7 +560,7 @@ fn handle_init(
|
||||
println!(" • Format: Version {} (Magic: SANCTUM\\0)", FORMAT_VERSION);
|
||||
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||
println!(" • Cipher: AES-256-GCM + LZ4-Kompression (1-MB Chunks, AEAD)");
|
||||
println!(" • Deniability: Slot 1 mit CSPRNG-Zufallsrauschen gefüllt");
|
||||
println!(" • Dual-Vault: Slot 1 mit Zufallsrauschen initialisiert (inaktiv)");
|
||||
println!();
|
||||
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\"", container_path.display())));
|
||||
@@ -592,16 +635,26 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
let (dek, target_slot_id, carrier_dek, carrier_node_id) = if let Some(phrase) = recovery_key {
|
||||
let actual_phrase = if phrase.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(phrase.to_string())
|
||||
};
|
||||
let (dek, target_slot_id, carrier_dek, carrier_node_id) = if let Some(actual_phrase) = resolve_recovery_key(recovery_key)? {
|
||||
ui::step(1, 3, "🔑", "Lese DEK aus 24-Wort Notfallschlüssel...");
|
||||
let d = mnemonic_to_dek(&actual_phrase).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
ui::step(2, 3, "🔓", "Notfallschlüssel verifiziert!");
|
||||
(d, 0, None, None)
|
||||
|
||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||
let detected_slot = detect_recovery_key_slot(&db, &d, meta.version).unwrap_or(0);
|
||||
if detected_slot == 1 {
|
||||
println!(" Notfallschlüssel gehört zu Slot 1 (Hidden Vault).");
|
||||
println!(" Zur Neuverpackung von Slot 1 wird das Passwort des Decoy-Tresors (Slot 0) benötigt.");
|
||||
let decoy_pw = Zeroizing::new(
|
||||
rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ")
|
||||
.context("Fehler beim Einlesen des Decoy-Passworts")?
|
||||
);
|
||||
let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen."))?;
|
||||
let c_dek = decoy_auth.dek().clone();
|
||||
(d, 1, Some(c_dek), db.find_carrier_node_id()?)
|
||||
} else {
|
||||
(d, 0, None, db.find_carrier_node_id()?)
|
||||
}
|
||||
} else {
|
||||
let old_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Aktuelles Master-Passwort eingeben: ")
|
||||
@@ -632,10 +685,7 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
rpassword::prompt_password("Neues Master-Passwort eingeben: ")
|
||||
.context("Fehler beim Einlesen des neuen Passworts")?,
|
||||
);
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das neue Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&new_password)?;
|
||||
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
@@ -654,7 +704,7 @@ fn handle_passwd(container_path: &Path, recovery_key: Option<&str>) -> Result<()
|
||||
.context("KDF-Schlüsselableitung mit neuem Passwort fehlgeschlagen")?;
|
||||
|
||||
let (new_wrapped_dek, new_nonce, new_tag) = if target_slot_id == 1 {
|
||||
let c_dek = carrier_dek.unwrap_or_else(generate_dek);
|
||||
let c_dek = carrier_dek.ok_or_else(|| anyhow::anyhow!("DEK_0 (Träger-Schlüssel) für Slot 1 fehlt"))?;
|
||||
let c_nid = carrier_node_id.unwrap_or(0);
|
||||
wrap_slot1_payload(&new_kek, &dek, &c_dek, c_nid)
|
||||
.context("Slot-1 Wrapping mit neuem KEK fehlgeschlagen")?
|
||||
@@ -810,12 +860,8 @@ fn handle_restore_header(
|
||||
println!();
|
||||
println!("{}", ui::green("✔ Header erfolgreich aus Sicherungsdatei wiederhergestellt!"));
|
||||
} else {
|
||||
let key_str = if let Some(key) = recovery_key {
|
||||
if key.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(key.to_string())
|
||||
}
|
||||
let key_str = if let Some(key) = resolve_recovery_key(recovery_key)? {
|
||||
key
|
||||
} else {
|
||||
// Interaktiver Auswahldialog statt Fehlermeldung
|
||||
println!(" Keine Wiederherstellungsquelle als CLI-Argument angegeben.");
|
||||
@@ -840,15 +886,21 @@ fn handle_restore_header(
|
||||
}
|
||||
};
|
||||
|
||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", slot);
|
||||
let dek = mnemonic_to_dek(&key_str).context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
|
||||
let effective_slot = if slot == 0 {
|
||||
detect_recovery_key_slot(&db, &dek, FORMAT_VERSION).unwrap_or(0)
|
||||
} else {
|
||||
slot
|
||||
};
|
||||
|
||||
println!(" Verwende 24-Wort BIP-39 Notfallschlüssel (Ziel-Slot {})...", effective_slot);
|
||||
let new_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort festlegen: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?,
|
||||
);
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(&new_password)?;
|
||||
|
||||
let confirm_password = Zeroizing::new(
|
||||
rpassword::prompt_password("Neues Master-Passwort bestätigen: ")
|
||||
@@ -859,9 +911,24 @@ fn handle_restore_header(
|
||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||
}
|
||||
|
||||
let dek_0_holder: Option<[u8; 32]> = if effective_slot == 1 {
|
||||
println!(" Wiederherstellung von Slot 1 (Hidden Vault):");
|
||||
println!(" Zur mathematischen Rekonstruktion wird das Passwort des Decoy-Tresors (Slot 0) benötigt.");
|
||||
let decoy_pw = Zeroizing::new(
|
||||
rpassword::prompt_password("Passwort für Decoy-Tresor (Slot 0): ")
|
||||
.context("Fehler beim Einlesen des Decoy-Passworts")?
|
||||
);
|
||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||
let decoy_auth = meta.authenticate(&decoy_pw).ok_or_else(|| anyhow::anyhow!("Ungültiges Decoy-Passwort! Authentifizierung fehlgeschlagen."))?;
|
||||
Some(**decoy_auth.dek())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let dek_0_opt = dek_0_holder.as_ref();
|
||||
|
||||
ui::step(1, 2, "🔑", "Dekodiere DEK & leite neuen KEK ab...");
|
||||
restore_slot_from_recovery_key(container_path, &key_str, &new_password, slot)?;
|
||||
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", slot));
|
||||
restore_slot_from_recovery_key(container_path, &key_str, &new_password, effective_slot, dek_0_opt)?;
|
||||
ui::step(2, 2, "💾", &format!("Header für Slot {} mit neuem Passwort neu synthetisiert!", effective_slot));
|
||||
println!();
|
||||
println!("{}", ui::green("✔ Container-Header via Notfallschlüssel erfolgreich rekonstruiert!"));
|
||||
}
|
||||
@@ -962,18 +1029,8 @@ fn handle_sync(
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
let auth = if let Some(key_arg) = recovery_key {
|
||||
let phrase_str = if key_arg.trim().is_empty() {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ 🔑 Maskierte interaktive Notfallschlüssel-Eingabe │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
rpassword::prompt_password("24-Wort Notfall-Wiederherstellungsschlüssel: ")
|
||||
.context("Fehler beim Einlesen des Schlüssels")?
|
||||
} else {
|
||||
key_arg.to_string()
|
||||
};
|
||||
ContainerAuth::RecoveryKey(Zeroizing::new(phrase_str))
|
||||
let auth = if let Some(phrase_str) = resolve_recovery_key(recovery_key)? {
|
||||
ContainerAuth::RecoveryKey(phrase_str)
|
||||
} else {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Ordner- & Dateisynchronisation (Sync) │");
|
||||
@@ -999,11 +1056,7 @@ fn handle_sync(
|
||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||
let dek = mnemonic_to_dek(phrase)
|
||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
let is_hidden = {
|
||||
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||
!children.is_empty()
|
||||
};
|
||||
let v_id = if is_hidden { 1 } else { 0 };
|
||||
let v_id = detect_recovery_key_slot(&db, &dek, meta.version).unwrap_or(0);
|
||||
(dek, meta.version, v_id)
|
||||
}
|
||||
};
|
||||
@@ -1102,6 +1155,10 @@ fn handle_sync(
|
||||
async fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.legacy_names {
|
||||
set_allow_legacy_names(true);
|
||||
}
|
||||
|
||||
match cli.command {
|
||||
Commands::Init {
|
||||
path,
|
||||
@@ -1154,12 +1211,7 @@ async fn run() -> Result<()> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let auth = if let Some(key) = recovery_key {
|
||||
let actual_key = if key.trim().is_empty() {
|
||||
prompt_recovery_key_interactive()?
|
||||
} else {
|
||||
Zeroizing::new(key)
|
||||
};
|
||||
let auth = if let Some(actual_key) = resolve_recovery_key(recovery_key.as_deref())? {
|
||||
ContainerAuth::RecoveryKey(actual_key)
|
||||
} else {
|
||||
let prompt_text = format!(
|
||||
@@ -1266,12 +1318,13 @@ async fn run() -> Result<()> {
|
||||
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
||||
println!();
|
||||
}
|
||||
Commands::Upgrade { check, yes, force, url } => {
|
||||
Commands::Upgrade { check, yes, force, url, insecure_url } => {
|
||||
let options = sanctum::upgrade::UpgradeOptions {
|
||||
check_only: check,
|
||||
yes,
|
||||
force,
|
||||
base_url: url,
|
||||
insecure_url,
|
||||
};
|
||||
sanctum::upgrade::run_upgrade(&options)?;
|
||||
}
|
||||
|
||||
+76
-14
@@ -209,13 +209,27 @@ pub async fn mount_container(
|
||||
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
|
||||
}
|
||||
|
||||
// Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann
|
||||
let is_hidden = {
|
||||
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||
!children.is_empty()
|
||||
let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0);
|
||||
let (carrier_dek, final_carrier_node_id, vault_id) = if carrier_node_id > 0 {
|
||||
let slot_id = crate::recovery::detect_recovery_key_slot(&db, &dek, meta.version)?;
|
||||
if slot_id == 1 {
|
||||
if !stealth {
|
||||
println!(" [i] Der angegebene Notfallschlüssel gehört zum Hidden-Vault (Slot 1).");
|
||||
println!(" Für den Zugriff auf die Trägerdatei wird das Passwort des Standard-Vaults benötigt.");
|
||||
}
|
||||
let decoy_pass = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
|
||||
.context("Fehler beim Einlesen des Standard-Vault Passworts")?;
|
||||
let decoy_keys = meta.authenticate(&decoy_pass)
|
||||
.ok_or_else(|| anyhow::anyhow!("Ungültiges Passwort für Standard-Vault."))?;
|
||||
(Some(decoy_keys.dek().clone()), Some(carrier_node_id), 1)
|
||||
} else {
|
||||
(None, Some(carrier_node_id), 0)
|
||||
}
|
||||
} else {
|
||||
(None, None, 0)
|
||||
};
|
||||
let vault_id = if is_hidden { 1 } else { 0 };
|
||||
(dek, None, None, meta.version, vault_id)
|
||||
|
||||
(dek, carrier_dek, final_carrier_node_id, meta.version, vault_id)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -402,12 +416,12 @@ pub async fn mount_container(
|
||||
println!(" • Modus: WebDAV Userland-VFS");
|
||||
}
|
||||
}
|
||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
|
||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/<session-token>/ (lokal geschützt)", bound_port);
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/{}/", bound_port, session_token);
|
||||
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/<session-token>/", bound_port);
|
||||
if let Some(mp) = mount_point {
|
||||
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/{}/ {}", bound_port, session_token, mp.display());
|
||||
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/<session-token>/ {}", bound_port, mp.display());
|
||||
}
|
||||
}
|
||||
if let Some(secs) = idle_timeout {
|
||||
@@ -602,15 +616,63 @@ pub async fn mount_container(
|
||||
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔"));
|
||||
println!();
|
||||
}
|
||||
|
||||
drop(db);
|
||||
// S-10: Verifikation und Bereinigung von temporären SQLite WAL- und SHM-Dateien
|
||||
let base_os = container_path.as_os_str().to_os_string();
|
||||
let mut wal = base_os.clone();
|
||||
wal.push("-wal");
|
||||
let _ = std::fs::remove_file(wal);
|
||||
let mut shm = base_os;
|
||||
shm.push("-shm");
|
||||
let _ = std::fs::remove_file(shm);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Überprüft, ob ein Host-Header auf Loopback (127.0.0.1, localhost, [::1]) zeigt (Anti-DNS-Rebinding).
|
||||
/// Überprüft, ob ein Host-Header exakt auf Loopback (127.0.0.1, localhost, [::1], ::1) zeigt (Anti-DNS-Rebinding & S-06).
|
||||
/// Striktes Parsing von Host und Port ohne anfällige starts_with-Präfixprüfungen.
|
||||
pub fn is_loopback_host(host_str: &str) -> bool {
|
||||
let host_lower = host_str.to_ascii_lowercase();
|
||||
host_lower.starts_with("127.0.0.1")
|
||||
|| host_lower.starts_with("localhost")
|
||||
|| host_lower.starts_with("[::1]")
|
||||
let host_trimmed = host_str.trim().to_ascii_lowercase();
|
||||
if host_trimmed.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if host_trimmed == "::1" {
|
||||
return true;
|
||||
}
|
||||
|
||||
let host_part = if host_trimmed.starts_with('[') {
|
||||
// IPv6 bracketed: [::1] oder [::1]:port
|
||||
if let Some(bracket_end) = host_trimmed.find(']') {
|
||||
let inside = &host_trimmed[1..bracket_end];
|
||||
let after = &host_trimmed[bracket_end + 1..];
|
||||
if !after.is_empty() {
|
||||
if !after.starts_with(':') {
|
||||
return false;
|
||||
}
|
||||
if after[1..].parse::<u16>().is_err() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
inside
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// IPv4 oder Hostname: localhost, localhost:8443, 127.0.0.1, 127.0.0.1:8443
|
||||
if let Some((h, p)) = host_trimmed.rsplit_once(':') {
|
||||
if p.parse::<u16>().is_ok() {
|
||||
h
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
&host_trimmed
|
||||
}
|
||||
};
|
||||
|
||||
matches!(host_part, "127.0.0.1" | "localhost" | "::1")
|
||||
}
|
||||
|
||||
/// Maximale Anzahl gleichzeitiger Verbindungen zum lokalen WebDAV-Endpunkt (Schutz gegen Socket-Exhaustion).
|
||||
|
||||
+38
-13
@@ -6,8 +6,8 @@ use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::{
|
||||
derive_kek, generate_dummy_slot, mnemonic_to_dek, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, FORMAT_VERSION,
|
||||
decrypt_chunk, derive_kek, generate_dummy_slot, mnemonic_to_dek, validate_password,
|
||||
wrap_slot0_payload, wrap_slot1_payload, KdfParams, FORMAT_VERSION,
|
||||
};
|
||||
use crate::storage::{ContainerMeta, Database, SlotMeta};
|
||||
|
||||
@@ -222,6 +222,30 @@ pub fn restore_header_backup(container_path: &Path, backup_path: &Path) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ermittelt automatisch, ob ein Notfallschlüssel zu Slot 0 (Standard/Decoy) oder Slot 1 (Hidden Vault) gehört (S-05).
|
||||
pub fn detect_recovery_key_slot(db: &Database, dek: &[u8; 32], version: u32) -> Result<u32> {
|
||||
if let Ok(Some(carrier_id)) = db.find_carrier_node_id() {
|
||||
if let Ok(Some(chunk0)) = db.read_chunk(carrier_id, 0) {
|
||||
let is_dek0 = decrypt_chunk(
|
||||
dek,
|
||||
carrier_id,
|
||||
0,
|
||||
&chunk0.ciphertext,
|
||||
&chunk0.nonce,
|
||||
&chunk0.tag,
|
||||
version,
|
||||
)
|
||||
.is_ok();
|
||||
if is_dek0 {
|
||||
return Ok(0);
|
||||
} else {
|
||||
return Ok(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Rekonstruiert einen Slot des Container-Headers mithilfe des 24-Wort BIP-39 Notfallschlüssels
|
||||
/// und initialisiert ein neues Master-Passwort für den entsprechenden Slot (0 = Decoy, 1 = Hidden Vault).
|
||||
pub fn restore_slot_from_recovery_key(
|
||||
@@ -229,14 +253,13 @@ pub fn restore_slot_from_recovery_key(
|
||||
recovery_key: &str,
|
||||
new_password: &str,
|
||||
target_slot_id: u32,
|
||||
dek_0: Option<&[u8; 32]>,
|
||||
) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
if new_password.trim().is_empty() {
|
||||
bail!("Das neue Master-Passwort darf nicht leer sein.");
|
||||
}
|
||||
validate_password(new_password)?;
|
||||
|
||||
// 1. DEK aus 24-Wort-Phrase dekodieren & validieren
|
||||
let dek = mnemonic_to_dek(recovery_key)
|
||||
@@ -258,9 +281,11 @@ pub fn restore_slot_from_recovery_key(
|
||||
let mut existing_slots = db.read_slots().unwrap_or_default();
|
||||
|
||||
let new_slot = if target_slot_id == 1 {
|
||||
// Für Slot 1 wird DEK_0 benötigt (z. B. aus vorhandenem Slot 0)
|
||||
let dek_0_bytes = [0u8; 32];
|
||||
let (wrapped, nonce, tag) = wrap_slot1_payload(&kek, &dek, &dek_0_bytes, carrier_node_id)?;
|
||||
// Für Slot 1 wird der echte DEK_0 benötigt (S-05)
|
||||
let dek_0_bytes = dek_0.ok_or_else(|| {
|
||||
anyhow::anyhow!("Für die Wiederherstellung von Slot 1 (Hidden Vault) wird der DEK von Slot 0 (Standard-Vault) benötigt.")
|
||||
})?;
|
||||
let (wrapped, nonce, tag) = wrap_slot1_payload(&kek, &dek, dek_0_bytes, carrier_node_id)?;
|
||||
SlotMeta {
|
||||
slot_id: 1,
|
||||
version: FORMAT_VERSION,
|
||||
@@ -333,7 +358,7 @@ pub fn restore_header_from_recovery_key(
|
||||
recovery_key: &str,
|
||||
new_password: &str,
|
||||
) -> Result<()> {
|
||||
restore_slot_from_recovery_key(container_path, recovery_key, new_password, 0)
|
||||
restore_slot_from_recovery_key(container_path, recovery_key, new_password, 0, None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -358,8 +383,8 @@ mod tests {
|
||||
let password = "SuperSecretPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: crate::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: crate::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
@@ -413,8 +438,8 @@ mod tests {
|
||||
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: crate::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: crate::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(old_password, &salt, &kdf_params).unwrap();
|
||||
|
||||
+199
-16
@@ -9,7 +9,8 @@ use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::crypto::{
|
||||
decrypt_node_name, derive_kek, encrypt_node_name, generate_dummy_slot, unwrap_key_payload,
|
||||
KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1, FORMAT_VERSION_V2, MAGIC_BYTES,
|
||||
validate_kdf_params, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1, FORMAT_VERSION_V2,
|
||||
MAGIC_BYTES,
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -177,11 +178,79 @@ impl Database {
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
// S-04: Untrusted Container & Fremddatei-Schutz.
|
||||
// Bevor Pragmas, VACUUM oder Schema-Upgrades ausgeführt werden:
|
||||
// Wenn Tabellen vorhanden sind, MUSS die 'meta'-Tabelle existieren und die Sanctum-Magic stimmen.
|
||||
let table_count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
if table_count > 0 {
|
||||
let has_sanctum_schema: bool = {
|
||||
let has_meta: bool = conn
|
||||
.query_row(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='meta' LIMIT 1;",
|
||||
[],
|
||||
|_| Ok(()),
|
||||
)
|
||||
.optional()?
|
||||
.is_some();
|
||||
|
||||
if !has_meta {
|
||||
false
|
||||
} else {
|
||||
let magic_opt: Option<Option<Vec<u8>>> = conn
|
||||
.query_row("SELECT magic FROM meta LIMIT 1;", [], |r| r.get(0))
|
||||
.optional()?;
|
||||
match magic_opt {
|
||||
Some(Some(magic)) => magic.as_slice() == MAGIC_BYTES.as_slice(),
|
||||
Some(None) => false,
|
||||
None => {
|
||||
// Die meta-Tabelle existiert, ist aber leer (z. B. zerstörter Header während einer Wiederherstellung).
|
||||
// Prüfe, ob die typische Sanctum-Tabellenstruktur (nodes, chunks) vorhanden ist,
|
||||
// um Fremddatenbanken weiterhin strikt abzuweisen.
|
||||
let has_nodes: bool = conn
|
||||
.query_row(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='nodes' LIMIT 1;",
|
||||
[],
|
||||
|_| Ok(()),
|
||||
)
|
||||
.optional()?
|
||||
.is_some();
|
||||
let has_chunks: bool = conn
|
||||
.query_row(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='chunks' LIMIT 1;",
|
||||
[],
|
||||
|_| Ok(()),
|
||||
)
|
||||
.optional()?
|
||||
.is_some();
|
||||
has_nodes && has_chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !has_sanctum_schema {
|
||||
bail!(
|
||||
"Die angegebene Datei '{}' ist kein gültiger Sanctum-Container (ungültige Signatur oder Metadaten fehlen). Datei wurde nicht modifiziert.",
|
||||
path_ref.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
};
|
||||
db.init_pragmas()?;
|
||||
db.ensure_schema_upgrades()?;
|
||||
if table_count > 0 {
|
||||
db.ensure_schema_upgrades()?;
|
||||
}
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
@@ -204,10 +273,16 @@ impl Database {
|
||||
/// Authentifiziert ein Master-Passwort gegen den Container in konstanter Zeit.
|
||||
pub fn authenticate_password(&self, password: &str) -> Result<Option<UnlockedKeys>> {
|
||||
let meta = self.read_meta()?;
|
||||
Ok(meta.authenticate(password))
|
||||
let res = meta.authenticate(password);
|
||||
if let Some(ref keys) = res {
|
||||
if let Some(cid) = keys.carrier_node_id() {
|
||||
let _ = self.mark_carrier_node_id(cid);
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Führt automatische, rückwärtskompatible Schema-Upgrades (z. B. Spalte slot_id, auto_vacuum) durch.
|
||||
/// Führt automatische, rückwärtskompatible Schema-Upgrades (z. B. Spalte slot_id, auto_vacuum, is_carrier) durch.
|
||||
pub fn ensure_schema_upgrades(&self) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let av: i64 = conn.query_row("PRAGMA auto_vacuum;", [], |r| r.get(0)).unwrap_or(0);
|
||||
@@ -219,6 +294,9 @@ impl Database {
|
||||
// Spalte slot_id in meta (falls aus v1 migriert)
|
||||
let _ = conn.execute("ALTER TABLE meta ADD COLUMN slot_id INTEGER NOT NULL DEFAULT 0", []);
|
||||
|
||||
// Spalte is_carrier in nodes (S-03 Carrier-Schutz)
|
||||
let _ = conn.execute("ALTER TABLE nodes ADD COLUMN is_carrier INTEGER NOT NULL DEFAULT 0", []);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -271,16 +349,23 @@ impl Database {
|
||||
Ok(count as usize)
|
||||
}
|
||||
|
||||
/// Sucht nach einem existierenden Carrier-Knoten im Decoy-Wurzelverzeichnis (parent_id = 1, is_dir = 0).
|
||||
/// Sucht nach einem existierenden Carrier-Knoten (is_carrier = 1) (S-03).
|
||||
pub fn find_carrier_node_id(&self) -> Result<Option<i64>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id FROM nodes WHERE parent_id = 1 AND is_dir = 0 ORDER BY id ASC LIMIT 1",
|
||||
"SELECT id FROM nodes WHERE is_carrier = 1 LIMIT 1",
|
||||
)?;
|
||||
let id = stmt.query_row([], |r| r.get::<_, i64>(0)).optional()?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Markiert einen existierenden Knoten explizit als Carrier.
|
||||
pub fn mark_carrier_node_id(&self, id: i64) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("UPDATE nodes SET is_carrier = 1 WHERE id = ?1", [id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft, ob ein Knoten (z. B. der Carrier-Knoten) ein Nachfahre (direkt oder indirekt) eines Verzeichnisses ist.
|
||||
pub fn is_descendant_of(&self, node_id: i64, ancestor_id: i64) -> Result<bool> {
|
||||
if node_id == ancestor_id {
|
||||
@@ -374,6 +459,7 @@ impl Database {
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
modified_at INTEGER NOT NULL,
|
||||
is_carrier INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(parent_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_parent_name ON nodes(parent_id, name) WHERE parent_id IS NOT NULL;
|
||||
@@ -430,10 +516,10 @@ impl Database {
|
||||
dek_1,
|
||||
)) = carrier_config
|
||||
{
|
||||
// Trägerdatei in nodes (parent_id = 1, Decoy Root) anlegen
|
||||
// Trägerdatei in nodes (parent_id = 1, Decoy Root) anlegen (is_carrier = 1)
|
||||
conn.execute(
|
||||
"INSERT INTO nodes (parent_id, name, is_dir, size, created_at, modified_at)
|
||||
VALUES (1, ?1, 0, ?2, ?3, ?4)",
|
||||
"INSERT INTO nodes (parent_id, name, is_dir, size, created_at, modified_at, is_carrier)
|
||||
VALUES (1, ?1, 0, ?2, ?3, ?4, 1)",
|
||||
params![c_name, c_size as i64, now, now],
|
||||
)?;
|
||||
let c_id = conn.last_insert_rowid();
|
||||
@@ -589,6 +675,7 @@ impl Database {
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
modified_at INTEGER NOT NULL,
|
||||
is_carrier INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(parent_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_parent_name ON nodes(parent_id, name) WHERE parent_id IS NOT NULL;
|
||||
@@ -701,19 +788,88 @@ impl Database {
|
||||
let tag_vec: Vec<u8> = row.get(6)?;
|
||||
|
||||
let mut kdf_salt = [0u8; 16];
|
||||
if salt_vec.len() == 16 {
|
||||
kdf_salt.copy_from_slice(&salt_vec);
|
||||
if salt_vec.len() != 16 {
|
||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
||||
2,
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Ungültige Salt-Länge in Slot {}: {} Bytes (erwartet: 16)", slot_id, salt_vec.len()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
kdf_salt.copy_from_slice(&salt_vec);
|
||||
|
||||
let mut header_nonce = [0u8; 12];
|
||||
if nonce_vec.len() == 12 {
|
||||
header_nonce.copy_from_slice(&nonce_vec);
|
||||
if nonce_vec.len() != 12 {
|
||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
||||
5,
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Ungültige Nonce-Länge in Slot {}: {} Bytes (erwartet: 12)", slot_id, nonce_vec.len()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
header_nonce.copy_from_slice(&nonce_vec);
|
||||
|
||||
let mut header_tag = [0u8; 16];
|
||||
if tag_vec.len() == 16 {
|
||||
header_tag.copy_from_slice(&tag_vec);
|
||||
if tag_vec.len() != 16 {
|
||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
||||
6,
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Ungültige Tag-Länge in Slot {}: {} Bytes (erwartet: 16)", slot_id, tag_vec.len()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
header_tag.copy_from_slice(&tag_vec);
|
||||
|
||||
// Strikte Validierung der wrapped_dek Länge (DoS- und Manipulationsschutz)
|
||||
if slot_id == 0 {
|
||||
if wrapped_dek.len() != 40 && wrapped_dek.len() != 32 {
|
||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
||||
4,
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Ungültige wrapped_dek-Länge in Slot 0: {} Bytes (erwartet: 40 oder 32)", wrapped_dek.len()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
} else if slot_id == 1 {
|
||||
if wrapped_dek.len() != 72 && wrapped_dek.len() != 64 && wrapped_dek.len() != 32 {
|
||||
return Err(rusqlite::Error::FromSqlConversionFailure(
|
||||
4,
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Ungültige wrapped_dek-Länge in Slot 1: {} Bytes (erwartet: 72, 64 oder 32)", wrapped_dek.len()),
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let kdf_params: KdfParams = serde_json::from_str(¶ms_str).unwrap_or_default();
|
||||
let kdf_params: KdfParams = serde_json::from_str(¶ms_str)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
3,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Ungültiges KdfParams-JSON in Slot {}: {e}", slot_id),
|
||||
)),
|
||||
))?;
|
||||
|
||||
validate_kdf_params(&kdf_params)
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
3,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("KDF-Parameter in Slot {} ungültig: {e}", slot_id),
|
||||
)),
|
||||
))?;
|
||||
|
||||
Ok(SlotMeta {
|
||||
slot_id,
|
||||
@@ -1083,8 +1239,32 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fail-Closed Carrier Guard: Verhindert, dass mutierende Dateioperationen
|
||||
/// (Löschen, Truncate, Umbenennen, VFS-Writes) versehentlich oder böswillig
|
||||
/// die Alibi-Trägerdatei im Decoy-Vault beschädigen oder zerstören (S-03).
|
||||
pub fn assert_not_carrier(&self, node_id: i64) -> Result<()> {
|
||||
if let Ok(Some(carrier_id)) = self.find_carrier_node_id() {
|
||||
if node_id == carrier_id {
|
||||
bail!(
|
||||
"Operation auf Alibi-Trägerdatei (Carrier, Node-ID {}) ist strikt untersagt (Carrier-Schutz).",
|
||||
node_id
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Löscht einen Knoten und shreddert alle assoziierten Chunks atomar.
|
||||
pub fn delete_node(&self, id: i64) -> Result<()> {
|
||||
self.assert_not_carrier(id)?;
|
||||
|
||||
// Verhindere auch das Löschen eines Verzeichnisses, das die Trägerdatei enthält
|
||||
if let Ok(Some(carrier_id)) = self.find_carrier_node_id() {
|
||||
if self.is_descendant_of(carrier_id, id).unwrap_or(false) {
|
||||
bail!("Verzeichnis enthält die Trägerdatei (Node-ID {}) und darf nicht gelöscht werden.", carrier_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Shredde Chunks dieses Knotens mit kryptografischem Zufallsrauschen
|
||||
let _ = self.shred_chunks_for_node(id);
|
||||
|
||||
@@ -1118,6 +1298,7 @@ impl Database {
|
||||
vault_id: u32,
|
||||
dek: &[u8; 32],
|
||||
) -> Result<()> {
|
||||
self.assert_not_carrier(id)?;
|
||||
let now = current_timestamp();
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let stored_name = if vault_id == 1 {
|
||||
@@ -1212,6 +1393,7 @@ impl Database {
|
||||
new_size: u64,
|
||||
modified_at: u64,
|
||||
) -> Result<()> {
|
||||
self.assert_not_carrier(node_id)?;
|
||||
let mut conn = self.conn.lock().unwrap();
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute(
|
||||
@@ -1240,6 +1422,7 @@ impl Database {
|
||||
/// Schneidet überzählige Chunks ab (z. B. beim Truncate oder Überschreiben mit kleinerer Datei)
|
||||
/// und shreddert die abzuschneidenden Chunks vorher mit kryptografischem Zufallsrauschen.
|
||||
pub fn truncate_chunks_after(&self, node_id: i64, max_chunk_index: u32) -> Result<()> {
|
||||
self.assert_not_carrier(node_id)?;
|
||||
let mut conn = self.conn.lock().unwrap();
|
||||
let tx = conn.transaction()?;
|
||||
{
|
||||
|
||||
+144
-2
@@ -6,11 +6,103 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
||||
use crate::storage::{Database, NodeRecord};
|
||||
use crate::ui;
|
||||
use crate::vfs::is_leak_file;
|
||||
|
||||
/// Validiert Knotennamen gegen Path-Traversal (CWE-22) und reservierte Windows-Gerätenamen (S-08).
|
||||
pub fn validate_node_name(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("Dateiname darf nicht leer sein.");
|
||||
}
|
||||
if name == "." || name == ".." {
|
||||
bail!("Ungültiger Dateiname (Verzeichnisreferenz verboten): '{}'", name);
|
||||
}
|
||||
if name.contains('/') || name.contains('\\') || name.contains('\0') {
|
||||
bail!(
|
||||
"Dateiname enthält unzulässige Trennzeichen oder Null-Bytes: '{}'",
|
||||
name
|
||||
);
|
||||
}
|
||||
for c in name.chars() {
|
||||
if (c as u32) < 0x20 {
|
||||
bail!("Dateiname enthält Steuerzeichen: '{}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
// Windows reservierte Gerätenamen (CON, PRN, AUX, NUL, COM1..9, LPT1..9)
|
||||
let base_name = if let Some(dot_idx) = name.find('.') {
|
||||
&name[..dot_idx]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let base_upper = base_name.to_ascii_uppercase();
|
||||
let reserved = [
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
];
|
||||
if reserved.contains(&base_upper.as_str()) {
|
||||
bail!(
|
||||
"Dateiname '{}' kollidiert mit einem reservierten Windows-Gerätenamen.",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Berechnet den SHA-256 Hash einer lokalen Datei für verlässliche Checksummen-Vergleiche (S-10).
|
||||
fn calc_local_file_sha256(path: &Path) -> Result<String> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0u8; 64 * 1024];
|
||||
loop {
|
||||
let n = file.read(&mut buffer)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Berechnet den SHA-256 Hash der entschlüsselten Nutzdaten eines Vault-Knotens (S-10).
|
||||
fn calc_vault_node_sha256(
|
||||
db: &Database,
|
||||
node: &NodeRecord,
|
||||
dek: &[u8; 32],
|
||||
version: u32,
|
||||
) -> Result<String> {
|
||||
let mut hasher = Sha256::new();
|
||||
let total_chunks = if node.size == 0 {
|
||||
0
|
||||
} else {
|
||||
((node.size - 1) / CHUNK_SIZE as u64 + 1) as u32
|
||||
};
|
||||
|
||||
for idx in 0..total_chunks {
|
||||
let record = db
|
||||
.read_chunk(node.id, idx)?
|
||||
.ok_or_else(|| anyhow::anyhow!("Fehlender Chunk {} für Knoten {}", idx, node.id))?;
|
||||
|
||||
let decrypted = decrypt_chunk(
|
||||
dek,
|
||||
node.id,
|
||||
idx,
|
||||
&record.ciphertext,
|
||||
&record.nonce,
|
||||
&record.tag,
|
||||
version,
|
||||
)?;
|
||||
hasher.update(&decrypted);
|
||||
}
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Synchronisationsrichtung
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SyncDirection {
|
||||
@@ -180,6 +272,8 @@ pub fn sync_single_file_to_vault(
|
||||
checksum: bool,
|
||||
dry_run: bool,
|
||||
) -> Result<FileTransferResult> {
|
||||
validate_node_name(file_name)?;
|
||||
|
||||
let meta = fs::metadata(local_path)
|
||||
.with_context(|| format!("Konnte Metadaten für '{}' nicht lesen", local_path.display()))?;
|
||||
let local_size = meta.len();
|
||||
@@ -198,10 +292,22 @@ pub fn sync_single_file_to_vault(
|
||||
bail!("Pfad-Konflikt: '{}' existiert im Tresor als Ordner", file_name);
|
||||
}
|
||||
|
||||
// Fast Check: Wenn Größe und mtime identisch sind, überspringen
|
||||
// Fast Check: Wenn Größe und mtime identisch sind, überspringen (ohne --checksum)
|
||||
if !checksum && node.size == local_size && node.modified_at == local_mtime {
|
||||
return Ok(FileTransferResult::Skipped { size: local_size });
|
||||
}
|
||||
|
||||
// Deep Check mit --checksum: Echter kryptografischer SHA-256 Hashvergleich (S-10)
|
||||
if checksum && node.size == local_size {
|
||||
if let (Ok(local_hash), Ok(vault_hash)) = (
|
||||
calc_local_file_sha256(local_path),
|
||||
calc_vault_node_sha256(db, node, dek, version),
|
||||
) {
|
||||
if local_hash == vault_hash {
|
||||
return Ok(FileTransferResult::Skipped { size: local_size });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
@@ -264,6 +370,8 @@ pub fn sync_single_file_to_host(
|
||||
checksum: bool,
|
||||
dry_run: bool,
|
||||
) -> Result<FileTransferResult> {
|
||||
validate_node_name(&node.name)?;
|
||||
|
||||
if local_path.exists() {
|
||||
if let Ok(meta) = fs::metadata(local_path) {
|
||||
let local_size = meta.len();
|
||||
@@ -277,6 +385,18 @@ pub fn sync_single_file_to_host(
|
||||
if !checksum && local_size == node.size && local_mtime == node.modified_at {
|
||||
return Ok(FileTransferResult::Skipped { size: node.size });
|
||||
}
|
||||
|
||||
// Deep Check mit --checksum: Echter kryptografischer SHA-256 Hashvergleich (S-10)
|
||||
if checksum && local_size == node.size {
|
||||
if let (Ok(local_hash), Ok(vault_hash)) = (
|
||||
calc_local_file_sha256(local_path),
|
||||
calc_vault_node_sha256(db, node, dek, version),
|
||||
) {
|
||||
if local_hash == vault_hash {
|
||||
return Ok(FileTransferResult::Skipped { size: node.size });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +577,7 @@ fn collect_and_push_dir(
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
validate_node_name(&file_name)?;
|
||||
|
||||
let rel_path = path
|
||||
.strip_prefix(base_dir)
|
||||
@@ -545,7 +666,7 @@ fn collect_and_push_dir(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_orphans_in_vault(
|
||||
pub fn delete_orphans_in_vault(
|
||||
db: &Database,
|
||||
vault_id: u32,
|
||||
dek: &[u8; 32],
|
||||
@@ -556,9 +677,15 @@ fn delete_orphans_in_vault(
|
||||
quiet: bool,
|
||||
stats: &mut SyncStats,
|
||||
) -> Result<()> {
|
||||
let carrier_id = db.find_carrier_node_id()?.unwrap_or(0);
|
||||
let children = db.list_children_in_vault(current_vault_id, vault_id, dek)?;
|
||||
|
||||
for child in children {
|
||||
// S-03 Carrier Guard: Trägerdatei und übergeordnete Verzeichnisse niemals löschen!
|
||||
if carrier_id > 0 && (child.id == carrier_id || db.is_descendant_of(carrier_id, child.id).unwrap_or(false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let child_rel = if prefix_rel.is_empty() {
|
||||
child.name.clone()
|
||||
} else {
|
||||
@@ -704,6 +831,8 @@ fn collect_and_pull_dir(
|
||||
let children = db.list_children_in_vault(vault_node_id, vault_id, dek)?;
|
||||
|
||||
for child in children {
|
||||
validate_node_name(&child.name)?;
|
||||
|
||||
let child_rel = if rel_prefix.is_empty() {
|
||||
child.name.clone()
|
||||
} else {
|
||||
@@ -714,9 +843,22 @@ fn collect_and_pull_dir(
|
||||
continue;
|
||||
}
|
||||
|
||||
// S-08 Path Traversal Guard:
|
||||
// Prüfe Komponenten des relativen Pfads und stelle sicher, dass der Pfad nicht ausbricht
|
||||
for comp in Path::new(&child_rel).components() {
|
||||
match comp {
|
||||
std::path::Component::Normal(_) => {}
|
||||
_ => bail!("Path traversal Versuch erkannt in relativem Pfad: '{}'", child_rel),
|
||||
}
|
||||
}
|
||||
|
||||
vault_relative_paths.insert(child_rel.clone());
|
||||
let local_child_path = local_target_base.join(&child_rel.replace('/', "\\"));
|
||||
|
||||
if !local_child_path.starts_with(local_target_base) {
|
||||
bail!("Path traversal Versuch erkannt: '{}' bricht aus Zielverzeichnis aus", child_rel);
|
||||
}
|
||||
|
||||
if child.is_dir {
|
||||
if !options.dry_run {
|
||||
fs::create_dir_all(&local_child_path)?;
|
||||
|
||||
+98
-29
@@ -1,4 +1,4 @@
|
||||
use std::fs::{self, File};
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
@@ -16,7 +16,10 @@ pub const TARGET_ASSET_NAME: &str = "sanctum.exe";
|
||||
#[cfg(not(windows))]
|
||||
pub const TARGET_ASSET_NAME: &str = "sanctum";
|
||||
|
||||
pub const OFFICIAL_UPGRADE_URL: &str = "https://gitea.pansi.eu";
|
||||
pub const CHECKSUM_ASSET_NAME: &str = "SHA256SUMS.txt";
|
||||
pub const CHECKSUM_SIG_ASSET_NAME: &str = "SHA256SUMS.txt.minisig";
|
||||
pub const SANCTUM_RELEASE_PUBKEY: &str = "RWSsVphPgr8157M9rTPkWDw3c0qIjc7xi28Gmw+cIWbMipOy4L6ToJEU";
|
||||
|
||||
/// Repräsentiert ein Asset in einem Gitea-Release
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -46,6 +49,7 @@ pub struct UpgradeOptions {
|
||||
pub yes: bool,
|
||||
pub force: bool,
|
||||
pub base_url: String,
|
||||
pub insecure_url: bool,
|
||||
}
|
||||
|
||||
impl Default for UpgradeOptions {
|
||||
@@ -54,11 +58,28 @@ impl Default for UpgradeOptions {
|
||||
check_only: false,
|
||||
yes: false,
|
||||
force: false,
|
||||
base_url: "https://gitea.pansi.eu".to_string(),
|
||||
base_url: OFFICIAL_UPGRADE_URL.to_string(),
|
||||
insecure_url: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Überprüft eine Minisign-Signatur für gegebene Binärdaten mit dem angegebenen Minisign Public Key (S-02).
|
||||
pub fn verify_minisign_signature(
|
||||
data: &[u8],
|
||||
sig_str: &str,
|
||||
pubkey_str: &str,
|
||||
) -> Result<()> {
|
||||
let pubkey = minisign_verify::PublicKey::from_base64(pubkey_str)
|
||||
.map_err(|e| anyhow::anyhow!("Ungültiger öffentlicher Minisign-Schlüssel: {e}"))?;
|
||||
let signature = minisign_verify::Signature::decode(sig_str)
|
||||
.map_err(|e| anyhow::anyhow!("Ungültiges Minisign-Signaturformat: {e}"))?;
|
||||
pubkey
|
||||
.verify(data, &signature, true)
|
||||
.map_err(|e| anyhow::anyhow!("Minisign-Signaturprüfung FEHLGESCHLAGEN: {e}. Das Release-Manifest ist nicht vertrauenswürdig!"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Erkannter Paketmanager
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PackageManager {
|
||||
@@ -198,19 +219,11 @@ pub fn download_and_verify_binary(
|
||||
|
||||
let mut reader = resp.into_reader();
|
||||
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let temp_file_path = temp_dir.join(format!(
|
||||
"sanctum_update_{}_{}",
|
||||
std::process::id(),
|
||||
TARGET_ASSET_NAME
|
||||
));
|
||||
|
||||
let mut temp_file = File::create(&temp_file_path).with_context(|| {
|
||||
format!(
|
||||
"Konnte temporäre Update-Datei '{}' nicht erstellen",
|
||||
temp_file_path.display()
|
||||
)
|
||||
})?;
|
||||
let mut temp_file = tempfile::Builder::new()
|
||||
.prefix("sanctum_update_")
|
||||
.suffix(if cfg!(windows) { ".exe" } else { "" })
|
||||
.tempfile()
|
||||
.context("Konnte temporäre Update-Datei nicht sicher erstellen")?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0u8; 64 * 1024]; // 64 KB Puffer
|
||||
@@ -229,6 +242,7 @@ pub fn download_and_verify_binary(
|
||||
}
|
||||
|
||||
temp_file
|
||||
.as_file_mut()
|
||||
.write_all(&buffer[..n])
|
||||
.context("Fehler beim Schreiben in die temporäre Datei")?;
|
||||
hasher.update(&buffer[..n]);
|
||||
@@ -254,16 +268,16 @@ pub fn download_and_verify_binary(
|
||||
println!();
|
||||
|
||||
temp_file
|
||||
.as_file_mut()
|
||||
.flush()
|
||||
.context("Fehler beim Flush der temporären Datei")?;
|
||||
drop(temp_file);
|
||||
|
||||
// SHA-256 Integritätsprüfung
|
||||
let calculated_hash = hex::encode(hasher.finalize()).to_lowercase();
|
||||
let expected_lower = expected_hash.to_lowercase();
|
||||
|
||||
if calculated_hash != expected_lower {
|
||||
let _ = fs::remove_file(&temp_file_path);
|
||||
// temp_file wird beim Drop automatisch gelöscht
|
||||
bail!(
|
||||
"Kryptografische Prüfsumme ungültig!\n Erwartet: {}\n Berechnet: {}\nDas Update wurde aus Sicherheitsgründen verworfen.",
|
||||
expected_lower,
|
||||
@@ -273,7 +287,11 @@ pub fn download_and_verify_binary(
|
||||
|
||||
println!(" • SHA-256: {} (verifiziert)", ui::green(&calculated_hash[..16]));
|
||||
|
||||
Ok(temp_file_path)
|
||||
let (_, path) = temp_file
|
||||
.keep()
|
||||
.context("Konnte temporäre Datei nicht für Ersetzung persistieren")?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Führt den In-Place-Austausch des laufenden Binaries durch.
|
||||
@@ -284,19 +302,18 @@ pub fn apply_upgrade(temp_binary: &Path) -> Result<PathBuf> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(temp_binary)
|
||||
.context("Konnte Metadaten der Update-Datei nicht lesen")?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(temp_binary, perms)
|
||||
.context("Konnte Ausführungsrechte (chmod +x / 0755) nicht setzen")?;
|
||||
if let Ok(meta) = fs::metadata(temp_binary) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = fs::set_permissions(temp_binary, perms);
|
||||
}
|
||||
}
|
||||
|
||||
self_replace::self_replace(temp_binary)
|
||||
.context("In-Place-Ersetzung der laufenden Anwendungsdatei fehlgeschlagen")?;
|
||||
|
||||
let replace_res = self_replace::self_replace(temp_binary);
|
||||
let _ = fs::remove_file(temp_binary);
|
||||
|
||||
replace_res.context("In-Place-Ersetzung der laufenden Anwendungsdatei fehlgeschlagen")?;
|
||||
|
||||
Ok(current_exe)
|
||||
}
|
||||
|
||||
@@ -311,6 +328,37 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
||||
println!(" • Server: {}", options.base_url);
|
||||
println!();
|
||||
|
||||
// S-02: Prüfung auf abweichende Server-URL
|
||||
let is_official_server = options.base_url.trim_end_matches('/') == OFFICIAL_UPGRADE_URL.trim_end_matches('/');
|
||||
if !is_official_server {
|
||||
if !options.insecure_url {
|
||||
bail!(
|
||||
"Sicherheitsverstoß: Abweichende Server-URL '{}' ist nicht vertrauenswürdig.\n\
|
||||
Aus Sicherheitsgründen sind Drittanbieter-URLs standardmäßig gesperrt.\n\
|
||||
Falls beabsichtigt, verwenden Sie das Flag '--insecure-url'.",
|
||||
options.base_url
|
||||
);
|
||||
}
|
||||
|
||||
// Auch mit --insecure-url ist zwingend eine manuelle Bestätigung erforderlich (nicht mit -y überschreibbar!)
|
||||
println!(
|
||||
" {} WARNUNG: Sie aktualisieren von einem inoffiziellen Server: {}",
|
||||
ui::red("[SICHERHEITSWARNUNG]"),
|
||||
options.base_url
|
||||
);
|
||||
println!(" Offizieller Server ist: {}", OFFICIAL_UPGRADE_URL);
|
||||
print!(" Möchten Sie dieses ungesicherte Update WIRKLICH fortsetzen? [Tippen Sie 'JA' zur Bestätigung]: ");
|
||||
let _ = std::io::stdout().flush();
|
||||
let mut confirm = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut confirm)
|
||||
.context("Fehler beim Einlesen der Bestätigung")?;
|
||||
if confirm.trim() != "JA" {
|
||||
bail!("Upgrade von Drittanbieter-URL durch Benutzer abgebrochen.");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// 1. Paketmanager-Prüfung
|
||||
if let Some(pm) = detect_package_manager() {
|
||||
println!(
|
||||
@@ -373,7 +421,7 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. Ziel-Asset und Checksumme identifizieren
|
||||
// 3. Ziel-Asset, Checksumme und Minisign-Signatur identifizieren
|
||||
let target_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
@@ -398,9 +446,30 @@ pub fn run_upgrade(options: &UpgradeOptions) -> Result<()> {
|
||||
)
|
||||
})?;
|
||||
|
||||
// 4. Prüfsummen herunterladen und extrahieren
|
||||
let sig_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name.eq_ignore_ascii_case(CHECKSUM_SIG_ASSET_NAME))
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Im Release '{}' wurde keine Minisign-Signatur ('{}') gefunden! Unsignierte Updates werden verweigert.",
|
||||
remote_tag,
|
||||
CHECKSUM_SIG_ASSET_NAME
|
||||
)
|
||||
})?;
|
||||
|
||||
// 4. Prüfsummen und Signatur herunterladen und kryptografisch prüfen (S-02)
|
||||
println!(" {} Lade Prüfsummen ({}) herunter ...", ui::dim("[-]"), CHECKSUM_ASSET_NAME);
|
||||
let sums_content = fetch_text(&checksum_asset.browser_download_url)?;
|
||||
|
||||
println!(" {} Lade Minisign-Signatur ({}) herunter ...", ui::dim("[-]"), CHECKSUM_SIG_ASSET_NAME);
|
||||
let sig_content = fetch_text(&sig_asset.browser_download_url)?;
|
||||
|
||||
println!(" {} Verifiziere Minisign-Signatur gegen eingebetteten Herstellerschlüssel ...", ui::dim("[-]"));
|
||||
verify_minisign_signature(sums_content.as_bytes(), &sig_content, SANCTUM_RELEASE_PUBKEY)
|
||||
.context("Minisign-Signaturprüfung für Prüfsummendatei FEHLGESCHLAGEN! Release ist nicht vertrauenswürdig.")?;
|
||||
println!(" • Signatur: {} (Minisign Ed25519)", ui::green("Gültig"));
|
||||
|
||||
let expected_hash = parse_checksum_for_asset(&sums_content, TARGET_ASSET_NAME).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Prüfsummendatei enthält keinen SHA-256 Eintrag für '{}'!",
|
||||
|
||||
+4
-4
@@ -310,8 +310,8 @@ mod tests {
|
||||
let password = "HealthyTestPassword123!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: crate::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: crate::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
@@ -362,8 +362,8 @@ mod tests {
|
||||
let password = "BitrotTestPassword123!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: crate::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: crate::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
|
||||
+3
-3
@@ -1052,11 +1052,11 @@ mod tests {
|
||||
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
memory_cost: crate::crypto::MIN_MEMORY_COST_KIB,
|
||||
time_cost: crate::crypto::MIN_TIME_COST,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek("testpwd", &salt, &kdf_params).unwrap();
|
||||
let kek = derive_kek("TestMasterPassword123!", &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag)
|
||||
|
||||
Reference in New Issue
Block a user