fix(crypto): K-01 — format v3 canonical metadata authentication
This commit is contained in:
+111
-1
@@ -7,12 +7,15 @@ use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use subtle::ConstantTimeEq;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub const MAGIC_BYTES: &[u8; 8] = b"SANCTUM\0";
|
||||
pub const FORMAT_VERSION_V1: u32 = 1;
|
||||
pub const FORMAT_VERSION_V2: u32 = 2;
|
||||
pub const FORMAT_VERSION: u32 = FORMAT_VERSION_V2;
|
||||
pub const FORMAT_VERSION_V3: u32 = 3;
|
||||
pub const FORMAT_VERSION: u32 = FORMAT_VERSION_V3;
|
||||
pub const CHUNK_SIZE: usize = 1024 * 1024; // 1 MB
|
||||
|
||||
/// Kompressions-Flags für Chunk-Payloads in Formatversion >= 2
|
||||
@@ -554,6 +557,81 @@ pub fn mnemonic_to_dek(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
|
||||
Ok(dek)
|
||||
}
|
||||
|
||||
/// Berechnet HMAC-SHA256 gemäß RFC 2104.
|
||||
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
|
||||
let mut key_block = [0u8; 64];
|
||||
if key.len() > 64 {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(key);
|
||||
let hash = hasher.finalize();
|
||||
key_block[..32].copy_from_slice(&hash);
|
||||
} else {
|
||||
key_block[..key.len()].copy_from_slice(key);
|
||||
}
|
||||
|
||||
let mut k_ipad = [0u8; 64];
|
||||
let mut k_opad = [0u8; 64];
|
||||
for i in 0..64 {
|
||||
k_ipad[i] = key_block[i] ^ 0x36;
|
||||
k_opad[i] = key_block[i] ^ 0x5c;
|
||||
}
|
||||
|
||||
let mut inner_hasher = Sha256::new();
|
||||
inner_hasher.update(&k_ipad);
|
||||
inner_hasher.update(data);
|
||||
let inner_hash = inner_hasher.finalize();
|
||||
|
||||
let mut outer_hasher = Sha256::new();
|
||||
outer_hasher.update(&k_opad);
|
||||
outer_hasher.update(&inner_hash);
|
||||
let out = outer_hasher.finalize();
|
||||
|
||||
let mut result = [0u8; 32];
|
||||
result.copy_from_slice(&out);
|
||||
result
|
||||
}
|
||||
|
||||
/// Leitet einen 32-Byte Schlüssel mittels HKDF-SHA256 (RFC 5869) aus IKM und Info-String ab.
|
||||
pub fn hkdf_sha256(ikm: &[u8], info: &[u8]) -> [u8; 32] {
|
||||
let salt = [0u8; 32];
|
||||
let prk = hmac_sha256(&salt, ikm);
|
||||
|
||||
let mut expand_input = Vec::with_capacity(info.len() + 1);
|
||||
expand_input.extend_from_slice(info);
|
||||
expand_input.push(0x01);
|
||||
|
||||
hmac_sha256(&prk, &expand_input)
|
||||
}
|
||||
|
||||
/// Leitet den dedizierten Metadaten-MAC-Schlüssel aus dem DEK ab (Format V3 / K-01).
|
||||
pub fn derive_metadata_mac_key(dek: &[u8; 32]) -> [u8; 32] {
|
||||
hkdf_sha256(dek, b"SANCTUM_META_MAC_V3")
|
||||
}
|
||||
|
||||
/// Berechnet den Metadaten-MAC über kanonisch serialisierte Knoten und Generation (Format V3 / K-01).
|
||||
pub fn compute_metadata_mac(
|
||||
mac_key: &[u8; 32],
|
||||
metadata_gen: u64,
|
||||
canonical_nodes: &[u8],
|
||||
) -> [u8; 32] {
|
||||
let mut data = Vec::with_capacity(24 + canonical_nodes.len());
|
||||
data.extend_from_slice(b"SANCTUM_META_V3\0");
|
||||
data.extend_from_slice(&metadata_gen.to_le_bytes());
|
||||
data.extend_from_slice(canonical_nodes);
|
||||
hmac_sha256(mac_key, &data)
|
||||
}
|
||||
|
||||
/// Prüft in konstanter Zeit, ob der gegebene Metadaten-MAC gültig ist (Format V3 / K-01).
|
||||
pub fn verify_metadata_mac(
|
||||
mac_key: &[u8; 32],
|
||||
metadata_gen: u64,
|
||||
canonical_nodes: &[u8],
|
||||
expected_mac: &[u8; 32],
|
||||
) -> bool {
|
||||
let computed = compute_metadata_mac(mac_key, metadata_gen, canonical_nodes);
|
||||
computed.ct_eq(expected_mac).into()
|
||||
}
|
||||
|
||||
/// Erzeugt die 16-Byte Associated Data (AAD) für einen Chunk, um Swap-Angriffe zu verhindern:
|
||||
/// node_id (8 Bytes Little-Endian) || chunk_index (8 Bytes Little-Endian).
|
||||
#[inline]
|
||||
@@ -1097,4 +1175,36 @@ mod tests {
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hmac_sha256_rfc4231_test_case_1() {
|
||||
// RFC 4231 Test Case 1: Key = 20x 0x0b, Data = "Hi There"
|
||||
let key = [0x0b; 20];
|
||||
let data = b"Hi There";
|
||||
let mac = hmac_sha256(&key, data);
|
||||
let expected_hex = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7";
|
||||
assert_eq!(hex::encode(mac), expected_hex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_mac_verification() {
|
||||
let dek = generate_dek();
|
||||
let mac_key = derive_metadata_mac_key(&dek);
|
||||
let canonical_nodes = b"node_canonical_bytes_mock_12345";
|
||||
let gen = 0u64;
|
||||
|
||||
let mac = compute_metadata_mac(&mac_key, gen, canonical_nodes);
|
||||
assert!(verify_metadata_mac(&mac_key, gen, canonical_nodes, &mac));
|
||||
|
||||
// Manipulierte Generation -> ungültig
|
||||
assert!(!verify_metadata_mac(&mac_key, gen + 1, canonical_nodes, &mac));
|
||||
|
||||
// Manipulierte Knoten-Bytes -> ungültig
|
||||
assert!(!verify_metadata_mac(&mac_key, gen, b"tampered_nodes", &mac));
|
||||
|
||||
// Falscher Schlüssel -> ungültig
|
||||
let wrong_dek = generate_dek();
|
||||
let wrong_key = derive_metadata_mac_key(&wrong_dek);
|
||||
assert!(!verify_metadata_mac(&wrong_key, gen, canonical_nodes, &mac));
|
||||
}
|
||||
}
|
||||
|
||||
+54
-1
@@ -8,7 +8,7 @@ use zeroize::Zeroizing;
|
||||
use sanctum::crypto::{
|
||||
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,
|
||||
wrap_slot1_payload, KdfParams, FORMAT_VERSION, FORMAT_VERSION_V3,
|
||||
};
|
||||
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
|
||||
use sanctum::recovery::{
|
||||
@@ -280,6 +280,13 @@ enum Commands {
|
||||
#[arg(long, default_value_t = false)]
|
||||
insecure_url: bool,
|
||||
},
|
||||
|
||||
/// Migriert einen bestehenden Container auf Format V3 (Metadaten-Authentifizierung & Replay-Schutz)
|
||||
UpgradeFormat {
|
||||
/// Pfad zur .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_drive_letter(s: &str) -> Result<char> {
|
||||
@@ -623,6 +630,7 @@ fn handle_init(
|
||||
let db =
|
||||
Database::open(container_path).context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||
|
||||
db.set_active_dek(dek.clone());
|
||||
db.init_schema_with_carrier(
|
||||
&salt,
|
||||
&kdf_params,
|
||||
@@ -1214,6 +1222,48 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_upgrade_format(container_path: &Path) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!(
|
||||
"Containerdatei '{}' existiert nicht.",
|
||||
container_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Format V3 Upgrade │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!();
|
||||
|
||||
let password = rpassword::prompt_password("Master-Passwort: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
|
||||
let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
let keys = match db.authenticate_password(&password)? {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
bail!("Authentifizierung fehlgeschlagen: Falsches Passwort");
|
||||
}
|
||||
};
|
||||
|
||||
if keys.version() >= FORMAT_VERSION_V3 {
|
||||
println!(
|
||||
" • Container ist bereits Format V{} (keine Migration erforderlich).",
|
||||
keys.version()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
db.upgrade_to_v3(keys.dek())?;
|
||||
db.checkpoint()?;
|
||||
|
||||
println!(" ✔ Container erfolgreich auf Format V3 aktualisiert!");
|
||||
println!(" • Metadaten-Authentifizierung (HMAC-SHA256) aktiviert.");
|
||||
println!(" • Replay-Schutz für Chunks (24-Byte AAD mit Generation) aktiviert.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_sync(
|
||||
container_path: &Path,
|
||||
source: &str,
|
||||
@@ -1520,6 +1570,9 @@ async fn run() -> Result<()> {
|
||||
Commands::Verify { path, full } => {
|
||||
handle_verify(&path, full)?;
|
||||
}
|
||||
Commands::UpgradeFormat { path } => {
|
||||
handle_upgrade_format(&path)?;
|
||||
}
|
||||
Commands::Sync {
|
||||
path,
|
||||
source,
|
||||
|
||||
+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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -439,6 +439,8 @@ pub fn run_sync(
|
||||
let start_time = Instant::now();
|
||||
let mut stats = SyncStats::default();
|
||||
|
||||
db.set_active_dek(zeroize::Zeroizing::new(*dek));
|
||||
|
||||
match options.direction {
|
||||
SyncDirection::Push => {
|
||||
sync_push(
|
||||
|
||||
+28
-2
@@ -4,7 +4,9 @@ use std::path::Path;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::crypto::{decrypt_chunk, FORMAT_VERSION_V1, FORMAT_VERSION_V2};
|
||||
use crate::crypto::{
|
||||
decrypt_chunk, FORMAT_VERSION_V1, FORMAT_VERSION_V2, FORMAT_VERSION_V3,
|
||||
};
|
||||
use crate::storage::Database;
|
||||
|
||||
/// Bericht über das Ergebnis einer Container-Integritätsprüfung.
|
||||
@@ -81,7 +83,10 @@ pub fn verify_container(
|
||||
let meta = match db.read_meta() {
|
||||
Ok(m) => {
|
||||
report.format_version = m.version;
|
||||
if m.version != FORMAT_VERSION_V1 && m.version != FORMAT_VERSION_V2 {
|
||||
if m.version != FORMAT_VERSION_V1
|
||||
&& m.version != FORMAT_VERSION_V2
|
||||
&& m.version != FORMAT_VERSION_V3
|
||||
{
|
||||
report.header_ok = false;
|
||||
report.header_error = Some(format!("Unbekannte Formatversion: {}", m.version));
|
||||
}
|
||||
@@ -247,6 +252,25 @@ pub fn verify_container(
|
||||
None
|
||||
};
|
||||
|
||||
// 3b. Metadaten-Authentifizierung (HMAC-SHA256) für Format V3 (K-01)
|
||||
if let Some(active_dek) = dek {
|
||||
if let Some(ref m) = meta {
|
||||
if m.version >= FORMAT_VERSION_V3 {
|
||||
match db.verify_metadata_mac(active_dek) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
report.errors.push(
|
||||
"Kritischer Metadaten-Authentifizierungsfehler (K-01): Metadaten-MAC ungültig. Manipulationsversuch an Dateinamen, Größen oder Verzeichnisstruktur in der SQLite-Datenbank erkannt.".to_string(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
report.errors.push(format!("Fehler bei der Metadaten-MAC-Prüfung: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Kryptografische Chunk- & AEAD-Authentifizierungsprüfung
|
||||
let chunk_headers = db
|
||||
.list_all_chunk_headers()
|
||||
@@ -339,6 +363,7 @@ mod tests {
|
||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
|
||||
let db = Database::open(&container_path).unwrap();
|
||||
db.set_active_dek(dek.clone());
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||
.unwrap();
|
||||
|
||||
@@ -401,6 +426,7 @@ mod tests {
|
||||
let (wrapped_dek, nonce, tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
|
||||
let db = Database::open(&container_path).unwrap();
|
||||
db.set_active_dek(dek.clone());
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &nonce, &tag)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -490,6 +490,8 @@ impl SanctumFs {
|
||||
let dek_arc = Arc::new(dek);
|
||||
let carrier_dek_arc = carrier_dek.map(Arc::new);
|
||||
|
||||
db.set_active_dek((*dek_arc).clone());
|
||||
|
||||
// Im Decoy-Vault (Slot 0): Stelle sicher, dass carrier_node_id stets bekannt ist,
|
||||
// um die Trägerdatei vor versehentlichem Löschen oder Überschreiben zu schützen.
|
||||
let carrier_node_id = carrier_node_id.or_else(|| {
|
||||
|
||||
Reference in New Issue
Block a user