fix(sync): S-05 — delete and shred all chunks when truncating to 0 bytes
This commit is contained in:
@@ -1776,6 +1776,50 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Löscht und shreddert alle Chunks eines Knotens (z. B. beim Kürzen auf 0 Bytes) (S-05).
|
||||||
|
pub fn delete_all_chunks(&self, node_id: i64) -> Result<()> {
|
||||||
|
self.assert_not_carrier(node_id)?;
|
||||||
|
let mut conn = self.conn.lock().unwrap();
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
{
|
||||||
|
let mut stmt = tx
|
||||||
|
.prepare("SELECT chunk_index, length(ciphertext) FROM chunks WHERE node_id = ?1")?;
|
||||||
|
let chunks_to_shred: Vec<(u32, usize)> = stmt
|
||||||
|
.query_map(params![node_id], |row| {
|
||||||
|
Ok((row.get(0)?, row.get::<_, usize>(1)?))
|
||||||
|
})?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut update_stmt = tx.prepare(
|
||||||
|
"UPDATE chunks SET nonce = ?1, tag = ?2, ciphertext = ?3 WHERE node_id = ?4 AND chunk_index = ?5",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for (idx, len) in chunks_to_shred {
|
||||||
|
let mut dummy_nonce = [0u8; 12];
|
||||||
|
let mut dummy_tag = [0u8; 16];
|
||||||
|
let mut dummy_payload = vec![0u8; len];
|
||||||
|
OsRng.fill_bytes(&mut dummy_nonce);
|
||||||
|
OsRng.fill_bytes(&mut dummy_tag);
|
||||||
|
OsRng.fill_bytes(&mut dummy_payload);
|
||||||
|
|
||||||
|
let _ = update_stmt.execute(params![
|
||||||
|
dummy_nonce.as_slice(),
|
||||||
|
dummy_tag.as_slice(),
|
||||||
|
dummy_payload.as_slice(),
|
||||||
|
node_id,
|
||||||
|
idx
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.execute("DELETE FROM chunks WHERE node_id = ?1", params![node_id])?;
|
||||||
|
tx.commit()?;
|
||||||
|
drop(conn);
|
||||||
|
let _ = self.update_metadata_mac();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
|
/// Erzeugt die deterministische kanonische Byterepräsentation aller Knoten für den Metadaten-MAC (K-01).
|
||||||
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
|
pub fn canonical_nodes_bytes(&self) -> Result<Vec<u8>> {
|
||||||
self.canonical_nodes_bytes_for_vault(0)
|
self.canonical_nodes_bytes_for_vault(0)
|
||||||
|
|||||||
+6
-1
@@ -335,7 +335,12 @@ pub fn sync_single_file_to_vault(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bei Überschreiben einer ehemals größeren Datei überzählige alte Chunks entfernen
|
// Bei Überschreiben einer ehemals größeren Datei überzählige alte Chunks entfernen
|
||||||
db.truncate_chunks_after(node_id, chunk_idx.saturating_sub(1))?;
|
if bytes_written == 0 {
|
||||||
|
// S-05: 0-Byte Datei: Alle Chunks (inkl. Chunk 0) restlos löschen und shreddern
|
||||||
|
db.delete_all_chunks(node_id)?;
|
||||||
|
} else {
|
||||||
|
db.truncate_chunks_after(node_id, chunk_idx.saturating_sub(1))?;
|
||||||
|
}
|
||||||
// S-04: Verwende bytes_written statt local_size zur Vermeidung von TOCTOU-Diskrepanzen
|
// S-04: Verwende bytes_written statt local_size zur Vermeidung von TOCTOU-Diskrepanzen
|
||||||
db.update_node_size_and_time(node_id, bytes_written, local_mtime)?;
|
db.update_node_size_and_time(node_id, bytes_written, local_mtime)?;
|
||||||
|
|
||||||
|
|||||||
@@ -689,3 +689,100 @@ fn test_s04_toctou_file_size_uses_bytes_written() {
|
|||||||
|
|
||||||
let _ = fs::remove_dir_all(&temp_root);
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_s05_zero_byte_file_truncate_removes_all_chunks() {
|
||||||
|
let temp_root =
|
||||||
|
std::env::temp_dir().join(format!("sanctum_s05_test_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("test.sanctum");
|
||||||
|
let file_path = temp_root.join("truncate_file.bin");
|
||||||
|
|
||||||
|
fs::create_dir_all(&temp_root).unwrap();
|
||||||
|
// 1. Zuerst große Datei mit 2.5 Chunks (> 2 MB) anlegen
|
||||||
|
let large_payload = vec![0xEEu8; 2_500_000];
|
||||||
|
fs::write(&file_path, &large_payload).unwrap();
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
// Initialer Push
|
||||||
|
sanctum::sync::sync_single_file_to_vault(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
&file_path,
|
||||||
|
1,
|
||||||
|
"truncate_file.bin",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("Initial push of large file");
|
||||||
|
|
||||||
|
let node = db
|
||||||
|
.resolve_path_in_vault("/truncate_file.bin", 0, &dek)
|
||||||
|
.unwrap()
|
||||||
|
.expect("Node must exist");
|
||||||
|
assert_eq!(node.size, 2_500_000);
|
||||||
|
|
||||||
|
// Prüfe, dass Chunks existieren (3 Chunks: 0, 1, 2)
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||||
|
let count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM chunks WHERE node_id = ?1",
|
||||||
|
rusqlite::params![node.id],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(count, 3, "Datei muss 3 Chunks besitzen");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Jetzt: Lokale Datei durch 0-Byte Datei ersetzen (Truncate auf 0 Bytes)
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1100)); // Timestamp ändern
|
||||||
|
fs::write(&file_path, b"").unwrap();
|
||||||
|
|
||||||
|
let res = sanctum::sync::sync_single_file_to_vault(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
&file_path,
|
||||||
|
1,
|
||||||
|
"truncate_file.bin",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("Push of 0-byte file");
|
||||||
|
|
||||||
|
match res {
|
||||||
|
sanctum::sync::FileTransferResult::Transferred { size } => {
|
||||||
|
assert_eq!(size, 0);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Transferred {{ size: 0 }}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verifiziere: In SQLite darf KEIN EINZIGER Chunk mehr für diesen Knoten existieren!
|
||||||
|
// (Ohne den S-05 Fix blieb Chunk 0 fälschlicherweise in der Tabelle zurück)
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||||
|
let count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM chunks WHERE node_id = ?1",
|
||||||
|
rusqlite::params![node.id],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count, 0,
|
||||||
|
"Bei 0-Byte Datei müssen ALLE Chunks (inkl. Chunk 0) gelöscht sein (S-05)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated_node = db
|
||||||
|
.resolve_path_in_vault("/truncate_file.bin", 0, &dek)
|
||||||
|
.unwrap()
|
||||||
|
.expect("Node must exist");
|
||||||
|
assert_eq!(updated_node.size, 0);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user