use citadel::{Database, DatabaseBuilder, KdfAlgorithm};
use std::path::Path;
fn create_test_db(dir: &Path, passphrase: &[u8]) -> Database {
DatabaseBuilder::new(dir.join("test.citadel"))
.passphrase(passphrase)
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.create()
.unwrap()
}
fn create_test_db_argon2(dir: &Path, passphrase: &[u8]) -> Database {
DatabaseBuilder::new(dir.join("test.citadel"))
.passphrase(passphrase)
.cache_size(64)
.create()
.unwrap()
}
#[test]
fn export_and_restore_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"database-password";
let backup_pass = b"backup-password";
let backup_path = dir.path().join("backup.citadel-key-backup");
let db = create_test_db(dir.path(), db_pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key1", b"value1").unwrap();
wtx.insert(b"key2", b"value2").unwrap();
wtx.commit().unwrap();
db.export_key_backup(db_pass, backup_pass, &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
let new_pass = b"new-database-password";
Database::restore_key_from_backup(
&backup_path,
backup_pass,
new_pass,
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(new_pass)
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key1").unwrap(), Some(b"value1".to_vec()));
assert_eq!(rtx.get(b"key2").unwrap(), Some(b"value2".to_vec()));
}
#[test]
fn backup_wrong_db_passphrase() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"correct");
let backup_path = dir.path().join("backup.bin");
let result = db.export_key_backup(b"wrong", b"backup", &backup_path);
assert!(result.is_err());
}
#[test]
fn restore_wrong_backup_passphrase() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"db-pass");
let backup_path = dir.path().join("backup.bin");
db.export_key_backup(b"db-pass", b"correct-backup", &backup_path)
.unwrap();
drop(db);
let result = Database::restore_key_from_backup(
&backup_path,
b"wrong-backup",
b"new-pass",
&dir.path().join("test.citadel"),
);
assert!(result.is_err());
}
#[test]
fn backup_file_tamper_detected() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"db-pass");
let backup_path = dir.path().join("backup.bin");
db.export_key_backup(b"db-pass", b"backup-pass", &backup_path)
.unwrap();
drop(db);
let mut data = std::fs::read(&backup_path).unwrap();
data[60] ^= 0x01;
std::fs::write(&backup_path, &data).unwrap();
let result = Database::restore_key_from_backup(
&backup_path,
b"backup-pass",
b"new-pass",
&dir.path().join("test.citadel"),
);
assert!(result.is_err());
}
#[test]
fn backup_different_passphrase_from_db() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"database-pass";
let backup_pass = b"totally-different-backup-pass";
let backup_path = dir.path().join("backup.bin");
let db = create_test_db(dir.path(), db_pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"secret", b"data").unwrap();
wtx.commit().unwrap();
db.export_key_backup(db_pass, backup_pass, &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
backup_pass,
b"yet-another-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"yet-another-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"secret").unwrap(), Some(b"data".to_vec()));
}
#[test]
fn restore_with_new_passphrase_old_fails() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"old-pass";
let backup_path = dir.path().join("backup.bin");
let db = create_test_db(dir.path(), db_pass);
db.export_key_backup(db_pass, b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
let new_pass = b"new-pass";
Database::restore_key_from_backup(
&backup_path,
b"backup",
new_pass,
&dir.path().join("test.citadel"),
)
.unwrap();
let result = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(db_pass)
.open();
assert!(result.is_err());
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(new_pass)
.open()
.unwrap();
drop(db);
}
#[test]
fn backup_preserves_file_id() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"pass";
let backup_path = dir.path().join("backup.bin");
let db = create_test_db(dir.path(), db_pass);
db.export_key_backup(db_pass, b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
drop(db);
}
#[test]
fn backup_pbkdf2_database() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"pbkdf2-pass";
let backup_path = dir.path().join("backup.bin");
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(db_pass)
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.create()
.unwrap();
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key", b"val").unwrap();
wtx.commit().unwrap();
db.export_key_backup(db_pass, b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key").unwrap(), Some(b"val".to_vec()));
}
#[cfg(not(feature = "fips"))]
#[test]
fn backup_argon2_database() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"argon2-pass";
let backup_path = dir.path().join("backup.bin");
let db = create_test_db_argon2(dir.path(), db_pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key", b"val").unwrap();
wtx.commit().unwrap();
db.export_key_backup(db_pass, b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key").unwrap(), Some(b"val".to_vec()));
}
#[test]
fn multiple_backups_same_db() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"db-pass";
let backup1_path = dir.path().join("backup1.bin");
let backup2_path = dir.path().join("backup2.bin");
let db = create_test_db(dir.path(), db_pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key", b"value").unwrap();
wtx.commit().unwrap();
db.export_key_backup(db_pass, b"backup-pass-1", &backup1_path)
.unwrap();
db.export_key_backup(db_pass, b"backup-pass-2", &backup2_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup1_path,
b"backup-pass-1",
b"restored-pass-1",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"restored-pass-1")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key").unwrap(), Some(b"value".to_vec()));
drop(rtx);
drop(db);
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup2_path,
b"backup-pass-2",
b"restored-pass-2",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"restored-pass-2")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key").unwrap(), Some(b"value".to_vec()));
}
#[test]
fn backup_after_passphrase_change() {
let dir = tempfile::tempdir().unwrap();
let old_pass = b"old-pass";
let new_pass = b"new-pass";
let backup_path = dir.path().join("backup.bin");
let db = create_test_db(dir.path(), old_pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key", b"value").unwrap();
wtx.commit().unwrap();
db.change_passphrase(old_pass, new_pass).unwrap();
db.export_key_backup(new_pass, b"backup-pass", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup-pass",
b"restored-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"restored-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key").unwrap(), Some(b"value".to_vec()));
}
#[test]
fn backup_file_size_exact() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("backup.bin");
db.export_key_backup(b"pass", b"backup", &backup_path)
.unwrap();
let data = std::fs::read(&backup_path).unwrap();
assert_eq!(data.len(), 124);
}
#[test]
fn backup_binary_format_magic() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("backup.bin");
db.export_key_backup(b"pass", b"backup", &backup_path)
.unwrap();
let data = std::fs::read(&backup_path).unwrap();
let magic = u32::from_le_bytes(data[0..4].try_into().unwrap());
assert_eq!(magic, 0x4B45_5942);
}
#[test]
fn backup_empty_db() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("backup.bin");
db.export_key_backup(b"pass", b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
assert_eq!(db.stats().entry_count, 0);
}
#[test]
fn backup_large_db() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("backup.bin");
let mut wtx = db.begin_write().unwrap();
for i in 0..1000u32 {
wtx.insert(&i.to_be_bytes(), &format!("val-{i}").into_bytes())
.unwrap();
}
wtx.commit().unwrap();
db.export_key_backup(b"pass", b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
for i in 0..1000u32 {
let val = rtx.get(&i.to_be_bytes()).unwrap().unwrap();
assert_eq!(val, format!("val-{i}").into_bytes());
}
}
#[test]
fn restore_overwrite_existing_key_file() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("backup.bin");
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key", b"val").unwrap();
wtx.commit().unwrap();
db.export_key_backup(b"pass", b"backup", &backup_path)
.unwrap();
drop(db);
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let result = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"pass")
.open();
assert!(result.is_err());
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(rtx.get(b"key").unwrap(), Some(b"val".to_vec()));
}
#[test]
fn backup_path_does_not_exist() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let result = db.export_key_backup(
b"pass",
b"backup",
&dir.path().join("nonexistent/subdir/backup.bin"),
);
assert!(result.is_err());
}
#[test]
fn restore_backup_file_wrong_size() {
let dir = tempfile::tempdir().unwrap();
let backup_path = dir.path().join("bad_backup.bin");
std::fs::write(&backup_path, b"too short").unwrap();
let result = Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
);
assert!(result.is_err());
}
#[test]
fn backup_named_tables_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let db_pass = b"pass";
let backup_path = dir.path().join("backup.bin");
let db = create_test_db(dir.path(), db_pass);
let mut wtx = db.begin_write().unwrap();
wtx.create_table(b"users").unwrap();
wtx.table_insert(b"users", b"alice", b"admin").unwrap();
wtx.create_table(b"logs").unwrap();
wtx.table_insert(b"logs", b"entry1", b"logged in").unwrap();
wtx.commit().unwrap();
db.export_key_backup(db_pass, b"backup", &backup_path)
.unwrap();
drop(db);
let key_path = dir.path().join("test.citadel.citadel-keys");
std::fs::remove_file(&key_path).unwrap();
Database::restore_key_from_backup(
&backup_path,
b"backup",
b"new-pass",
&dir.path().join("test.citadel"),
)
.unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"new-pass")
.open()
.unwrap();
let mut rtx = db.begin_read();
assert_eq!(
rtx.table_get(b"users", b"alice").unwrap(),
Some(b"admin".to_vec())
);
assert_eq!(
rtx.table_get(b"logs", b"entry1").unwrap(),
Some(b"logged in".to_vec())
);
}