fix(core): resolve all 12 adversarial review findings
- Zeroize passwords in CLI prompts, handlers, and mount authentication - Preserve carrier_node_id when recovering Slot 0 via recovery key - Add --slot parameter to restore-header for targeted slot recovery - Implement online_backup and restore_from_backup using SQLite Online Backup API - Expose 'sanctum backup' and 'sanctum restore' CLI subcommands - Fix inactivity auto-lock by removing touch() from PROPFIND metadata/read_dir - Support O_APPEND by setting file cursor to file size on handle creation - Prevent data loss by implementing Drop for CarrierFile to flush dirty blocks - Optimize CSPRNG padding to only fill unwritten slack space - Batch carrier initialization in 500-block transactions to prevent UI/CLI freeze - Enforce 64-byte savings threshold for LZ4 compression - Add WebClient service diagnostic hint for Windows net use mount errors - Add unit and integration tests covering all new features
This commit is contained in:
@@ -358,3 +358,115 @@ async fn test_model_a_container_file_size_invariance() {
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_carrier_file_drop_and_append_mode() {
|
||||
let path = temp_db_path("carrier_drop_append");
|
||||
let carrier_size_bytes = 10 * 1024 * 1024; // 10 MB
|
||||
let carrier_name = "test_carrier.iso";
|
||||
|
||||
let pass_decoy = "DecoyPassword2026!";
|
||||
let pass_hidden = "HiddenSecretPassword2026!";
|
||||
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
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 hidden_fs = SanctumFs::with_carrier(
|
||||
db.clone(),
|
||||
auth_hidden.dek().clone(),
|
||||
auth_hidden.carrier_dek(),
|
||||
auth_hidden.carrier_node_id(),
|
||||
auth_hidden.version(),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
// 1. TEST CarrierFile::drop: Write bytes OHNE expliziten flush(), dann drop(handle)
|
||||
let test_file = DavPath::new("/drop_flush_test.txt").unwrap();
|
||||
let mut write_handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { create: true, write: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for write");
|
||||
|
||||
let initial_data = b"Hello from unflushed write!";
|
||||
write_handle.write_bytes(Bytes::from_static(initial_data)).await.expect("Write initial data");
|
||||
// WICHTIG: KEIN write_handle.flush()! Nur drop:
|
||||
drop(write_handle);
|
||||
|
||||
// Jetzt Datei wieder lesend öffnen und prüfen, ob Daten durch Drop persistiert wurden
|
||||
let mut read_handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { read: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for read");
|
||||
let read_back = read_handle.read_bytes(100).await.expect("Read data back");
|
||||
assert_eq!(&read_back[..], initial_data, "Drop muss ungeflushte Datenblöcke und Inode automatisch sichern");
|
||||
drop(read_handle);
|
||||
|
||||
// 2. TEST O_APPEND: Im Append-Modus öffnen und weitere Daten anhängen
|
||||
let append_data = b" - Appended data at EOF!";
|
||||
let mut append_handle = hidden_fs
|
||||
.open(&test_file, OpenOptions { write: true, append: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for append");
|
||||
|
||||
append_handle.write_bytes(Bytes::from_static(append_data)).await.expect("Write appended data");
|
||||
drop(append_handle); // Drop sichert auch hier
|
||||
|
||||
// Prüfe den vollständigen Dateiinhalt nach Append
|
||||
let mut read_handle_2 = hidden_fs
|
||||
.open(&test_file, OpenOptions { read: true, ..Default::default() })
|
||||
.await
|
||||
.expect("Open file for read after append");
|
||||
let full_content = read_handle_2.read_bytes(200).await.expect("Read full content");
|
||||
let mut expected = Vec::new();
|
||||
expected.extend_from_slice(initial_data);
|
||||
expected.extend_from_slice(append_data);
|
||||
assert_eq!(&full_content[..], &expected[..], "O_APPEND muss Daten am Dateiende anhängen");
|
||||
drop(read_handle_2);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -974,6 +974,82 @@ async fn test_plausible_deniability_phase1_indistinguishability_and_safeguards()
|
||||
let _ = std::fs::remove_file(&backup_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sanctum_online_backup_and_restore() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path = temp_dir.join(format!("test_backup_src_{}.sanctum", std::process::id()));
|
||||
let backup_path = temp_dir.join(format!("test_backup_out_{}.sanctum.bak", std::process::id()));
|
||||
let restored_path = temp_dir.join(format!("test_backup_restored_{}.sanctum", std::process::id()));
|
||||
|
||||
for p in [&container_path, &backup_path, &restored_path] {
|
||||
if p.exists() {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
|
||||
let password = "BackupTestPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).unwrap();
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap();
|
||||
|
||||
let db = Database::open(&container_path).unwrap();
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).unwrap();
|
||||
db.checkpoint().unwrap();
|
||||
|
||||
// Datei schreiben
|
||||
let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION);
|
||||
let test_file = DavPath::new("/important.txt").unwrap();
|
||||
let mut file = fs.open(&test_file, OpenOptions { write: true, create_new: true, ..Default::default() }).await.unwrap();
|
||||
file.write_bytes(Bytes::from_static(b"Sanctum Online Backup Test Data")).await.unwrap();
|
||||
file.flush().await.unwrap();
|
||||
drop(file);
|
||||
|
||||
// 1. Online-Live-Backup erstellen
|
||||
db.online_backup(&backup_path).expect("Online backup should succeed");
|
||||
assert!(backup_path.exists(), "Backup-Datei muss existieren");
|
||||
|
||||
// 2. Original-Container verändern (neue Datei hinzufügen)
|
||||
let extra_file = DavPath::new("/extra_after_backup.txt").unwrap();
|
||||
let mut file2 = fs.open(&extra_file, OpenOptions { write: true, create_new: true, ..Default::default() }).await.unwrap();
|
||||
file2.write_bytes(Bytes::from_static(b"After Backup Data")).await.unwrap();
|
||||
file2.flush().await.unwrap();
|
||||
drop(file2);
|
||||
|
||||
// 3. Restore aus dem Backup in neuen Pfad
|
||||
Database::restore_from_backup(&backup_path, &restored_path).expect("Restore should succeed");
|
||||
assert!(restored_path.exists(), "Wiederhergestellter Container muss existieren");
|
||||
|
||||
// 4. Verifiziere den wiederhergestellten Container
|
||||
let restored_db = Database::open(&restored_path).unwrap();
|
||||
let meta = restored_db.read_meta().unwrap();
|
||||
let auth = meta.authenticate(password).expect("Passwort muss den wiederhergestellten Container entsperren");
|
||||
assert_eq!(*auth.0, *dek);
|
||||
|
||||
let restored_fs = SanctumFs::new(restored_db.clone(), auth.0, meta.version);
|
||||
// /important.txt muss existieren und den korrekten Inhalt haben
|
||||
let mut read_handle = restored_fs.open(&test_file, OpenOptions { read: true, ..Default::default() }).await.unwrap();
|
||||
let content = read_handle.read_bytes(100).await.unwrap();
|
||||
assert_eq!(&content[..], b"Sanctum Online Backup Test Data");
|
||||
drop(read_handle);
|
||||
|
||||
// /extra_after_backup.txt darf im Backup-Zustand NICHT existieren
|
||||
assert!(restored_fs.open(&extra_file, OpenOptions { read: true, ..Default::default() }).await.is_err());
|
||||
|
||||
// Integritätsprüfung (FSCK) auf wiederhergestelltem Container
|
||||
let report = verify_container(&restored_path, Some(&dek), true).unwrap();
|
||||
assert!(report.is_healthy());
|
||||
|
||||
for p in [&container_path, &backup_path, &restored_path] {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user