fix(recovery): resolve metadata MAC desync after recovery (R-NEW-1) & complete release workflow (CI-01)
- R-NEW-1: Recalculate metadata HMAC upon recovery-key restore and mark backup restores as PendingRebuild to rebuild transparently on first mount - CI-01: Update release.yaml to compile both Windows x86_64 and Linux musl with pinned Zig 0.16.0 and cargo-zigbuild 0.23.4 - W-1: Implement statvfs quota determination on Unix via libc - Add RELEASE_PROCESS.md documenting release architecture and steps - Bump version to 0.9.2 across manifests, lockfile, docs, Scoop and WinGet
This commit is contained in:
+47
-17
@@ -208,6 +208,18 @@ fn current_timestamp() -> u64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Status der Metadaten-MAC-Integritätsprüfung (K-01 / R-NEW-1).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MetadataMacStatus {
|
||||
/// Metadaten-MAC ist vorhanden und stimmt mit den kanonischen Metadaten überein.
|
||||
Valid,
|
||||
/// Container-Header wurde frisch aus einem Backup restauriert (metadata_gen == 0 && metadata_mac IS NULL).
|
||||
/// Der MAC muss beim Mounten transparent mit dem aktiven DEK neu aufgebaut werden.
|
||||
PendingRebuild,
|
||||
/// Metadaten-MAC fehlt (bei gen > 0) oder stimmt nicht mit den berechneten Daten überein (Manipulationsverdacht).
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Öffnet oder erstellt die Container-Datenbank und initialisiert die Pragmas.
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
@@ -2316,6 +2328,15 @@ impl Database {
|
||||
|
||||
/// Prüft die Integrität des Metadaten-MAC für einen spezifischen Slot (0: Decoy, 1: Hidden).
|
||||
pub fn verify_metadata_mac_for_slot(&self, slot_id: u32, dek: &[u8; 32]) -> Result<bool> {
|
||||
Ok(self.verify_metadata_mac_status_for_slot(slot_id, dek)? == MetadataMacStatus::Valid)
|
||||
}
|
||||
|
||||
/// Prüft den detaillierten Integritätsstatus des Metadaten-MAC für einen spezifischen Slot (R-NEW-1).
|
||||
pub fn verify_metadata_mac_status_for_slot(
|
||||
&self,
|
||||
slot_id: u32,
|
||||
dek: &[u8; 32],
|
||||
) -> Result<MetadataMacStatus> {
|
||||
let conn = self.conn();
|
||||
let meta_row: Option<(u32, Option<Vec<u8>>, u64)> = conn
|
||||
.query_row(
|
||||
@@ -2326,19 +2347,24 @@ impl Database {
|
||||
.optional()?;
|
||||
|
||||
let Some((version, mac_opt, gen)) = meta_row else {
|
||||
return Ok(false);
|
||||
return Ok(MetadataMacStatus::Invalid);
|
||||
};
|
||||
|
||||
if version < FORMAT_VERSION_V3 {
|
||||
return Ok(true);
|
||||
return Ok(MetadataMacStatus::Valid);
|
||||
}
|
||||
|
||||
// R-NEW-1: Frisch restaurierter Container (gen == 0 && mac_opt IS NULL)
|
||||
if gen == 0 && mac_opt.is_none() {
|
||||
return Ok(MetadataMacStatus::PendingRebuild);
|
||||
}
|
||||
|
||||
let Some(mac_bytes) = mac_opt else {
|
||||
return Ok(false);
|
||||
return Ok(MetadataMacStatus::Invalid);
|
||||
};
|
||||
|
||||
if mac_bytes.len() != 32 {
|
||||
return Ok(false);
|
||||
return Ok(MetadataMacStatus::Invalid);
|
||||
}
|
||||
|
||||
let mut expected_mac = [0u8; 32];
|
||||
@@ -2347,12 +2373,11 @@ impl Database {
|
||||
|
||||
let canonical = self.canonical_nodes_bytes_for_vault(slot_id)?;
|
||||
let mac_key = derive_metadata_mac_key(dek);
|
||||
Ok(verify_metadata_mac(
|
||||
&mac_key,
|
||||
gen,
|
||||
&canonical,
|
||||
&expected_mac,
|
||||
))
|
||||
if verify_metadata_mac(&mac_key, gen, &canonical, &expected_mac) {
|
||||
Ok(MetadataMacStatus::Valid)
|
||||
} else {
|
||||
Ok(MetadataMacStatus::Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt ein Upgrade des Containerformats auf Format V3 durch (Format V3 / K-01 & K-02).
|
||||
@@ -2528,7 +2553,12 @@ 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,
|
||||
lock_pid INTEGER,
|
||||
lock_host TEXT,
|
||||
lock_time INTEGER
|
||||
);",
|
||||
)?;
|
||||
|
||||
@@ -2542,8 +2572,8 @@ impl Database {
|
||||
}
|
||||
let params_json = serde_json::to_string(&slot.kdf_params)?;
|
||||
conn.execute(
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, 0)",
|
||||
params![
|
||||
slot.slot_id,
|
||||
MAGIC_BYTES.as_slice(),
|
||||
@@ -2559,8 +2589,8 @@ impl Database {
|
||||
} else {
|
||||
let params_json = serde_json::to_string(&meta.kdf_params)?;
|
||||
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)",
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen)
|
||||
VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0)",
|
||||
params![
|
||||
MAGIC_BYTES.as_slice(),
|
||||
meta.version,
|
||||
@@ -2578,8 +2608,8 @@ impl Database {
|
||||
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)",
|
||||
"INSERT INTO meta (slot_id, magic, version, kdf_salt, kdf_params, wrapped_dek, header_nonce, header_tag, metadata_mac, metadata_gen)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, 0)",
|
||||
params![
|
||||
MAGIC_BYTES.as_slice(),
|
||||
FORMAT_VERSION,
|
||||
|
||||
Reference in New Issue
Block a user