use crate::cipher::NONCE_LEN;
use crate::kdf::SALT_LEN;
pub const MAGIC: [u8; 4] = *b"QUIP";
pub const VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContainerError {
TooShort,
BadMagic,
UnsupportedVersion(u8),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
pub version: u8,
pub flags: u8,
pub codebook_id: u16,
pub codebook_hash_prefix: [u8; 8],
pub salt: [u8; SALT_LEN],
pub nonce: [u8; NONCE_LEN],
pub kdf_mem_kib: u32,
pub kdf_iterations: u32,
pub kdf_parallelism: u32,
}
impl Header {
pub const SIZE: usize = 68;
pub fn to_bytes(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(Self::SIZE);
b.extend_from_slice(&MAGIC);
b.push(self.version);
b.push(self.flags);
b.extend_from_slice(&self.codebook_id.to_be_bytes());
b.extend_from_slice(&self.codebook_hash_prefix);
b.extend_from_slice(&self.salt);
b.extend_from_slice(&self.nonce);
b.extend_from_slice(&self.kdf_mem_kib.to_be_bytes());
b.extend_from_slice(&self.kdf_iterations.to_be_bytes());
b.extend_from_slice(&self.kdf_parallelism.to_be_bytes());
debug_assert_eq!(b.len(), Self::SIZE);
b
}
}
pub fn serialize(header: &Header, ciphertext: &[u8]) -> Vec<u8> {
let mut out = header.to_bytes();
out.extend_from_slice(ciphertext);
out
}
pub fn parse(blob: &[u8]) -> Result<(Header, &[u8]), ContainerError> {
if blob.len() < Header::SIZE {
return Err(ContainerError::TooShort);
}
let (head, rest) = blob.split_at(Header::SIZE);
if head[0..4] != MAGIC {
return Err(ContainerError::BadMagic);
}
let version = head[4];
if version != VERSION {
return Err(ContainerError::UnsupportedVersion(version));
}
let header = Header {
version,
flags: head[5],
codebook_id: u16::from_be_bytes([head[6], head[7]]),
codebook_hash_prefix: head[8..16].try_into().expect("8 bytes"),
salt: head[16..32].try_into().expect("16 bytes"),
nonce: head[32..56].try_into().expect("24 bytes"),
kdf_mem_kib: u32::from_be_bytes(head[56..60].try_into().expect("4 bytes")),
kdf_iterations: u32::from_be_bytes(head[60..64].try_into().expect("4 bytes")),
kdf_parallelism: u32::from_be_bytes(head[64..68].try_into().expect("4 bytes")),
};
Ok((header, rest))
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_header() -> Header {
Header {
version: VERSION,
flags: 0,
codebook_id: 0x0102,
codebook_hash_prefix: [1, 2, 3, 4, 5, 6, 7, 8],
salt: [9u8; SALT_LEN],
nonce: [7u8; NONCE_LEN],
kdf_mem_kib: 65536,
kdf_iterations: 3,
kdf_parallelism: 1,
}
}
#[test]
fn round_trips_header_and_ciphertext() {
let h = sample_header();
let ct = vec![10u8, 20, 30, 40, 50];
let blob = serialize(&h, &ct);
let (parsed, parsed_ct) = parse(&blob).unwrap();
assert_eq!(parsed, h);
assert_eq!(parsed_ct, &ct[..]);
}
#[test]
fn header_serializes_to_fixed_size() {
assert_eq!(sample_header().to_bytes().len(), Header::SIZE);
}
#[test]
fn rejects_too_short_blob() {
let short = [0u8; Header::SIZE - 1];
assert_eq!(parse(&short), Err(ContainerError::TooShort));
}
#[test]
fn rejects_bad_magic() {
let mut blob = serialize(&sample_header(), b"ct");
blob[0] = b'X';
assert_eq!(parse(&blob), Err(ContainerError::BadMagic));
}
#[test]
fn rejects_unsupported_version() {
let mut blob = serialize(&sample_header(), b"ct");
blob[4] = 99;
assert_eq!(parse(&blob), Err(ContainerError::UnsupportedVersion(99)));
}
}