use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use crate::archive;
use crate::crypto::keys::{HeaderKey, PayloadKey};
use crate::crypto::stream::{STREAM_NONCE_SIZE, payload_encryptor};
use crate::error::{CryptoError, FormatDefect};
use crate::format::{
self, HEADER_FIXED_SIZE, HEADER_MAC_SIZE, HeaderFixed, Kind, PREFIX_SIZE, Prefix,
read_exact_or_truncated,
};
use crate::fs::atomic;
use crate::fs::paths::{INCOMPLETE_SUFFIX, parent_or_cwd, reject_occupied};
use crate::recipient::{self, RecipientEntry};
const TEMP_FILE_PREFIX: &str = ".ferrocrypt-";
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct HeaderReadLimits {
pub(crate) max_header_len: u32,
pub(crate) max_recipient_count: u16,
pub(crate) max_recipient_body_len: u32,
}
impl Default for HeaderReadLimits {
fn default() -> Self {
Self {
max_header_len: Self::HEADER_LEN_DEFAULT,
max_recipient_count: Self::RECIPIENT_COUNT_DEFAULT,
max_recipient_body_len: Self::RECIPIENT_BODY_LEN_DEFAULT,
}
}
}
impl HeaderReadLimits {
pub const HEADER_LEN_STRUCTURAL_MAX: u32 = format::HEADER_LEN_MAX;
pub const RECIPIENT_COUNT_STRUCTURAL_MAX: u16 = format::RECIPIENT_COUNT_MAX;
pub const RECIPIENT_BODY_LEN_STRUCTURAL_MAX: u32 = format::BODY_LEN_MAX;
pub const HEADER_LEN_DEFAULT: u32 = format::HEADER_LEN_LOCAL_CAP_DEFAULT;
pub const RECIPIENT_COUNT_DEFAULT: u16 = format::RECIPIENT_COUNT_LOCAL_CAP_DEFAULT;
pub const RECIPIENT_BODY_LEN_DEFAULT: u32 = format::BODY_LEN_LOCAL_CAP_DEFAULT;
pub fn max_header_len(mut self, value: u32) -> Self {
self.max_header_len = value.min(Self::HEADER_LEN_STRUCTURAL_MAX);
self
}
pub fn max_recipient_count(mut self, value: u16) -> Self {
self.max_recipient_count = value.min(Self::RECIPIENT_COUNT_STRUCTURAL_MAX);
self
}
pub fn max_recipient_body_len(mut self, value: u32) -> Self {
self.max_recipient_body_len = value.min(Self::RECIPIENT_BODY_LEN_STRUCTURAL_MAX);
self
}
pub(crate) fn enforce_header_len(&self, header_len: u32) -> Result<(), CryptoError> {
if header_len > self.max_header_len {
return Err(CryptoError::HeaderLenCapExceeded {
header_len,
local_cap: self.max_header_len,
});
}
Ok(())
}
pub(crate) fn enforce_recipient_count(&self, count: u16) -> Result<(), CryptoError> {
if count > self.max_recipient_count {
return Err(CryptoError::RecipientCountCapExceeded {
count,
local_cap: self.max_recipient_count,
});
}
Ok(())
}
pub(crate) fn enforce_recipient_body_len(&self, body_len: u32) -> Result<(), CryptoError> {
if body_len > self.max_recipient_body_len {
return Err(CryptoError::RecipientBodyCapExceeded {
body_len,
local_cap: self.max_recipient_body_len,
});
}
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct ParsedEncryptedHeader {
pub prefix_bytes: [u8; PREFIX_SIZE],
pub fixed: HeaderFixed,
pub header_bytes: Vec<u8>,
pub recipient_entries: Vec<RecipientEntry>,
pub ext_bytes: Vec<u8>,
pub header_mac: [u8; HEADER_MAC_SIZE],
}
pub(crate) struct BuiltEncryptedHeader {
pub prefix_bytes: [u8; PREFIX_SIZE],
pub header_bytes: Vec<u8>,
pub header_mac: [u8; HEADER_MAC_SIZE],
pub stream_nonce: [u8; STREAM_NONCE_SIZE],
pub payload_key: PayloadKey,
}
struct HexBytes<'a>(&'a [u8]);
impl std::fmt::Debug for HexBytes<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for b in self.0 {
write!(f, "{b:02x}")?;
}
Ok(())
}
}
impl std::fmt::Debug for BuiltEncryptedHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BuiltEncryptedHeader")
.field("prefix_bytes", &HexBytes(&self.prefix_bytes))
.field("header_bytes_len", &self.header_bytes.len())
.field("header_mac", &HexBytes(&self.header_mac))
.field("stream_nonce", &HexBytes(&self.stream_nonce))
.field("payload_key", &"<redacted>")
.finish()
}
}
pub(crate) fn read_encrypted_header<R: Read>(
reader: &mut R,
limits: HeaderReadLimits,
) -> Result<ParsedEncryptedHeader, CryptoError> {
let (prefix_bytes, prefix) = format::read_prefix_from_reader(reader, Kind::Encrypted)?;
limits.enforce_header_len(prefix.header_len)?;
let header_len = prefix.header_len as usize;
let mut header_bytes = vec![0u8; header_len];
read_exact_or_truncated(reader, &mut header_bytes)?;
let mut header_mac = [0u8; HEADER_MAC_SIZE];
read_exact_or_truncated(reader, &mut header_mac)?;
let fixed_bytes: &[u8; HEADER_FIXED_SIZE] = header_bytes
.first_chunk()
.ok_or(CryptoError::InvalidFormat(FormatDefect::MalformedHeader))?;
let fixed = HeaderFixed::parse(fixed_bytes, prefix.header_len)?;
limits.enforce_recipient_count(fixed.recipient_count)?;
let entries_start = HEADER_FIXED_SIZE;
let entries_end = entries_start
.checked_add(fixed.recipient_entries_len as usize)
.ok_or(CryptoError::InvalidFormat(FormatDefect::MalformedHeader))?;
let ext_end = entries_end
.checked_add(fixed.ext_len as usize)
.ok_or(CryptoError::InvalidFormat(FormatDefect::MalformedHeader))?;
if ext_end != header_len {
return Err(CryptoError::InvalidFormat(FormatDefect::MalformedHeader));
}
let recipient_entries = recipient::parse_recipient_entries(
&header_bytes[entries_start..entries_end],
fixed.recipient_count,
limits.max_recipient_body_len,
)?;
let ext_bytes = header_bytes[entries_end..ext_end].to_vec();
Ok(ParsedEncryptedHeader {
prefix_bytes,
fixed,
header_bytes,
recipient_entries,
ext_bytes,
header_mac,
})
}
pub(crate) fn build_encrypted_header(
recipient_entries: &[RecipientEntry],
ext_bytes: &[u8],
stream_nonce: [u8; STREAM_NONCE_SIZE],
payload_key: PayloadKey,
header_key: &HeaderKey,
) -> Result<BuiltEncryptedHeader, CryptoError> {
let mut entries_bytes = Vec::new();
for entry in recipient_entries {
entries_bytes.extend_from_slice(&entry.to_bytes_checked()?);
}
let recipient_count: u16 = recipient_entries.len().try_into().unwrap_or(u16::MAX);
let recipient_entries_len: u32 = entries_bytes.len().try_into().unwrap_or(u32::MAX);
let ext_len: u32 = ext_bytes.len().try_into().unwrap_or(u32::MAX);
let header_len_u64 = (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))?;
let header_len: u32 = header_len_u64.try_into().map_err(|_| {
CryptoError::InvalidFormat(FormatDefect::OversizedHeader {
header_len: u32::MAX,
})
})?;
let fixed = HeaderFixed {
header_flags: 0,
recipient_count,
recipient_entries_len,
ext_len,
stream_nonce,
};
fixed.validate_structural(header_len)?;
let prefix_bytes = Prefix::build_encrypted(header_len)?;
let fixed_bytes = fixed.to_bytes();
let mut header_bytes = Vec::with_capacity(header_len as usize);
header_bytes.extend_from_slice(&fixed_bytes);
header_bytes.extend_from_slice(&entries_bytes);
header_bytes.extend_from_slice(ext_bytes);
debug_assert_eq!(header_bytes.len(), header_len as usize);
let header_mac = format::compute_header_mac(&prefix_bytes, &header_bytes, header_key)?;
Ok(BuiltEncryptedHeader {
prefix_bytes,
header_bytes,
header_mac,
stream_nonce,
payload_key,
})
}
pub(crate) fn resolve_encrypted_output_path(
output_dir: &Path,
output_file: Option<&Path>,
base_name: &str,
) -> PathBuf {
match output_file {
Some(path) => path.to_path_buf(),
None => output_dir.join(format!("{}.{}", base_name, format::ENCRYPTED_EXTENSION)),
}
}
pub(crate) fn write_encrypted_file(
input_path: &Path,
output_dir: &Path,
output_file: Option<&Path>,
base_name: &str,
built: &BuiltEncryptedHeader,
archive_limits: archive::ArchiveLimits,
) -> Result<PathBuf, CryptoError> {
let output_path = resolve_encrypted_output_path(output_dir, output_file, base_name);
reject_occupied(&output_path, "Output")?;
let mut builder = tempfile::Builder::new();
builder.prefix(TEMP_FILE_PREFIX).suffix(INCOMPLETE_SUFFIX);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
builder.permissions(std::fs::Permissions::from_mode(0o600));
}
let mut tmp = builder.tempfile_in(parent_or_cwd(&output_path))?;
tmp.as_file_mut().write_all(&built.prefix_bytes)?;
tmp.as_file_mut().write_all(&built.header_bytes)?;
tmp.as_file_mut().write_all(&built.header_mac)?;
let encrypt_writer = payload_encryptor(&built.payload_key, &built.stream_nonce, tmp);
let (_, encrypt_writer) = archive::archive(input_path, encrypt_writer, archive_limits)?;
let tmp = encrypt_writer.finish()?;
tmp.as_file().sync_all()?;
atomic::finalize_file(tmp, &output_path)?;
Ok(output_path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::keys::{DerivedSubkeys, FILE_KEY_SIZE, FileKey, derive_subkeys};
use crate::recipient::entry::RECIPIENT_FLAG_CRITICAL;
use crate::recipient::{argon2id, x25519};
fn dummy_entry(type_name: &str, body_len: usize) -> RecipientEntry {
RecipientEntry {
type_name: type_name.to_string(),
recipient_flags: 0,
body: vec![0xAB; body_len],
}
}
fn dummy_subkeys() -> DerivedSubkeys {
let file_key = FileKey::from_bytes_for_tests([0x42u8; FILE_KEY_SIZE]);
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
derive_subkeys(&file_key, &stream_nonce).unwrap()
}
fn on_disk_bytes(built: &BuiltEncryptedHeader) -> Vec<u8> {
let mut bytes = Vec::with_capacity(
built.prefix_bytes.len() + built.header_bytes.len() + built.header_mac.len(),
);
bytes.extend_from_slice(&built.prefix_bytes);
bytes.extend_from_slice(&built.header_bytes);
bytes.extend_from_slice(&built.header_mac);
bytes
}
#[test]
fn build_then_read_round_trip_single_recipient() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entry = dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH);
let built = build_encrypted_header(
std::slice::from_ref(&entry),
b"",
stream_nonce,
payload_key,
&header_key,
)
.unwrap();
let bytes = on_disk_bytes(&built);
let parsed =
read_encrypted_header(&mut bytes.as_slice(), HeaderReadLimits::default()).unwrap();
assert_eq!(parsed.prefix_bytes, built.prefix_bytes);
assert_eq!(parsed.header_bytes, built.header_bytes);
assert_eq!(parsed.header_mac, built.header_mac);
assert_eq!(parsed.fixed.recipient_count, 1);
assert_eq!(parsed.fixed.stream_nonce, stream_nonce);
assert_eq!(parsed.fixed.ext_len, 0);
assert_eq!(parsed.recipient_entries.len(), 1);
assert_eq!(parsed.recipient_entries[0].type_name, argon2id::TYPE_NAME);
assert_eq!(parsed.recipient_entries[0].body, entry.body);
assert!(parsed.ext_bytes.is_empty());
format::verify_header_mac(
&parsed.prefix_bytes,
&parsed.header_bytes,
&header_key,
&parsed.header_mac,
)
.unwrap();
}
#[test]
fn build_then_read_round_trip_two_recipients_with_ext() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x09u8; STREAM_NONCE_SIZE];
let entries = vec![
dummy_entry(x25519::TYPE_NAME, x25519::BODY_LENGTH),
RecipientEntry {
type_name: x25519::TYPE_NAME.to_string(),
recipient_flags: RECIPIENT_FLAG_CRITICAL,
body: vec![0xCDu8; x25519::BODY_LENGTH],
},
];
let ext = b"hello-ext";
let built =
build_encrypted_header(&entries, ext, stream_nonce, payload_key, &header_key).unwrap();
let bytes = on_disk_bytes(&built);
let parsed =
read_encrypted_header(&mut bytes.as_slice(), HeaderReadLimits::default()).unwrap();
assert_eq!(parsed.fixed.recipient_count, 2);
assert_eq!(parsed.recipient_entries.len(), 2);
assert_eq!(parsed.recipient_entries[0].body, entries[0].body);
assert_eq!(parsed.recipient_entries[1].body, entries[1].body);
assert!(parsed.recipient_entries[1].is_critical());
assert_eq!(parsed.ext_bytes, ext);
format::verify_header_mac(
&parsed.prefix_bytes,
&parsed.header_bytes,
&header_key,
&parsed.header_mac,
)
.unwrap();
}
#[test]
fn build_rejects_zero_recipients() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let err =
build_encrypted_header(&[], b"", [0u8; STREAM_NONCE_SIZE], payload_key, &header_key)
.unwrap_err();
match err {
CryptoError::InvalidFormat(FormatDefect::RecipientCountOutOfRange { count: 0 }) => {}
other => panic!("expected RecipientCountOutOfRange(0), got {other:?}"),
}
}
#[test]
fn build_rejects_entry_with_invalid_type_name_grammar() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let bad_entry = RecipientEntry {
type_name: "X25519".to_owned(),
recipient_flags: 0,
body: vec![0u8; crate::recipient::native::x25519::BODY_LENGTH],
};
let err = build_encrypted_header(
&[bad_entry],
b"",
[0u8; STREAM_NONCE_SIZE],
payload_key,
&header_key,
)
.unwrap_err();
match err {
CryptoError::InvalidFormat(FormatDefect::MalformedTypeName) => {}
other => panic!("expected MalformedTypeName, got {other:?}"),
}
}
#[test]
fn build_rejects_entry_with_reserved_recipient_flags() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let bad_entry = RecipientEntry {
type_name: crate::recipient::native::x25519::TYPE_NAME.to_owned(),
recipient_flags: 1u16 << 5,
body: vec![0u8; crate::recipient::native::x25519::BODY_LENGTH],
};
let err = build_encrypted_header(
&[bad_entry],
b"",
[0u8; STREAM_NONCE_SIZE],
payload_key,
&header_key,
)
.unwrap_err();
match err {
CryptoError::InvalidFormat(FormatDefect::RecipientFlagsReserved) => {}
other => panic!("expected RecipientFlagsReserved, got {other:?}"),
}
}
#[test]
fn build_rejects_ext_above_structural_cap() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let entry = dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH);
let oversize = vec![0u8; format::EXT_LEN_MAX as usize + 1];
let err = build_encrypted_header(
&[entry],
&oversize,
[0u8; STREAM_NONCE_SIZE],
payload_key,
&header_key,
)
.unwrap_err();
match err {
CryptoError::InvalidFormat(FormatDefect::ExtTooLarge { .. }) => {}
other => panic!("expected ExtTooLarge, got {other:?}"),
}
}
#[test]
fn read_rejects_header_len_above_local_cap() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entry = dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH);
let big_ext = vec![0u8; 4096];
let built =
build_encrypted_header(&[entry], &big_ext, stream_nonce, payload_key, &header_key)
.unwrap();
let bytes = on_disk_bytes(&built);
let tight_limits = HeaderReadLimits {
max_header_len: 256,
..HeaderReadLimits::default()
};
let err = read_encrypted_header(&mut bytes.as_slice(), tight_limits).unwrap_err();
match err {
CryptoError::HeaderLenCapExceeded { local_cap: 256, .. } => {}
other => panic!("expected HeaderLenCapExceeded, got {other:?}"),
}
}
#[test]
fn enforce_header_len_boundary() {
let limits = HeaderReadLimits::default().max_header_len(256);
limits.enforce_header_len(256).expect("at-cap must succeed");
match limits.enforce_header_len(257) {
Err(CryptoError::HeaderLenCapExceeded {
header_len: 257,
local_cap: 256,
}) => {}
other => panic!("expected HeaderLenCapExceeded(257, 256), got {other:?}"),
}
}
#[test]
fn enforce_recipient_count_boundary() {
let limits = HeaderReadLimits::default().max_recipient_count(2);
limits
.enforce_recipient_count(2)
.expect("at-cap must succeed");
match limits.enforce_recipient_count(3) {
Err(CryptoError::RecipientCountCapExceeded {
count: 3,
local_cap: 2,
}) => {}
other => panic!("expected RecipientCountCapExceeded(3, 2), got {other:?}"),
}
}
#[test]
fn header_read_limits_builder_round_trips_normal_values() {
let limits = HeaderReadLimits::default()
.max_header_len(2 * 1024 * 1024)
.max_recipient_count(128)
.max_recipient_body_len(16 * 1024);
assert_eq!(limits.max_header_len, 2 * 1024 * 1024);
assert_eq!(limits.max_recipient_count, 128);
assert_eq!(limits.max_recipient_body_len, 16 * 1024);
}
#[test]
fn header_read_limits_builder_clamps_at_structural_max() {
let clamped = HeaderReadLimits::default()
.max_header_len(u32::MAX)
.max_recipient_count(u16::MAX)
.max_recipient_body_len(u32::MAX);
assert_eq!(
clamped.max_header_len,
HeaderReadLimits::HEADER_LEN_STRUCTURAL_MAX
);
assert_eq!(
clamped.max_recipient_count,
HeaderReadLimits::RECIPIENT_COUNT_STRUCTURAL_MAX
);
assert_eq!(
clamped.max_recipient_body_len,
HeaderReadLimits::RECIPIENT_BODY_LEN_STRUCTURAL_MAX
);
}
#[test]
fn default_local_caps_are_mutually_consistent() {
use crate::format::{EXT_LEN_MAX, HEADER_FIXED_SIZE};
use crate::recipient::entry::ENTRY_HEADER_SIZE;
use crate::recipient::name::TYPE_NAME_MAX_LEN;
let per_entry_max = ENTRY_HEADER_SIZE as u64
+ TYPE_NAME_MAX_LEN as u64
+ HeaderReadLimits::RECIPIENT_BODY_LEN_DEFAULT as u64;
let worst_case_header = (HeaderReadLimits::RECIPIENT_COUNT_DEFAULT as u64) * per_entry_max
+ EXT_LEN_MAX as u64
+ HEADER_FIXED_SIZE as u64;
let header_len_default = HeaderReadLimits::HEADER_LEN_DEFAULT as u64;
assert!(
worst_case_header <= header_len_default,
"Default local caps allow a worst-case header of {worst_case_header} bytes, \
which exceeds HeaderReadLimits::HEADER_LEN_DEFAULT ({header_len_default}). \
Either lower one of the per-cap defaults or raise \
HeaderReadLimits::HEADER_LEN_DEFAULT to keep default round-trip viable."
);
}
#[test]
fn read_accepts_when_caller_raises_recipient_count_cap() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entries: Vec<_> = (0..80)
.map(|_| dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH))
.collect();
let built =
build_encrypted_header(&entries, b"", stream_nonce, payload_key, &header_key).unwrap();
let bytes = on_disk_bytes(&built);
match read_encrypted_header(&mut bytes.as_slice(), HeaderReadLimits::default()) {
Err(CryptoError::RecipientCountCapExceeded { count: 80, .. }) => {}
other => panic!("expected RecipientCountCapExceeded with default cap, got {other:?}"),
}
let raised = HeaderReadLimits::default().max_recipient_count(128);
let parsed = read_encrypted_header(&mut bytes.as_slice(), raised)
.expect("raised recipient_count cap must accept the file");
assert_eq!(parsed.recipient_entries.len(), 80);
}
#[test]
fn read_accepts_when_caller_raises_body_len_cap() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let oversize_body_len: usize = 10 * 1024;
let entry = dummy_entry(argon2id::TYPE_NAME, oversize_body_len);
let built =
build_encrypted_header(&[entry], b"", stream_nonce, payload_key, &header_key).unwrap();
let bytes = on_disk_bytes(&built);
match read_encrypted_header(&mut bytes.as_slice(), HeaderReadLimits::default()) {
Err(CryptoError::RecipientBodyCapExceeded { body_len, .. })
if body_len as usize == oversize_body_len => {}
other => panic!(
"expected RecipientBodyCapExceeded({oversize_body_len}, ..) with default cap, got {other:?}"
),
}
let raised = HeaderReadLimits::default().max_recipient_body_len(16 * 1024);
let parsed = read_encrypted_header(&mut bytes.as_slice(), raised)
.expect("raised body_len cap must accept the file");
assert_eq!(parsed.recipient_entries.len(), 1);
assert_eq!(parsed.recipient_entries[0].body.len(), oversize_body_len);
}
#[test]
fn read_rejects_recipient_count_above_local_cap() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entries: Vec<_> = (0..3)
.map(|_| dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH))
.collect();
let built =
build_encrypted_header(&entries, b"", stream_nonce, payload_key, &header_key).unwrap();
let bytes = on_disk_bytes(&built);
let tight_limits = HeaderReadLimits {
max_recipient_count: 2,
..HeaderReadLimits::default()
};
let err = read_encrypted_header(&mut bytes.as_slice(), tight_limits).unwrap_err();
match err {
CryptoError::RecipientCountCapExceeded {
count: 3,
local_cap: 2,
} => {}
other => panic!("expected RecipientCountCapExceeded(3, 2), got {other:?}"),
}
}
#[test]
fn read_rejects_recipient_body_above_local_cap() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let oversize_body_len = argon2id::BODY_LENGTH + 84;
let local_body_cap: u32 = (argon2id::BODY_LENGTH + 12) as u32;
let entry = dummy_entry(argon2id::TYPE_NAME, oversize_body_len);
let built =
build_encrypted_header(&[entry], b"", stream_nonce, payload_key, &header_key).unwrap();
let bytes = on_disk_bytes(&built);
let tight_limits = HeaderReadLimits {
max_recipient_body_len: local_body_cap,
..HeaderReadLimits::default()
};
let err = read_encrypted_header(&mut bytes.as_slice(), tight_limits).unwrap_err();
match err {
CryptoError::RecipientBodyCapExceeded {
body_len,
local_cap,
} if body_len as usize == oversize_body_len && local_cap == local_body_cap => {}
other => panic!(
"expected RecipientBodyCapExceeded({oversize_body_len}, {local_body_cap}), got {other:?}"
),
}
}
#[test]
fn read_rejects_missing_mac_tag_as_truncated() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entry = dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH);
let built =
build_encrypted_header(&[entry], b"", stream_nonce, payload_key, &header_key).unwrap();
let mut bytes = Vec::with_capacity(built.prefix_bytes.len() + built.header_bytes.len());
bytes.extend_from_slice(&built.prefix_bytes);
bytes.extend_from_slice(&built.header_bytes);
match read_encrypted_header(&mut bytes.as_slice(), HeaderReadLimits::default()) {
Err(CryptoError::InvalidFormat(FormatDefect::Truncated)) => {}
other => panic!("expected InvalidFormat(Truncated), got {other:?}"),
}
}
#[test]
fn read_returns_truncated_before_structural_error_when_mac_missing() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entry = dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH);
let built =
build_encrypted_header(&[entry], b"", stream_nonce, payload_key, &header_key).unwrap();
let mut header_bytes = built.header_bytes.clone();
header_bytes[0] = 0x01;
let mut bytes = Vec::with_capacity(built.prefix_bytes.len() + header_bytes.len());
bytes.extend_from_slice(&built.prefix_bytes);
bytes.extend_from_slice(&header_bytes);
match read_encrypted_header(&mut bytes.as_slice(), HeaderReadLimits::default()) {
Err(CryptoError::InvalidFormat(FormatDefect::Truncated)) => {}
other => panic!(
"expected InvalidFormat(Truncated) (framing precedes structural parse), got {other:?}"
),
}
}
#[test]
fn mac_verify_rejects_tampered_header_byte() {
let DerivedSubkeys {
payload_key,
header_key,
} = dummy_subkeys();
let stream_nonce = [0x07u8; STREAM_NONCE_SIZE];
let entry = dummy_entry(argon2id::TYPE_NAME, argon2id::BODY_LENGTH);
let built =
build_encrypted_header(&[entry], b"", stream_nonce, payload_key, &header_key).unwrap();
let mut tampered = built.header_bytes.clone();
tampered[20] ^= 0xFF;
format::verify_header_mac(
&built.prefix_bytes,
&tampered,
&header_key,
&built.header_mac,
)
.unwrap_err();
}
}