use std::io::{Read, Write};
use hkdf::Hkdf;
use sha2::Sha256;
use sha3::{Digest, Sha3_256};
use zeroize::Zeroizing;
use crate::error::PqfileError;
pub const MAGIC: &[u8; 4] = b"PQFL";
pub const VERSION: u8 = 0x02;
pub const VERSION_V3: u8 = 0x03;
pub const VERSION_V4: u8 = 0x04;
pub const VERSION_V5: u8 = 0x05;
pub const VERSION_V6: u8 = 0x06;
pub const VERSION_V7: u8 = 0x07;
pub const VERSION_V8: u8 = 0x08;
pub const VERSION_V9: u8 = 0x09;
pub const PADDED_CT_LEN: usize = KEM_CT_LEN_1024;
pub const KEM_VARIANT_512: u16 = 512;
pub const KEM_VARIANT_768: u16 = 768;
pub const KEM_VARIANT_1024: u16 = 1024;
pub const KEM_VARIANT_HYBRID_768: u16 = 0x0301;
pub const KEM_CT_LEN_512: usize = 768;
pub const EK_LEN_512: usize = 800;
pub const KEM_CT_LEN_768: usize = 1088;
pub const EK_LEN_768: usize = 1184;
pub const KEM_CT_LEN_1024: usize = 1568;
pub const EK_LEN_1024: usize = 1568;
pub const X25519_PUBKEY_LEN: usize = 32;
pub const X25519_SCALAR_LEN: usize = 32;
pub const HYBRID_CT_LEN_768: usize = X25519_PUBKEY_LEN + KEM_CT_LEN_768;
pub const HYBRID_EK_LEN_768: usize = X25519_PUBKEY_LEN + EK_LEN_768;
pub const HYBRID_SEED_LEN_768: usize = X25519_SCALAR_LEN + 64;
pub const WRAPPED_KEY_LEN: usize = 48;
pub(crate) const MAX_RECIPIENTS: usize = 256;
pub(crate) const MAX_ORIGINAL_SIZE: u64 = 1u64 << 40;
pub const NONCE_LEN: usize = 12;
const HEADER_PREFIX_LEN: usize = 7;
const HEADER_SUFFIX_LEN: usize = 20;
#[allow(dead_code)]
pub(crate) const HEADER_LEN_512: usize = HEADER_PREFIX_LEN + KEM_CT_LEN_512 + HEADER_SUFFIX_LEN;
pub(crate) const HEADER_LEN_768: usize = HEADER_PREFIX_LEN + KEM_CT_LEN_768 + HEADER_SUFFIX_LEN;
#[allow(dead_code)]
pub(crate) const HEADER_LEN_1024: usize = HEADER_PREFIX_LEN + KEM_CT_LEN_1024 + HEADER_SUFFIX_LEN;
pub const V5_CHUNK_SIZE_FIELD_LEN: usize = 4;
pub const V6_COMPRESSION_FIELD_LEN: usize = 1;
pub const COMPRESSION_NONE: u8 = 0x00;
pub const COMPRESSION_ZSTD: u8 = 0x01;
pub const CHUNK_SIZE: usize = 65536;
pub const BASE_NONCE_LEN: usize = 8;
pub(crate) const STREAM_AAD_PREFIX: &[u8] = b"pqfile";
pub(crate) struct PqfHeader {
pub version: u8,
pub kem_variant: u16,
pub kem_ciphertext: Vec<u8>,
pub nonce: [u8; NONCE_LEN],
pub original_size: u64,
pub chunk_size: u32,
pub compression_algo: u8,
}
impl PqfHeader {
pub fn header_len(&self) -> usize {
let base = HEADER_PREFIX_LEN + self.kem_ciphertext.len() + HEADER_SUFFIX_LEN;
match self.version {
v if v == VERSION_V5 => base + V5_CHUNK_SIZE_FIELD_LEN,
v if v == VERSION_V6 => base + V5_CHUNK_SIZE_FIELD_LEN + V6_COMPRESSION_FIELD_LEN,
_ => base,
}
}
pub fn write<W: Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
w.write_all(MAGIC)?;
w.write_all(&[self.version])?;
w.write_all(&self.kem_variant.to_le_bytes())?;
w.write_all(&self.kem_ciphertext)?;
w.write_all(&self.nonce)?;
w.write_all(&self.original_size.to_le_bytes())?;
if self.version == VERSION_V5 || self.version == VERSION_V6 {
w.write_all(&self.chunk_size.to_le_bytes())?;
}
if self.version == VERSION_V6 {
w.write_all(&[self.compression_algo])?;
}
Ok(())
}
pub fn read<R: Read + ?Sized>(r: &mut R) -> Result<Self, PqfileError> {
let version = Self::read_magic_version(r)?;
if version != VERSION
&& version != VERSION_V3
&& version != VERSION_V5
&& version != VERSION_V6
{
return Err(PqfileError::UnsupportedVersion(version));
}
Self::read_body(r, version)
}
pub fn read_magic_version<R: Read + ?Sized>(r: &mut R) -> Result<u8, PqfileError> {
let mut magic = [0u8; 4];
r.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(PqfileError::InvalidMagic);
}
let mut v = [0u8; 1];
r.read_exact(&mut v)?;
Ok(v[0])
}
pub fn read_body<R: Read + ?Sized>(r: &mut R, version: u8) -> Result<Self, PqfileError> {
let mut kem_variant_bytes = [0u8; 2];
r.read_exact(&mut kem_variant_bytes)?;
let kem_variant = u16::from_le_bytes(kem_variant_bytes);
let ct_len = ct_len_for_variant(kem_variant)?;
let mut kem_ciphertext = vec![0u8; ct_len];
r.read_exact(&mut kem_ciphertext)?;
let (nonce, original_size) = read_nonce_and_size(r)?;
let (chunk_size, compression_algo) = if version == VERSION_V5 {
let mut cs = [0u8; 4];
r.read_exact(&mut cs)?;
let val = u32::from_le_bytes(cs);
validate_chunk_size(val)?;
(val, COMPRESSION_NONE)
} else if version == VERSION_V6 {
let mut cs = [0u8; 4];
r.read_exact(&mut cs)?;
let val = u32::from_le_bytes(cs);
validate_chunk_size(val)?;
let mut algo = [0u8; 1];
r.read_exact(&mut algo)?;
(val, algo[0])
} else {
(CHUNK_SIZE as u32, COMPRESSION_NONE)
};
Ok(PqfHeader {
version,
kem_variant,
kem_ciphertext,
nonce,
original_size,
chunk_size,
compression_algo,
})
}
}
pub(crate) struct RecipientEntryV4 {
pub kem_variant: u16,
pub kem_ciphertext: Vec<u8>,
pub wrapped_key: [u8; WRAPPED_KEY_LEN],
}
pub(crate) struct PqfHeaderV4 {
pub recipients: Vec<RecipientEntryV4>,
pub nonce: [u8; NONCE_LEN],
pub original_size: u64,
}
fn write_multi_header_prefix<W: Write + ?Sized>(
w: &mut W,
version: u8,
count: usize,
) -> Result<(), std::io::Error> {
w.write_all(MAGIC)?;
w.write_all(&[version])?;
w.write_all(&(count as u16).to_le_bytes())
}
fn write_nonce_and_size<W: Write + ?Sized>(
w: &mut W,
nonce: &[u8; NONCE_LEN],
size: u64,
) -> Result<(), std::io::Error> {
w.write_all(nonce)?;
w.write_all(&size.to_le_bytes())
}
impl PqfHeaderV4 {
pub fn write<W: Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
write_multi_header_prefix(w, VERSION_V4, self.recipients.len())?;
for r in &self.recipients {
w.write_all(&r.kem_variant.to_le_bytes())?;
w.write_all(&r.kem_ciphertext)?;
w.write_all(&r.wrapped_key)?;
}
write_nonce_and_size(w, &self.nonce, self.original_size)
}
pub fn read_body<R: Read + ?Sized>(r: &mut R) -> Result<Self, PqfileError> {
let mut count_bytes = [0u8; 2];
r.read_exact(&mut count_bytes)?;
let count = u16::from_le_bytes(count_bytes) as usize;
if count > MAX_RECIPIENTS {
return Err(PqfileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("recipient count {count} exceeds maximum ({MAX_RECIPIENTS})"),
)));
}
let mut recipients = Vec::with_capacity(count);
for _ in 0..count {
let mut variant_bytes = [0u8; 2];
r.read_exact(&mut variant_bytes)?;
let kem_variant = u16::from_le_bytes(variant_bytes);
let ct_len = ct_len_for_variant(kem_variant)?;
let mut kem_ciphertext = vec![0u8; ct_len];
r.read_exact(&mut kem_ciphertext)?;
let mut wrapped_key = [0u8; WRAPPED_KEY_LEN];
r.read_exact(&mut wrapped_key)?;
recipients.push(RecipientEntryV4 {
kem_variant,
kem_ciphertext,
wrapped_key,
});
}
let (nonce, original_size) = read_nonce_and_size(r)?;
Ok(PqfHeaderV4 {
recipients,
nonce,
original_size,
})
}
}
pub(crate) struct RecipientEntryV7 {
pub kem_variant: u16,
pub kem_ciphertext: Vec<u8>,
pub wrapped_key: [u8; WRAPPED_KEY_LEN],
}
pub(crate) struct PqfHeaderV7 {
pub recipients: Vec<RecipientEntryV7>,
pub nonce: [u8; NONCE_LEN],
pub original_size: u64,
}
impl PqfHeaderV7 {
pub fn write<W: Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
write_multi_header_prefix(w, VERSION_V7, self.recipients.len())?;
let pad = [0u8; PADDED_CT_LEN];
for r in &self.recipients {
w.write_all(&r.kem_variant.to_le_bytes())?;
w.write_all(&r.kem_ciphertext)?;
let written = r.kem_ciphertext.len();
if written < PADDED_CT_LEN {
w.write_all(&pad[..PADDED_CT_LEN - written])?;
}
w.write_all(&r.wrapped_key)?;
}
write_nonce_and_size(w, &self.nonce, self.original_size)
}
pub fn read_body<R: Read + ?Sized>(r: &mut R) -> Result<Self, PqfileError> {
let mut count_bytes = [0u8; 2];
r.read_exact(&mut count_bytes)?;
let count = u16::from_le_bytes(count_bytes) as usize;
if count > MAX_RECIPIENTS {
return Err(PqfileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("recipient count {count} exceeds maximum ({MAX_RECIPIENTS})"),
)));
}
let mut recipients = Vec::with_capacity(count);
for _ in 0..count {
let mut variant_bytes = [0u8; 2];
r.read_exact(&mut variant_bytes)?;
let kem_variant = u16::from_le_bytes(variant_bytes);
let mut padded = vec![0u8; PADDED_CT_LEN];
r.read_exact(&mut padded)?;
let ct_len = ct_len_for_variant(kem_variant)?;
padded.truncate(ct_len);
let mut wrapped_key = [0u8; WRAPPED_KEY_LEN];
r.read_exact(&mut wrapped_key)?;
recipients.push(RecipientEntryV7 {
kem_variant,
kem_ciphertext: padded,
wrapped_key,
});
}
let (nonce, original_size) = read_nonce_and_size(r)?;
Ok(PqfHeaderV7 {
recipients,
nonce,
original_size,
})
}
}
pub(crate) struct RecipientEntryV8 {
pub padded_ct: [u8; PADDED_CT_LEN],
pub wrapped_key: [u8; WRAPPED_KEY_LEN],
}
pub(crate) struct PqfHeaderV8 {
pub recipients: Vec<RecipientEntryV8>,
pub nonce: [u8; NONCE_LEN],
pub original_size: u64,
}
impl PqfHeaderV8 {
pub fn write<W: Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
self.write_with_version(w, VERSION_V8)
}
pub(crate) fn write_with_version<W: Write + ?Sized>(
&self,
w: &mut W,
version: u8,
) -> Result<(), std::io::Error> {
write_multi_header_prefix(w, version, self.recipients.len())?;
for r in &self.recipients {
w.write_all(&r.padded_ct)?;
w.write_all(&r.wrapped_key)?;
}
write_nonce_and_size(w, &self.nonce, self.original_size)
}
pub fn read_body<R: Read + ?Sized>(r: &mut R) -> Result<Self, PqfileError> {
let mut count_bytes = [0u8; 2];
r.read_exact(&mut count_bytes)?;
let count = u16::from_le_bytes(count_bytes) as usize;
if count > MAX_RECIPIENTS {
return Err(PqfileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("recipient count {count} exceeds maximum ({MAX_RECIPIENTS})"),
)));
}
let mut recipients = Vec::with_capacity(count);
for _ in 0..count {
let mut padded_ct = [0u8; PADDED_CT_LEN];
r.read_exact(&mut padded_ct)?;
let mut wrapped_key = [0u8; WRAPPED_KEY_LEN];
r.read_exact(&mut wrapped_key)?;
recipients.push(RecipientEntryV8 {
padded_ct,
wrapped_key,
});
}
let (nonce, original_size) = read_nonce_and_size(r)?;
Ok(PqfHeaderV8 {
recipients,
nonce,
original_size,
})
}
}
pub const MAX_CHUNK_SIZE: u32 = 256 * 1024 * 1024;
pub fn adaptive_chunk_size(file_size: u64) -> usize {
const MB: u64 = 1024 * 1024;
if file_size < MB {
16 * 1024
} else if file_size > 256 * MB {
256 * 1024
} else {
CHUNK_SIZE
}
}
fn validate_chunk_size(val: u32) -> Result<(), PqfileError> {
if val == 0 || val > MAX_CHUNK_SIZE {
return Err(PqfileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("chunk_size {val} is out of valid range (1..={MAX_CHUNK_SIZE})"),
)));
}
Ok(())
}
pub(crate) fn ct_len_for_variant(kem_variant: u16) -> Result<usize, PqfileError> {
match kem_variant {
KEM_VARIANT_512 => Ok(KEM_CT_LEN_512),
KEM_VARIANT_768 => Ok(KEM_CT_LEN_768),
KEM_VARIANT_1024 => Ok(KEM_CT_LEN_1024),
KEM_VARIANT_HYBRID_768 => Ok(HYBRID_CT_LEN_768),
v => Err(PqfileError::UnsupportedKem(v)),
}
}
fn read_nonce_and_size<R: Read + ?Sized>(r: &mut R) -> Result<([u8; NONCE_LEN], u64), PqfileError> {
let mut nonce = [0u8; NONCE_LEN];
r.read_exact(&mut nonce)?;
let mut size_bytes = [0u8; 8];
r.read_exact(&mut size_bytes)?;
let size = u64::from_le_bytes(size_bytes);
if size > MAX_ORIGINAL_SIZE {
return Err(PqfileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("original_size {size} exceeds maximum ({MAX_ORIGINAL_SIZE})"),
)));
}
Ok((nonce, size))
}
pub(crate) fn chunk_nonce(base_nonce: &[u8; BASE_NONCE_LEN], counter: u32) -> [u8; NONCE_LEN] {
let mut nonce = [0u8; NONCE_LEN];
nonce[..BASE_NONCE_LEN].copy_from_slice(base_nonce);
nonce[BASE_NONCE_LEN..].copy_from_slice(&counter.to_be_bytes());
nonce
}
const KEY_COMMITMENT_CTX: &[u8] = b"pqfile-session-key-commitment-v2";
pub(crate) fn compute_key_commitment(
session_key: &[u8],
nonce: &[u8; NONCE_LEN],
original_size: u64,
) -> [u8; 32] {
let mut h = Sha3_256::new();
h.update(KEY_COMMITMENT_CTX);
h.update(session_key);
h.update(nonce.as_ref());
h.update(original_size.to_le_bytes());
h.finalize().into()
}
pub(crate) const MAX_CHUNK_AAD_LEN: usize = STREAM_AAD_PREFIX.len() + 4 + 1 + 32;
pub(crate) fn make_chunk_aad(
counter: u32,
is_last: bool,
key_commitment: &[u8; 32],
) -> ([u8; MAX_CHUNK_AAD_LEN], usize) {
let mut buf = [0u8; MAX_CHUNK_AAD_LEN];
buf[..6].copy_from_slice(STREAM_AAD_PREFIX);
buf[6..10].copy_from_slice(&counter.to_be_bytes());
buf[10] = is_last as u8;
if counter == 0 {
buf[11..43].copy_from_slice(key_commitment);
(buf, MAX_CHUNK_AAD_LEN)
} else {
(buf, 11)
}
}
pub(crate) fn fill_chunk<R: Read + ?Sized>(
reader: &mut R,
buf: &mut [u8],
) -> Result<usize, PqfileError> {
let mut total = 0;
while total < buf.len() {
match reader.read(&mut buf[total..])? {
0 => break,
n => total += n,
}
}
Ok(total)
}
pub(crate) fn hybrid_hkdf(
x25519_ss: &[u8; 32],
ml_ss: &[u8],
) -> Result<Zeroizing<[u8; 32]>, PqfileError> {
let mut ikm = Zeroizing::new(Vec::with_capacity(64));
ikm.extend_from_slice(x25519_ss);
ikm.extend_from_slice(ml_ss);
let hk = Hkdf::<Sha256>::new(None, &ikm);
let mut okm = Zeroizing::new([0u8; 32]);
hk.expand(b"pqfile-hybrid-v1", okm.as_mut())
.map_err(|_| PqfileError::EncryptionFailure)?;
Ok(okm)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adaptive_chunk_size_small_file() {
assert_eq!(adaptive_chunk_size(0), 16 * 1024);
assert_eq!(adaptive_chunk_size(1), 16 * 1024);
assert_eq!(adaptive_chunk_size(1024 * 1024 - 1), 16 * 1024);
}
#[test]
fn adaptive_chunk_size_medium_file() {
assert_eq!(adaptive_chunk_size(1024 * 1024), CHUNK_SIZE);
assert_eq!(adaptive_chunk_size(10 * 1024 * 1024), CHUNK_SIZE);
assert_eq!(adaptive_chunk_size(256 * 1024 * 1024), CHUNK_SIZE);
}
#[test]
fn adaptive_chunk_size_large_file() {
assert_eq!(adaptive_chunk_size(256 * 1024 * 1024 + 1), 256 * 1024);
assert_eq!(adaptive_chunk_size(1024 * 1024 * 1024), 256 * 1024);
}
}