use crate::crypto::{derive_kek, unwrap_dek, KdfId, Key};
use crate::page::{format_major, FORMAT_MAJOR_VERSION_ENCRYPTED, PAGE_SIZE};
use crate::superblock::{
CryptoHeader, KeySlot, ALGO_XCHACHA20POLY1305, CRYPTO_HEADER_OFFSET, KEY_SLOT_COUNT,
KEY_SLOT_SIZE,
};
use crate::Options;
use std::fs;
use std::io::Read as _;
use zeroize::Zeroizing;
struct TempDb(std::path::PathBuf);
impl TempDb {
fn new(stem: &str) -> Self {
let p = std::env::temp_dir().join(format!(
"chisel_enc_test_{}_{}.db",
stem,
std::process::id()
));
let _ = fs::remove_file(&p);
TempDb(p)
}
fn path(&self) -> &std::path::Path {
&self.0
}
fn read_page0(&self) -> [u8; PAGE_SIZE] {
let mut f = fs::File::open(&self.0).unwrap();
let mut buf = [0u8; PAGE_SIZE];
f.read_exact(&mut buf).unwrap();
buf
}
}
impl Drop for TempDb {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
#[test]
fn create_encrypted_db_stamps_major_2() {
let tmp = TempDb::new("major2_raw");
let key = Key::Raw(Zeroizing::new(vec![0xAB_u8; 32]));
let db = crate::Chisel::open(tmp.path(), Options::default().encryption_key(key))
.expect("create encrypted db");
drop(db);
let page0 = tmp.read_page0();
let fv = u32::from_le_bytes(page0[4..8].try_into().unwrap());
assert_eq!(
format_major(fv),
FORMAT_MAJOR_VERSION_ENCRYPTED,
"format_version MAJOR must be 2 for encrypted DB; got {fv:#010x}"
);
}
#[test]
fn create_encrypted_db_passphrase_stamps_major_2() {
let tmp = TempDb::new("major2_pass");
let key = Key::Passphrase(Zeroizing::new("hunter2".to_string()));
let db = crate::Chisel::open(tmp.path(), Options::default().encryption_key(key))
.expect("create encrypted db passphrase");
drop(db);
let page0 = tmp.read_page0();
let fv = u32::from_le_bytes(page0[4..8].try_into().unwrap());
assert_eq!(format_major(fv), FORMAT_MAJOR_VERSION_ENCRYPTED);
}
#[test]
fn create_encrypted_db_populates_slot_0_only() {
let tmp = TempDb::new("slot0");
let key = Key::Raw(Zeroizing::new(vec![0x77_u8; 32]));
let db =
crate::Chisel::open(tmp.path(), Options::default().encryption_key(key)).expect("create");
drop(db);
let page0 = tmp.read_page0();
assert_eq!(
page0[CRYPTO_HEADER_OFFSET], ALGO_XCHACHA20POLY1305,
"algorithm byte must be 1 (XChaCha20-Poly1305)"
);
let slot_table_offset = CRYPTO_HEADER_OFFSET + 8;
assert_eq!(
page0[slot_table_offset], 1,
"slot 0 state must be active (1)"
);
for i in 1..KEY_SLOT_COUNT {
let base = slot_table_offset + i * KEY_SLOT_SIZE;
assert_eq!(page0[base], 0, "slot {i} must be empty");
}
}
#[test]
fn create_encrypted_db_sealed_body_is_present() {
const SEALED_BODY_OFFSET: usize = 1356;
let tmp = TempDb::new("cleartext_check");
let key = Key::Raw(Zeroizing::new(vec![0xCC_u8; 32]));
let db =
crate::Chisel::open(tmp.path(), Options::default().encryption_key(key)).expect("create");
drop(db);
let page0 = tmp.read_page0();
let nonce_region = &page0[SEALED_BODY_OFFSET..SEALED_BODY_OFFSET + 24];
assert!(
nonce_region.iter().any(|&b| b != 0),
"nonce region at SEALED_BODY_OFFSET is all-zero — body was not sealed"
);
}
#[test]
fn slot0_dek_unwraps_with_correct_key() {
let tmp = TempDb::new("unwrap");
let key = Key::Raw(Zeroizing::new(vec![0x5A_u8; 32]));
let db = crate::Chisel::open(tmp.path(), Options::default().encryption_key(key.clone()))
.expect("create");
drop(db);
let page0 = tmp.read_page0();
let header = CryptoHeader::deserialize(&page0)
.expect("page 0 must have a crypto-header for an encrypted DB");
assert_eq!(header.algorithm, ALGO_XCHACHA20POLY1305);
let slot = &header.slots[0];
assert!(slot.is_active(), "slot 0 must be active");
assert_eq!(slot.kdf_id, KdfId::Hkdf as u8, "raw key → HKDF");
let kek = derive_kek(&key, KdfId::Hkdf, &slot.salt, &slot.argon2)
.expect("derive_kek must succeed with the correct key");
let mut aad_slot = KeySlot::EMPTY;
aad_slot.state = slot.state;
aad_slot.kdf_id = slot.kdf_id;
aad_slot.argon2 = slot.argon2;
aad_slot.salt = slot.salt;
aad_slot.wrap_nonce = slot.wrap_nonce;
let aad = aad_slot.aad();
let dek = unwrap_dek(
&kek,
&slot.wrapped_dek,
&slot.wrap_tag,
&slot.wrap_nonce,
&aad,
)
.expect("unwrap_dek must succeed with the correct key and AAD");
assert_ne!(
dek.as_bytes(),
&[0u8; 32],
"unwrapped DEK must not be all zeros"
);
}
#[test]
fn slot0_dek_unwrap_fails_with_wrong_key() {
let tmp = TempDb::new("wrong_key");
let key = Key::Raw(Zeroizing::new(vec![0x5A_u8; 32]));
let db =
crate::Chisel::open(tmp.path(), Options::default().encryption_key(key)).expect("create");
drop(db);
let page0 = tmp.read_page0();
let header = CryptoHeader::deserialize(&page0).expect("crypto header");
let slot = &header.slots[0];
let wrong_key = Key::Raw(Zeroizing::new(vec![0xFF_u8; 32]));
let kek = derive_kek(&wrong_key, KdfId::Hkdf, &slot.salt, &slot.argon2).expect("derive_kek");
let mut aad_slot = KeySlot::EMPTY;
aad_slot.state = slot.state;
aad_slot.kdf_id = slot.kdf_id;
aad_slot.argon2 = slot.argon2;
aad_slot.salt = slot.salt;
aad_slot.wrap_nonce = slot.wrap_nonce;
let aad = aad_slot.aad();
assert!(
unwrap_dek(
&kek,
&slot.wrapped_dek,
&slot.wrap_tag,
&slot.wrap_nonce,
&aad
)
.is_err(),
"wrong key must fail DEK unwrap"
);
}