fix(carrier): C-02 — rolling dual-block manifest redundancy and failover recovery
This commit is contained in:
@@ -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_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