use std::io::SeekFrom; use std::path::PathBuf; use bytes::Bytes; use dav_server::{ davpath::DavPath, fs::{DavFileSystem, OpenOptions, ReadDirMeta}, }; use futures_util::StreamExt; use sanctum::{ crypto::{ derive_kek, generate_dek, generate_salt, unwrap_dek, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION, }, storage::Database, vfs::SanctumFs, }; #[tokio::test] async fn test_sanctum_full_container_lifecycle() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_sanctum_{}.sanctum", std::process::id())); // Aufräumen, falls alte Testdatei existiert if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password = "CorrectMasterPassword2026!"; let wrong_password = "WrongMasterPassword!"; // 1. Initialisierung let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, // Schnell für Tests time_cost: 1, parallelism: 1, }; let kek = derive_kek(password, &salt, &kdf_params).expect("KEK derivation"); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).expect("DEK wrapping"); let db = Database::open(&container_path).expect("Open database"); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag) .expect("Init schema"); db.checkpoint().expect("Checkpoint"); // 2. Header & Magic Bytes Prüfung let meta = db.read_meta().expect("Read meta"); assert_eq!(meta.version, FORMAT_VERSION); assert_eq!(meta.kdf_salt, salt); assert_eq!(meta.wrapped_dek, wrapped_dek); // Falsches Passwort schlägt fehl let wrong_kek = derive_kek(wrong_password, &meta.kdf_salt, &meta.kdf_params).unwrap(); assert!(unwrap_dek(&wrong_kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag).is_err()); // Richtiges Passwort entschlüsselt DEK let unwrapped_dek = unwrap_dek(&kek, &meta.wrapped_dek, &meta.header_nonce, &meta.header_tag).unwrap(); assert_eq!(*dek, *unwrapped_dek); // 3. VFS Filesystem-Operationen (WebDAV Trait) let fs = SanctumFs::new(db.clone(), unwrapped_dek); // Ordner erstellen let docs_path = DavPath::new("/documents").unwrap(); fs.create_dir(&docs_path).await.expect("Create /documents"); let docs_meta = fs.metadata(&docs_path).await.expect("Metadata /documents"); assert!(docs_meta.is_dir()); // Multi-MB Datei über mehrere Chunks hinweg schreiben (2.5 MB = 3 Chunks à 1 MB) let file_path = DavPath::new("/documents/large_payload.bin").unwrap(); let mut opts_write = OpenOptions::default(); opts_write.write = true; opts_write.create_new = true; let mut file = fs.open(&file_path, opts_write).await.expect("Open for write"); let payload_size = (2.5 * CHUNK_SIZE as f64) as usize; let mut sample_data = Vec::with_capacity(payload_size); for i in 0..payload_size { sample_data.push((i % 251) as u8); } file.write_bytes(Bytes::copy_from_slice(&sample_data)) .await .expect("Write 2.5 MB"); file.flush().await.expect("Flush file"); drop(file); // Metadaten verifizieren let file_meta = fs.metadata(&file_path).await.expect("Metadata file"); assert_eq!(file_meta.len(), payload_size as u64); assert!(!file_meta.is_dir()); // Datei lesen & Seek über Chunk-Grenzen testen let mut opts_read = OpenOptions::default(); opts_read.read = true; let mut read_file = fs.open(&file_path, opts_read).await.expect("Open for read"); // Seek mitten in den 2. Chunk (1 MB + 500 Bytes) let seek_offset = (CHUNK_SIZE + 500) as u64; let new_pos = read_file .seek(SeekFrom::Start(seek_offset)) .await .expect("Seek"); assert_eq!(new_pos, seek_offset); // 2000 Bytes lesen (überschreitet evtl. Chunk-Grenze oder bleibt im Chunk) let read_len = 2000; let read_chunk_bytes = read_file.read_bytes(read_len).await.expect("Read bytes"); assert_eq!(read_chunk_bytes.len(), read_len); assert_eq!( &read_chunk_bytes[..], &sample_data[seek_offset as usize..seek_offset as usize + read_len] ); drop(read_file); // Datei umbenennen let renamed_path = DavPath::new("/documents/renamed_payload.bin").unwrap(); fs.rename(&file_path, &renamed_path).await.expect("Rename file"); assert!(fs.metadata(&file_path).await.is_err()); let renamed_meta = fs.metadata(&renamed_path).await.expect("Metadata renamed"); assert_eq!(renamed_meta.len(), payload_size as u64); // Datei kopieren let copy_path = DavPath::new("/documents/copy_payload.bin").unwrap(); fs.copy(&renamed_path, ©_path).await.expect("Copy file"); let copy_meta = fs.metadata(©_path).await.expect("Metadata copy"); assert_eq!(copy_meta.len(), payload_size as u64); // Verzeichnis auflisten let stream = fs.read_dir(&docs_path, ReadDirMeta::None).await.expect("Read dir"); let entries: Vec<_> = stream.collect().await; assert_eq!(entries.len(), 2); // renamed_payload.bin & copy_payload.bin // Dateien und Verzeichnis löschen fs.remove_file(&renamed_path).await.expect("Remove renamed"); fs.remove_file(©_path).await.expect("Remove copy"); fs.remove_dir(&docs_path).await.expect("Remove dir"); // Prüfen, dass Root leer ist let root_path = DavPath::new("/").unwrap(); let root_stream = fs.read_dir(&root_path, ReadDirMeta::None).await.expect("Read root"); let root_entries: Vec<_> = root_stream.collect().await; assert_eq!(root_entries.len(), 0); // Finaler DB Checkpoint db.checkpoint().expect("Final checkpoint"); drop(fs); drop(db); // Verifizieren, dass die Containerdatei existiert und aufgeräumt werden kann assert!(container_path.exists()); let _ = std::fs::remove_file(&container_path); } #[tokio::test] async fn test_sanctum_password_change() { let temp_dir = std::env::temp_dir(); let container_path: PathBuf = temp_dir.join(format!("test_sanctum_passwd_{}.sanctum", std::process::id())); if container_path.exists() { let _ = std::fs::remove_file(&container_path); } let password_v1 = "InitialSecret123!"; let password_v2 = "NewSecret456!"; // 1. Initialisierung mit Passwort v1 let salt_v1 = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek_v1 = derive_kek(password_v1, &salt_v1, &kdf_params).expect("KEK v1"); let original_dek = generate_dek(); let (wrapped_dek_v1, nonce_v1, tag_v1) = wrap_dek(&kek_v1, &original_dek).expect("Wrap DEK v1"); let db = Database::open(&container_path).expect("Open database"); db.init_schema(&salt_v1, &kdf_params, &wrapped_dek_v1, &nonce_v1, &tag_v1) .expect("Init schema"); db.checkpoint().expect("Checkpoint"); // Datei im Container mit DEK anlegen let fs_v1 = SanctumFs::new(db.clone(), original_dek.clone()); let file_path = DavPath::new("/secret.txt").unwrap(); let file_data = b"Verschluesselte Geheimdaten vor der Passwortaenderung!"; let mut opts_write = OpenOptions::default(); opts_write.write = true; opts_write.create_new = true; let mut write_file = fs_v1 .open(&file_path, opts_write) .await .expect("Open write"); write_file .write_bytes(Bytes::from_static(file_data)) .await .expect("Write bytes"); write_file.flush().await.expect("Flush"); drop(write_file); drop(fs_v1); db.checkpoint().expect("Checkpoint"); // 2. Passwortwechsel durchführen let meta_before = db.read_meta().expect("Read meta"); let old_kek = derive_kek(password_v1, &meta_before.kdf_salt, &meta_before.kdf_params).expect("Derive old KEK"); let recovered_dek = unwrap_dek(&old_kek, &meta_before.wrapped_dek, &meta_before.header_nonce, &meta_before.header_tag) .expect("Unwrap with old password"); let salt_v2 = generate_salt(); let kek_v2 = derive_kek(password_v2, &salt_v2, &kdf_params).expect("Derive new KEK"); let (wrapped_dek_v2, nonce_v2, tag_v2) = wrap_dek(&kek_v2, &recovered_dek).expect("Wrap with new KEK"); db.update_meta_keys(&salt_v2, &kdf_params, &wrapped_dek_v2, &nonce_v2, &tag_v2).expect("Update meta keys"); db.checkpoint().expect("Checkpoint"); // 3. Verifikation: Altes Passwort darf NICHT mehr funktionieren let meta_after = db.read_meta().expect("Read meta after"); let old_kek_again = derive_kek(password_v1, &meta_after.kdf_salt, &meta_after.kdf_params).unwrap(); assert!( unwrap_dek(&old_kek_again, &meta_after.wrapped_dek, &meta_after.header_nonce, &meta_after.header_tag).is_err(), "Altes Passwort darf nach Passwortaenderung nicht mehr funktionieren!" ); // 4. Verifikation: Neues Passwort funktioniert und entschlüsselt alte Daten intakt let new_kek = derive_kek(password_v2, &meta_after.kdf_salt, &meta_after.kdf_params).expect("Derive new KEK"); 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 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); let _ = std::fs::remove_file(&container_path); }