//! Backup container round-trip + error-path coverage. use relicario_core::backup::{pack_backup, unpack_backup, BackupInput}; fn empty_input() -> BackupInput<'static> { BackupInput { salt: &[0u8; 32], params_json: r#"{"format_version":2,"kdf":{"argon2_m":256,"argon2_t":1,"argon2_p":1},"aead":"xchacha20poly1305","salt_path":".relicario/salt"}"#, devices_json: "[]", manifest_enc: &[], settings_enc: &[], items: vec![], attachments: vec![], reference_jpg: None, git_archive: None, } } #[test] fn empty_vault_round_trip() { let out = pack_backup(empty_input(), "test-passphrase-1234").unwrap(); assert_eq!(&out[..4], b"RBAK", "magic header"); assert_eq!(out[4], 0x01, "format version"); let unpacked = unpack_backup(&out, "test-passphrase-1234").unwrap(); assert_eq!(unpacked.salt, [0u8; 32]); assert!(unpacked.devices_json.contains("[]")); assert!(unpacked.items.is_empty()); assert!(unpacked.attachments.is_empty()); assert!(unpacked.reference_jpg.is_none()); assert!(unpacked.git_archive.is_none()); } use relicario_core::backup::{BackupAttachment, BackupItem}; #[test] fn populated_vault_round_trip() { let manifest_enc = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]; let settings_enc = vec![0x01, 0x02, 0x03]; let item_a_ct = vec![0xAA; 100]; let item_b_ct = vec![0xBB; 200]; let attach_x_ct = vec![0xCC; 4096]; let attach_y_ct = vec![0xDD; 8192]; let input = BackupInput { salt: &[0x77u8; 32], params_json: r#"{"format_version":2,"kdf":{"argon2_m":256,"argon2_t":1,"argon2_p":1},"aead":"xchacha20poly1305","salt_path":".relicario/salt"}"#, devices_json: r#"[{"name":"laptop","public_key":"deadbeef"}]"#, manifest_enc: &manifest_enc, settings_enc: &settings_enc, items: vec![ BackupItem { id: "1111111111111111".to_string(), ciphertext: &item_a_ct }, BackupItem { id: "2222222222222222".to_string(), ciphertext: &item_b_ct }, ], attachments: vec![ BackupAttachment { item_id: "1111111111111111".to_string(), attachment_id: "aaaa1111".to_string(), ciphertext: &attach_x_ct, }, BackupAttachment { item_id: "2222222222222222".to_string(), attachment_id: "bbbb2222".to_string(), ciphertext: &attach_y_ct, }, ], reference_jpg: None, git_archive: None, }; let out = pack_backup(input, "another-strong-passphrase").unwrap(); let unpacked = unpack_backup(&out, "another-strong-passphrase").unwrap(); assert_eq!(unpacked.salt, [0x77u8; 32]); assert!(unpacked.devices_json.contains("laptop")); assert_eq!(unpacked.manifest_enc, manifest_enc); assert_eq!(unpacked.settings_enc, settings_enc); assert_eq!(unpacked.items.len(), 2); let by_id: std::collections::HashMap<_, _> = unpacked.items.iter().map(|i| (i.id.as_str(), &i.ciphertext)).collect(); assert_eq!(by_id.get("1111111111111111").unwrap(), &&item_a_ct); assert_eq!(by_id.get("2222222222222222").unwrap(), &&item_b_ct); assert_eq!(unpacked.attachments.len(), 2); let by_aid: std::collections::HashMap<_, _> = unpacked .attachments .iter() .map(|a| ((a.item_id.as_str(), a.attachment_id.as_str()), &a.ciphertext)) .collect(); assert_eq!(by_aid.get(&("1111111111111111", "aaaa1111")).unwrap(), &&attach_x_ct); assert_eq!(by_aid.get(&("2222222222222222", "bbbb2222")).unwrap(), &&attach_y_ct); } #[test] fn round_trip_with_reference_image() { let jpg_bytes: Vec = (0u8..=255).cycle().take(1024 * 64).collect(); // 64 KiB let mut input = empty_input(); input.reference_jpg = Some(&jpg_bytes); let out = pack_backup(input, "p").unwrap(); let unpacked = unpack_backup(&out, "p").unwrap(); assert_eq!(unpacked.reference_jpg.as_deref(), Some(jpg_bytes.as_slice())); assert!(unpacked.git_archive.is_none()); } #[test] fn round_trip_with_git_archive() { let tar_bytes: Vec = b"FAKE TAR BYTES; core treats opaquely".repeat(50); let mut input = empty_input(); input.git_archive = Some(&tar_bytes); let out = pack_backup(input, "p").unwrap(); let unpacked = unpack_backup(&out, "p").unwrap(); assert_eq!(unpacked.git_archive.as_deref(), Some(tar_bytes.as_slice())); } #[test] fn no_history_produces_strict_subset() { let mut a = empty_input(); a.git_archive = Some(b"some-tar-bytes"); let with = pack_backup(a, "p").unwrap(); let without = pack_backup(empty_input(), "p").unwrap(); // The "without" file is strictly smaller (one fewer base64-encoded blob in JSON). assert!(without.len() < with.len(), "no-history backup should be smaller: with={}, without={}", with.len(), without.len() ); } use relicario_core::RelicarioError; #[test] fn bad_magic_rejected() { let mut bytes = pack_backup(empty_input(), "p").unwrap(); bytes[0] = b'X'; match unpack_backup(&bytes, "p") { Err(RelicarioError::BackupBadMagic) => {} other => panic!("expected BackupBadMagic, got {other:?}"), } } #[test] fn unsupported_version_rejected() { let mut bytes = pack_backup(empty_input(), "p").unwrap(); bytes[4] = 0xFF; match unpack_backup(&bytes, "p") { Err(RelicarioError::BackupUnsupportedVersion { found, expected }) => { assert_eq!(found, 0xFF); assert_eq!(expected, 0x01); } other => panic!("expected BackupUnsupportedVersion, got {other:?}"), } } #[test] fn wrong_passphrase_rejected_as_decrypt_error() { let bytes = pack_backup(empty_input(), "right-passphrase").unwrap(); match unpack_backup(&bytes, "wrong-passphrase") { Err(RelicarioError::Decrypt) => {} other => panic!("expected Decrypt (opaque), got {other:?}"), } } #[test] fn truncated_file_rejected() { let bytes = pack_backup(empty_input(), "p").unwrap(); let truncated = &bytes[..bytes.len().min(60)]; // shorter than HEADER_LEN + TAG_LEN match unpack_backup(truncated, "p") { Err(RelicarioError::Format(_)) => {} other => panic!("expected Format(truncated), got {other:?}"), } } #[test] fn tampered_ciphertext_rejected_as_decrypt_error() { let mut bytes = pack_backup(empty_input(), "p").unwrap(); let last = bytes.len() - 1; bytes[last] ^= 0xFF; // flip a byte in the auth-tag region match unpack_backup(&bytes, "p") { Err(RelicarioError::Decrypt) => {} other => panic!("expected Decrypt for tampered tag, got {other:?}"), } } #[test] fn backup_roundtrip_with_nfd_passphrase() { // "café" in NFD (decomposed: e + combining acute accent) let nfd_passphrase = "caf\u{0065}\u{0301}"; // "café" in NFC (precomposed é) let nfc_passphrase = "caf\u{00E9}"; let input = BackupInput { salt: &[0u8; 32], params_json: r#"{"format_version":2,"kdf":{"argon2_m":256,"argon2_t":1,"argon2_p":1},"aead":"xchacha20poly1305","salt_path":".relicario/salt"}"#, devices_json: "[]", manifest_enc: &[1, 2, 3], settings_enc: &[4, 5, 6], items: vec![], attachments: vec![], reference_jpg: None, git_archive: None, }; // Pack with NFD passphrase let packed = pack_backup(input, nfd_passphrase).unwrap(); // Unpack with NFC passphrase — should work after fix let unpacked = unpack_backup(&packed, nfc_passphrase).unwrap(); assert_eq!(unpacked.manifest_enc, vec![1, 2, 3]); }