use std::io::Read;
use crate::CryptoError;
use crate::crypto::mac::{HMAC_TAG_SIZE, hmac_sha3_256_parts, hmac_sha3_256_parts_verify};
use crate::error::{FormatDefect, UnsupportedVersion};
pub(crate) fn read_u16_be(bytes: &[u8], offset: usize) -> Result<u16, CryptoError> {
let chunk: &[u8; 2] = bytes
.get(offset..offset + size_of::<u16>())
.and_then(|s| s.try_into().ok())
.ok_or(CryptoError::InvalidFormat(FormatDefect::MalformedHeader))?;
Ok(u16::from_be_bytes(*chunk))
}
pub(crate) fn read_u32_be(bytes: &[u8], offset: usize) -> Result<u32, CryptoError> {
let chunk: &[u8; 4] = bytes
.get(offset..offset + size_of::<u32>())
.and_then(|s| s.try_into().ok())
.ok_or(CryptoError::InvalidFormat(FormatDefect::MalformedHeader))?;
Ok(u32::from_be_bytes(*chunk))
}
pub(crate) fn write_u16_be(bytes: &mut [u8], offset: usize, value: u16) {
bytes[offset..offset + size_of::<u16>()].copy_from_slice(&value.to_be_bytes());
}
pub(crate) fn write_u32_be(bytes: &mut [u8], offset: usize, value: u32) {
bytes[offset..offset + size_of::<u32>()].copy_from_slice(&value.to_be_bytes());
}
pub(crate) fn read_exact_or_truncated(
reader: &mut impl Read,
buf: &mut [u8],
) -> Result<(), CryptoError> {
reader.read_exact(buf).map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
CryptoError::InvalidFormat(FormatDefect::Truncated)
} else {
CryptoError::Io(e)
}
})
}
pub const MAGIC: [u8; 4] = [b'F', b'C', b'R', 0];
pub(crate) const MAGIC_SIZE: usize = MAGIC.len();
pub const FCR_FILE_VERSION: u8 = 0x01;
pub(crate) const KIND_ENCRYPTED: u8 = 0x45; pub(crate) const KIND_PRIVATE_KEY: u8 = 0x4B;
pub const ENCRYPTED_EXTENSION: &str = "fcr";
pub(crate) const PREFIX_SIZE: usize = 12;
pub(crate) const HEADER_LEN_MAX: u32 = 16_777_216;
pub(crate) const HEADER_LEN_LOCAL_CAP_DEFAULT: u32 = 1_048_576;
pub(crate) const HEADER_FIXED_SIZE: usize = 31;
pub(crate) const STREAM_NONCE_SIZE: usize = 19;
pub(crate) const RECIPIENT_COUNT_MAX: u16 = 4096;
pub(crate) const RECIPIENT_COUNT_LOCAL_CAP_DEFAULT: u16 = 64;
pub(crate) const EXT_LEN_MAX: u32 = 65_536;
pub(crate) const BODY_LEN_MAX: u32 = 16_777_216;
pub(crate) const BODY_LEN_LOCAL_CAP_DEFAULT: u32 = 8_192;
pub(crate) const HEADER_MAC_SIZE: usize = HMAC_TAG_SIZE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Kind {
Encrypted,
PrivateKey,
}
impl Kind {
pub(crate) const fn byte(self) -> u8 {
match self {
Self::Encrypted => KIND_ENCRYPTED,
Self::PrivateKey => KIND_PRIVATE_KEY,
}
}
pub(crate) const fn from_byte(byte: u8) -> Option<Self> {
match byte {
KIND_ENCRYPTED => Some(Self::Encrypted),
KIND_PRIVATE_KEY => Some(Self::PrivateKey),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum KeypairSuite {
V1,
}
impl KeypairSuite {
pub(crate) const fn public_key_version(self) -> u8 {
match self {
Self::V1 => 0x01,
}
}
pub(crate) const fn private_key_version(self) -> u8 {
match self {
Self::V1 => 0x01,
}
}
}
pub(crate) const WRITER_KEYPAIR_SUITE: KeypairSuite = KeypairSuite::V1;
pub(crate) const fn keypair_suite_is_supported(suite: KeypairSuite) -> bool {
match suite {
KeypairSuite::V1 => true,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum KeypairVersionRejection {
Reserved,
Older { version: u8 },
Newer { version: u8 },
}
fn keypair_suite_from_wire_version_with(
byte: u8,
suite_byte: impl Fn(KeypairSuite) -> u8,
) -> Result<KeypairSuite, KeypairVersionRejection> {
match byte {
0 => Err(KeypairVersionRejection::Reserved),
v if v == suite_byte(KeypairSuite::V1) => Ok(KeypairSuite::V1),
v if v < suite_byte(WRITER_KEYPAIR_SUITE) => {
Err(KeypairVersionRejection::Older { version: v })
}
v => Err(KeypairVersionRejection::Newer { version: v }),
}
}
pub(crate) fn keypair_suite_from_public_key_version(
byte: u8,
) -> Result<KeypairSuite, KeypairVersionRejection> {
keypair_suite_from_wire_version_with(byte, KeypairSuite::public_key_version)
}
pub(crate) fn keypair_suite_from_private_key_version(
byte: u8,
) -> Result<KeypairSuite, KeypairVersionRejection> {
keypair_suite_from_wire_version_with(byte, KeypairSuite::private_key_version)
}
const PREFIX_VERSION_OFFSET: usize = MAGIC_SIZE;
const PREFIX_KIND_OFFSET: usize = PREFIX_VERSION_OFFSET + 1;
const PREFIX_FLAGS_OFFSET: usize = PREFIX_KIND_OFFSET + 1;
const PREFIX_HEADER_LEN_OFFSET: usize = PREFIX_FLAGS_OFFSET + size_of::<u16>();
const _: () = assert!(PREFIX_HEADER_LEN_OFFSET + size_of::<u32>() == PREFIX_SIZE);
#[derive(Debug, Clone, Copy)]
pub(crate) struct Prefix {
pub(crate) version: u8,
pub(crate) kind: Kind,
pub(crate) prefix_flags: u16,
pub(crate) header_len: u32,
}
impl Prefix {
pub(crate) const fn for_encrypted(header_len: u32) -> Self {
Self {
version: FCR_FILE_VERSION,
kind: Kind::Encrypted,
prefix_flags: 0,
header_len,
}
}
pub(crate) fn validate(&self) -> Result<(), CryptoError> {
check_version(self.version)?;
check_prefix_flags(self.prefix_flags)?;
check_header_len(self.header_len)?;
Ok(())
}
pub(crate) fn to_bytes(self) -> [u8; PREFIX_SIZE] {
let mut out = [0u8; PREFIX_SIZE];
out[..MAGIC_SIZE].copy_from_slice(&MAGIC);
out[PREFIX_VERSION_OFFSET] = self.version;
out[PREFIX_KIND_OFFSET] = self.kind.byte();
write_u16_be(&mut out, PREFIX_FLAGS_OFFSET, self.prefix_flags);
write_u32_be(&mut out, PREFIX_HEADER_LEN_OFFSET, self.header_len);
out
}
pub(crate) fn parse(
bytes: &[u8; PREFIX_SIZE],
expected_kind: Kind,
) -> Result<Self, CryptoError> {
if bytes[..MAGIC_SIZE] != MAGIC {
return Err(CryptoError::InvalidFormat(FormatDefect::BadMagic));
}
let version = bytes[PREFIX_VERSION_OFFSET];
check_version(version)?;
let kind_byte = bytes[PREFIX_KIND_OFFSET];
let kind = Kind::from_byte(kind_byte)
.filter(|k| *k == expected_kind)
.ok_or(CryptoError::InvalidFormat(FormatDefect::WrongKind {
kind: kind_byte,
}))?;
let prefix_flags = read_u16_be(bytes, PREFIX_FLAGS_OFFSET)?;
check_prefix_flags(prefix_flags)?;
let header_len = read_u32_be(bytes, PREFIX_HEADER_LEN_OFFSET)?;
check_header_len(header_len)?;
Ok(Self {
version,
kind,
prefix_flags,
header_len,
})
}
pub(crate) fn build_encrypted(header_len: u32) -> Result<[u8; PREFIX_SIZE], CryptoError> {
let prefix = Self::for_encrypted(header_len);
prefix.validate()?;
Ok(prefix.to_bytes())
}
}
fn check_version(version: u8) -> Result<(), CryptoError> {
if version != FCR_FILE_VERSION {
return Err(unsupported_file_version_error(version));
}
Ok(())
}
fn check_prefix_flags(flags: u16) -> Result<(), CryptoError> {
if flags != 0 {
return Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader));
}
Ok(())
}
fn check_header_len(header_len: u32) -> Result<(), CryptoError> {
if header_len > HEADER_LEN_MAX {
return Err(CryptoError::InvalidFormat(FormatDefect::OversizedHeader {
header_len,
}));
}
if (header_len as usize) < HEADER_FIXED_SIZE {
return Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader));
}
Ok(())
}
pub(crate) fn read_prefix_from_reader(
reader: &mut impl Read,
expected_kind: Kind,
) -> Result<([u8; PREFIX_SIZE], Prefix), CryptoError> {
let mut bytes = [0u8; PREFIX_SIZE];
reader.read_exact(&mut bytes).map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
CryptoError::InvalidFormat(FormatDefect::Truncated)
} else {
CryptoError::Io(e)
}
})?;
let prefix = Prefix::parse(&bytes, expected_kind)?;
Ok((bytes, prefix))
}
const HEADER_FIXED_FLAGS_OFFSET: usize = 0;
const HEADER_FIXED_RECIPIENT_COUNT_OFFSET: usize = HEADER_FIXED_FLAGS_OFFSET + size_of::<u16>();
const HEADER_FIXED_RECIPIENT_ENTRIES_LEN_OFFSET: usize =
HEADER_FIXED_RECIPIENT_COUNT_OFFSET + size_of::<u16>();
const HEADER_FIXED_EXT_LEN_OFFSET: usize =
HEADER_FIXED_RECIPIENT_ENTRIES_LEN_OFFSET + size_of::<u32>();
const HEADER_FIXED_STREAM_NONCE_OFFSET: usize = HEADER_FIXED_EXT_LEN_OFFSET + size_of::<u32>();
const _: () = assert!(HEADER_FIXED_STREAM_NONCE_OFFSET + STREAM_NONCE_SIZE == HEADER_FIXED_SIZE);
#[derive(Debug, Clone, Copy)]
pub(crate) struct HeaderFixed {
pub(crate) header_flags: u16,
pub(crate) recipient_count: u16,
pub(crate) recipient_entries_len: u32,
pub(crate) ext_len: u32,
pub(crate) stream_nonce: [u8; STREAM_NONCE_SIZE],
}
impl HeaderFixed {
pub(crate) fn to_bytes(self) -> [u8; HEADER_FIXED_SIZE] {
let mut out = [0u8; HEADER_FIXED_SIZE];
write_u16_be(&mut out, HEADER_FIXED_FLAGS_OFFSET, self.header_flags);
write_u16_be(
&mut out,
HEADER_FIXED_RECIPIENT_COUNT_OFFSET,
self.recipient_count,
);
write_u32_be(
&mut out,
HEADER_FIXED_RECIPIENT_ENTRIES_LEN_OFFSET,
self.recipient_entries_len,
);
write_u32_be(&mut out, HEADER_FIXED_EXT_LEN_OFFSET, self.ext_len);
out[HEADER_FIXED_STREAM_NONCE_OFFSET..HEADER_FIXED_SIZE]
.copy_from_slice(&self.stream_nonce);
out
}
pub(crate) fn parse(
bytes: &[u8; HEADER_FIXED_SIZE],
header_len: u32,
) -> Result<Self, CryptoError> {
let mut stream_nonce = [0u8; STREAM_NONCE_SIZE];
stream_nonce.copy_from_slice(&bytes[HEADER_FIXED_STREAM_NONCE_OFFSET..HEADER_FIXED_SIZE]);
let parsed = Self {
header_flags: read_u16_be(bytes, HEADER_FIXED_FLAGS_OFFSET)?,
recipient_count: read_u16_be(bytes, HEADER_FIXED_RECIPIENT_COUNT_OFFSET)?,
recipient_entries_len: read_u32_be(bytes, HEADER_FIXED_RECIPIENT_ENTRIES_LEN_OFFSET)?,
ext_len: read_u32_be(bytes, HEADER_FIXED_EXT_LEN_OFFSET)?,
stream_nonce,
};
parsed.validate_structural(header_len)?;
Ok(parsed)
}
pub(crate) fn validate_structural(&self, header_len: u32) -> Result<(), CryptoError> {
check_header_flags(self.header_flags)?;
check_recipient_count(self.recipient_count)?;
check_ext_len(self.ext_len)?;
check_header_section_lengths(self.recipient_entries_len, self.ext_len, header_len)?;
Ok(())
}
}
fn check_header_flags(flags: u16) -> Result<(), CryptoError> {
if flags != 0 {
return Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader));
}
Ok(())
}
fn check_recipient_count(count: u16) -> Result<(), CryptoError> {
if count == 0 || count > RECIPIENT_COUNT_MAX {
return Err(CryptoError::InvalidFormat(
FormatDefect::RecipientCountOutOfRange { count },
));
}
Ok(())
}
fn check_ext_len(ext_len: u32) -> Result<(), CryptoError> {
if ext_len > EXT_LEN_MAX {
return Err(CryptoError::InvalidFormat(FormatDefect::ExtTooLarge {
len: ext_len,
}));
}
Ok(())
}
fn check_header_section_lengths(
recipient_entries_len: u32,
ext_len: u32,
header_len: u32,
) -> Result<(), CryptoError> {
let computed = (HEADER_FIXED_SIZE as u64)
.checked_add(recipient_entries_len as u64)
.and_then(|v| v.checked_add(ext_len as u64))
.ok_or(CryptoError::InvalidFormat(FormatDefect::MalformedHeader))?;
if computed != header_len as u64 {
return Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader));
}
Ok(())
}
pub(crate) fn compute_header_mac(
prefix_bytes: &[u8; PREFIX_SIZE],
header_bytes: &[u8],
header_key: &crate::crypto::keys::HeaderKey,
) -> Result<[u8; HEADER_MAC_SIZE], CryptoError> {
hmac_sha3_256_parts(header_key.expose(), &[prefix_bytes, header_bytes])
}
pub(crate) fn verify_header_mac(
prefix_bytes: &[u8; PREFIX_SIZE],
header_bytes: &[u8],
header_key: &crate::crypto::keys::HeaderKey,
tag: &[u8; HEADER_MAC_SIZE],
) -> Result<(), CryptoError> {
hmac_sha3_256_parts_verify(header_key.expose(), &[prefix_bytes, header_bytes], tag)
}
pub(crate) fn unsupported_file_version_error(version: u8) -> CryptoError {
if version == 0 {
return CryptoError::InvalidFormat(FormatDefect::MalformedHeader);
}
if version < FCR_FILE_VERSION {
CryptoError::UnsupportedVersion(UnsupportedVersion::OlderFile { version })
} else {
CryptoError::UnsupportedVersion(UnsupportedVersion::NewerFile { version })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::mac::HMAC_KEY_SIZE;
#[test]
fn keypair_suite_v1_maps_to_canonical_wire_bytes() {
assert_eq!(KeypairSuite::V1.private_key_version(), 0x01);
assert_eq!(KeypairSuite::V1.public_key_version(), 0x01);
}
#[test]
fn writer_keypair_suite_is_v1() {
assert_eq!(WRITER_KEYPAIR_SUITE, KeypairSuite::V1);
}
#[test]
fn keypair_suite_support_gate_accepts_v1() {
assert!(keypair_suite_is_supported(KeypairSuite::V1));
}
#[test]
fn writer_public_key_version_maps_to_writer_suite() {
assert_eq!(
keypair_suite_from_public_key_version(WRITER_KEYPAIR_SUITE.public_key_version())
.unwrap(),
WRITER_KEYPAIR_SUITE,
);
}
#[test]
fn writer_private_key_version_maps_to_writer_suite() {
assert_eq!(
keypair_suite_from_private_key_version(WRITER_KEYPAIR_SUITE.private_key_version())
.unwrap(),
WRITER_KEYPAIR_SUITE,
);
}
#[test]
fn keypair_reverse_mappers_share_structural_rejection_classes() {
assert_eq!(
keypair_suite_from_public_key_version(0x00),
Err(KeypairVersionRejection::Reserved),
);
assert_eq!(
keypair_suite_from_private_key_version(0x00),
Err(KeypairVersionRejection::Reserved),
);
let above_writer_public = WRITER_KEYPAIR_SUITE
.public_key_version()
.checked_add(1)
.expect("writer public-key byte cannot be 0xFF in v1");
assert_eq!(
keypair_suite_from_public_key_version(above_writer_public),
Err(KeypairVersionRejection::Newer {
version: above_writer_public,
}),
);
let above_writer_private = WRITER_KEYPAIR_SUITE
.private_key_version()
.checked_add(1)
.expect("writer private-key byte cannot be 0xFF in v1");
assert_eq!(
keypair_suite_from_private_key_version(above_writer_private),
Err(KeypairVersionRejection::Newer {
version: above_writer_private,
}),
);
}
#[test]
fn kind_round_trips_through_byte() {
for variant in [Kind::Encrypted, Kind::PrivateKey] {
assert_eq!(Kind::from_byte(variant.byte()), Some(variant));
}
assert_eq!(Kind::Encrypted.byte(), KIND_ENCRYPTED);
assert_eq!(Kind::PrivateKey.byte(), KIND_PRIVATE_KEY);
}
#[test]
fn kind_from_unknown_byte_returns_none() {
assert_eq!(Kind::from_byte(0x00), None);
assert_eq!(Kind::from_byte(0x53), None); assert_eq!(Kind::from_byte(0xFF), None);
}
#[test]
fn prefix_round_trips_for_encrypted_kind() {
let prefix = Prefix::for_encrypted(200);
let bytes = prefix.to_bytes();
let parsed = Prefix::parse(&bytes, Kind::Encrypted).unwrap();
assert_eq!(parsed.version, FCR_FILE_VERSION);
assert_eq!(parsed.kind, Kind::Encrypted);
assert_eq!(parsed.prefix_flags, 0);
assert_eq!(parsed.header_len, 200);
}
#[test]
fn prefix_wire_format_has_magic_at_offset_0() {
let prefix = Prefix {
header_len: 0xAABBCCDD,
..Prefix::for_encrypted(0)
};
let bytes = prefix.to_bytes();
assert_eq!(&bytes[0..4], b"FCR\0");
assert_eq!(bytes[4], FCR_FILE_VERSION);
assert_eq!(bytes[5], KIND_ENCRYPTED);
assert_eq!(&bytes[6..8], &[0, 0]);
assert_eq!(&bytes[8..12], &[0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn prefix_rejects_bad_magic() {
let mut bytes = Prefix::build_encrypted(200).unwrap();
bytes[0] = 0;
match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::BadMagic)) => {}
other => panic!("expected BadMagic, got {other:?}"),
}
}
#[test]
fn prefix_rejects_unsupported_version() {
let mut bytes = Prefix::build_encrypted(200).unwrap();
bytes[4] = 3;
match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::UnsupportedVersion(UnsupportedVersion::NewerFile { version: 3 })) => {}
other => panic!("expected NewerFile(3), got {other:?}"),
}
}
#[test]
fn prefix_rejects_wrong_kind() {
let bytes = Prefix::build_encrypted(200).unwrap();
match Prefix::parse(&bytes, Kind::PrivateKey) {
Err(CryptoError::InvalidFormat(FormatDefect::WrongKind { kind })) => {
assert_eq!(kind, KIND_ENCRYPTED);
}
other => panic!("expected WrongKind, got {other:?}"),
}
}
#[test]
fn prefix_rejects_unknown_kind_byte() {
let mut bytes = Prefix::build_encrypted(200).unwrap();
bytes[5] = 0x53; match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::WrongKind { kind })) => {
assert_eq!(kind, 0x53);
}
other => panic!("expected WrongKind, got {other:?}"),
}
}
#[test]
fn prefix_rejects_non_zero_flags() {
let mut bytes = Prefix::build_encrypted(200).unwrap();
bytes[6] = 1;
match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader)) => {}
other => panic!("expected MalformedHeader, got {other:?}"),
}
}
#[test]
fn prefix_rejects_oversized_header_len_at_parse() {
let oversized = Prefix {
header_len: HEADER_LEN_MAX + 1,
..Prefix::for_encrypted(0)
};
let bytes = oversized.to_bytes();
match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::OversizedHeader { header_len })) => {
assert_eq!(header_len, HEADER_LEN_MAX + 1);
}
other => panic!("expected OversizedHeader, got {other:?}"),
}
}
#[test]
fn build_encrypted_rejects_oversized_header_len() {
match Prefix::build_encrypted(HEADER_LEN_MAX + 1) {
Err(CryptoError::InvalidFormat(FormatDefect::OversizedHeader { header_len })) => {
assert_eq!(header_len, HEADER_LEN_MAX + 1);
}
other => panic!("expected OversizedHeader from writer side, got {other:?}"),
}
}
#[test]
fn build_encrypted_rejects_undersized_header_len() {
match Prefix::build_encrypted((HEADER_FIXED_SIZE as u32) - 1) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader)) => {}
other => panic!("expected MalformedHeader from writer side, got {other:?}"),
}
}
#[test]
fn prefix_rejects_header_len_below_header_fixed_size() {
let too_small = Prefix {
header_len: (HEADER_FIXED_SIZE as u32) - 1,
..Prefix::for_encrypted(0)
};
let bytes = too_small.to_bytes();
match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader)) => {}
other => panic!("expected MalformedHeader, got {other:?}"),
}
}
#[test]
fn build_encrypted_accepts_header_len_at_lower_boundary() {
let bytes = Prefix::build_encrypted(HEADER_FIXED_SIZE as u32).unwrap();
let parsed = Prefix::parse(&bytes, Kind::Encrypted).unwrap();
assert_eq!(parsed.header_len, HEADER_FIXED_SIZE as u32);
}
#[test]
fn build_encrypted_accepts_header_len_at_upper_boundary() {
let bytes = Prefix::build_encrypted(HEADER_LEN_MAX).unwrap();
let parsed = Prefix::parse(&bytes, Kind::Encrypted).unwrap();
assert_eq!(parsed.header_len, HEADER_LEN_MAX);
}
#[test]
fn parse_prefers_unsupported_version_over_wrong_kind() {
let mut bytes = Prefix::build_encrypted(HEADER_FIXED_SIZE as u32).unwrap();
bytes[4] = 3; bytes[5] = 0x99; match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::UnsupportedVersion(UnsupportedVersion::NewerFile { version: 3 })) => {}
other => panic!("expected NewerFile(3) before WrongKind, got {other:?}"),
}
}
#[test]
fn header_fixed_round_trips() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: 1,
recipient_entries_len: 100,
ext_len: 0,
stream_nonce: [0xAB; STREAM_NONCE_SIZE],
};
let bytes = hf.to_bytes();
let parsed = HeaderFixed::parse(&bytes, HEADER_FIXED_SIZE as u32 + 100).unwrap();
assert_eq!(parsed.header_flags, 0);
assert_eq!(parsed.recipient_count, 1);
assert_eq!(parsed.recipient_entries_len, 100);
assert_eq!(parsed.ext_len, 0);
assert_eq!(parsed.stream_nonce, [0xAB; STREAM_NONCE_SIZE]);
}
#[test]
fn header_fixed_rejects_non_zero_flags() {
let hf = HeaderFixed {
header_flags: 0x0001,
recipient_count: 1,
recipient_entries_len: 100,
ext_len: 0,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
match HeaderFixed::parse(&hf.to_bytes(), HEADER_FIXED_SIZE as u32 + 100) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader)) => {}
other => panic!("expected MalformedHeader, got {other:?}"),
}
}
#[test]
fn header_fixed_rejects_zero_recipient_count() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: 0,
recipient_entries_len: 100,
ext_len: 0,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
match HeaderFixed::parse(&hf.to_bytes(), HEADER_FIXED_SIZE as u32 + 100) {
Err(CryptoError::InvalidFormat(FormatDefect::RecipientCountOutOfRange {
count: 0,
})) => {}
other => panic!("expected RecipientCountOutOfRange(0), got {other:?}"),
}
}
#[test]
fn header_fixed_rejects_excessive_recipient_count() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: RECIPIENT_COUNT_MAX + 1,
recipient_entries_len: 100,
ext_len: 0,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
let expected = RECIPIENT_COUNT_MAX + 1;
match HeaderFixed::parse(&hf.to_bytes(), HEADER_FIXED_SIZE as u32 + 100) {
Err(CryptoError::InvalidFormat(FormatDefect::RecipientCountOutOfRange { count }))
if count == expected => {}
other => panic!("expected RecipientCountOutOfRange({expected}), got {other:?}"),
}
}
#[test]
fn header_fixed_rejects_oversized_ext_len() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: 1,
recipient_entries_len: 100,
ext_len: EXT_LEN_MAX + 1,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
let total = HEADER_FIXED_SIZE as u32 + 100 + (EXT_LEN_MAX + 1);
match HeaderFixed::parse(&hf.to_bytes(), total) {
Err(CryptoError::InvalidFormat(FormatDefect::ExtTooLarge { len }))
if len == EXT_LEN_MAX + 1 => {}
other => panic!("expected ExtTooLarge({}), got {other:?}", EXT_LEN_MAX + 1),
}
}
#[test]
fn header_fixed_accepts_recipient_count_at_upper_boundary() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: RECIPIENT_COUNT_MAX,
recipient_entries_len: 100,
ext_len: 0,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
let parsed = HeaderFixed::parse(&hf.to_bytes(), HEADER_FIXED_SIZE as u32 + 100).unwrap();
assert_eq!(parsed.recipient_count, RECIPIENT_COUNT_MAX);
}
#[test]
fn header_fixed_accepts_ext_len_at_upper_boundary() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: 1,
recipient_entries_len: 0,
ext_len: EXT_LEN_MAX,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
let total = HEADER_FIXED_SIZE as u32 + EXT_LEN_MAX;
let parsed = HeaderFixed::parse(&hf.to_bytes(), total).unwrap();
assert_eq!(parsed.ext_len, EXT_LEN_MAX);
}
#[test]
fn read_prefix_distinguishes_eof_as_truncated() {
let truncated: &[u8] = &[b'F', b'C', b'R', 0, FCR_FILE_VERSION];
let mut cur = std::io::Cursor::new(truncated);
match read_prefix_from_reader(&mut cur, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::Truncated)) => {}
other => panic!("expected Truncated for short read, got {other:?}"),
}
}
#[test]
fn read_prefix_propagates_non_eof_io_errors() {
struct PermissionDenied;
impl std::io::Read for PermissionDenied {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"test",
))
}
}
let mut reader = PermissionDenied;
match read_prefix_from_reader(&mut reader, Kind::Encrypted) {
Err(CryptoError::Io(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied);
}
other => panic!("expected Io(PermissionDenied), got {other:?}"),
}
}
#[test]
fn read_exact_or_truncated_maps_unexpected_eof_to_truncated() {
let mut reader: &[u8] = &[0u8; 3];
let mut buf = [0u8; 8];
match read_exact_or_truncated(&mut reader, &mut buf) {
Err(CryptoError::InvalidFormat(FormatDefect::Truncated)) => {}
other => panic!("expected Truncated for short read, got {other:?}"),
}
}
#[test]
fn read_exact_or_truncated_propagates_non_eof_io_errors() {
struct PermissionDenied;
impl std::io::Read for PermissionDenied {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"test",
))
}
}
let mut reader = PermissionDenied;
let mut buf = [0u8; 8];
match read_exact_or_truncated(&mut reader, &mut buf) {
Err(CryptoError::Io(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied);
}
other => panic!("expected Io(PermissionDenied), got {other:?}"),
}
}
#[test]
fn prefix_rejects_reserved_zero_version_as_malformed() {
let mut bytes = Prefix::build_encrypted(HEADER_FIXED_SIZE as u32).unwrap();
bytes[4] = 0;
match Prefix::parse(&bytes, Kind::Encrypted) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader)) => {}
other => panic!("expected MalformedHeader for reserved 0x00, got {other:?}"),
}
}
#[test]
fn header_fixed_rejects_inconsistent_lengths() {
let hf = HeaderFixed {
header_flags: 0,
recipient_count: 1,
recipient_entries_len: 100,
ext_len: 0,
stream_nonce: [0; STREAM_NONCE_SIZE],
};
match HeaderFixed::parse(&hf.to_bytes(), 200) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader)) => {}
other => panic!("expected MalformedHeader, got {other:?}"),
}
}
fn header_mac_fixture() -> ([u8; PREFIX_SIZE], Vec<u8>, crate::crypto::keys::HeaderKey) {
let prefix = Prefix::build_encrypted(200).unwrap();
let header = vec![0xCDu8; 200];
let key = crate::crypto::keys::HeaderKey::from_bytes_for_tests([0xABu8; HMAC_KEY_SIZE]);
(prefix, header, key)
}
#[test]
fn header_mac_round_trips() {
let (prefix, header, key) = header_mac_fixture();
let tag = compute_header_mac(&prefix, &header, &key).unwrap();
verify_header_mac(&prefix, &header, &key, &tag).unwrap();
}
#[test]
fn header_mac_is_deterministic() {
let (prefix, header, key) = header_mac_fixture();
let a = compute_header_mac(&prefix, &header, &key).unwrap();
let b = compute_header_mac(&prefix, &header, &key).unwrap();
assert_eq!(a, b);
}
#[test]
fn header_mac_rejects_tampered_prefix() {
let (mut prefix, header, key) = header_mac_fixture();
let tag = compute_header_mac(&prefix, &header, &key).unwrap();
prefix[8] ^= 0x01;
match verify_header_mac(&prefix, &header, &key, &tag) {
Err(CryptoError::HeaderTampered) => {}
other => panic!("expected HeaderTampered for prefix tamper, got {other:?}"),
}
}
#[test]
fn header_mac_rejects_tampered_header() {
let (prefix, mut header, key) = header_mac_fixture();
let tag = compute_header_mac(&prefix, &header, &key).unwrap();
header[10] ^= 0x01;
match verify_header_mac(&prefix, &header, &key, &tag) {
Err(CryptoError::HeaderTampered) => {}
other => panic!("expected HeaderTampered for header tamper, got {other:?}"),
}
}
#[test]
fn header_mac_rejects_tampered_tag() {
let (prefix, header, key) = header_mac_fixture();
let mut tag = compute_header_mac(&prefix, &header, &key).unwrap();
tag[0] ^= 0x01;
match verify_header_mac(&prefix, &header, &key, &tag) {
Err(CryptoError::HeaderTampered) => {}
other => panic!("expected HeaderTampered for tag tamper, got {other:?}"),
}
}
#[test]
fn header_mac_rejects_wrong_key() {
let (prefix, header, key) = header_mac_fixture();
let tag = compute_header_mac(&prefix, &header, &key).unwrap();
let mut other_bytes = *key.expose();
other_bytes[0] ^= 0x01;
let other_key = crate::crypto::keys::HeaderKey::from_bytes_for_tests(other_bytes);
match verify_header_mac(&prefix, &header, &other_key, &tag) {
Err(CryptoError::HeaderTampered) => {}
other => panic!("expected HeaderTampered for wrong key, got {other:?}"),
}
}
#[test]
fn header_mac_input_is_prefix_then_header_in_order() {
let (prefix, header, key) = header_mac_fixture();
let tag = compute_header_mac(&prefix, &header, &key).unwrap();
let mut swapped_prefix = prefix;
let mut swapped_header = header.clone();
std::mem::swap(&mut swapped_prefix[0], &mut swapped_header[0]);
let swapped_tag = compute_header_mac(&swapped_prefix, &swapped_header, &key).unwrap();
assert_ne!(tag, swapped_tag);
}
#[test]
fn header_mac_binds_recipient_entry_order() {
let prefix = Prefix::build_encrypted(200).unwrap();
let key = crate::crypto::keys::HeaderKey::from_bytes_for_tests([0x77u8; HMAC_KEY_SIZE]);
let entry_a = [0x11u8; 50];
let entry_b = [0x22u8; 50];
let header_ab: Vec<u8> = entry_a.iter().chain(entry_b.iter()).copied().collect();
let header_ba: Vec<u8> = entry_b.iter().chain(entry_a.iter()).copied().collect();
let mac_ab = compute_header_mac(&prefix, &header_ab, &key).unwrap();
let mac_ba = compute_header_mac(&prefix, &header_ba, &key).unwrap();
assert_ne!(mac_ab, mac_ba);
}
#[test]
fn header_mac_size_matches_hmac_tag_size() {
assert_eq!(HEADER_MAC_SIZE, HMAC_TAG_SIZE);
assert_eq!(HEADER_MAC_SIZE, 32);
}
}