use hkdf::Hkdf;
use sha2::Sha256;
use crate::RealmId;
use crate::Result;
use crate::errors::PagedbError;
use super::keys::{DerivedKey, MasterKey};
const INFO_MASTER_PREFIX: &[u8] = b"pagedb/master/v1/";
const INFO_REALM_PREFIX: &[u8] = b"pagedb/realm/";
const INFO_INTEGRITY_PREFIX: &[u8] = b"pagedb/integrity/";
const INFO_FILE_INFIX: &[u8] = b"/file/";
const INFO_V2_SUFFIX: &[u8] = b"/v2";
const INFO_HEADER_MAC: &[u8] = b"pagedb/header-mac/v1";
fn scoped_info<const N: usize>(prefix: &[u8], realm_id: RealmId, file_id: &[u8; 16]) -> [u8; N] {
let mut info = [0u8; N];
let mut off = 0;
info[off..off + prefix.len()].copy_from_slice(prefix);
off += prefix.len();
info[off..off + 16].copy_from_slice(&realm_id.0);
off += 16;
info[off..off + INFO_FILE_INFIX.len()].copy_from_slice(INFO_FILE_INFIX);
off += INFO_FILE_INFIX.len();
info[off..off + 16].copy_from_slice(file_id);
off += 16;
info[off..off + INFO_V2_SUFFIX.len()].copy_from_slice(INFO_V2_SUFFIX);
info
}
const fn scoped_info_len(prefix_len: usize) -> usize {
prefix_len + 16 + INFO_FILE_INFIX.len() + 16 + INFO_V2_SUFFIX.len()
}
const DEK_INFO_LEN: usize = scoped_info_len(INFO_REALM_PREFIX.len());
const IK_INFO_LEN: usize = scoped_info_len(INFO_INTEGRITY_PREFIX.len());
pub fn derive_mk(kek: &[u8; 32], kek_salt: &[u8; 16], mk_epoch: u64) -> Result<MasterKey> {
let mut info = [0u8; INFO_MASTER_PREFIX.len() + 8];
info[..INFO_MASTER_PREFIX.len()].copy_from_slice(INFO_MASTER_PREFIX);
info[INFO_MASTER_PREFIX.len()..].copy_from_slice(&mk_epoch.to_le_bytes());
let hk = Hkdf::<Sha256>::new(Some(kek_salt), kek);
let mut out = [0u8; 32];
hk.expand(&info, &mut out)
.map_err(|_| PagedbError::Io(std::io::Error::other("hkdf expand failed (mk)")))?;
Ok(MasterKey::from_bytes(out))
}
pub fn derive_dek(mk: &MasterKey, realm_id: RealmId, file_id: &[u8; 16]) -> Result<DerivedKey> {
let info: [u8; DEK_INFO_LEN] = scoped_info(INFO_REALM_PREFIX, realm_id, file_id);
expand(mk.as_bytes(), &info)
}
pub fn derive_ik(mk: &MasterKey, realm_id: RealmId, file_id: &[u8; 16]) -> Result<DerivedKey> {
let info: [u8; IK_INFO_LEN] = scoped_info(INFO_INTEGRITY_PREFIX, realm_id, file_id);
expand(mk.as_bytes(), &info)
}
pub fn derive_hk(mk: &MasterKey) -> Result<DerivedKey> {
expand(mk.as_bytes(), INFO_HEADER_MAC)
}
pub fn derive_spill_key(
mk: &MasterKey,
file_id: &[u8; 16],
spill_epoch: &[u8; 16],
txn_seq: u64,
) -> Result<DerivedKey> {
const PREFIX: &[u8] = b"pagedb/spill/";
let mut info = [0u8; PREFIX.len() + 16 + 16 + 8];
let mut off = 0;
info[off..off + PREFIX.len()].copy_from_slice(PREFIX);
off += PREFIX.len();
info[off..off + 16].copy_from_slice(file_id);
off += 16;
info[off..off + 16].copy_from_slice(spill_epoch);
off += 16;
info[off..off + 8].copy_from_slice(&txn_seq.to_le_bytes());
expand(mk.as_bytes(), &info)
}
fn expand(ikm: &[u8; 32], info: &[u8]) -> Result<DerivedKey> {
let hk = Hkdf::<Sha256>::new(None, ikm);
let mut out = [0u8; 32];
hk.expand(info, &mut out)
.map_err(|_| PagedbError::Io(std::io::Error::other("hkdf expand failed")))?;
Ok(DerivedKey::from_bytes(out))
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed_kek() -> [u8; 32] {
let mut k = [0u8; 32];
for (i, b) in k.iter_mut().enumerate() {
*b = u8::try_from(i).unwrap();
}
k
}
#[test]
fn mk_is_deterministic() {
let salt = [0xAB; 16];
let kek = fixed_kek();
let a = derive_mk(&kek, &salt, 7).unwrap();
let b = derive_mk(&kek, &salt, 7).unwrap();
assert_eq!(a.as_bytes(), b.as_bytes());
}
#[test]
fn different_epoch_yields_different_mk() {
let salt = [0xAB; 16];
let kek = fixed_kek();
let a = derive_mk(&kek, &salt, 7).unwrap();
let b = derive_mk(&kek, &salt, 8).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn different_salt_yields_different_mk() {
let kek = fixed_kek();
let a = derive_mk(&kek, &[0xAB; 16], 7).unwrap();
let b = derive_mk(&kek, &[0xCD; 16], 7).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn spill_key_isolates_per_open_at_the_same_sequence() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let file_id = [0x5A; 16];
let a = derive_spill_key(&mk, &file_id, &[0x01; 16], 1).unwrap();
let b = derive_spill_key(&mk, &file_id, &[0x02; 16], 1).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn spill_key_isolates_per_sequence_within_one_open() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let epoch = [0x01; 16];
let a = derive_spill_key(&mk, &[0x5A; 16], &epoch, 1).unwrap();
let b = derive_spill_key(&mk, &[0x5A; 16], &epoch, 2).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn spill_key_isolates_per_store() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let epoch = [0x01; 16];
let a = derive_spill_key(&mk, &[0x5A; 16], &epoch, 1).unwrap();
let b = derive_spill_key(&mk, &[0xA5; 16], &epoch, 1).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
const FILE_A: [u8; 16] = [0x5A; 16];
const FILE_B: [u8; 16] = [0xA5; 16];
#[test]
fn dek_isolates_per_realm() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let a = derive_dek(&mk, RealmId([0x11; 16]), &FILE_A).unwrap();
let b = derive_dek(&mk, RealmId([0x22; 16]), &FILE_A).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn dek_isolates_per_file_within_one_realm() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let realm = RealmId([0x11; 16]);
let a = derive_dek(&mk, realm, &FILE_A).unwrap();
let b = derive_dek(&mk, realm, &FILE_B).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn dek_isolates_files_sharing_a_nonce_prefix() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let realm = RealmId([0x11; 16]);
let mut shared_prefix = FILE_A;
shared_prefix[15] ^= 0xFF;
assert_eq!(FILE_A[..6], shared_prefix[..6]);
let a = derive_dek(&mk, realm, &FILE_A).unwrap();
let b = derive_dek(&mk, realm, &shared_prefix).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
#[test]
fn ik_isolates_per_realm_and_file() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let base = derive_ik(&mk, RealmId([0x11; 16]), &FILE_A).unwrap();
let other_realm = derive_ik(&mk, RealmId([0x22; 16]), &FILE_A).unwrap();
let other_file = derive_ik(&mk, RealmId([0x11; 16]), &FILE_B).unwrap();
assert_ne!(base.as_bytes(), other_realm.as_bytes());
assert_ne!(base.as_bytes(), other_file.as_bytes());
}
#[test]
fn ik_hk_are_distinct_from_dek() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let realm = RealmId([0x11; 16]);
let dek = derive_dek(&mk, realm, &FILE_A).unwrap();
let ik = derive_ik(&mk, realm, &FILE_A).unwrap();
let hk = derive_hk(&mk).unwrap();
assert_ne!(dek.as_bytes(), ik.as_bytes());
assert_ne!(dek.as_bytes(), hk.as_bytes());
assert_ne!(ik.as_bytes(), hk.as_bytes());
}
#[test]
fn realm_and_file_fields_are_not_interchangeable() {
let mk = derive_mk(&fixed_kek(), &[0; 16], 0).unwrap();
let a = derive_dek(&mk, RealmId(FILE_A), &FILE_B).unwrap();
let b = derive_dek(&mk, RealmId(FILE_B), &FILE_A).unwrap();
assert_ne!(a.as_bytes(), b.as_bytes());
}
}