citadeldb-core 0.8.0

Core types, errors, and constants for Citadel encrypted database
Documentation
// File magic number: 0xC17AD3EL ("CITADEL" without vowels)
pub const MAGIC: u32 = 0xC17A_D3E1;
pub const FORMAT_VERSION: u32 = 1;

// Key file magic: "KEYS" in ASCII
pub const KEY_FILE_MAGIC: u32 = 0x4B45_5953;
pub const KEY_FILE_VERSION: u32 = 1;

pub const PAGE_SIZE: usize = 8208; // IV(16) + ciphertext(8160) + MAC(32)
pub const BODY_SIZE: usize = 8160; // decrypted page body
pub const IV_SIZE: usize = 16;
pub const MAC_SIZE: usize = 32; // HMAC-SHA256

pub const PAGE_HEADER_SIZE: usize = 64;
pub const USABLE_SIZE: usize = BODY_SIZE - PAGE_HEADER_SIZE; // 8096 bytes for cells
pub const CHECKSUM_SIZE: usize = 8; // xxHash64

pub const FILE_HEADER_SIZE: usize = 512;
pub const GOD_BYTE_OFFSET: usize = 20;
pub const FILE_ID_OFFSET: usize = 24;
pub const COMMIT_SLOT_OFFSET: usize = 32;
pub const COMMIT_SLOT_SIZE: usize = 240;

pub const GOD_BIT_ACTIVE_SLOT: u8 = 0x01; // bit 0: active commit slot (0 or 1)
pub const GOD_BIT_RECOVERY: u8 = 0x02; // bit 1: recovery_required flag

pub const KEY_SIZE: usize = 32; // AES-256 key = 32 bytes
pub const REK_SIZE: usize = 32; // Root Encryption Key
pub const DEK_SIZE: usize = 32; // Data Encryption Key
pub const MAC_KEY_SIZE: usize = 32; // HMAC key
pub const WRAPPED_KEY_SIZE: usize = 40; // AES-KW(32B key) = 40B (32 + 8 integrity)
pub const ARGON2_SALT_SIZE: usize = 16;

pub const KEY_FILE_SIZE: usize = 172;

// HKDF info strings for domain separation.
pub const HKDF_INFO_DEK: &[u8] = b"citadel-dek-v1";
pub const HKDF_INFO_MAC_KEY: &[u8] = b"citadel-mac-key-v1";
pub const HKDF_INFO_KEYFILE_MAC: &[u8] = b"citadel-keyfile-mac";
pub const HKDF_INFO_KMS_MASTER: &[u8] = b"citadel-master-key";
pub const HKDF_KMS_SALT: &[u8] = b"citadel-v1";

pub const PBKDF2_MIN_ITERATIONS: u32 = 600_000;

pub const KEY_BACKUP_MAGIC: u32 = 0x4B45_5942; // "KEYB"
pub const KEY_BACKUP_VERSION: u32 = 1;
pub const KEY_BACKUP_SIZE: usize = 124;
pub const HKDF_INFO_BACKUP_MAC: &[u8] = b"citadel-backup-mac";

pub const MAX_KEY_SIZE: usize = 2048;
pub const MAX_INLINE_VALUE_SIZE: usize = 1920;
pub const OVERFLOW_THRESHOLD: usize = MAX_INLINE_VALUE_SIZE;

pub const PENDING_FREE_ENTRY_SIZE: usize = 12; // page_id(4) + freed_at_txn(8)
pub const PENDING_FREE_ENTRIES_PER_PAGE: usize = USABLE_SIZE / PENDING_FREE_ENTRY_SIZE; // 674

// Merkle hash: BLAKE3 truncated to 28 bytes (224 bits) to fit inline in the page header.
pub const MERKLE_HASH_SIZE: usize = 28;
pub const MERKLE_HASH_OFFSET: usize = 36; // page header offset [36..64]
pub const SLOT_MERKLE_ROOT: usize = 84; // CommitSlot offset [84..112]

pub const AUDIT_LOG_MAGIC: u32 = 0x4155_4454; // "AUDT"
pub const AUDIT_LOG_VERSION: u32 = 1;
pub const AUDIT_HEADER_SIZE: usize = 64;
pub const AUDIT_ENTRY_MAGIC: u32 = 0x454E_5452; // "ENTR" - per-entry sentinel for scanning past corruption
pub const HKDF_INFO_AUDIT_KEY: &[u8] = b"citadel-audit-key-v1";

pub const DEFAULT_BUFFER_POOL_SIZE: usize = 256; // pages (2 MiB decrypted)

// Commit slot field offsets (relative to slot start).
pub const SLOT_TXN_ID: usize = 0;
pub const SLOT_TREE_ROOT: usize = 8;
pub const SLOT_TREE_DEPTH: usize = 12;
pub const SLOT_TREE_ENTRIES: usize = 16;
pub const SLOT_CATALOG_ROOT: usize = 24;
pub const SLOT_TOTAL_PAGES: usize = 28;
pub const SLOT_HIGH_WATER_MARK: usize = 32;
pub const SLOT_PENDING_FREE_ROOT: usize = 36;
pub const SLOT_ENCRYPTION_EPOCH: usize = 40;
pub const SLOT_DEK_ID: usize = 44;
pub const SLOT_CHECKSUM: usize = 76;

// Named table entries in CommitSlot [112..240], 18 bytes each
pub const SLOT_NAMED_ENTRIES: usize = 112;
pub const SLOT_NAMED_ENTRY_SIZE: usize = 18;
pub const SLOT_NAMED_MAX_ENTRIES: usize =
    (COMMIT_SLOT_SIZE - SLOT_NAMED_ENTRIES - 2) / SLOT_NAMED_ENTRY_SIZE;

pub const GROWTH_CHUNK_1MB: u64 = 1024 * 1024;
pub const GROWTH_CHUNK_4MB: u64 = 4 * 1024 * 1024;
pub const GROWTH_CHUNK_16MB: u64 = 16 * 1024 * 1024;
pub const GROWTH_THRESHOLD_4MB: u64 = 4 * 1024 * 1024;
pub const GROWTH_THRESHOLD_64MB: u64 = 64 * 1024 * 1024;
pub const GROWTH_THRESHOLD_1GB: u64 = 1024 * 1024 * 1024;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn page_size_consistency() {
        assert_eq!(PAGE_SIZE, IV_SIZE + BODY_SIZE + MAC_SIZE);
    }

    #[test]
    fn usable_size_consistency() {
        assert_eq!(USABLE_SIZE, BODY_SIZE - PAGE_HEADER_SIZE);
        assert_eq!(USABLE_SIZE, 8096);
    }

    #[test]
    fn two_cells_per_page_invariant() {
        // 2 * max_cell_size must fit in USABLE_SIZE for splits
        let max_branch_cell = 4 + 2 + MAX_KEY_SIZE;
        let max_leaf_cell = 2 + 4 + MAX_KEY_SIZE + 1 + MAX_INLINE_VALUE_SIZE;
        let max_cell = max_branch_cell.max(max_leaf_cell);
        assert!(2 * max_cell <= USABLE_SIZE, "2 cells must fit in one page");
    }

    #[test]
    fn file_header_fits() {
        let needed = COMMIT_SLOT_OFFSET + 2 * COMMIT_SLOT_SIZE;
        assert!(
            needed <= FILE_HEADER_SIZE,
            "commit slots must fit in header"
        );
    }

    #[test]
    fn pending_free_entries_per_page() {
        assert_eq!(PENDING_FREE_ENTRIES_PER_PAGE, 674);
    }
}