use std::fs;
use std::io;
use std::mem;
use std::path::Path;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::fsprg::FSPRG_RECOMMENDED_SEEDLEN;
use zeroize::Zeroizing;
use crate::def::{
Header, ObjectType, OBJECT_HEADER_SIZE,
ENTRY_OBJECT_HEADER_SIZE, FIELD_OBJECT_HEADER_SIZE,
};
use crate::fsprg;
use crate::writer::data_payload_offset;
type HmacSha256 = Hmac<Sha256>;
pub const FSS_HEADER_SIGNATURE: [u8; 8] = *b"KSHHRHLP";
pub const TAG_LENGTH: usize = 32;
#[repr(C, packed)]
#[derive(Debug, Clone, Copy)]
pub struct FssHeader {
pub signature: [u8; 8], pub compatible_flags: [u8; 4], pub incompatible_flags: [u8; 4], pub machine_id: [u8; 16], pub boot_id: [u8; 16], pub header_size: [u8; 8], pub start_usec: [u8; 8], pub interval_usec: [u8; 8], pub fsprg_secpar: [u8; 2], pub reserved: [u8; 6], pub fsprg_state_size: [u8; 8], }
const _: () = assert!(mem::size_of::<FssHeader>() == 88);
pub struct JournalHmac {
hmac: Option<HmacSha256>,
running: bool,
}
pub struct FssState {
pub header: FssHeader,
pub fsprg_state: Zeroizing<Vec<u8>>,
pub start_usec: u64,
pub interval_usec: u64,
}
pub fn journal_file_hmac_setup() -> JournalHmac {
JournalHmac {
hmac: None,
running: false,
}
}
pub fn journal_file_hmac_start(state: &mut JournalHmac, fsprg_state: &[u8]) {
if state.running {
return;
}
let key = fsprg::get_key(fsprg_state, 32, 0);
state.hmac = Some(
HmacSha256::new_from_slice(&key).expect("HMAC-SHA256 accepts any key size"),
);
state.running = true;
}
pub fn journal_file_hmac_put_header(state: &mut JournalHmac, header: &Header) {
let hmac = match state.hmac.as_mut() {
Some(h) => h,
None => return,
};
let base = header as *const Header as *const u8;
let off_state = offset_of_state();
hmac.update(unsafe { std::slice::from_raw_parts(base, off_state) });
let off_file_id = offset_of_file_id();
let off_tail_entry_boot_id = offset_of_tail_entry_boot_id();
hmac.update(unsafe {
std::slice::from_raw_parts(base.add(off_file_id), off_tail_entry_boot_id - off_file_id)
});
let off_seqnum_id = offset_of_seqnum_id();
let off_arena_size = offset_of_arena_size();
hmac.update(unsafe {
std::slice::from_raw_parts(base.add(off_seqnum_id), off_arena_size - off_seqnum_id)
});
let off_data_hash_table_offset = offset_of_data_hash_table_offset();
let off_tail_object_offset = offset_of_tail_object_offset();
hmac.update(unsafe {
std::slice::from_raw_parts(
base.add(off_data_hash_table_offset),
off_tail_object_offset - off_data_hash_table_offset,
)
});
}
pub fn journal_file_hmac_put_object(
state: &mut JournalHmac,
obj_type: ObjectType,
obj_data: &[u8],
_offset: u64,
compact: bool,
) -> Result<(), &'static str> {
let hmac = match state.hmac.as_mut() {
Some(h) => h,
None => return Ok(()),
};
if obj_data.len() < OBJECT_HEADER_SIZE {
return Err("object too small for ObjectHeader");
}
let embedded_type = obj_data[0];
if obj_type != ObjectType::Unused && embedded_type != obj_type as u8 {
return Err("object type mismatch in HMAC put");
}
hmac.update(&obj_data[..OBJECT_HEADER_SIZE]);
let obj_size = obj_data.len();
let dispatch_type = if obj_type == ObjectType::Unused {
match embedded_type {
1 => ObjectType::Data,
2 => ObjectType::Field,
3 => ObjectType::Entry,
4 => ObjectType::DataHashTable,
5 => ObjectType::FieldHashTable,
6 => ObjectType::EntryArray,
7 => ObjectType::Tag,
_ => return Err("unknown or unused embedded object type"),
}
} else {
obj_type
};
match dispatch_type {
ObjectType::Data => {
let payload_off = data_payload_offset(compact) as usize;
if obj_size < payload_off {
return Err("DATA object too small");
}
hmac.update(&obj_data[OBJECT_HEADER_SIZE..OBJECT_HEADER_SIZE + 8]);
if obj_size > payload_off {
hmac.update(&obj_data[payload_off..]);
}
}
ObjectType::Field => {
if obj_size < FIELD_OBJECT_HEADER_SIZE {
return Err("FIELD object too small");
}
hmac.update(&obj_data[OBJECT_HEADER_SIZE..OBJECT_HEADER_SIZE + 8]);
if obj_size > FIELD_OBJECT_HEADER_SIZE {
hmac.update(&obj_data[FIELD_OBJECT_HEADER_SIZE..]);
}
}
ObjectType::Entry => {
if obj_size < ENTRY_OBJECT_HEADER_SIZE {
return Err("ENTRY object too small");
}
hmac.update(&obj_data[OBJECT_HEADER_SIZE..]);
}
ObjectType::DataHashTable | ObjectType::FieldHashTable | ObjectType::EntryArray => {
}
ObjectType::Tag => {
let tag_fixed_end = OBJECT_HEADER_SIZE + 8 + 8; if obj_size < tag_fixed_end {
return Err("TAG object too small");
}
hmac.update(&obj_data[OBJECT_HEADER_SIZE..tag_fixed_end]);
}
ObjectType::Unused => {
unreachable!("Unused dispatch handled above");
}
}
Ok(())
}
pub fn journal_file_append_tag(
state: &mut JournalHmac,
tag_object: &[u8],
) -> [u8; TAG_LENGTH] {
let hmac = state
.hmac
.as_mut()
.expect("journal_file_append_tag called without active HMAC");
let tag_fixed_end = OBJECT_HEADER_SIZE + 8 + 8; assert!(
tag_object.len() >= tag_fixed_end,
"tag_object too small for TAG header + seqnum + epoch"
);
hmac.update(&tag_object[..OBJECT_HEADER_SIZE]);
hmac.update(&tag_object[OBJECT_HEADER_SIZE..tag_fixed_end]);
let hmac = state.hmac.take().unwrap();
let result = hmac.finalize();
let tag_bytes = result.into_bytes();
state.running = false;
let mut tag = [0u8; TAG_LENGTH];
tag.copy_from_slice(&tag_bytes);
tag
}
pub fn journal_file_fss_load(path: &Path) -> io::Result<FssState> {
let data = fs::read(path)?;
if data.len() < mem::size_of::<FssHeader>() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "FSS file too small"));
}
let header: FssHeader =
unsafe { std::ptr::read_unaligned(data.as_ptr() as *const FssHeader) };
if header.signature != FSS_HEADER_SIGNATURE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FSS signature mismatch",
));
}
let incompat = u32::from_le_bytes(header.incompatible_flags);
if incompat != 0 {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"unsupported FSS incompatible flags",
));
}
let header_size = u64::from_le_bytes(header.header_size);
if header_size < mem::size_of::<FssHeader>() as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FSS header_size too small",
));
}
let secpar = u16::from_le_bytes(header.fsprg_secpar);
let fsprg_state_size = u64::from_le_bytes(header.fsprg_state_size);
if fsprg_state_size != fsprg::stateinbytes(secpar as u32) as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FSPRG state size mismatch",
));
}
let total = header_size + fsprg_state_size;
if (data.len() as u64) < total {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FSS file truncated",
));
}
let start_usec = u64::from_le_bytes(header.start_usec);
let interval_usec = u64::from_le_bytes(header.interval_usec);
if start_usec == 0 || interval_usec == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FSS start_usec or interval_usec is zero",
));
}
#[cfg(target_os = "linux")]
{
if let Ok(s) = fs::read_to_string("/etc/machine-id") {
let trimmed = s.trim().replace('-', "");
if trimmed.len() == 32 {
let mut local_id = [0u8; 16];
let mut valid = true;
for i in 0..16 {
if let Ok(b) = u8::from_str_radix(&trimmed[i * 2..i * 2 + 2], 16) {
local_id[i] = b;
} else {
valid = false;
break;
}
}
if valid && local_id != header.machine_id {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"FSS key file machine_id does not match this machine",
));
}
}
}
}
let state_start = header_size as usize;
let state_end = total as usize;
let fsprg_state = Zeroizing::new(data[state_start..state_end].to_vec());
Ok(FssState {
header,
fsprg_state,
start_usec,
interval_usec,
})
}
pub fn journal_file_parse_verification_key(
key_str: &str,
) -> Result<(Zeroizing<Vec<u8>>, u64, u64), &'static str> {
let slash_pos = key_str
.rfind('/')
.ok_or("verification key missing '/'")?;
let hex_part = &key_str[..slash_pos];
let tail = &key_str[slash_pos + 1..];
let seed_size = FSPRG_RECOMMENDED_SEEDLEN;
let mut seed = Zeroizing::new(Vec::with_capacity(seed_size));
let mut chars = hex_part.chars().filter(|&c| c != '-');
for _ in 0..seed_size {
let hi = chars
.next()
.and_then(|c| c.to_digit(16))
.ok_or("invalid hex in seed")? as u8;
let lo = chars
.next()
.and_then(|c| c.to_digit(16))
.ok_or("invalid hex in seed")? as u8;
seed.push(hi * 16 + lo);
}
if chars.next().is_some() {
return Err("trailing characters after hex seed");
}
let dash_pos = tail.find('-').ok_or("missing '-' in epoch/interval")?;
let start = u64::from_str_radix(&tail[..dash_pos], 16)
.map_err(|_| "invalid start epoch hex")?;
let interval = u64::from_str_radix(&tail[dash_pos + 1..], 16)
.map_err(|_| "invalid interval hex")?;
let start_usec = start * interval;
let interval_usec = interval;
Ok((seed, start_usec, interval_usec))
}
pub fn journal_file_get_epoch(
start_usec: u64,
interval_usec: u64,
realtime: u64,
) -> Result<u64, &'static str> {
if start_usec == 0 || interval_usec == 0 {
return Err("FSS not configured");
}
if realtime < start_usec {
return Err("realtime before FSS start");
}
Ok((realtime - start_usec) / interval_usec)
}
pub fn journal_file_fsprg_evolve<F>(
fss: &mut FssState,
goal_epoch: u64,
mut tag_cb: F,
) -> Result<(), &'static str>
where
F: FnMut(&[u8], u64) -> Result<(), &'static str>,
{
loop {
let epoch = fsprg::get_epoch(&fss.fsprg_state);
if epoch > goal_epoch {
return Err("FSPRG epoch ahead of goal (stale)");
}
if epoch == goal_epoch {
return Ok(());
}
fsprg::evolve(&mut fss.fsprg_state);
let new_epoch = fsprg::get_epoch(&fss.fsprg_state);
if new_epoch < goal_epoch {
tag_cb(&fss.fsprg_state, new_epoch)?;
}
}
}
pub fn journal_file_fsprg_seek(
fss: &mut FssState,
goal: u64,
seed: &[u8],
) {
let current = fsprg::get_epoch(&fss.fsprg_state);
if goal == current {
return;
}
if goal == current + 1 {
fsprg::evolve(&mut fss.fsprg_state);
return;
}
let secpar = u16::from_le_bytes(fss.header.fsprg_secpar) as u32;
let (msk, _mpk) = fsprg::gen_mk(Some(seed), secpar);
fsprg::seek(&mut fss.fsprg_state, goal, &msk, seed);
}
pub fn journal_file_maybe_append_tag(
fss: &FssState,
realtime: u64,
) -> Result<Option<u64>, &'static str> {
let goal = journal_file_get_epoch(fss.start_usec, fss.interval_usec, realtime)?;
let current = fsprg::get_epoch(&fss.fsprg_state);
if goal <= current {
return Ok(None);
}
Ok(Some(current))
}
#[inline]
const fn offset_of_state() -> usize {
8 + 4 + 4
}
#[inline]
const fn offset_of_file_id() -> usize {
offset_of_state() + 1 + 7
}
#[inline]
const fn offset_of_tail_entry_boot_id() -> usize {
offset_of_file_id() + 16 + 16
}
#[inline]
const fn offset_of_seqnum_id() -> usize {
offset_of_tail_entry_boot_id() + 16
}
#[inline]
const fn offset_of_arena_size() -> usize {
offset_of_seqnum_id() + 16 + 8
}
#[inline]
const fn offset_of_data_hash_table_offset() -> usize {
offset_of_arena_size() + 8
}
#[inline]
const fn offset_of_tail_object_offset() -> usize {
offset_of_data_hash_table_offset() + 8 + 8 + 8 + 8
}
const _: () = assert!(offset_of_state() == 16);
const _: () = assert!(offset_of_file_id() == 24);
const _: () = assert!(offset_of_tail_entry_boot_id() == 56);
const _: () = assert!(offset_of_seqnum_id() == 72);
const _: () = assert!(offset_of_arena_size() == 96);
const _: () = assert!(offset_of_data_hash_table_offset() == 104);
const _: () = assert!(offset_of_tail_object_offset() == 136);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fss_header_size() {
assert_eq!(mem::size_of::<FssHeader>(), 88);
}
#[test]
fn test_epoch_calculation() {
assert_eq!(journal_file_get_epoch(1000, 100, 1550).unwrap(), 5);
}
#[test]
fn test_epoch_before_start() {
assert!(journal_file_get_epoch(1000, 100, 500).is_err());
}
#[test]
fn test_parse_verification_key() {
let key = "0102030405060708090a0b0c/a-3e8";
let (seed, start_usec, interval_usec) = journal_file_parse_verification_key(key).unwrap();
assert_eq!(&*seed, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
assert_eq!(start_usec, 10000);
assert_eq!(interval_usec, 1000);
}
#[test]
fn test_hmac_roundtrip() {
let mut hmac = journal_file_hmac_setup();
let key = [0x42u8; 32];
hmac.hmac = Some(HmacSha256::new_from_slice(&key).unwrap());
hmac.running = true;
hmac.hmac.as_mut().unwrap().update(b"test data");
let tag_object = [0u8; 64];
let tag = journal_file_append_tag(&mut hmac, &tag_object);
assert_eq!(tag.len(), TAG_LENGTH);
assert!(!hmac.running);
}
}