fix(carrier): C-02 — rolling dual-block manifest redundancy and failover recovery
This commit is contained in:
+86
-12
@@ -18,7 +18,7 @@ use futures_util::stream;
|
|||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{error, warn};
|
use tracing::{debug, error, warn};
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE};
|
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_MAGIC: &[u8; 8] = b"SANCTCAR";
|
||||||
pub const CARRIER_VERSION: u32 = 1;
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct CarrierManifest {
|
pub struct CarrierManifest {
|
||||||
pub magic: [u8; 8],
|
pub magic: [u8; 8],
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub manifest_generation: u64,
|
||||||
pub total_blocks: u32,
|
pub total_blocks: u32,
|
||||||
pub free_blocks: Vec<u32>,
|
pub free_blocks: Vec<u32>,
|
||||||
pub next_inode_id: i64,
|
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.
|
// Block 0 und Block 1 sind für das redundante Manifest reserviert (C-02). Nutzblöcke sind 2..total_blocks-1.
|
||||||
let free_blocks = (1..total_blocks).collect();
|
let free_blocks = (2..total_blocks).collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
magic: *CARRIER_MAGIC,
|
magic: *CARRIER_MAGIC,
|
||||||
version: CARRIER_VERSION,
|
version: CARRIER_VERSION,
|
||||||
|
manifest_generation: 0,
|
||||||
total_blocks,
|
total_blocks,
|
||||||
free_blocks,
|
free_blocks,
|
||||||
next_inode_id: 2,
|
next_inode_id: 2,
|
||||||
@@ -282,6 +285,7 @@ impl CarrierFsInner {
|
|||||||
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
self.anti_leak && is_leak_file_with_custom(name, &self.custom_leak_rules)
|
||||||
}
|
}
|
||||||
pub fn save_manifest(&mut self) -> Result<()> {
|
pub fn save_manifest(&mut self) -> Result<()> {
|
||||||
|
self.manifest.manifest_generation += 1;
|
||||||
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
let manifest_bytes = serde_json::to_vec(&self.manifest)?;
|
||||||
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
if manifest_bytes.len() > (crate::crypto::CHUNK_SIZE - 64) {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -297,10 +301,19 @@ impl CarrierFsInner {
|
|||||||
crate::crypto::CHUNK_SIZE - 64
|
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(
|
write_carrier_block(
|
||||||
&self.db,
|
&self.db,
|
||||||
self.carrier_node_id,
|
self.carrier_node_id,
|
||||||
0,
|
target_block,
|
||||||
&self.dek_outer,
|
&self.dek_outer,
|
||||||
&self.dek_inner,
|
&self.dek_inner,
|
||||||
&manifest_bytes,
|
&manifest_bytes,
|
||||||
@@ -374,7 +387,8 @@ pub struct CarrierFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub fn load_with_leak_rules(
|
||||||
db: Database,
|
db: Database,
|
||||||
carrier_node_id: i64,
|
carrier_node_id: i64,
|
||||||
@@ -384,19 +398,79 @@ impl CarrierFs {
|
|||||||
anti_leak: bool,
|
anti_leak: bool,
|
||||||
custom_leak_rules: Vec<String>,
|
custom_leak_rules: Vec<String>,
|
||||||
) -> Result<Self> {
|
) -> 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,
|
&db,
|
||||||
carrier_node_id,
|
carrier_node_id,
|
||||||
0,
|
0,
|
||||||
&dek_outer,
|
&dek_outer,
|
||||||
&dek_inner,
|
&dek_inner,
|
||||||
format_version,
|
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)?;
|
let block_1_res = read_carrier_block(
|
||||||
if manifest.magic != *CARRIER_MAGIC {
|
&db,
|
||||||
bail!("Ungültige Carrier-Magic-Bytes in Block 0");
|
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()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
+43
-16
@@ -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 = crate::carrier::CarrierManifest::new(total_blocks);
|
||||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
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)?;
|
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];
|
let mut outer_plaintext_0 = vec![0u8; CHUNK_SIZE];
|
||||||
OsRng.fill_bytes(&mut outer_plaintext);
|
OsRng.fill_bytes(&mut outer_plaintext_0);
|
||||||
|
|
||||||
outer_plaintext[0..12].copy_from_slice(&inner_nonce);
|
outer_plaintext_0[0..12].copy_from_slice(&inner_nonce_0);
|
||||||
outer_plaintext[12..28].copy_from_slice(&inner_tag);
|
outer_plaintext_0[12..28].copy_from_slice(&inner_tag_0);
|
||||||
outer_plaintext[28..32].copy_from_slice(&inner_ct_len.to_le_bytes());
|
outer_plaintext_0[28..32].copy_from_slice(&inner_ct_len_0.to_le_bytes());
|
||||||
let ct_end = 32 + inner_ct.len();
|
let ct_end_0 = 32 + inner_ct_0.len();
|
||||||
if ct_end > CHUNK_SIZE {
|
if ct_end_0 > CHUNK_SIZE {
|
||||||
bail!("Manifest-Payload zu groß für Block 0");
|
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) =
|
let (outer_ct_0, outer_nonce_0, outer_tag_0) =
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext, FORMAT_VERSION, 0)?;
|
crate::crypto::encrypt_chunk(dek_0, c_id, 0, &outer_plaintext_0, FORMAT_VERSION, 0)?;
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, 0, 0, ?2, ?3, ?4)",
|
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(
|
let mut chunk_stmt = conn.prepare(
|
||||||
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
"INSERT INTO chunks (node_id, chunk_index, generation, nonce, tag, ciphertext)
|
||||||
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
VALUES (?1, ?2, 0, ?3, ?4, ?5)",
|
||||||
@@ -770,7 +797,7 @@ impl Database {
|
|||||||
OsRng.fill_bytes(&mut dummy_noise);
|
OsRng.fill_bytes(&mut dummy_noise);
|
||||||
|
|
||||||
conn.execute_batch("BEGIN TRANSACTION;")?;
|
conn.execute_batch("BEGIN TRANSACTION;")?;
|
||||||
for b in 1..total_blocks {
|
for b in 2..total_blocks {
|
||||||
let (ct, nonce, tag) =
|
let (ct, nonce, tag) =
|
||||||
crate::crypto::encrypt_chunk(dek_0, c_id, b, &dummy_noise, FORMAT_VERSION, 0)?;
|
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,])?;
|
chunk_stmt.execute(params![c_id, b, nonce.as_slice(), tag.as_slice(), ct,])?;
|
||||||
|
|||||||
@@ -923,3 +923,213 @@ fn test_m05_deniability_schema_equality_standard_vs_hidden() {
|
|||||||
let _ = std::fs::remove_file(&path_std);
|
let _ = std::fs::remove_file(&path_std);
|
||||||
let _ = std::fs::remove_file(&path_hidden);
|
let _ = std::fs::remove_file(&path_hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_c02_dual_block_rolling_manifest_redundancy_and_recovery() {
|
||||||
|
let path = temp_db_path("c02_rolling_manifest");
|
||||||
|
let carrier_size_bytes = 10 * 1024 * 1024;
|
||||||
|
let carrier_name = "test_rolling.dat";
|
||||||
|
let pass_decoy = "DecoyPassword2026!";
|
||||||
|
let pass_hidden = "SuperSecretHiddenPassword2026!";
|
||||||
|
|
||||||
|
let kdf_params = KdfParams {
|
||||||
|
memory_cost: MIN_MEMORY_COST_KIB,
|
||||||
|
time_cost: MIN_TIME_COST,
|
||||||
|
parallelism: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let salt_0 = generate_salt();
|
||||||
|
let kek_0 = derive_kek(pass_decoy, &salt_0, &kdf_params).unwrap();
|
||||||
|
let dek_0 = generate_dek();
|
||||||
|
|
||||||
|
let salt_1 = generate_salt();
|
||||||
|
let kek_1 = derive_kek(pass_hidden, &salt_1, &kdf_params).unwrap();
|
||||||
|
let dek_1 = generate_dek();
|
||||||
|
|
||||||
|
let carrier_node_id = 3i64;
|
||||||
|
let (wrapped_0, nonce_0, tag_0) = wrap_slot0_payload(&kek_0, &dek_0, carrier_node_id).unwrap();
|
||||||
|
let (wrapped_1, nonce_1, tag_1) =
|
||||||
|
wrap_slot1_payload(&kek_1, &dek_1, &dek_0, carrier_node_id).unwrap();
|
||||||
|
|
||||||
|
let db = Database::open(&path).expect("Open database");
|
||||||
|
db.init_schema_with_carrier(
|
||||||
|
&salt_0,
|
||||||
|
&kdf_params,
|
||||||
|
&wrapped_0,
|
||||||
|
&nonce_0,
|
||||||
|
&tag_0,
|
||||||
|
Some((
|
||||||
|
carrier_name,
|
||||||
|
carrier_size_bytes,
|
||||||
|
&salt_1,
|
||||||
|
&kdf_params,
|
||||||
|
&wrapped_1,
|
||||||
|
&nonce_1,
|
||||||
|
&tag_1,
|
||||||
|
&dek_0,
|
||||||
|
&dek_1,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.expect("Init carrier schema");
|
||||||
|
db.checkpoint().unwrap();
|
||||||
|
|
||||||
|
let meta = db.read_meta().unwrap();
|
||||||
|
let auth_hidden = meta.authenticate(pass_hidden).expect("Auth hidden");
|
||||||
|
|
||||||
|
let cfs = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("Initial load of dual-block manifest");
|
||||||
|
|
||||||
|
// Datei anlegen -> save_manifest() -> Gen 1 -> schreibt Block 1
|
||||||
|
let file1_path = DavPath::new("/file_gen1.txt").unwrap();
|
||||||
|
let open_write = OpenOptions {
|
||||||
|
create_new: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut file1 = cfs.open(&file1_path, open_write).await.expect("Create file1");
|
||||||
|
file1.write_bytes(Bytes::from_static(b"generation 1 content")).await.unwrap();
|
||||||
|
file1.flush().await.unwrap();
|
||||||
|
drop(file1);
|
||||||
|
drop(cfs);
|
||||||
|
|
||||||
|
// Verifiziere: Block 1 hat jetzt Gen 1
|
||||||
|
let cfs_reloaded = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("Reload higher generation from block 1");
|
||||||
|
|
||||||
|
// Zweite Datei anlegen -> save_manifest() -> Gen 2 -> schreibt Block 0
|
||||||
|
let file2_path = DavPath::new("/file_gen2.txt").unwrap();
|
||||||
|
let open_write2 = OpenOptions {
|
||||||
|
create_new: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut file2 = cfs_reloaded.open(&file2_path, open_write2).await.expect("Create file2");
|
||||||
|
file2.write_bytes(Bytes::from_static(b"generation 2 content")).await.unwrap();
|
||||||
|
file2.flush().await.unwrap();
|
||||||
|
drop(file2);
|
||||||
|
drop(cfs_reloaded);
|
||||||
|
|
||||||
|
// 2. Simuliere Bitrot auf Block 0 (gezielt mit Rauschen überschreiben)
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&path).unwrap();
|
||||||
|
let mut noise = vec![0u8; 100];
|
||||||
|
OsRng.fill_bytes(&mut noise);
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = 0",
|
||||||
|
rusqlite::params![noise, carrier_node_id],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laden MUSS über Block 1 gelingen (Failover-Schutz)
|
||||||
|
let cfs_recovered_from_b1 = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("Recovery from Block 1 must succeed despite damaged Block 0");
|
||||||
|
|
||||||
|
assert!(cfs_recovered_from_b1.metadata(&file1_path).await.is_ok());
|
||||||
|
drop(cfs_recovered_from_b1);
|
||||||
|
|
||||||
|
// 3. Simuliere Beschädigung von Block 1, während Block 0 intakt ist
|
||||||
|
let cfs_repair = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let file3_path = DavPath::new("/file_gen3.txt").unwrap();
|
||||||
|
let mut file3 = cfs_repair
|
||||||
|
.open(
|
||||||
|
&file3_path,
|
||||||
|
OpenOptions {
|
||||||
|
create_new: true,
|
||||||
|
write: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
file3.write_bytes(Bytes::from_static(b"gen 3")).await.unwrap();
|
||||||
|
file3.flush().await.unwrap();
|
||||||
|
drop(file3);
|
||||||
|
drop(cfs_repair);
|
||||||
|
|
||||||
|
// Jetzt Block 1 beschädigen
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&path).unwrap();
|
||||||
|
let mut noise = vec![0u8; 100];
|
||||||
|
OsRng.fill_bytes(&mut noise);
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = 1",
|
||||||
|
rusqlite::params![noise, carrier_node_id],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laden MUSS über Block 0 gelingen
|
||||||
|
let cfs_recovered_from_b0 = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("Recovery from Block 0 must succeed despite damaged Block 1");
|
||||||
|
drop(cfs_recovered_from_b0);
|
||||||
|
|
||||||
|
// 4. Wenn BEIDE Blöcke zerstört sind: Totalverlust mit klarer Fehlermeldung
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&path).unwrap();
|
||||||
|
let mut noise = vec![0u8; 100];
|
||||||
|
OsRng.fill_bytes(&mut noise);
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE chunks SET ciphertext = ?1 WHERE node_id = ?2 AND chunk_index = 0",
|
||||||
|
rusqlite::params![noise, carrier_node_id],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let both_damaged = sanctum::carrier::CarrierFs::load(
|
||||||
|
db.clone(),
|
||||||
|
carrier_node_id,
|
||||||
|
std::sync::Arc::new(auth_hidden.carrier_dek().unwrap().clone()),
|
||||||
|
std::sync::Arc::new(auth_hidden.dek().clone()),
|
||||||
|
auth_hidden.version(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let err_msg = match both_damaged {
|
||||||
|
Ok(_) => panic!("Both blocks damaged must return Error"),
|
||||||
|
Err(e) => e.to_string(),
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("Kritischer Datenverlust (C-02)"),
|
||||||
|
"Error message must clearly identify C-02 failure: {}",
|
||||||
|
err_msg
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(db);
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user