use crate::aead::Algorithm;
use crate::error::{Error, Result};
pub const MAGIC: &[u8; 8] = b"\x89CRYPTIO";
pub const HEADER_LEN: usize = 24;
pub const NONCE_LEN: usize = 12;
pub const NONCE_PREFIX_LEN: usize = 7;
pub const TAG_LEN: usize = 16;
pub const VERSION: u8 = 0x01;
pub const DEFAULT_CHUNK_SIZE_LOG2: u8 = 16;
pub const MIN_CHUNK_SIZE_LOG2: u8 = 10;
pub const MAX_CHUNK_SIZE_LOG2: u8 = 24;
pub(super) const ALG_CHACHA20_POLY1305: u8 = 0x00;
pub(super) const ALG_AES_256_GCM: u8 = 0x01;
pub(super) fn encode_algorithm(algorithm: Algorithm) -> u8 {
match algorithm {
Algorithm::ChaCha20Poly1305 => ALG_CHACHA20_POLY1305,
Algorithm::Aes256Gcm => ALG_AES_256_GCM,
}
}
pub(super) fn decode_algorithm(byte: u8) -> Result<Algorithm> {
match byte {
ALG_CHACHA20_POLY1305 => Ok(Algorithm::ChaCha20Poly1305),
ALG_AES_256_GCM => Ok(Algorithm::Aes256Gcm),
_ => Err(Error::InvalidCiphertext(alloc::format!(
"unknown algorithm byte: 0x{byte:02x}"
))),
}
}
#[must_use]
pub(super) fn build_header(
algorithm: Algorithm,
chunk_size_log2: u8,
nonce_prefix: &[u8; NONCE_PREFIX_LEN],
) -> [u8; HEADER_LEN] {
let mut h = [0u8; HEADER_LEN];
h[0..8].copy_from_slice(MAGIC);
h[8] = VERSION;
h[9] = encode_algorithm(algorithm);
h[10] = chunk_size_log2;
h[16..23].copy_from_slice(nonce_prefix);
h
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ParsedHeader {
pub algorithm: Algorithm,
pub chunk_size_log2: u8,
pub nonce_prefix: [u8; NONCE_PREFIX_LEN],
pub raw: [u8; HEADER_LEN],
}
pub(super) fn parse_header(bytes: &[u8]) -> Result<ParsedHeader> {
if bytes.len() < HEADER_LEN {
return Err(Error::InvalidCiphertext(alloc::format!(
"stream header too short ({} bytes, need {HEADER_LEN})",
bytes.len()
)));
}
let raw_slice = &bytes[..HEADER_LEN];
if &raw_slice[0..8] != MAGIC {
return Err(Error::InvalidCiphertext(alloc::string::String::from(
"stream magic mismatch (not a crypt-io stream)",
)));
}
if raw_slice[8] != VERSION {
return Err(Error::InvalidCiphertext(alloc::format!(
"unsupported stream version: 0x{:02x} (this build understands 0x{VERSION:02x})",
raw_slice[8],
)));
}
let algorithm = decode_algorithm(raw_slice[9])?;
let chunk_size_log2 = raw_slice[10];
if !(MIN_CHUNK_SIZE_LOG2..=MAX_CHUNK_SIZE_LOG2).contains(&chunk_size_log2) {
return Err(Error::InvalidCiphertext(alloc::format!(
"chunk_size_log2 out of range: {chunk_size_log2}"
)));
}
let mut nonce_prefix = [0u8; NONCE_PREFIX_LEN];
nonce_prefix.copy_from_slice(&raw_slice[16..23]);
let mut raw = [0u8; HEADER_LEN];
raw.copy_from_slice(raw_slice);
Ok(ParsedHeader {
algorithm,
chunk_size_log2,
nonce_prefix,
raw,
})
}
#[must_use]
pub(super) fn build_nonce(
nonce_prefix: &[u8; NONCE_PREFIX_LEN],
counter: u32,
is_final: bool,
) -> [u8; NONCE_LEN] {
let mut n = [0u8; NONCE_LEN];
n[0..7].copy_from_slice(nonce_prefix);
n[7..11].copy_from_slice(&counter.to_be_bytes());
n[11] = u8::from(is_final);
n
}
#[must_use]
pub(super) fn chunk_size_from_log2(log2: u8) -> usize {
1usize << log2
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn header_round_trip_chacha() {
let prefix = [0xaau8; NONCE_PREFIX_LEN];
let h = build_header(Algorithm::ChaCha20Poly1305, 16, &prefix);
let p = parse_header(&h).unwrap();
assert_eq!(p.algorithm, Algorithm::ChaCha20Poly1305);
assert_eq!(p.chunk_size_log2, 16);
assert_eq!(p.nonce_prefix, prefix);
assert_eq!(p.raw, h);
}
#[test]
fn header_round_trip_aes() {
let prefix = [0xbbu8; NONCE_PREFIX_LEN];
let h = build_header(Algorithm::Aes256Gcm, 12, &prefix);
let p = parse_header(&h).unwrap();
assert_eq!(p.algorithm, Algorithm::Aes256Gcm);
assert_eq!(p.chunk_size_log2, 12);
assert_eq!(p.nonce_prefix, prefix);
}
#[test]
fn header_rejects_wrong_magic() {
let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
h[0] = b'X';
let err = parse_header(&h).unwrap_err();
assert!(matches!(err, Error::InvalidCiphertext(_)));
}
#[test]
fn header_rejects_unknown_version() {
let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
h[8] = 0xff;
let err = parse_header(&h).unwrap_err();
assert!(matches!(err, Error::InvalidCiphertext(_)));
}
#[test]
fn header_rejects_unknown_algorithm() {
let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
h[9] = 0x42;
let err = parse_header(&h).unwrap_err();
assert!(matches!(err, Error::InvalidCiphertext(_)));
}
#[test]
fn header_rejects_out_of_range_chunk_size_log2() {
for bad in [0u8, 9, 25, 64, 255] {
let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
h[10] = bad;
let err = parse_header(&h).unwrap_err();
assert!(matches!(err, Error::InvalidCiphertext(_)), "bad={bad}");
}
}
#[test]
fn header_rejects_too_short() {
let err = parse_header(&[0u8; HEADER_LEN - 1]).unwrap_err();
assert!(matches!(err, Error::InvalidCiphertext(_)));
}
#[test]
fn nonce_distinct_per_counter_and_flag() {
let prefix = [0xccu8; NONCE_PREFIX_LEN];
let n0 = build_nonce(&prefix, 0, false);
let n1 = build_nonce(&prefix, 1, false);
let n0_final = build_nonce(&prefix, 0, true);
assert_ne!(n0, n1);
assert_ne!(n0, n0_final);
assert_ne!(n1, n0_final);
assert_eq!(&n0[..7], &prefix);
assert_eq!(n0[7..11], 0u32.to_be_bytes());
assert_eq!(n0[11], 0);
assert_eq!(n0_final[11], 1);
}
#[test]
fn chunk_size_from_log2_matches_pow2() {
assert_eq!(chunk_size_from_log2(10), 1024);
assert_eq!(chunk_size_from_log2(16), 65_536);
assert_eq!(chunk_size_from_log2(20), 1_048_576);
}
}