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:
+14
-4
@@ -187,17 +187,27 @@ pub async fn mount_container(
|
||||
// S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren
|
||||
let _advisory_lock = db.acquire_advisory_lock_guard(false)?;
|
||||
|
||||
// K-01: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3)
|
||||
// K-01 / R-NEW-1: Metadaten-Integrität via HMAC-SHA256 validieren (Format V3)
|
||||
if version >= crate::crypto::FORMAT_VERSION_V3 {
|
||||
match db.verify_metadata_mac_for_slot(vault_id, &dek) {
|
||||
Ok(true) => {
|
||||
match db.verify_metadata_mac_status_for_slot(vault_id, &dek) {
|
||||
Ok(crate::storage::MetadataMacStatus::Valid) => {
|
||||
if !stealth {
|
||||
println!(
|
||||
" • Metadaten-MAC: ✔ Integrität erfolgreich verifiziert (HMAC-SHA256)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
Ok(crate::storage::MetadataMacStatus::PendingRebuild) => {
|
||||
if !stealth {
|
||||
println!(
|
||||
" • Metadaten-MAC: ℹ Metadaten-MAC nach Wiederherstellung neu aufgebaut"
|
||||
);
|
||||
}
|
||||
db.set_active_slot_and_dek(vault_id, dek.clone());
|
||||
db.update_metadata_mac()?;
|
||||
db.checkpoint()?;
|
||||
}
|
||||
Ok(crate::storage::MetadataMacStatus::Invalid) => {
|
||||
bail!(
|
||||
"Metadaten-MAC-Verifikation fehlgeschlagen: Die Container-Metadaten wurden manipuliert oder sind beschädigt (K-01)."
|
||||
);
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
//! Plattform-Abstraktionsschicht für Sanctum.
|
||||
//!
|
||||
//! Dieses Modul bündelt alle betriebssystemspezifischen Funktionen
|
||||
//! (Speichersperren, Dateimanager-Aufrufe, Signal-Monitoring, Shell-Integration)
|
||||
//! für Windows, Linux und macOS unter einer einheitlichen, speichersicheren Schnittstelle.
|
||||
//! Re-Exportiert die plattformspezifischen Implementierungen (Speichersperren,
|
||||
//! Prozessüberwachung, Session-Lock, Plattenplatzprüfung und Shell-Integration)
|
||||
//! aus `crate::windows` (welches via `#[cfg]`-Gates Windows-, Linux- und POSIX-Code bereitstellt).
|
||||
|
||||
pub use crate::windows::*;
|
||||
|
||||
@@ -412,6 +412,11 @@ pub fn restore_slot_from_recovery_key(
|
||||
db.restore_meta(&meta)
|
||||
.context("Fehler beim Schreiben des rekonstruierten Headers")?;
|
||||
|
||||
// R-NEW-1: Metadaten-MAC für den wiederhergestellten Slot unmittelbar berechnen und persistieren
|
||||
db.set_active_slot_and_dek(target_slot_id, dek.clone());
|
||||
db.update_metadata_mac()
|
||||
.context("Fehler beim Aktualisieren des Metadaten-MAC nach Header-Rekonstruktion")?;
|
||||
|
||||
db.checkpoint()
|
||||
.context("Fehler beim WAL-Checkpoint nach Header-Rekonstruktion")?;
|
||||
|
||||
@@ -561,6 +566,16 @@ mod tests {
|
||||
.expect("Unwrap restored DEK");
|
||||
assert_eq!(*dek, *active_dek);
|
||||
|
||||
// R-NEW-1: Nach restore_header_backup muss der MAC-Status PendingRebuild sein
|
||||
let mac_status = restored_db
|
||||
.verify_metadata_mac_status_for_slot(0, &active_dek)
|
||||
.expect("Verify MAC status");
|
||||
assert_eq!(
|
||||
mac_status,
|
||||
crate::storage::MetadataMacStatus::PendingRebuild,
|
||||
"Nach Header-Restore ohne Plaintext-DEK muss MAC-Status PendingRebuild sein"
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&container_path);
|
||||
let _ = fs::remove_file(&backup_path);
|
||||
}
|
||||
@@ -618,6 +633,19 @@ mod tests {
|
||||
.expect("Unwrap rescued DEK");
|
||||
assert_eq!(*dek, *unwrapped);
|
||||
|
||||
// R-NEW-1: Nach recovery key restore muss der MAC sofort gültig und persistent sein
|
||||
let mac_status = rescued_db
|
||||
.verify_metadata_mac_status_for_slot(0, &unwrapped)
|
||||
.expect("Verify MAC status");
|
||||
assert_eq!(
|
||||
mac_status,
|
||||
crate::storage::MetadataMacStatus::Valid,
|
||||
"Nach Recovery-Key-Restore muss MAC sofort Valid sein"
|
||||
);
|
||||
assert!(rescued_db
|
||||
.verify_metadata_mac_for_slot(0, &unwrapped)
|
||||
.unwrap());
|
||||
|
||||
let _ = fs::remove_file(&container_path);
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
+26
-1
@@ -890,7 +890,32 @@ pub fn get_available_disk_space<P: AsRef<std::path::Path>>(path: P) -> Option<u6
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::ffi::CString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let p = path.as_ref();
|
||||
let dir = if p.is_dir() {
|
||||
p.to_path_buf()
|
||||
} else {
|
||||
p.parent()
|
||||
.map(|parent| parent.to_path_buf())
|
||||
.unwrap_or_else(|| p.to_path_buf())
|
||||
};
|
||||
let Ok(c_path) = CString::new(dir.as_os_str().as_bytes()) else {
|
||||
return None;
|
||||
};
|
||||
unsafe {
|
||||
let mut stat: libc::statvfs = std::mem::zeroed();
|
||||
if libc::statvfs(c_path.as_ptr(), &mut stat) == 0 {
|
||||
let free = (stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64);
|
||||
Some(free)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(windows, unix)))]
|
||||
{
|
||||
let _ = path;
|
||||
Some(1024 * 1024 * 1024 * 1024)
|
||||
|
||||
Reference in New Issue
Block a user