fix(carrier): C-02 — rolling dual-block manifest redundancy and failover recovery

This commit is contained in:
2026-09-19 11:59:51 +02:00
parent ca6d54d5ca
commit 8455dc02db
3 changed files with 339 additions and 28 deletions
+86 -12
View File
@@ -18,7 +18,7 @@ use futures_util::stream;
use rand::rngs::OsRng;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use tracing::{error, warn};
use tracing::{debug, error, warn};
use zeroize::{Zeroize, Zeroizing};
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
@@ -28,11 +28,13 @@ use crate::vfs::{is_leak_file_with_custom, SanctumDirEntry, SanctumMetaData};
pub const CARRIER_MAGIC: &[u8; 8] = b"SANCTCAR";
pub const CARRIER_VERSION: u32 = 1;
/// Manifest für das steganografische Dateisystem innerhalb des Alibi-Carriers (Block 0).
/// Manifest für das steganografische Dateisystem innerhalb des Alibi-Carriers (Block 0 und 1, C-02).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CarrierManifest {
pub magic: [u8; 8],
pub version: u32,
#[serde(default)]
pub manifest_generation: u64,
pub total_blocks: u32,
pub free_blocks: Vec<u32>,
pub next_inode_id: i64,
@@ -62,12 +64,13 @@ impl CarrierManifest {
},
);
// Block 0 ist für das Manifest reserviert. Nutzblöcke sind 1..total_blocks-1.
let free_blocks = (1..total_blocks).collect();
// Block 0 und Block 1 sind für das redundante Manifest reserviert (C-02). Nutzblöcke sind 2..total_blocks-1.
let free_blocks = (2..total_blocks).collect();
Self {
magic: *CARRIER_MAGIC,
version: CARRIER_VERSION,
manifest_generation: 0,
total_blocks,
free_blocks,
next_inode_id: 2,
@@ -282,6 +285,7 @@ impl CarrierFsInner {
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
}
pub fn save_manifest(&mut self) -> Result<()> {
self.manifest.manifest_generation += 1;
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
bail!(
@@ -297,10 +301,19 @@ impl CarrierFsInner {
crate::crypto::CHUNK_SIZE - 64
);
}
// C-02: Rollierendes Dual-Block-Manifest.
// Gerade Generation -> Block 0, ungerade Generation -> Block 1.
let target_block = if self.manifest.manifest_generation % 2 == 0 {
0
} else {
1
};
write_carrier_block(
&self.db,
self.carrier_node_id,
0,
target_block,
&self.dek_outer,
&self.dek_inner,
&manifest_bytes,
@@ -374,7 +387,8 @@ pub struct CarrierFs {
}
impl CarrierFs {
/// Lädt ein bestehendes Carrier-Dateisystem mit benutzerdefinierten Anti-Leak-Regeln (V-04).
/// Lädt ein bestehendes Carrier-Dateisystem mit redundanter Block-0/Block-1 Prüfung (C-02)
/// und benutzerdefinierten Anti-Leak-Regeln (V-04).
pub fn load_with_leak_rules(
db: Database,
carrier_node_id: i64,
@@ -384,19 +398,79 @@ impl CarrierFs {
anti_leak: bool,
custom_leak_rules: Vec<String>,
) -> Result<Self> {
let manifest_bytes = read_carrier_block(
// C-02: Versuche beide redundante Blöcke (Block 0 und Block 1) zu lesen und zu verifizieren
let block_0_res = read_carrier_block(
&db,
carrier_node_id,
0,
&dek_outer,
&dek_inner,
format_version,
)?;
)
.and_then(|bytes| {
let m: CarrierManifest = serde_json::from_slice(&bytes)?;
if m.magic != *CARRIER_MAGIC {
bail!("Ungültige Carrier-Magic-Bytes in Block 0");
}
Ok(m)
});
let manifest: CarrierManifest = serde_json::from_slice(&manifest_bytes)?;
if manifest.magic != *CARRIER_MAGIC {
bail!("Ungültige Carrier-Magic-Bytes in Block 0");
}
let block_1_res = read_carrier_block(
&db,
carrier_node_id,
1,
&dek_outer,
&dek_inner,
format_version,
)
.and_then(|bytes| {
let m: CarrierManifest = serde_json::from_slice(&bytes)?;
if m.magic != *CARRIER_MAGIC {
bail!("Ungültige Carrier-Magic-Bytes in Block 1");
}
Ok(m)
});
let mut manifest = match (block_0_res, block_1_res) {
(Ok(m0), Ok(m1)) => {
if m1.manifest_generation > m0.manifest_generation {
debug!(
"C-02: Beide Manifest-Kopien intakt. Wähle neuere Generation {} aus Block 1 (Block 0: Gen {})",
m1.manifest_generation, m0.manifest_generation
);
m1
} else {
debug!(
"C-02: Beide Manifest-Kopien intakt. Wähle Generation {} aus Block 0 (Block 1: Gen {})",
m0.manifest_generation, m1.manifest_generation
);
m0
}
}
(Ok(m0), Err(e1)) => {
debug!(
"C-02: Verwende primäres Manifest aus Block 0 (Gen {}). Block 1 unlesbar oder uninitialisiert: {}",
m0.manifest_generation, e1
);
m0
}
(Err(e0), Ok(m1)) => {
warn!(
"C-02: Block 0 korrupt oder unlesbar ({})! Erfolgreiche Wiederherstellung aus redundanter Kopie in Block 1 (Gen {}).",
e0, m1.manifest_generation
);
m1
}
(Err(e0), Err(e1)) => {
bail!(
"Kritischer Datenverlust (C-02): Weder Block 0 ({}) noch Block 1 ({}) enthalten ein gültiges Carrier-Manifest! Hidden Vault nicht wiederherstellbar.",
e0, e1
);
}
};
// Rückwärtskompatibilität zu v0.8.0-Containern: Block 1 als redundante Manifest-Kopie reservieren
manifest.free_blocks.retain(|&b| b >= 2);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
+43 -16
View File
@@ -731,36 +731,63 @@ impl Database {
],
)?;
// Initialisiere CarrierManifest für Block 0
// Initialisiere CarrierManifest für redundante Blöcke 0 und 1 (C-02)
let manifest = crate::carrier::CarrierManifest::new(total_blocks);
let manifest_bytes = serde_json::to_vec(&manifest)?;
let (inner_ct, inner_nonce, inner_tag) =
// 1. Block 0 schreiben (Primärkopie, Gen 0)
let (inner_ct_0, inner_nonce_0, inner_tag_0) =
crate::crypto::encrypt_chunk(dek_1, c_id, 0, &manifest_bytes, FORMAT_VERSION, 0)?;
let inner_ct_len = inner_ct.len() as u32;
let inner_ct_len_0 = inner_ct_0.len() as u32;
let mut outer_plaintext = vec![0u8; CHUNK_SIZE];
OsRng.fill_bytes(&mut outer_plaintext);
let mut outer_plaintext_0 = vec![0u8; CHUNK_SIZE];
OsRng.fill_bytes(&mut outer_plaintext_0);
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 {
outer_plaintext_0[0..12].copy_from_slice(&inner_nonce_0);
outer_plaintext_0[12..28].copy_from_slice(&inner_tag_0);
outer_plaintext_0[28..32].copy_from_slice(&inner_ct_len_0.to_le_bytes());
let ct_end_0 = 32 + inner_ct_0.len();
if ct_end_0 > CHUNK_SIZE {
bail!("Manifest-Payload zu groß für Block 0");
}
outer_plaintext[32..ct_end].copy_from_slice(&inner_ct);
outer_plaintext_0[32..ct_end_0].copy_from_slice(&inner_ct_0);
let (outer_ct, outer_nonce, outer_tag) =
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext, FORMAT_VERSION, 0)?;
let (outer_ct_0, outer_nonce_0, outer_tag_0) =
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext_0, FORMAT_VERSION, 0)?;
conn.execute(
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
VALUES (?1, 0, 0, ?2, ?3, ?4)",
params![c_id, outer_nonce.as_slice(), outer_tag.as_slice(), outer_ct],
params![c_id, outer_nonce_0.as_slice(), outer_tag_0.as_slice(), outer_ct_0],
)?;
// Blöcke 1..total_blocks-1 mit DEK_0 vorallokieren
// 2. Block 1 schreiben (Redundante Zweitkopie, Gen 0, C-02)
let (inner_ct_1, inner_nonce_1, inner_tag_1) =
crate::crypto::encrypt_chunk(dek_1, c_id, 1, &manifest_bytes, FORMAT_VERSION, 0)?;
let inner_ct_len_1 = inner_ct_1.len() as u32;
let mut outer_plaintext_1 = vec![0u8; CHUNK_SIZE];
OsRng.fill_bytes(&mut outer_plaintext_1);
outer_plaintext_1[0..12].copy_from_slice(&inner_nonce_1);
outer_plaintext_1[12..28].copy_from_slice(&inner_tag_1);
outer_plaintext_1[28..32].copy_from_slice(&inner_ct_len_1.to_le_bytes());
let ct_end_1 = 32 + inner_ct_1.len();
if ct_end_1 > CHUNK_SIZE {
bail!("Manifest-Payload zu groß für Block 1");
}
outer_plaintext_1[32..ct_end_1].copy_from_slice(&inner_ct_1);
let (outer_ct_1, outer_nonce_1, outer_tag_1) =
crate::crypto::encrypt_chunk(dek_0, c_id, 1, &outer_plaintext_1, FORMAT_VERSION, 0)?;
conn.execute(
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
VALUES (?1, 1, 0, ?2, ?3, ?4)",
params![c_id, outer_nonce_1.as_slice(), outer_tag_1.as_slice(), outer_ct_1],
)?;
// Blöcke 2..total_blocks-1 mit DEK_0 vorallokieren (C-02)
let mut chunk_stmt = conn.prepare(
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
@@ -770,7 +797,7 @@ impl Database {
OsRng.fill_bytes(&mut dummy_noise);
conn.execute_batch("BEGIN TRANSACTION;")?;
for b in 1..total_blocks {
for b in 2..total_blocks {
let (ct, nonce, tag) =
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION, 0)?;
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;