use crate::crypto::{CryptoError, NONCE_LEN, TAG_LEN};
use crate::page::{self, MAGIC, PAGE_SIZE};
use std::fmt;
mod crypto_header;
#[allow(unused_imports)]
pub use crypto_header::{
CryptoHeader, KeySlot, ALGO_XCHACHA20POLY1305, CRYPTO_HEADER_OFFSET, CRYPTO_HEADER_SIZE,
KEY_SLOT_COUNT, KEY_SLOT_SIZE,
};
pub const MIN_SUPERBLOCKS: u32 = 2;
pub const MAX_SUPERBLOCKS: u32 = 16;
pub const DEFAULT_SUPERBLOCK_COUNT: u32 = 2;
const SUPERBLOCK_COUNT_OFFSET: usize = 308;
const ROOT_MEMBERSHIP_INDEX_OFFSET: usize = 312;
const FREEMAP_DEPTH_OFFSET: usize = 320;
pub const NAMED_ROOT_COUNT: usize = 8;
pub const NAMED_ROOT_NAME_LEN: usize = 24;
const NAMED_ROOT_ENTRY_SIZE: usize = NAMED_ROOT_NAME_LEN + 8;
const NAMED_ROOTS_OFFSET: usize = 52;
#[allow(dead_code)]
const NAMED_ROOTS_END: usize = NAMED_ROOTS_OFFSET + NAMED_ROOT_COUNT * NAMED_ROOT_ENTRY_SIZE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NamedRoot {
pub name: [u8; NAMED_ROOT_NAME_LEN],
pub handle: u64,
}
impl NamedRoot {
pub const EMPTY: NamedRoot = NamedRoot {
name: [0u8; NAMED_ROOT_NAME_LEN],
handle: 0,
};
pub fn is_empty(&self) -> bool {
self.name[0] == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[allow(clippy::enum_variant_names)]
pub enum SuperblockDefect {
BadChecksum,
BadMagic,
BadCount(u32), }
impl fmt::Display for SuperblockDefect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SuperblockDefect::BadChecksum => write!(f, "bad checksum"),
SuperblockDefect::BadMagic => write!(f, "bad magic"),
SuperblockDefect::BadCount(n) => write!(f, "bad superblock_count {n}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlotDefect {
pub slot: u32,
pub defect: SuperblockDefect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Superblock {
pub magic: u32,
pub format_version: u32,
pub txn_counter: u64,
pub root_handle_table_page: u64,
pub root_freemap_page: u64,
pub total_pages: u64,
pub next_handle: u64,
pub page_size: u32,
pub named_roots: [NamedRoot; NAMED_ROOT_COUNT],
pub superblock_count: u32,
pub root_membership_index_page: u64,
pub freemap_depth: u32,
pub encryption: Option<CryptoHeader>,
}
fn validate(buf: &[u8; PAGE_SIZE]) -> Result<(), SuperblockDefect> {
if !page::verify_checksum(buf) {
return Err(SuperblockDefect::BadChecksum);
}
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
if magic != MAGIC {
return Err(SuperblockDefect::BadMagic);
}
let count = u32::from_le_bytes(
buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.try_into()
.unwrap(),
);
if !(MIN_SUPERBLOCKS..=MAX_SUPERBLOCKS).contains(&count) {
return Err(SuperblockDefect::BadCount(count));
}
Ok(())
}
pub const SEALED_BODY_OFFSET: usize =
crypto_header::CRYPTO_HEADER_OFFSET + crypto_header::CRYPTO_HEADER_SIZE;
const BODY_LEN: usize = 8 * 5 + 4 + (NAMED_ROOT_COUNT * NAMED_ROOT_ENTRY_SIZE);
const _: () =
assert!(SEALED_BODY_OFFSET + NONCE_LEN + TAG_LEN + 2 + BODY_LEN <= page::CHECKSUM_OFFSET);
impl Superblock {
pub fn serialize(&self) -> [u8; PAGE_SIZE] {
let mut buf = [0u8; PAGE_SIZE];
buf[0..4].copy_from_slice(&self.magic.to_le_bytes());
buf[4..8].copy_from_slice(&self.format_version.to_le_bytes());
buf[8..16].copy_from_slice(&self.txn_counter.to_le_bytes());
buf[16..24].copy_from_slice(&self.root_handle_table_page.to_le_bytes());
buf[24..32].copy_from_slice(&self.root_freemap_page.to_le_bytes());
buf[32..40].copy_from_slice(&self.total_pages.to_le_bytes());
buf[40..48].copy_from_slice(&self.next_handle.to_le_bytes());
buf[48..52].copy_from_slice(&self.page_size.to_le_bytes());
for (i, entry) in self.named_roots.iter().enumerate() {
let base = NAMED_ROOTS_OFFSET + i * NAMED_ROOT_ENTRY_SIZE;
buf[base..base + NAMED_ROOT_NAME_LEN].copy_from_slice(&entry.name);
buf[base + NAMED_ROOT_NAME_LEN..base + NAMED_ROOT_NAME_LEN + 8]
.copy_from_slice(&entry.handle.to_le_bytes());
}
buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.copy_from_slice(&self.superblock_count.to_le_bytes());
buf[ROOT_MEMBERSHIP_INDEX_OFFSET..ROOT_MEMBERSHIP_INDEX_OFFSET + 8]
.copy_from_slice(&self.root_membership_index_page.to_le_bytes());
buf[FREEMAP_DEPTH_OFFSET..FREEMAP_DEPTH_OFFSET + 4]
.copy_from_slice(&self.freemap_depth.to_le_bytes());
page::stamp_checksum(&mut buf);
buf
}
pub fn sb_identity_aad(&self) -> [u8; 24] {
let mut a = [0u8; 24];
a[0..4].copy_from_slice(&self.magic.to_le_bytes());
a[4..8].copy_from_slice(&self.format_version.to_le_bytes());
a[8..16].copy_from_slice(&self.txn_counter.to_le_bytes());
a[16..20].copy_from_slice(&self.superblock_count.to_le_bytes());
a
}
fn body_plaintext(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(BODY_LEN);
b.extend_from_slice(&self.root_handle_table_page.to_le_bytes());
b.extend_from_slice(&self.root_freemap_page.to_le_bytes());
b.extend_from_slice(&self.root_membership_index_page.to_le_bytes());
b.extend_from_slice(&self.total_pages.to_le_bytes());
b.extend_from_slice(&self.next_handle.to_le_bytes());
b.extend_from_slice(&self.freemap_depth.to_le_bytes());
for entry in &self.named_roots {
b.extend_from_slice(&entry.name);
b.extend_from_slice(&entry.handle.to_le_bytes());
}
b
}
fn load_body(&mut self, body: &[u8]) {
debug_assert_eq!(body.len(), BODY_LEN);
self.root_handle_table_page = u64::from_le_bytes(body[0..8].try_into().unwrap());
self.root_freemap_page = u64::from_le_bytes(body[8..16].try_into().unwrap());
self.root_membership_index_page = u64::from_le_bytes(body[16..24].try_into().unwrap());
self.total_pages = u64::from_le_bytes(body[24..32].try_into().unwrap());
self.next_handle = u64::from_le_bytes(body[32..40].try_into().unwrap());
self.freemap_depth = u32::from_le_bytes(body[40..44].try_into().unwrap());
let mut off = 44;
for entry in self.named_roots.iter_mut() {
entry
.name
.copy_from_slice(&body[off..off + NAMED_ROOT_NAME_LEN]);
entry.handle = u64::from_le_bytes(
body[off + NAMED_ROOT_NAME_LEN..off + NAMED_ROOT_NAME_LEN + 8]
.try_into()
.unwrap(),
);
off += NAMED_ROOT_ENTRY_SIZE;
}
}
pub fn serialize_encrypted(&self, cipher: &crate::crypto::PageCipher) -> [u8; PAGE_SIZE] {
let header = self
.encryption
.as_ref()
.expect("serialize_encrypted requires Superblock.encryption = Some");
let mut buf = [0u8; PAGE_SIZE];
buf[0..4].copy_from_slice(&self.magic.to_le_bytes());
buf[4..8].copy_from_slice(&self.format_version.to_le_bytes());
buf[8..16].copy_from_slice(&self.txn_counter.to_le_bytes());
buf[48..52].copy_from_slice(&self.page_size.to_le_bytes());
buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.copy_from_slice(&self.superblock_count.to_le_bytes());
header.serialize_into(&mut buf);
let aad = self.sb_identity_aad();
let (nonce, tag, ct) = cipher.seal_body(&aad, &self.body_plaintext());
let base = SEALED_BODY_OFFSET;
buf[base..base + NONCE_LEN].copy_from_slice(&nonce);
buf[base + NONCE_LEN..base + NONCE_LEN + TAG_LEN].copy_from_slice(&tag);
buf[base + NONCE_LEN + TAG_LEN..base + NONCE_LEN + TAG_LEN + 2]
.copy_from_slice(&(ct.len() as u16).to_le_bytes());
let coff = base + NONCE_LEN + TAG_LEN + 2;
buf[coff..coff + ct.len()].copy_from_slice(&ct);
page::stamp_checksum(&mut buf);
buf
}
pub fn decrypt_body(
&mut self,
cipher: &crate::crypto::PageCipher,
raw: &[u8; PAGE_SIZE],
) -> Result<(), CryptoError> {
let base = SEALED_BODY_OFFSET;
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(&raw[base..base + NONCE_LEN]);
let mut tag = [0u8; TAG_LEN];
tag.copy_from_slice(&raw[base + NONCE_LEN..base + NONCE_LEN + TAG_LEN]);
let ct_len = u16::from_le_bytes(
raw[base + NONCE_LEN + TAG_LEN..base + NONCE_LEN + TAG_LEN + 2]
.try_into()
.unwrap(),
) as usize;
let coff = base + NONCE_LEN + TAG_LEN + 2;
if coff + ct_len > page::CHECKSUM_OFFSET {
return Err(CryptoError::Auth);
}
let ct = &raw[coff..coff + ct_len];
let aad = self.sb_identity_aad();
let body = cipher.open_body(&aad, &nonce, &tag, ct)?;
self.load_body(&body);
Ok(())
}
pub fn deserialize(buf: &[u8; PAGE_SIZE]) -> Option<Superblock> {
validate(buf).ok()?;
let encryption = crypto_header::CryptoHeader::deserialize(buf);
if encryption.is_some() {
let superblock_count = u32::from_le_bytes(
buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.try_into()
.unwrap(),
);
return Some(Superblock {
magic: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
format_version: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
txn_counter: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
root_handle_table_page: 0,
root_freemap_page: 0,
total_pages: 0,
next_handle: 0,
page_size: u32::from_le_bytes(buf[48..52].try_into().unwrap()),
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count,
root_membership_index_page: 0,
freemap_depth: 0,
encryption,
});
}
let mut named_roots = [NamedRoot::EMPTY; NAMED_ROOT_COUNT];
for (i, entry) in named_roots.iter_mut().enumerate() {
let base = NAMED_ROOTS_OFFSET + i * NAMED_ROOT_ENTRY_SIZE;
entry
.name
.copy_from_slice(&buf[base..base + NAMED_ROOT_NAME_LEN]);
entry.handle = u64::from_le_bytes(
buf[base + NAMED_ROOT_NAME_LEN..base + NAMED_ROOT_NAME_LEN + 8]
.try_into()
.unwrap(),
);
}
let superblock_count = u32::from_le_bytes(
buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.try_into()
.unwrap(),
);
Some(Superblock {
magic: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
format_version: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
txn_counter: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
root_handle_table_page: u64::from_le_bytes(buf[16..24].try_into().unwrap()),
root_freemap_page: u64::from_le_bytes(buf[24..32].try_into().unwrap()),
total_pages: u64::from_le_bytes(buf[32..40].try_into().unwrap()),
next_handle: u64::from_le_bytes(buf[40..48].try_into().unwrap()),
page_size: u32::from_le_bytes(buf[48..52].try_into().unwrap()),
named_roots,
superblock_count,
root_membership_index_page: u64::from_le_bytes(
buf[ROOT_MEMBERSHIP_INDEX_OFFSET..ROOT_MEMBERSHIP_INDEX_OFFSET + 8]
.try_into()
.unwrap(),
),
freemap_depth: u32::from_le_bytes(
buf[FREEMAP_DEPTH_OFFSET..FREEMAP_DEPTH_OFFSET + 4]
.try_into()
.unwrap(),
),
encryption: None,
})
}
#[cfg(test)]
pub fn select(buffers: &[[u8; PAGE_SIZE]]) -> Option<Superblock> {
buffers
.iter()
.filter_map(Superblock::deserialize)
.max_by_key(|sb| sb.txn_counter)
}
pub(crate) fn diagnose(buffers: &[[u8; PAGE_SIZE]]) -> Vec<SlotDefect> {
let bound = buffers
.iter()
.map(|b| {
u32::from_le_bytes(
b[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.try_into()
.unwrap(),
)
})
.find(|&n| (MIN_SUPERBLOCKS..=MAX_SUPERBLOCKS).contains(&n))
.unwrap_or(MIN_SUPERBLOCKS) as usize;
buffers[..bound.min(buffers.len())]
.iter()
.enumerate()
.filter_map(|(i, b)| {
validate(b).err().map(|defect| SlotDefect {
slot: i as u32,
defect,
})
})
.collect()
}
pub fn new_empty(superblock_count: u32) -> Superblock {
Superblock {
magic: MAGIC,
format_version: page::FORMAT_VERSION,
txn_counter: (superblock_count - 1) as u64,
root_handle_table_page: page::PAGE_ID_NONE,
root_freemap_page: page::PAGE_ID_NONE,
total_pages: superblock_count as u64,
next_handle: 1,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count,
root_membership_index_page: page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: None,
}
}
pub fn new_empty_encrypted(superblock_count: u32, header: CryptoHeader) -> Superblock {
Superblock {
magic: MAGIC,
format_version: page::format_version_encrypted(),
txn_counter: (superblock_count - 1) as u64,
root_handle_table_page: page::PAGE_ID_NONE,
root_freemap_page: page::PAGE_ID_NONE,
total_pages: superblock_count as u64,
next_handle: 1,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count,
root_membership_index_page: page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: Some(header),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prop_assert_eq;
#[test]
fn new_empty_reserves_handle_zero() {
let sb = Superblock::new_empty(2);
assert_eq!(sb.next_handle, 1);
}
#[test]
fn deserialize_rejects_out_of_range_superblock_count() {
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
for bogus in [0u32, 1, MAX_SUPERBLOCKS + 1, 1_000_000, u32::MAX] {
sb.superblock_count = bogus;
let mut buf = sb.serialize();
buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.copy_from_slice(&bogus.to_le_bytes());
page::stamp_checksum(&mut buf);
assert!(
Superblock::deserialize(&buf).is_none(),
"deserialize accepted out-of-range superblock_count = {bogus}"
);
}
}
#[test]
fn deserialize_accepts_all_valid_superblock_counts() {
for n in MIN_SUPERBLOCKS..=MAX_SUPERBLOCKS {
let sb = Superblock::new_empty(n);
let buf = sb.serialize();
let got = Superblock::deserialize(&buf).expect("valid count rejected");
assert_eq!(got.superblock_count, n);
}
}
#[test]
fn select_falls_back_when_one_slot_has_bad_count() {
let good = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
let good_buf = good.serialize();
let mut bad_buf = good.serialize();
bad_buf[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.copy_from_slice(&1_000_000u32.to_le_bytes());
page::stamp_checksum(&mut bad_buf);
let picked = Superblock::select(&[bad_buf, good_buf]).expect("no slot picked");
assert_eq!(picked.superblock_count, DEFAULT_SUPERBLOCK_COUNT);
}
#[test]
fn validate_classifies_each_defect() {
let good = Superblock::new_empty(2).serialize();
assert_eq!(validate(&good), Ok(()));
let mut bad_checksum = good;
bad_checksum[16] ^= 0xFF;
assert_eq!(validate(&bad_checksum), Err(SuperblockDefect::BadChecksum));
let mut bad_magic = good;
bad_magic[0] ^= 0xFF;
page::stamp_checksum(&mut bad_magic);
assert_eq!(validate(&bad_magic), Err(SuperblockDefect::BadMagic));
let mut bad_count = good;
bad_count[SUPERBLOCK_COUNT_OFFSET..SUPERBLOCK_COUNT_OFFSET + 4]
.copy_from_slice(&99u32.to_le_bytes());
page::stamp_checksum(&mut bad_count);
assert_eq!(validate(&bad_count), Err(SuperblockDefect::BadCount(99)));
}
#[test]
fn diagnose_reports_each_slots_defect() {
let good = Superblock::new_empty(2).serialize();
let mut slot0 = good;
slot0[16] ^= 0xFF;
let mut slot1 = good;
slot1[0] ^= 0xFF;
page::stamp_checksum(&mut slot1);
assert_eq!(
Superblock::diagnose(&[slot0, slot1]),
vec![
SlotDefect {
slot: 0,
defect: SuperblockDefect::BadChecksum
},
SlotDefect {
slot: 1,
defect: SuperblockDefect::BadMagic
},
]
);
}
#[test]
fn test_superblock_roundtrip() {
let sb = Superblock {
magic: MAGIC,
format_version: crate::page::FORMAT_VERSION,
txn_counter: 42,
root_handle_table_page: 5,
root_freemap_page: 8,
total_pages: 100,
next_handle: 50,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count: DEFAULT_SUPERBLOCK_COUNT,
root_membership_index_page: crate::page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: None,
};
let buf = sb.serialize();
let sb2 = Superblock::deserialize(&buf).unwrap();
assert_eq!(sb, sb2);
}
#[test]
fn test_superblock_checksum_validation() {
let sb = Superblock {
magic: MAGIC,
format_version: crate::page::FORMAT_VERSION,
txn_counter: 1,
root_handle_table_page: crate::page::PAGE_ID_NONE,
root_freemap_page: crate::page::PAGE_ID_NONE,
total_pages: 2,
next_handle: 0,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count: DEFAULT_SUPERBLOCK_COUNT,
root_membership_index_page: crate::page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: None,
};
let mut buf = sb.serialize();
buf[10] ^= 0xFF;
assert!(Superblock::deserialize(&buf).is_none());
}
#[test]
fn test_superblock_selection() {
let sb1 = Superblock {
magic: MAGIC,
format_version: crate::page::FORMAT_VERSION,
txn_counter: 5,
root_handle_table_page: 2,
root_freemap_page: 3,
total_pages: 10,
next_handle: 3,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count: DEFAULT_SUPERBLOCK_COUNT,
root_membership_index_page: crate::page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: None,
};
let sb2 = Superblock {
magic: MAGIC,
format_version: crate::page::FORMAT_VERSION,
txn_counter: 7,
root_handle_table_page: 4,
root_freemap_page: 5,
total_pages: 12,
next_handle: 5,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count: DEFAULT_SUPERBLOCK_COUNT,
root_membership_index_page: crate::page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: None,
};
let buf1 = sb1.serialize();
let buf2 = sb2.serialize();
let selected = Superblock::select(&[buf1, buf2]).unwrap();
assert_eq!(selected.txn_counter, 7);
}
#[test]
fn test_superblock_selection_with_one_corrupt() {
let sb1 = Superblock {
magic: MAGIC,
format_version: crate::page::FORMAT_VERSION,
txn_counter: 5,
root_handle_table_page: 2,
root_freemap_page: 3,
total_pages: 10,
next_handle: 3,
page_size: PAGE_SIZE as u32,
named_roots: [NamedRoot::EMPTY; NAMED_ROOT_COUNT],
superblock_count: DEFAULT_SUPERBLOCK_COUNT,
root_membership_index_page: crate::page::PAGE_ID_NONE,
freemap_depth: 0,
encryption: None,
};
let sb2_buf = [0u8; PAGE_SIZE];
let buf1 = sb1.serialize();
let selected = Superblock::select(&[buf1, sb2_buf]).unwrap();
assert_eq!(selected.txn_counter, 5);
}
#[test]
fn test_superblock_selection_both_corrupt() {
let buf1 = [0u8; PAGE_SIZE];
let buf2 = [0u8; PAGE_SIZE];
assert!(Superblock::select(&[buf1, buf2]).is_none());
}
#[test]
fn freemap_depth_round_trips_and_defaults_zero() {
let mut sb = Superblock::new_empty(2);
sb.root_freemap_page = 9;
sb.freemap_depth = 3;
let buf = sb.serialize();
let back = Superblock::deserialize(&buf).unwrap();
assert_eq!(back.freemap_depth, 3);
assert_eq!(back.root_freemap_page, 9);
let mut legacy = sb.serialize();
legacy[320..324].fill(0);
page::stamp_checksum(&mut legacy);
let back0 = Superblock::deserialize(&legacy).unwrap();
assert_eq!(back0.freemap_depth, 0);
}
#[test]
fn membership_root_round_trips_through_serialize() {
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
assert_eq!(sb.root_membership_index_page, page::PAGE_ID_NONE);
sb.root_membership_index_page = 1234;
let buf = sb.serialize();
let back = Superblock::deserialize(&buf).unwrap();
assert_eq!(back.root_membership_index_page, 1234);
let mut old = sb.serialize();
old[312..320].fill(0);
page::stamp_checksum(&mut old);
assert_eq!(
Superblock::deserialize(&old)
.unwrap()
.root_membership_index_page,
0
);
}
#[test]
fn encrypted_superblock_hides_sensitive_fields_and_round_trips() {
use crate::crypto::{random_dek, PageCipher};
let cipher = PageCipher::new(random_dek());
let mut header_slots = [KeySlot::EMPTY; KEY_SLOT_COUNT];
header_slots[0].state = 1; let header = CryptoHeader {
algorithm: ALGO_XCHACHA20POLY1305,
stride: 8232,
slots: header_slots,
};
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
sb.root_handle_table_page = 7;
sb.next_handle = 99;
sb.total_pages = 41;
sb.named_roots[0].name[..5].copy_from_slice(b"users");
sb.named_roots[0].handle = 12345;
sb.encryption = Some(header);
let buf = sb.serialize_encrypted(&cipher);
assert_eq!(
&buf[52..308],
&[0u8; 256][..],
"named_roots leaked in cleartext"
);
assert_eq!(&buf[16..48], &[0u8; 32][..], "sensitive scalars leaked");
assert_eq!(
&buf[312..324],
&[0u8; 12][..],
"membership/freemap_depth leaked"
);
assert_eq!(u32::from_le_bytes(buf[0..4].try_into().unwrap()), MAGIC);
assert_eq!(
u64::from_le_bytes(buf[8..16].try_into().unwrap()),
sb.txn_counter
);
let mut back = Superblock::deserialize(&buf).expect("encrypted sb deserializes");
assert!(
back.encryption.is_some(),
"encryption field must be populated"
);
assert_eq!(back.root_handle_table_page, 0, "not yet decrypted");
assert_eq!(back.next_handle, 0, "not yet decrypted");
back.decrypt_body(&cipher, &buf).expect("DEK opens body");
assert_eq!(back.root_handle_table_page, 7);
assert_eq!(back.next_handle, 99);
assert_eq!(back.total_pages, 41);
assert_eq!(&back.named_roots[0].name[..5], b"users");
assert_eq!(back.named_roots[0].handle, 12345);
}
#[test]
fn wrong_dek_fails_body_authentication() {
use crate::crypto::{random_dek, PageCipher};
let cipher = PageCipher::new(random_dek());
let mut header_slots = [KeySlot::EMPTY; KEY_SLOT_COUNT];
header_slots[0].state = 1;
let header = CryptoHeader {
algorithm: ALGO_XCHACHA20POLY1305,
stride: 8232,
slots: header_slots,
};
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
sb.encryption = Some(header);
let buf = sb.serialize_encrypted(&cipher);
let wrong = PageCipher::new(random_dek());
let mut back = Superblock::deserialize(&buf).unwrap();
assert!(back.decrypt_body(&wrong, &buf).is_err());
}
#[test]
fn forged_ct_len_returns_err_not_panic() {
use crate::crypto::{random_dek, PageCipher};
let cipher = PageCipher::new(random_dek());
let mut header_slots = [KeySlot::EMPTY; KEY_SLOT_COUNT];
header_slots[0].state = 1;
let header = CryptoHeader {
algorithm: ALGO_XCHACHA20POLY1305,
stride: 8232,
slots: header_slots,
};
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
sb.encryption = Some(header);
let mut buf = sb.serialize_encrypted(&cipher);
let len_off = SEALED_BODY_OFFSET + NONCE_LEN + TAG_LEN;
buf[len_off..len_off + 2].copy_from_slice(&0xFFFFu16.to_le_bytes());
page::stamp_checksum(&mut buf);
let mut back = Superblock::deserialize(&buf).unwrap();
assert!(back.decrypt_body(&cipher, &buf).is_err());
}
#[test]
fn encrypted_named_root_name_absent_from_cleartext() {
use crate::crypto::{random_dek, PageCipher};
let mut sentinel = [0u8; NAMED_ROOT_NAME_LEN];
sentinel[..16].copy_from_slice(b"secret-LEAKCHECK");
let cipher = PageCipher::new(random_dek());
let mut header_slots = [KeySlot::EMPTY; KEY_SLOT_COUNT];
header_slots[0].state = 1;
let header = CryptoHeader {
algorithm: ALGO_XCHACHA20POLY1305,
stride: 8232,
slots: header_slots,
};
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
sb.named_roots[0].name = sentinel;
sb.named_roots[0].handle = 0xDEAD_BEEF_CAFE_0001;
sb.encryption = Some(header);
let buf = sb.serialize_encrypted(&cipher);
assert_eq!(
&buf[52..308],
&[0u8; 256][..],
"named_roots region (52..308) is not zeroed in encrypted superblock"
);
let needle = &sentinel[..16];
assert!(
!buf.windows(needle.len()).any(|w| w == needle),
"sentinel name appears in cleartext page — named_root name leaked"
);
let mut back = Superblock::deserialize(&buf).expect("encrypted sb must deserialize");
back.decrypt_body(&cipher, &buf)
.expect("correct DEK must open body");
assert_eq!(
back.named_roots[0].name, sentinel,
"named_root name not recovered after decrypt_body"
);
assert_eq!(back.named_roots[0].handle, 0xDEAD_BEEF_CAFE_0001);
}
#[test]
fn plaintext_superblock_round_trips_unchanged() {
let mut sb = Superblock::new_empty(DEFAULT_SUPERBLOCK_COUNT);
sb.root_handle_table_page = 5;
sb.root_freemap_page = 6;
sb.total_pages = 20;
sb.next_handle = 3;
sb.named_roots[0].name[..4].copy_from_slice(b"test");
sb.named_roots[0].handle = 42;
let buf = sb.serialize();
let back = Superblock::deserialize(&buf).expect("plaintext must deserialize");
assert!(back.encryption.is_none());
assert_eq!(back.root_handle_table_page, 5);
assert_eq!(back.named_roots[0].handle, 42);
assert_eq!(&back.named_roots[0].name[..4], b"test");
}
proptest::proptest! {
#[test]
fn prop_serialize_deserialize_roundtrip(
txn_counter in 0u64..u64::MAX,
root_handle_table_page in 0u64..u64::MAX,
root_freemap_page in 0u64..u64::MAX,
total_pages in 0u64..u64::MAX,
next_handle in 0u64..u64::MAX,
superblock_count in MIN_SUPERBLOCKS..=MAX_SUPERBLOCKS,
named_root_bytes in proptest::array::uniform8(
proptest::array::uniform24(0u8..=255u8)
),
named_root_handles in proptest::array::uniform8(0u64..u64::MAX),
root_membership_index_page in 0u64..u64::MAX,
) {
let mut named_roots = [NamedRoot::EMPTY; NAMED_ROOT_COUNT];
for (i, slot) in named_roots.iter_mut().enumerate() {
slot.name = named_root_bytes[i];
slot.handle = named_root_handles[i];
}
let sb = Superblock {
magic: MAGIC,
format_version: crate::page::FORMAT_VERSION,
txn_counter,
root_handle_table_page,
root_freemap_page,
total_pages,
next_handle,
page_size: PAGE_SIZE as u32,
named_roots,
superblock_count,
root_membership_index_page,
freemap_depth: 0,
encryption: None,
};
let buf = sb.serialize();
let parsed = Superblock::deserialize(&buf)
.expect("a freshly-serialized superblock must deserialize");
prop_assert_eq!(parsed.magic, sb.magic);
prop_assert_eq!(parsed.format_version, sb.format_version);
prop_assert_eq!(parsed.txn_counter, sb.txn_counter);
prop_assert_eq!(parsed.root_handle_table_page, sb.root_handle_table_page);
prop_assert_eq!(parsed.root_freemap_page, sb.root_freemap_page);
prop_assert_eq!(parsed.total_pages, sb.total_pages);
prop_assert_eq!(parsed.next_handle, sb.next_handle);
prop_assert_eq!(parsed.page_size, sb.page_size);
prop_assert_eq!(parsed.superblock_count, sb.superblock_count);
prop_assert_eq!(parsed.root_membership_index_page, sb.root_membership_index_page);
prop_assert_eq!(parsed.freemap_depth, sb.freemap_depth);
prop_assert_eq!(parsed.encryption, sb.encryption);
for i in 0..NAMED_ROOT_COUNT {
prop_assert_eq!(parsed.named_roots[i].name, sb.named_roots[i].name);
prop_assert_eq!(parsed.named_roots[i].handle, sb.named_roots[i].handle);
}
}
}
}