use core::convert::TryFrom;
pub use crate::table::block::{BlockIdentity, BlockType};
pub const AAD_LEN: usize = 23;
pub const MAGIC_METADATA_LE: [u8; 4] = [0x50, 0x2A, 0x4D, 0x18];
pub const FORMAT_VERSION_V1: u8 = 0b0001;
pub const HEADER_BYTE_V1: u8 = FORMAT_VERSION_V1 << 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum SuiteId {
Aes256Gcm = 0x02,
ChaCha20Poly1305 = 0x03,
}
impl SuiteId {
#[must_use]
pub const fn nonce_len(self) -> usize {
match self {
Self::Aes256Gcm | Self::ChaCha20Poly1305 => 12,
}
}
#[must_use]
pub const fn as_byte(self) -> u8 {
self as u8
}
}
impl TryFrom<u8> for SuiteId {
type Error = u8;
fn try_from(value: u8) -> Result<Self, u8> {
match value {
0x02 => Ok(Self::Aes256Gcm),
0x03 => Ok(Self::ChaCha20Poly1305),
other => Err(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncryptionContext {
pub header_byte: u8,
pub key_epoch: u8,
pub suite_id: SuiteId,
pub compression_type: u8,
pub block_flags: u8,
}
impl EncryptionContext {
#[must_use]
pub const fn v1(
key_epoch: u8,
suite_id: SuiteId,
compression_type: u8,
block_flags: u8,
) -> Self {
Self {
header_byte: HEADER_BYTE_V1,
key_epoch,
suite_id,
compression_type,
block_flags,
}
}
}
#[must_use]
pub fn build(ctx: &EncryptionContext, identity: &BlockIdentity) -> [u8; AAD_LEN] {
let mut buf = [0u8; AAD_LEN];
buf[0..4].copy_from_slice(&MAGIC_METADATA_LE);
buf[4] = ctx.header_byte;
buf[5] = ctx.key_epoch;
buf[6] = u8::from(identity.block_type);
buf[7] = ctx.suite_id.as_byte();
buf[8..16].copy_from_slice(&identity.table_id.to_be_bytes());
buf[16] = ctx.compression_type;
buf[17..21].copy_from_slice(&identity.dict_id.to_be_bytes());
buf[21] = identity.window_log;
buf[22] = ctx.block_flags;
buf
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(table_id: u64, bt: BlockType) -> BlockIdentity {
BlockIdentity {
table_id,
block_type: bt,
dict_id: 0,
window_log: 0,
}
}
#[test]
fn aad_len_matches_spec() {
assert_eq!(AAD_LEN, 23);
}
#[test]
fn magic_bytes_are_little_endian_for_skippable_frame() {
assert_eq!(MAGIC_METADATA_LE, [0x50, 0x2A, 0x4D, 0x18]);
assert_eq!(u32::from_le_bytes(MAGIC_METADATA_LE), 0x184D_2A50);
}
#[test]
fn header_byte_v1_packs_version_in_high_nibble() {
assert_eq!(HEADER_BYTE_V1, 0x10);
assert_eq!(HEADER_BYTE_V1 >> 4, FORMAT_VERSION_V1);
assert_eq!(HEADER_BYTE_V1 & 0x0F, 0);
}
#[test]
fn suite_nonce_lengths_match_registry() {
assert_eq!(SuiteId::Aes256Gcm.nonce_len(), 12);
assert_eq!(SuiteId::ChaCha20Poly1305.nonce_len(), 12);
}
#[test]
fn suite_id_byte_round_trip() {
for suite in [SuiteId::Aes256Gcm, SuiteId::ChaCha20Poly1305] {
assert_eq!(SuiteId::try_from(suite.as_byte()), Ok(suite));
}
}
#[test]
fn suite_id_rejects_unknown_byte() {
for byte in [0u8, 1, 4, 0x10, 0xFF] {
assert_eq!(SuiteId::try_from(byte), Err(byte));
}
}
#[test]
fn aad_layout_is_byte_exact_for_a_concrete_block() {
let ctx = EncryptionContext::v1(
0x55, SuiteId::ChaCha20Poly1305,
3, 0x05, );
let identity = BlockIdentity {
table_id: 0x1112_1314_1516_1718,
block_type: BlockType::Index, dict_id: 0xDEAD_BEEF,
window_log: 21,
};
let aad = build(&ctx, &identity);
assert_eq!(&aad[0..4], &[0x50, 0x2A, 0x4D, 0x18]);
assert_eq!(aad[4], 0x10);
assert_eq!(aad[5], 0x55);
assert_eq!(aad[6], 1);
assert_eq!(aad[7], 0x03);
assert_eq!(&aad[8..16], &0x1112_1314_1516_1718u64.to_be_bytes());
assert_eq!(aad[16], 3);
assert_eq!(&aad[17..21], &0xDEAD_BEEFu32.to_be_bytes());
assert_eq!(aad[21], 21);
assert_eq!(aad[22], 0x05);
}
#[test]
fn aad_for_zero_identity_is_well_formed() {
let ctx = EncryptionContext::v1(0, SuiteId::Aes256Gcm, 0, 0);
let id = identity(0, BlockType::Data);
let aad = build(&ctx, &id);
assert_eq!(aad.len(), AAD_LEN);
assert_eq!(&aad[0..4], &MAGIC_METADATA_LE);
assert_eq!(aad[4], HEADER_BYTE_V1);
assert_eq!(aad[5], 0); assert_eq!(aad[6], 0); assert_eq!(aad[7], 0x02); assert!(aad[8..].iter().all(|&b| b == 0));
}
#[test]
fn aad_changes_when_block_type_changes() {
let ctx = EncryptionContext::v1(1, SuiteId::Aes256Gcm, 0, 0);
let a = build(&ctx, &identity(2, BlockType::Data));
let b = build(&ctx, &identity(2, BlockType::Index));
assert_ne!(a, b);
assert_eq!(&a[..6], &b[..6]);
assert_eq!(&a[7..], &b[7..]);
}
#[test]
fn aad_changes_when_block_flags_changes() {
let a = build(
&EncryptionContext::v1(1, SuiteId::Aes256Gcm, 0, 0),
&identity(2, BlockType::Data),
);
let b = build(
&EncryptionContext::v1(1, SuiteId::Aes256Gcm, 0, 0x01),
&identity(2, BlockType::Data),
);
assert_ne!(a, b);
assert_eq!(&a[..22], &b[..22]);
assert_ne!(a[22], b[22]);
}
}