use crate::checked::{array_16, read_u16_le, read_u32_le, read_u64_le};
use crate::constants::{HEADER_LEN, HEADER_MAGIC};
use crate::crypto::strong_checksum;
use crate::lockbox_id::LockboxId;
use crate::{ArtifactKind, Error, Result};
pub const LOCKBOX_FORMAT_VERSION: u16 = 1;
const HEADER_CHECKSUM_START: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LockboxHeader {
pub(crate) commit_root_offset: u64,
pub(crate) sequence: u64,
pub(crate) key_directory_offset: u64,
pub(crate) lockbox_id: LockboxId,
pub(crate) commit_auth_offset: u64,
}
pub(crate) fn write_header(
bytes: &mut Vec<u8>,
commit_root_offset: u64,
sequence: u64,
key_directory_offset: u64,
lockbox_id: LockboxId,
commit_auth_offset: u64,
) {
if bytes.len() < HEADER_LEN {
bytes.resize(HEADER_LEN, 0);
}
bytes[..HEADER_LEN].fill(0);
bytes[0..8].copy_from_slice(HEADER_MAGIC);
bytes[8..10].copy_from_slice(&LOCKBOX_FORMAT_VERSION.to_le_bytes());
bytes[12..16].copy_from_slice(&(HEADER_LEN as u32).to_le_bytes());
bytes[16..24].copy_from_slice(&commit_root_offset.to_le_bytes());
bytes[24..32].copy_from_slice(&sequence.to_le_bytes());
bytes[32..40].copy_from_slice(&key_directory_offset.to_le_bytes());
bytes[40..56].copy_from_slice(lockbox_id.as_bytes());
bytes[56..64].copy_from_slice(&commit_auth_offset.to_le_bytes());
let digest = strong_checksum(&bytes[0..HEADER_CHECKSUM_START]);
bytes[HEADER_CHECKSUM_START..HEADER_LEN].copy_from_slice(&digest);
}
pub(crate) fn read_header(bytes: &[u8]) -> Result<LockboxHeader> {
if bytes.len() < HEADER_LEN {
return Err(Error::Truncated);
}
if &bytes[0..8] != HEADER_MAGIC {
return Err(Error::CorruptHeader);
}
if read_u16_le(&bytes[10..12]).map_err(|_| Error::CorruptHeader)? != 0 {
return Err(Error::CorruptHeader);
}
if read_u32_le(&bytes[12..16]).map_err(|_| Error::CorruptHeader)? as usize != HEADER_LEN {
return Err(Error::CorruptHeader);
}
let expected = strong_checksum(&bytes[0..HEADER_CHECKSUM_START]);
if bytes[HEADER_CHECKSUM_START..HEADER_LEN] != expected {
return Err(Error::CorruptHeader);
}
let version = read_u16_le(&bytes[8..10]).map_err(|_| Error::CorruptHeader)?;
if version != LOCKBOX_FORMAT_VERSION {
return Err(Error::UnsupportedFormatVersion {
artifact: ArtifactKind::Lockbox,
found: u32::from(version),
supported: u32::from(LOCKBOX_FORMAT_VERSION),
});
}
let commit_root_offset = read_u64_le(&bytes[16..24]).map_err(|_| Error::CorruptHeader)?;
let sequence = read_u64_le(&bytes[24..32]).map_err(|_| Error::CorruptHeader)?;
let key_directory_offset = read_u64_le(&bytes[32..40]).map_err(|_| Error::CorruptHeader)?;
let lockbox_id =
LockboxId::from_bytes(array_16(&bytes[40..56]).map_err(|_| Error::CorruptHeader)?);
let commit_auth_offset = read_u64_le(&bytes[56..64]).map_err(|_| Error::CorruptHeader)?;
Ok(LockboxHeader {
commit_root_offset,
sequence,
key_directory_offset,
lockbox_id,
commit_auth_offset,
})
}
pub fn probe_lockbox_format_version(bytes: &[u8]) -> Result<u16> {
if bytes.len() < HEADER_LEN {
return Err(Error::Truncated);
}
if &bytes[0..8] != HEADER_MAGIC {
return Err(Error::CorruptHeader);
}
let expected = strong_checksum(&bytes[0..HEADER_CHECKSUM_START]);
if bytes[HEADER_CHECKSUM_START..HEADER_LEN] != expected {
return Err(Error::CorruptHeader);
}
read_u16_le(&bytes[8..10]).map_err(|_| Error::CorruptHeader)
}
#[cfg(feature = "vault-integration")]
pub fn read_lockbox_id(bytes: &[u8]) -> Result<LockboxId> {
Ok(read_header(bytes)?.lockbox_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_numeric_fields_are_little_endian() {
let lockbox_id = LockboxId::from_bytes([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
0xee, 0xff,
]);
let mut bytes = Vec::new();
write_header(
&mut bytes,
0x0102_0304_0506_0708,
0x1112_1314_1516_1718,
0x2122_2324_2526_2728,
lockbox_id,
0x3132_3334_3536_3738,
);
assert_eq!(&bytes[0..8], HEADER_MAGIC);
assert_eq!(&bytes[8..10], &[0x01, 0x00]);
assert_eq!(&bytes[12..16], &[0x60, 0x00, 0x00, 0x00]);
assert_eq!(
&bytes[16..24],
&[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
);
assert_eq!(
&bytes[24..32],
&[0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11]
);
assert_eq!(
&bytes[32..40],
&[0x28, 0x27, 0x26, 0x25, 0x24, 0x23, 0x22, 0x21]
);
assert_eq!(&bytes[40..56], lockbox_id.as_bytes());
assert_eq!(
&bytes[56..64],
&[0x38, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31]
);
assert_eq!(
read_header(&bytes).unwrap(),
LockboxHeader {
commit_root_offset: 0x0102_0304_0506_0708,
sequence: 0x1112_1314_1516_1718,
key_directory_offset: 0x2122_2324_2526_2728,
lockbox_id,
commit_auth_offset: 0x3132_3334_3536_3738,
}
);
}
#[test]
fn header_rejects_public_checksum_tampering() {
let lockbox_id = LockboxId::new_random().unwrap();
let mut bytes = Vec::new();
write_header(&mut bytes, 1, 2, 3, lockbox_id, 4);
bytes[16] ^= 0x01;
assert!(matches!(read_header(&bytes), Err(Error::CorruptHeader)));
}
#[test]
fn header_reports_valid_but_unsupported_format_separately_from_corruption() {
let lockbox_id = LockboxId::new_random().unwrap();
let mut bytes = Vec::new();
write_header(&mut bytes, 1, 2, 3, lockbox_id, 4);
bytes[8..10].copy_from_slice(&2u16.to_le_bytes());
let digest = strong_checksum(&bytes[0..HEADER_CHECKSUM_START]);
bytes[HEADER_CHECKSUM_START..HEADER_LEN].copy_from_slice(&digest);
assert_eq!(probe_lockbox_format_version(&bytes).unwrap(), 2);
assert!(matches!(
read_header(&bytes),
Err(Error::UnsupportedFormatVersion {
artifact: ArtifactKind::Lockbox,
found: 2,
supported: 1,
})
));
}
}