fix(crypto): K-01 — format v3 canonical metadata authentication
This commit is contained in:
+218
-6
@@ -8,9 +8,10 @@ use rand::RngCore;
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::crypto::{
|
||||
decrypt_node_name, derive_kek, encrypt_node_name, generate_dummy_slot, unwrap_key_payload,
|
||||
validate_kdf_params, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1,
|
||||
FORMAT_VERSION_V2, MAGIC_BYTES,
|
||||
compute_metadata_mac, decrypt_node_name, derive_kek, derive_metadata_mac_key,
|
||||
encrypt_node_name, generate_dummy_slot, unwrap_key_payload, validate_kdf_params,
|
||||
verify_metadata_mac, KdfParams, CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1,
|
||||
FORMAT_VERSION_V2, FORMAT_VERSION_V3, MAGIC_BYTES,
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -184,6 +185,7 @@ impl ContainerMeta {
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
active_dek: Arc<Mutex<Option<Zeroizing<[u8; 32]>>>>,
|
||||
}
|
||||
|
||||
fn current_timestamp() -> u64 {
|
||||
@@ -304,6 +306,7 @@ impl Database {
|
||||
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
active_dek: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
db.init_pragmas()?;
|
||||
if table_count > 0 {
|
||||
@@ -318,6 +321,7 @@ impl Database {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
let db = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
active_dek: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
db.init_pragmas()?;
|
||||
db.ensure_schema_upgrades()?;
|
||||
@@ -328,11 +332,22 @@ impl Database {
|
||||
self.conn.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Setzt den aktiven DEK für automatische Metadaten-Authentifizierung (K-01).
|
||||
pub fn set_active_dek(&self, dek: Zeroizing<[u8; 32]>) {
|
||||
*self.active_dek.lock().unwrap() = Some(dek);
|
||||
}
|
||||
|
||||
/// Gibt den aktuellen aktiven DEK zurück, falls gesetzt.
|
||||
pub fn active_dek(&self) -> Option<Zeroizing<[u8; 32]>> {
|
||||
self.active_dek.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// 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()?;
|
||||
let res = meta.authenticate(password);
|
||||
if let Some(ref keys) = res {
|
||||
self.set_active_dek(keys.dek().clone());
|
||||
if let Some(cid) = keys.carrier_node_id() {
|
||||
let _ = self.mark_carrier_node_id(cid);
|
||||
}
|
||||
@@ -363,6 +378,15 @@ impl Database {
|
||||
[],
|
||||
);
|
||||
|
||||
// Spalte metadata_mac in meta (Format V3 / K-01)
|
||||
let _ = conn.execute("ALTER TABLE meta ADD COLUMN metadata_mac BLOB", []);
|
||||
|
||||
// Spalte metadata_gen in meta (Format V3 / K-01)
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE meta ADD COLUMN metadata_gen INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -526,7 +550,9 @@ impl Database {
|
||||
kdf_params TEXT NOT NULL,
|
||||
wrapped_dek BLOB NOT NULL,
|
||||
header_nonce BLOB NOT NULL,
|
||||
header_tag BLOB NOT NULL
|
||||
header_tag BLOB NOT NULL,
|
||||
metadata_mac BLOB,
|
||||
metadata_gen INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
@@ -671,6 +697,7 @@ impl Database {
|
||||
}
|
||||
conn.execute_batch("COMMIT;")?;
|
||||
|
||||
self.set_active_dek(Zeroizing::new(*dek_0));
|
||||
Some(c_id)
|
||||
} else {
|
||||
// Slot 1 mit CSPRNG-Zufallsdaten gleicher Struktur und Entropie (72 Bytes für Modell A)
|
||||
@@ -693,6 +720,9 @@ impl Database {
|
||||
None
|
||||
};
|
||||
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
|
||||
Ok(carrier_node_id)
|
||||
}
|
||||
|
||||
@@ -721,7 +751,9 @@ impl Database {
|
||||
kdf_params TEXT NOT NULL,
|
||||
wrapped_dek BLOB NOT NULL,
|
||||
header_nonce BLOB NOT NULL,
|
||||
header_tag BLOB NOT NULL
|
||||
header_tag BLOB NOT NULL,
|
||||
metadata_mac BLOB,
|
||||
metadata_gen INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
@@ -812,6 +844,9 @@ impl Database {
|
||||
)?;
|
||||
}
|
||||
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -969,7 +1004,10 @@ impl Database {
|
||||
bail!("Ungültige Sanctum-Containerdatei: Magic Bytes stimmen nicht überein");
|
||||
}
|
||||
|
||||
if slot0.version != FORMAT_VERSION_V1 && slot0.version != FORMAT_VERSION_V2 {
|
||||
if slot0.version != FORMAT_VERSION_V1
|
||||
&& slot0.version != FORMAT_VERSION_V2
|
||||
&& slot0.version != FORMAT_VERSION_V3
|
||||
{
|
||||
bail!(
|
||||
"Nicht unterstützte Sanctum-Formatversion: {}",
|
||||
slot0.version
|
||||
@@ -1289,6 +1327,9 @@ impl Database {
|
||||
params![now, parent_id],
|
||||
);
|
||||
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
|
||||
Ok(NodeRecord {
|
||||
id: new_id,
|
||||
parent_id: Some(parent_id),
|
||||
@@ -1312,6 +1353,8 @@ impl Database {
|
||||
"UPDATE nodes SET size = ?1, modified_at = ?2 WHERE id = ?3",
|
||||
params![size as i64, modified_at as i64, id],
|
||||
)?;
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1362,6 +1405,8 @@ impl Database {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM chunks WHERE node_id = ?1", params![id])?;
|
||||
conn.execute("DELETE FROM nodes WHERE id = ?1", params![id])?;
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1388,6 +1433,8 @@ impl Database {
|
||||
"UPDATE nodes SET parent_id = ?1, name = ?2, modified_at = ?3 WHERE id = ?4",
|
||||
params![new_parent_id, stored_name, now, id],
|
||||
)?;
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1493,6 +1540,8 @@ impl Database {
|
||||
params![new_size as i64, modified_at as i64, node_id],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1540,6 +1589,169 @@ impl Database {
|
||||
params![node_id, max_chunk_index],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
drop(conn);
|
||||
let _ = self.update_metadata_mac();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
|
||||
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT n.id, n.parent_id, n.name, n.is_dir, n.size, n.created_at, n.modified_at, n.is_carrier,
|
||||
(SELECT COUNT(*) FROM chunks c WHERE c.node_id = n.id) as chunk_count
|
||||
FROM nodes n
|
||||
ORDER BY n.id ASC",
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query([])?;
|
||||
let mut buf = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let id: i64 = row.get(0)?;
|
||||
let parent_id: Option<i64> = row.get(1)?;
|
||||
let name: String = row.get(2)?;
|
||||
let is_dir: i64 = row.get(3)?;
|
||||
let size: i64 = row.get(4)?;
|
||||
let created_at: i64 = row.get(5)?;
|
||||
let modified_at: i64 = row.get(6)?;
|
||||
let is_carrier: i64 = row.get(7)?;
|
||||
let chunk_count: i64 = row.get(8)?;
|
||||
|
||||
buf.extend_from_slice(&id.to_le_bytes());
|
||||
match parent_id {
|
||||
Some(pid) => {
|
||||
buf.push(1u8);
|
||||
buf.extend_from_slice(&pid.to_le_bytes());
|
||||
}
|
||||
None => {
|
||||
buf.push(0u8);
|
||||
buf.extend_from_slice(&0i64.to_le_bytes());
|
||||
}
|
||||
}
|
||||
let name_bytes = name.as_bytes();
|
||||
buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(name_bytes);
|
||||
buf.push(if is_dir != 0 { 1u8 } else { 0u8 });
|
||||
buf.extend_from_slice(&size.to_le_bytes());
|
||||
buf.extend_from_slice(&created_at.to_le_bytes());
|
||||
buf.extend_from_slice(&modified_at.to_le_bytes());
|
||||
buf.push(if is_carrier != 0 { 1u8 } else { 0u8 });
|
||||
buf.extend_from_slice(&chunk_count.to_le_bytes());
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Aktualisiert den Metadaten-MAC in Slot 0 bei strukturellen Modifikationen (Format V3 / K-01).
|
||||
pub fn update_metadata_mac(&self) -> Result<()> {
|
||||
let dek_opt = self.active_dek.lock().unwrap().clone();
|
||||
let Some(dek) = dek_opt else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let version_and_gen: Option<(u32, u64)> = conn
|
||||
.query_row(
|
||||
"SELECT version, metadata_gen FROM meta WHERE slot_id = 0 LIMIT 1",
|
||||
[],
|
||||
|r| Ok((r.get(0)?, r.get(1).unwrap_or(0))),
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
let Some((version, current_gen)) = version_and_gen else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if version < FORMAT_VERSION_V3 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
drop(conn);
|
||||
|
||||
let next_gen = current_gen + 1;
|
||||
let canonical = self.canonical_nodes_bytes()?;
|
||||
let mac_key = derive_metadata_mac_key(&dek);
|
||||
let new_mac = compute_metadata_mac(&mac_key, next_gen, &canonical);
|
||||
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE meta SET metadata_mac = ?1, metadata_gen = ?2 WHERE slot_id = 0",
|
||||
params![new_mac.as_slice(), next_gen],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft die Integrität des Metadaten-MAC gegen den gegebenen DEK (Format V3 / K-01).
|
||||
pub fn verify_metadata_mac(&self, dek: &[u8; 32]) -> Result<bool> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn
|
||||
.query_row(
|
||||
"SELECT version, metadata_mac, metadata_gen FROM meta WHERE slot_id = 0 LIMIT 1",
|
||||
[],
|
||||
|r| Ok((r.get(0)?, r.get(1).ok(), r.get(2).unwrap_or(0))),
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
let Some((version, mac_opt, gen)) = meta_row else {
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
if version < FORMAT_VERSION_V3 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let Some(mac_bytes) = mac_opt else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if mac_bytes.len() != 32 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut expected_mac = [0u8; 32];
|
||||
expected_mac.copy_from_slice(&mac_bytes);
|
||||
drop(conn);
|
||||
|
||||
let canonical = self.canonical_nodes_bytes()?;
|
||||
let mac_key = derive_metadata_mac_key(dek);
|
||||
Ok(verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac))
|
||||
}
|
||||
|
||||
/// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01 & K-02).
|
||||
pub fn upgrade_to_v3(&self, dek: &[u8; 32]) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let version: u32 = conn.query_row(
|
||||
"SELECT version FROM meta WHERE slot_id = 0 LIMIT 1",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
|
||||
if version >= FORMAT_VERSION_V3 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = conn.execute("ALTER TABLE meta ADD COLUMN metadata_mac BLOB", []);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE meta ADD COLUMN metadata_gen INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE chunks ADD COLUMN generation INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
|
||||
conn.execute(
|
||||
"UPDATE meta SET version = ?1, metadata_gen = 0 WHERE slot_id = 0",
|
||||
[FORMAT_VERSION_V3],
|
||||
)?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
self.set_active_dek(Zeroizing::new(*dek));
|
||||
self.update_metadata_mac()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user