feat(compression): implement transparent LZ4 chunk compression with V1 backwards compatibility
This commit is contained in:
+129
-6
@@ -9,8 +9,8 @@ use dav_server::{
|
||||
use futures_util::StreamExt;
|
||||
use sanctum::{
|
||||
crypto::{
|
||||
derive_kek, generate_dek, generate_salt, unwrap_dek, wrap_dek, KdfParams, CHUNK_SIZE,
|
||||
FORMAT_VERSION,
|
||||
derive_kek, encrypt_chunk, generate_dek, generate_salt, unwrap_dek, wrap_dek, KdfParams,
|
||||
CHUNK_SIZE, FORMAT_VERSION, FORMAT_VERSION_V1,
|
||||
},
|
||||
storage::Database,
|
||||
vfs::SanctumFs,
|
||||
@@ -62,7 +62,7 @@ async fn test_sanctum_full_container_lifecycle() {
|
||||
assert_eq!(*dek, *unwrapped_dek);
|
||||
|
||||
// 3. VFS Filesystem-Operationen (WebDAV Trait)
|
||||
let fs = SanctumFs::new(db.clone(), unwrapped_dek);
|
||||
let fs = SanctumFs::new(db.clone(), unwrapped_dek, meta.version);
|
||||
|
||||
// Ordner erstellen
|
||||
let docs_path = DavPath::new("/documents").unwrap();
|
||||
@@ -189,7 +189,7 @@ async fn test_sanctum_password_change() {
|
||||
db.checkpoint().expect("Checkpoint");
|
||||
|
||||
// Datei im Container mit DEK anlegen
|
||||
let fs_v1 = SanctumFs::new(db.clone(), original_dek.clone());
|
||||
let fs_v1 = SanctumFs::new(db.clone(), original_dek.clone(), FORMAT_VERSION);
|
||||
let file_path = DavPath::new("/secret.txt").unwrap();
|
||||
let file_data = b"Verschluesselte Geheimdaten vor der Passwortaenderung!";
|
||||
let mut opts_write = OpenOptions::default();
|
||||
@@ -234,14 +234,13 @@ async fn test_sanctum_password_change() {
|
||||
let active_dek = unwrap_dek(&new_kek, &meta_after.wrapped_dek, &meta_after.header_nonce, &meta_after.header_tag)
|
||||
.expect("Unwrap with new password");
|
||||
|
||||
let fs_v2 = SanctumFs::new(db.clone(), active_dek);
|
||||
let fs_v2 = SanctumFs::new(db.clone(), active_dek, meta_after.version);
|
||||
let mut opts_read = OpenOptions::default();
|
||||
opts_read.read = true;
|
||||
let mut read_file = fs_v2.open(&file_path, opts_read).await.expect("Open read with new password");
|
||||
let read_bytes = read_file.read_bytes(file_data.len()).await.expect("Read bytes");
|
||||
assert_eq!(&read_bytes[..], file_data, "Daten muessen nach Passwortaenderung unveraendert lesbar sein!");
|
||||
|
||||
|
||||
drop(read_file);
|
||||
drop(fs_v2);
|
||||
drop(db);
|
||||
@@ -249,3 +248,127 @@ async fn test_sanctum_password_change() {
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sanctum_lz4_compression_efficiency() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf = temp_dir.join(format!("test_sanctum_compress_{}.sanctum", std::process::id()));
|
||||
|
||||
if container_path.exists() {
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let password = "CompressTestPassword2026!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK");
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap");
|
||||
|
||||
let db = Database::open(&container_path).expect("open db");
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).expect("init schema");
|
||||
|
||||
let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION);
|
||||
|
||||
// 2 MB hochkompressible Textdaten erzeugen (z. B. wiederholende Zeilen)
|
||||
let pattern = b"Sanctum high performance encrypted container storage with transparent LZ4 compression.\n";
|
||||
let target_size = 2 * 1024 * 1024;
|
||||
let mut repetitive_data = Vec::with_capacity(target_size);
|
||||
while repetitive_data.len() < target_size {
|
||||
repetitive_data.extend_from_slice(pattern);
|
||||
}
|
||||
repetitive_data.truncate(target_size);
|
||||
|
||||
// Datei über VFS schreiben
|
||||
let test_file_path = DavPath::new("/compressed_test.log").unwrap();
|
||||
let mut opts = OpenOptions::default();
|
||||
opts.write = true;
|
||||
opts.create_new = true;
|
||||
let mut file = fs.open(&test_file_path, opts).await.expect("open write");
|
||||
file.write_bytes(Bytes::copy_from_slice(&repetitive_data)).await.expect("write");
|
||||
file.flush().await.expect("flush");
|
||||
drop(file);
|
||||
db.checkpoint().expect("checkpoint");
|
||||
|
||||
// Chunk-Knoten ermitteln und Ciphertext-Größe in der Datenbank prüfen
|
||||
let node = db.resolve_path("/compressed_test.log").expect("resolve").expect("found");
|
||||
assert_eq!(node.size, target_size as u64);
|
||||
|
||||
let chunk0 = db.read_chunk(node.id, 0).expect("read chunk").expect("chunk 0 exists");
|
||||
// 1 MB Rohdaten komprimiert mit LZ4 sollte typischerweise < 100 KB sein
|
||||
assert!(
|
||||
chunk0.ciphertext.len() < 100_000,
|
||||
"Ciphertext should be compressed from 1MB to < 100KB, was {}",
|
||||
chunk0.ciphertext.len()
|
||||
);
|
||||
|
||||
// Datei zurücklesen und mit Original vergleichen
|
||||
let mut read_opts = OpenOptions::default();
|
||||
read_opts.read = true;
|
||||
let mut read_file = fs.open(&test_file_path, read_opts).await.expect("open read");
|
||||
let read_back = read_file.read_bytes(target_size).await.expect("read back");
|
||||
assert_eq!(read_back.len(), target_size);
|
||||
assert_eq!(&read_back[..], &repetitive_data[..]);
|
||||
|
||||
drop(read_file);
|
||||
drop(fs);
|
||||
drop(db);
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sanctum_v1_backward_compatibility() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let container_path: PathBuf = temp_dir.join(format!("test_sanctum_v1_compat_{}.sanctum", std::process::id()));
|
||||
|
||||
if container_path.exists() {
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
let password = "V1LegacyPassword!";
|
||||
let salt = generate_salt();
|
||||
let kdf_params = KdfParams {
|
||||
memory_cost: 1024,
|
||||
time_cost: 1,
|
||||
parallelism: 1,
|
||||
};
|
||||
let kek = derive_kek(password, &salt, &kdf_params).expect("KEK");
|
||||
let dek = generate_dek();
|
||||
let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("wrap");
|
||||
|
||||
let db = Database::open(&container_path).expect("open db");
|
||||
db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).expect("init schema");
|
||||
// Formatversion auf V1 setzen
|
||||
db.set_meta_version(FORMAT_VERSION_V1).expect("set v1");
|
||||
|
||||
let meta = db.read_meta().expect("read meta");
|
||||
assert_eq!(meta.version, FORMAT_VERSION_V1);
|
||||
|
||||
// Datei-Knoten direkt in DB erstellen
|
||||
let node = db.create_node(1, "legacy_v1.txt", false).expect("create node");
|
||||
|
||||
// V1 Chunk mit encrypt_chunk(..., FORMAT_VERSION_V1) erzeugen und direkt in DB schreiben
|
||||
let v1_plaintext = b"Legacy Sanctum V1 uncompressed data payload.";
|
||||
let (ct, nonce, tag) = encrypt_chunk(&dek, node.id, 0, v1_plaintext, FORMAT_VERSION_V1).expect("encrypt v1");
|
||||
db.write_chunk(node.id, 0, &nonce, &tag, &ct).expect("write chunk");
|
||||
db.update_node_size_and_time(node.id, v1_plaintext.len() as u64, 12345678).expect("update size");
|
||||
db.checkpoint().expect("checkpoint");
|
||||
|
||||
// Öffnen über SanctumFs konfiguriert für V1
|
||||
let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION_V1);
|
||||
let path = DavPath::new("/legacy_v1.txt").unwrap();
|
||||
let mut opts = OpenOptions::default();
|
||||
opts.read = true;
|
||||
let mut file = fs.open(&path, opts).await.expect("open v1 file");
|
||||
let read_data = file.read_bytes(v1_plaintext.len()).await.expect("read v1");
|
||||
assert_eq!(&read_data[..], v1_plaintext);
|
||||
|
||||
drop(file);
|
||||
drop(fs);
|
||||
drop(db);
|
||||
let _ = std::fs::remove_file(&container_path);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user