feat(security): implement Phase 2 Modell A (Steganografischer Alibi-Carrier für Plausible Deniability)
This commit is contained in:
+296
-8
@@ -8,8 +8,8 @@ use rand::RngCore;
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::crypto::{
|
||||
decrypt_node_name, derive_kek, encrypt_node_name, generate_dummy_slot, unwrap_dek, KdfParams,
|
||||
FORMAT_VERSION, FORMAT_VERSION_V1, FORMAT_VERSION_V2, MAGIC_BYTES,
|
||||
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,
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -46,6 +46,36 @@ pub struct SlotMeta {
|
||||
pub header_tag: [u8; 16],
|
||||
}
|
||||
|
||||
/// Ergebnis einer erfolgreichen Authentifizierung eines Container-Slots.
|
||||
/// Die Tupel-Struktur (0: DEK, 1: Version, 2: Slot-ID, 3: Carrier-DEK, 4: Carrier-Node-ID)
|
||||
/// garantiert 100%ige Abwärtskompatibilität zu bestehendem Code (z. B. `auth.0`, `auth.2`).
|
||||
#[derive(Clone)]
|
||||
pub struct UnlockedKeys(
|
||||
pub Zeroizing<[u8; 32]>, // 0: DEK (DEK_0 bei Slot 0, DEK_1 bei Slot 1)
|
||||
pub u32, // 1: Formatversion
|
||||
pub u32, // 2: Slot-ID (0 = Decoy/Standard, 1 = Hidden Vault)
|
||||
pub Option<Zeroizing<[u8; 32]>>, // 3: Carrier DEK_0 (bei Slot 1 im Modell A vorhanden)
|
||||
pub Option<i64>, // 4: Carrier Node ID (Inode der Alibi-Datei in nodes)
|
||||
);
|
||||
|
||||
impl UnlockedKeys {
|
||||
pub fn dek(&self) -> &Zeroizing<[u8; 32]> {
|
||||
&self.0
|
||||
}
|
||||
pub fn version(&self) -> u32 {
|
||||
self.1
|
||||
}
|
||||
pub fn slot_id(&self) -> u32 {
|
||||
self.2
|
||||
}
|
||||
pub fn carrier_dek(&self) -> Option<Zeroizing<[u8; 32]>> {
|
||||
self.3.clone()
|
||||
}
|
||||
pub fn carrier_node_id(&self) -> Option<i64> {
|
||||
self.4
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContainerMeta {
|
||||
@@ -62,17 +92,50 @@ impl ContainerMeta {
|
||||
/// Authentifiziert ein Master-Passwort über alle Header-Slots in strikt konstanter Zeit (Anti-Timing Side-Channel).
|
||||
/// Führt für ausnahmslos ALLE vorhandenen Slots die KDF-Ableitung und das DEK-Unwrapping durch.
|
||||
/// Dadurch ist die Rechenzeit für Decoy und Hidden Vault bit-genau identisch (2x Argon2id).
|
||||
pub fn authenticate(&self, password: &str) -> Option<(Zeroizing<[u8; 32]>, u32, u32)> {
|
||||
pub fn authenticate(&self, password: &str) -> Option<UnlockedKeys> {
|
||||
let mut matching = None;
|
||||
for slot in &self.slots {
|
||||
let res = derive_kek(password, &slot.kdf_salt, &slot.kdf_params)
|
||||
.ok()
|
||||
.and_then(|kek| {
|
||||
unwrap_dek(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag).ok()
|
||||
unwrap_key_payload(&kek, &slot.wrapped_dek, &slot.header_nonce, &slot.header_tag).ok()
|
||||
});
|
||||
if let Some(dek) = res {
|
||||
|
||||
if let Some(payload) = res {
|
||||
if matching.is_none() {
|
||||
matching = Some((dek, slot.version, slot.slot_id));
|
||||
if slot.slot_id == 0 {
|
||||
let mut dek = Zeroizing::new([0u8; 32]);
|
||||
let carrier_node_id = if payload.len() >= 40 {
|
||||
dek.copy_from_slice(&payload[0..32]);
|
||||
let cid = i64::from_le_bytes(payload[32..40].try_into().unwrap());
|
||||
if cid > 0 { Some(cid) } else { None }
|
||||
} else if payload.len() >= 32 {
|
||||
dek.copy_from_slice(&payload[0..32]);
|
||||
None
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
matching = Some(UnlockedKeys(dek, slot.version, 0, None, carrier_node_id));
|
||||
} else if slot.slot_id == 1 {
|
||||
let mut dek_1 = Zeroizing::new([0u8; 32]);
|
||||
let mut dek_0 = Zeroizing::new([0u8; 32]);
|
||||
let (carrier_dek, carrier_node_id) = if payload.len() >= 72 {
|
||||
dek_1.copy_from_slice(&payload[0..32]);
|
||||
dek_0.copy_from_slice(&payload[32..64]);
|
||||
let cid = i64::from_le_bytes(payload[64..72].try_into().unwrap());
|
||||
(Some(dek_0), if cid > 0 { Some(cid) } else { None })
|
||||
} else if payload.len() >= 64 {
|
||||
dek_1.copy_from_slice(&payload[0..32]);
|
||||
dek_0.copy_from_slice(&payload[32..64]);
|
||||
(Some(dek_0), None)
|
||||
} else if payload.len() >= 32 {
|
||||
dek_1.copy_from_slice(&payload[0..32]);
|
||||
(None, None)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
matching = Some(UnlockedKeys(dek_1, slot.version, 1, carrier_dek, carrier_node_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +184,7 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Authentifiziert ein Master-Passwort gegen den Container in konstanter Zeit.
|
||||
pub fn authenticate_password(&self, password: &str) -> Result<Option<(Zeroizing<[u8; 32]>, u32, u32)>> {
|
||||
pub fn authenticate_password(&self, password: &str) -> Result<Option<UnlockedKeys>> {
|
||||
let meta = self.read_meta()?;
|
||||
Ok(meta.authenticate(password))
|
||||
}
|
||||
@@ -217,6 +280,227 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialisiert das Datenbankschema für Modell A (Alibi-Carrier / Steganografischer Tresor).
|
||||
/// Legt die Trägerdatei im Decoy-Vault an und allokiert alle Carrier-Chunks mit initialem Rauschen.
|
||||
/// Sowohl Standard-Container als auch Container mit Hidden Vault besitzen eine bit- und schemagleiche Struktur:
|
||||
/// - Slot 0: 40 Bytes gewrappter Payload (32B DEK_0 || 8B carrier_node_id)
|
||||
/// - Slot 1: 72 Bytes gewrappter Payload (32B DEK_1 || 32B DEK_0 || 8B carrier_node_id oder CSPRNG-Rauschen)
|
||||
/// - 2 Root-Knoten (id=1 für Vault 0, id=2 für Vault 1)
|
||||
/// - 0 unzugeordnete Chunks: 100% aller Chunks gehören zu legitimen Decoy-Inodes und authentifizieren fehlerfrei unter DEK_0!
|
||||
pub fn init_schema_with_carrier(
|
||||
&self,
|
||||
salt_0: &[u8; 16],
|
||||
kdf_params_0: &KdfParams,
|
||||
wrapped_dek_0: &[u8],
|
||||
header_nonce_0: &[u8; 12],
|
||||
header_tag_0: &[u8; 16],
|
||||
carrier_config: Option<(
|
||||
&str, // carrier_name
|
||||
u64, // carrier_size_bytes
|
||||
&[u8; 16], // salt_1
|
||||
&KdfParams, // kdf_params_1
|
||||
&[u8], // wrapped_dek_1 (72B)
|
||||
&[u8; 12], // header_nonce_1
|
||||
&[u8; 16], // header_tag_1
|
||||
&[u8; 32], // raw DEK_0
|
||||
&[u8; 32], // raw DEK_1
|
||||
)>,
|
||||
) -> Result<Option<i64>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS meta (
|
||||
slot_id INTEGER NOT NULL PRIMARY KEY,
|
||||
magic BLOB NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
kdf_salt BLOB NOT NULL,
|
||||
kdf_params TEXT NOT NULL,
|
||||
wrapped_dek BLOB NOT NULL,
|
||||
header_nonce BLOB NOT NULL,
|
||||
header_tag BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
is_dir INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
modified_at INTEGER NOT NULL,
|
||||
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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
node_id INTEGER NOT NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
nonce BLOB NOT NULL,
|
||||
tag BLOB NOT NULL,
|
||||
ciphertext BLOB NOT NULL,
|
||||
PRIMARY KEY (node_id, chunk_index),
|
||||
FOREIGN KEY(node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
);",
|
||||
)?;
|
||||
|
||||
// Slot 0 einfügen (Standard / Decoy Vault)
|
||||
let params_json_0 = serde_json::to_string(kdf_params_0)?;
|
||||
conn.execute(
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
|
||||
VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
MAGIC_BYTES.as_slice(),
|
||||
FORMAT_VERSION,
|
||||
salt_0.as_slice(),
|
||||
params_json_0,
|
||||
wrapped_dek_0,
|
||||
header_nonce_0.as_slice(),
|
||||
header_tag_0.as_slice(),
|
||||
],
|
||||
)?;
|
||||
|
||||
let now = current_timestamp();
|
||||
// Wurzelknoten für beide Vaults anlegen (immer vorhanden für einheitliche Struktur)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO nodes (id, parent_id, name, is_dir, size, created_at, modified_at)
|
||||
VALUES (1, NULL, '', 1, 0, ?1, ?2)",
|
||||
params![now, now],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO nodes (id, parent_id, name, is_dir, size, created_at, modified_at)
|
||||
VALUES (2, NULL, '', 1, 0, ?1, ?2)",
|
||||
params![now, now],
|
||||
)?;
|
||||
|
||||
let carrier_node_id = if let Some((
|
||||
c_name,
|
||||
c_size,
|
||||
h_salt,
|
||||
h_params,
|
||||
h_wrapped,
|
||||
h_nonce,
|
||||
h_tag,
|
||||
dek_0,
|
||||
dek_1,
|
||||
)) = carrier_config
|
||||
{
|
||||
// Trägerdatei in nodes (parent_id = 1, Decoy Root) anlegen
|
||||
conn.execute(
|
||||
"INSERT INTO nodes (parent_id, name, is_dir, size, created_at, modified_at)
|
||||
VALUES (1, ?1, 0, ?2, ?3, ?4)",
|
||||
params![c_name, c_size as i64, now, now],
|
||||
)?;
|
||||
let c_id = conn.last_insert_rowid();
|
||||
|
||||
// Berechne Blockanzahl (min. 2 Blöcke: Block 0 für Manifest, Block 1+ für Nutzdaten)
|
||||
let total_blocks = c_size.div_ceil(CHUNK_SIZE as u64).max(2) as u32;
|
||||
|
||||
// Slot 1 (Hidden Vault) einfügen
|
||||
let params_json_1 = serde_json::to_string(h_params)?;
|
||||
conn.execute(
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
MAGIC_BYTES.as_slice(),
|
||||
FORMAT_VERSION,
|
||||
h_salt.as_slice(),
|
||||
params_json_1,
|
||||
h_wrapped,
|
||||
h_nonce.as_slice(),
|
||||
h_tag.as_slice(),
|
||||
],
|
||||
)?;
|
||||
|
||||
// Initialisiere CarrierManifest für Block 0
|
||||
let manifest = crate::carrier::CarrierManifest::new(total_blocks);
|
||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||
|
||||
let (inner_ct, inner_nonce, inner_tag) = crate::crypto::encrypt_chunk(
|
||||
dek_1,
|
||||
c_id,
|
||||
0,
|
||||
&manifest_bytes,
|
||||
FORMAT_VERSION,
|
||||
)?;
|
||||
let inner_ct_len = inner_ct.len() as u32;
|
||||
|
||||
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
|
||||
OsRng.fill_bytes(&mut outer_plaintext);
|
||||
|
||||
outer_plaintext[0..12].copy_from_slice(&inner_nonce);
|
||||
outer_plaintext[12..28].copy_from_slice(&inner_tag);
|
||||
outer_plaintext[28..32].copy_from_slice(&inner_ct_len.to_le_bytes());
|
||||
let ct_end = 32 + inner_ct.len();
|
||||
if ct_end > CHUNK_SIZE {
|
||||
bail!("Manifest-Payload zu groß für Block 0");
|
||||
}
|
||||
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
|
||||
|
||||
let (outer_ct, outer_nonce, outer_tag) = crate::crypto::encrypt_chunk(
|
||||
dek_0,
|
||||
c_id,
|
||||
0,
|
||||
&outer_plaintext,
|
||||
FORMAT_VERSION,
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||
VALUES (?1, 0, ?2, ?3, ?4)",
|
||||
params![c_id, outer_nonce.as_slice(), outer_tag.as_slice(), outer_ct],
|
||||
)?;
|
||||
|
||||
// Blöcke 1..total_blocks-1 mit DEK_0 vorallokieren
|
||||
let mut chunk_stmt = conn.prepare(
|
||||
"INSERT INTO chunks (node_id, chunk_index, nonce, tag, ciphertext)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
)?;
|
||||
|
||||
let mut dummy_noise = vec![0u8; CHUNK_SIZE];
|
||||
OsRng.fill_bytes(&mut dummy_noise);
|
||||
|
||||
for b in 1..total_blocks {
|
||||
let (ct, nonce, tag) = crate::crypto::encrypt_chunk(
|
||||
dek_0,
|
||||
c_id,
|
||||
b,
|
||||
&dummy_noise,
|
||||
FORMAT_VERSION,
|
||||
)?;
|
||||
chunk_stmt.execute(params![
|
||||
c_id,
|
||||
b,
|
||||
nonce.as_slice(),
|
||||
tag.as_slice(),
|
||||
ct,
|
||||
])?;
|
||||
}
|
||||
|
||||
Some(c_id)
|
||||
} else {
|
||||
// Slot 1 mit CSPRNG-Zufallsdaten gleicher Struktur und Entropie (72 Bytes für Modell A)
|
||||
let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot();
|
||||
let dummy_params_json = serde_json::to_string(&KdfParams::default())?;
|
||||
conn.execute(
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
MAGIC_BYTES.as_slice(),
|
||||
FORMAT_VERSION,
|
||||
dummy_salt.as_slice(),
|
||||
dummy_params_json,
|
||||
dummy_dek.as_slice(),
|
||||
dummy_nonce.as_slice(),
|
||||
dummy_tag.as_slice(),
|
||||
],
|
||||
)?;
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
Ok(carrier_node_id)
|
||||
}
|
||||
|
||||
/// Initialisiert das Datenbankschema mit Unterstützung für Plausible Deniability (optionaler Hidden Vault).
|
||||
/// Sowohl Standard-Container als auch Container mit Hidden Vault besitzen eine bit- und schemagleiche Struktur:
|
||||
/// - 2 Slots in der meta-Tabelle (Slot 0 + Slot 1 mit echtem KEK oder ununterscheidbarem CSPRNG-Rauschen)
|
||||
@@ -912,7 +1196,11 @@ impl Database {
|
||||
/// Erzwingt einen SQLite WAL Checkpoint und leert das Write-Ahead-Log.
|
||||
pub fn checkpoint(&self) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
|
||||
let _res: (i64, i64, i64) = conn.query_row(
|
||||
"PRAGMA wal_checkpoint(TRUNCATE);",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user