use std::fs;
use std::io::{Read, Write};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use chacha20poly1305::{
ChaCha20Poly1305, Key, Nonce,
aead::{Aead, AeadInPlace, KeyInit, Payload},
};
use ml_kem::{
EncapsulationKey512, EncapsulationKey768, EncapsulationKey1024,
array::Array,
kem::Encapsulate,
};
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret};
use zeroize::Zeroizing;
use crate::error::PqfileError;
use aes_gcm::{Aes256Gcm, Key as AesKey, Nonce as AesNonce};
use crate::format::{
BASE_NONCE_LEN, CHUNK_SIZE, COMPRESSION_NONE, COMPRESSION_ZSTD, EK_LEN, EK_LEN_512, EK_LEN_1024,
HEADER_LEN, HYBRID_CT_LEN_768, HYBRID_EK_LEN_768, KEM_VARIANT, KEM_VARIANT_512, KEM_VARIANT_1024,
KEM_VARIANT_HYBRID_768, NONCE_LEN, VERSION, VERSION_V3, VERSION_V5, VERSION_V6,
PqfHeader, PqfHeaderV4, PqfHeaderV7, RecipientEntryV4, RecipientEntryV7, WRAPPED_KEY_LEN,
chunk_aad, chunk_nonce, fill_chunk, hybrid_hkdf,
};
use crate::keygen::{PUB_TAG, PUB_TAG_512, PUB_TAG_1024, PUB_TAG_HYBRID_768};
enum EkVariant {
Kem512(EncapsulationKey512),
Kem768(EncapsulationKey768),
Kem1024(EncapsulationKey1024),
HybridKem768 { x25519_pk: [u8; 32], ml_ek: EncapsulationKey768 },
}
#[allow(dead_code)]
pub fn encrypt(
pubkey_path: &Path,
input_path: &Path,
output_path: Option<&Path>,
) -> Result<(), PqfileError> {
let pubkey_pem = fs::read_to_string(pubkey_path)?;
let plaintext = fs::read(input_path)?;
let output = encrypt_bytes(&pubkey_pem, &plaintext)?;
let out: PathBuf = match output_path {
Some(p) => p.to_path_buf(),
None => {
let mut s = input_path.as_os_str().to_owned();
s.push(".pqf");
PathBuf::from(s)
}
};
fs::write(&out, output)?;
Ok(())
}
#[must_use = "encryption result must be used"]
pub fn encrypt_bytes(pubkey_pem: &str, plaintext: &[u8]) -> Result<Vec<u8>, PqfileError> {
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct_bytes, ss_bytes) = encapsulate(ek)?;
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes).map_err(|_| PqfileError::EncryptionFailure)?;
let original_size = plaintext.len() as u64;
let header = PqfHeader {
version: VERSION,
kem_variant,
kem_ciphertext: kem_ct_bytes,
nonce: nonce_bytes,
original_size,
chunk_size: CHUNK_SIZE as u32,
compression_algo: COMPRESSION_NONE,
};
let mut output = Vec::with_capacity(HEADER_LEN + plaintext.len() + 16);
header.write(&mut output)?;
let key = Key::from_slice(ss_bytes.as_ref());
let nonce = Nonce::from_slice(&nonce_bytes);
let cipher = ChaCha20Poly1305::new(key);
let ciphertext = cipher
.encrypt(nonce, Payload { msg: plaintext, aad: &output })
.map_err(|_| PqfileError::EncryptionFailure)?;
output.extend_from_slice(&ciphertext);
Ok(output)
}
#[must_use = "encryption result must be used"]
pub fn encrypt_stream(
pubkey_pem: &str,
original_size: u64,
chunk_size: usize,
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> Result<(), PqfileError> {
if chunk_size == 0 {
return Err(PqfileError::EncryptionFailure);
}
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct_bytes, ss_bytes) = encapsulate(ek)?;
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes[..BASE_NONCE_LEN])
.map_err(|_| PqfileError::EncryptionFailure)?;
let version = if chunk_size == CHUNK_SIZE { VERSION_V3 } else { VERSION_V5 };
let header = PqfHeader {
version,
kem_variant,
kem_ciphertext: kem_ct_bytes,
nonce: nonce_bytes,
original_size,
chunk_size: chunk_size as u32,
compression_algo: COMPRESSION_NONE,
};
header.write(writer)?;
let base_nonce: &[u8; BASE_NONCE_LEN] = nonce_bytes[..BASE_NONCE_LEN].try_into()
.expect("BASE_NONCE_LEN <= NONCE_LEN; slice length is always valid");
let key = Key::from_slice(ss_bytes.as_ref());
let cipher = ChaCha20Poly1305::new(key);
encrypt_chunks(&cipher, base_nonce, chunk_size, reader, writer)
}
fn parse_encapsulation_key(pubkey_pem: &str) -> Result<(EkVariant, u16), PqfileError> {
let pem = pem::parse(pubkey_pem).map_err(|e| PqfileError::InvalidPem(e.to_string()))?;
let raw = pem.contents();
match pem.tag() {
t if t == PUB_TAG_512 => {
let raw_arr = Array::try_from(raw)
.map_err(|_| PqfileError::InvalidKeyLength { expected: EK_LEN_512, got: raw.len() })?;
let ek = EncapsulationKey512::new(&raw_arr)
.map_err(|_| PqfileError::InvalidPem("invalid ML-KEM-512 public key".to_owned()))?;
Ok((EkVariant::Kem512(ek), KEM_VARIANT_512))
}
t if t == PUB_TAG => {
let raw_arr = Array::try_from(raw)
.map_err(|_| PqfileError::InvalidKeyLength { expected: EK_LEN, got: raw.len() })?;
let ek = EncapsulationKey768::new(&raw_arr)
.map_err(|_| PqfileError::InvalidPem("invalid ML-KEM-768 public key".to_owned()))?;
Ok((EkVariant::Kem768(ek), KEM_VARIANT))
}
t if t == PUB_TAG_1024 => {
let raw_arr = Array::try_from(raw)
.map_err(|_| PqfileError::InvalidKeyLength { expected: EK_LEN_1024, got: raw.len() })?;
let ek = EncapsulationKey1024::new(&raw_arr)
.map_err(|_| PqfileError::InvalidPem("invalid ML-KEM-1024 public key".to_owned()))?;
Ok((EkVariant::Kem1024(ek), KEM_VARIANT_1024))
}
t if t == PUB_TAG_HYBRID_768 => {
if raw.len() != HYBRID_EK_LEN_768 {
return Err(PqfileError::InvalidKeyLength { expected: HYBRID_EK_LEN_768, got: raw.len() });
}
let x25519_pk_bytes: [u8; 32] = raw[..32].try_into().unwrap();
let ml_raw = &raw[32..];
let ml_arr = Array::try_from(ml_raw)
.map_err(|_| PqfileError::InvalidKeyLength { expected: EK_LEN, got: ml_raw.len() })?;
let ml_ek = EncapsulationKey768::new(&ml_arr)
.map_err(|_| PqfileError::InvalidPem("invalid ML-KEM-768 public key in hybrid".to_owned()))?;
Ok((EkVariant::HybridKem768 { x25519_pk: x25519_pk_bytes, ml_ek }, KEM_VARIANT_HYBRID_768))
}
_ => Err(PqfileError::InvalidPem("unrecognised public key tag".to_owned())),
}
}
fn encapsulate(ek: EkVariant) -> Result<(Vec<u8>, Zeroizing<[u8; 32]>), PqfileError> {
match ek {
EkVariant::Kem512(ek) => {
let (ct, ss) = ek.encapsulate();
let mut ss_bytes = Zeroizing::new([0u8; 32]);
ss_bytes.copy_from_slice(ss.as_slice());
Ok((ct.as_slice().to_vec(), ss_bytes))
}
EkVariant::Kem768(ek) => {
let (ct, ss) = ek.encapsulate();
let mut ss_bytes = Zeroizing::new([0u8; 32]);
ss_bytes.copy_from_slice(ss.as_slice());
Ok((ct.as_slice().to_vec(), ss_bytes))
}
EkVariant::Kem1024(ek) => {
let (ct, ss) = ek.encapsulate();
let mut ss_bytes = Zeroizing::new([0u8; 32]);
ss_bytes.copy_from_slice(ss.as_slice());
Ok((ct.as_slice().to_vec(), ss_bytes))
}
EkVariant::HybridKem768 { x25519_pk, ml_ek } => {
let mut eph_scalar = Zeroizing::new([0u8; 32]);
getrandom::fill(eph_scalar.as_mut()).map_err(|_| PqfileError::EncryptionFailure)?;
let eph_sk = X25519StaticSecret::from(*eph_scalar);
let eph_pk = X25519PublicKey::from(&eph_sk);
let recipient_pk = X25519PublicKey::from(x25519_pk);
let x25519_ss = Zeroizing::new(eph_sk.diffie_hellman(&recipient_pk));
let (ml_ct, ml_ss) = ml_ek.encapsulate();
let mut kem_ct = Vec::with_capacity(HYBRID_CT_LEN_768);
kem_ct.extend_from_slice(eph_pk.as_bytes());
kem_ct.extend_from_slice(ml_ct.as_slice());
let ss = hybrid_hkdf(x25519_ss.as_bytes(), ml_ss.as_slice())?;
Ok((kem_ct, ss))
}
}
}
#[must_use = "encryption result must be used"]
pub fn encrypt_stream_multi(
pubkey_pems: &[&str],
original_size: u64,
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> Result<(), PqfileError> {
let mut session_key = Zeroizing::new([0u8; 32]);
getrandom::fill(session_key.as_mut()).map_err(|_| PqfileError::EncryptionFailure)?;
let mut recipients: Vec<RecipientEntryV4> = Vec::with_capacity(pubkey_pems.len());
for pubkey_pem in pubkey_pems {
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct, ss) = encapsulate(ek)?;
let wrapped_key = wrap_session_key(&session_key, &ss)?;
recipients.push(RecipientEntryV4 { kem_variant, kem_ciphertext: kem_ct, wrapped_key });
}
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes[..BASE_NONCE_LEN])
.map_err(|_| PqfileError::EncryptionFailure)?;
let header = PqfHeaderV4 { recipients, nonce: nonce_bytes, original_size };
header.write(writer)?;
let base_nonce: &[u8; BASE_NONCE_LEN] = nonce_bytes[..BASE_NONCE_LEN].try_into()
.expect("BASE_NONCE_LEN <= NONCE_LEN; slice length is always valid");
let key = chacha20poly1305::Key::from_slice(session_key.as_ref());
let cipher = ChaCha20Poly1305::new(key);
encrypt_chunks(&cipher, base_nonce, CHUNK_SIZE, reader, writer)
}
#[must_use = "encryption result must be used"]
pub fn encrypt_stream_multi_anon(
pubkey_pems: &[&str],
original_size: u64,
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> Result<(), PqfileError> {
let mut session_key = Zeroizing::new([0u8; 32]);
getrandom::fill(session_key.as_mut()).map_err(|_| PqfileError::EncryptionFailure)?;
let mut recipients: Vec<RecipientEntryV7> = Vec::with_capacity(pubkey_pems.len());
for pubkey_pem in pubkey_pems {
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct, ss) = encapsulate(ek)?;
let wrapped_key = wrap_session_key(&session_key, &ss)?;
recipients.push(RecipientEntryV7 { kem_variant, kem_ciphertext: kem_ct, wrapped_key });
}
for i in (1..recipients.len()).rev() {
let range = (i + 1) as u64;
let threshold = (1u64 << 32) - ((1u64 << 32) % range);
let j = loop {
let mut r = [0u8; 4];
getrandom::fill(&mut r).map_err(|_| PqfileError::EncryptionFailure)?;
let v = u32::from_le_bytes(r) as u64;
if v < threshold {
break (v % range) as usize;
}
};
recipients.swap(i, j);
}
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes[..BASE_NONCE_LEN])
.map_err(|_| PqfileError::EncryptionFailure)?;
let header = PqfHeaderV7 { recipients, nonce: nonce_bytes, original_size };
header.write(writer)?;
let base_nonce: &[u8; BASE_NONCE_LEN] = nonce_bytes[..BASE_NONCE_LEN].try_into()
.expect("BASE_NONCE_LEN <= NONCE_LEN; slice length is always valid");
let key = chacha20poly1305::Key::from_slice(session_key.as_ref());
let cipher = ChaCha20Poly1305::new(key);
encrypt_chunks(&cipher, base_nonce, CHUNK_SIZE, reader, writer)
}
pub(crate) fn encapsulate_for_rekey(
pubkey_pem: &str,
session_key: &[u8; 32],
) -> Result<(Vec<u8>, u16, [u8; WRAPPED_KEY_LEN]), PqfileError> {
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct, ss) = encapsulate(ek)?;
let wrapped_key = wrap_session_key(session_key, &ss)?;
Ok((kem_ct, kem_variant, wrapped_key))
}
#[must_use = "encryption result must be used"]
pub fn encrypt_stream_compressed(
pubkey_pem: &str,
original_size: u64,
chunk_size: usize,
compress_level: i32,
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> Result<(), PqfileError> {
#[cfg(not(target_arch = "wasm32"))]
{
if chunk_size == 0 {
return Err(PqfileError::EncryptionFailure);
}
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct_bytes, ss_bytes) = encapsulate(ek)?;
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes[..BASE_NONCE_LEN])
.map_err(|_| PqfileError::EncryptionFailure)?;
let header = PqfHeader {
version: VERSION_V6,
kem_variant,
kem_ciphertext: kem_ct_bytes,
nonce: nonce_bytes,
original_size,
chunk_size: chunk_size as u32,
compression_algo: COMPRESSION_ZSTD,
};
header.write(writer)?;
let base_nonce: &[u8; BASE_NONCE_LEN] = nonce_bytes[..BASE_NONCE_LEN].try_into()
.expect("BASE_NONCE_LEN <= NONCE_LEN");
let key = Key::from_slice(ss_bytes.as_ref());
let cipher = ChaCha20Poly1305::new(key);
let mut zstd_src = zstd::stream::read::Encoder::new(reader, compress_level)
.map_err(PqfileError::Io)?;
encrypt_chunks(&cipher, base_nonce, chunk_size, &mut zstd_src, writer)
}
#[cfg(target_arch = "wasm32")]
{
let _ = (pubkey_pem, original_size, chunk_size, compress_level, reader, writer);
Err(PqfileError::CompressionNotSupported)
}
}
fn encrypt_chunks(
cipher: &ChaCha20Poly1305,
base_nonce: &[u8; BASE_NONCE_LEN],
chunk_size: usize,
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> Result<(), PqfileError> {
let mut current = vec![0u8; chunk_size];
let mut current_len = fill_chunk(reader, &mut current)?;
let mut next = vec![0u8; chunk_size];
let mut counter: u32 = 0;
loop {
let next_len = fill_chunk(reader, &mut next)?;
let is_last = next_len == 0;
let cn = chunk_nonce(base_nonce, counter);
let aad = chunk_aad(counter, is_last);
let tag = cipher
.encrypt_in_place_detached(Nonce::from_slice(&cn), &aad, &mut current[..current_len])
.map_err(|_| PqfileError::EncryptionFailure)?;
writer.write_all(¤t[..current_len])?;
writer.write_all(tag.as_ref())?;
if is_last { break; }
counter = counter.checked_add(1).ok_or(PqfileError::EncryptionFailure)?;
std::mem::swap(&mut current, &mut next);
current_len = next_len;
}
Ok(())
}
pub(crate) fn wrap_session_key(session_key: &[u8; 32], ss: &[u8; 32]) -> Result<[u8; WRAPPED_KEY_LEN], PqfileError> {
let cipher = Aes256Gcm::new(AesKey::<Aes256Gcm>::from_slice(ss));
let nonce = AesNonce::from([0u8; 12]);
let ct = cipher
.encrypt(&nonce, session_key.as_slice())
.map_err(|_| PqfileError::EncryptionFailure)?;
let mut out = [0u8; WRAPPED_KEY_LEN];
out.copy_from_slice(&ct);
Ok(out)
}
#[must_use = "encryption result must be used"]
pub fn encrypt_stream_parallel(
pubkey_pem: &str,
original_size: u64,
chunk_size: usize,
batch_size: usize,
reader: &mut dyn Read,
writer: &mut dyn Write,
) -> Result<(), PqfileError> {
if batch_size <= 1 {
return encrypt_stream(pubkey_pem, original_size, chunk_size, reader, writer);
}
if chunk_size == 0 {
return Err(PqfileError::EncryptionFailure);
}
let (ek, kem_variant) = parse_encapsulation_key(pubkey_pem)?;
let (kem_ct_bytes, ss_bytes) = encapsulate(ek)?;
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce_bytes[..BASE_NONCE_LEN])
.map_err(|_| PqfileError::EncryptionFailure)?;
let version = if chunk_size == CHUNK_SIZE { VERSION_V3 } else { VERSION_V5 };
let header = PqfHeader {
version,
kem_variant,
kem_ciphertext: kem_ct_bytes,
nonce: nonce_bytes,
original_size,
chunk_size: chunk_size as u32,
compression_algo: COMPRESSION_NONE,
};
header.write(writer)?;
let base_nonce: [u8; BASE_NONCE_LEN] = nonce_bytes[..BASE_NONCE_LEN].try_into().unwrap();
let key_bytes = Zeroizing::new(*ss_bytes);
let mut first = vec![0u8; chunk_size];
let first_len = fill_chunk(reader, &mut first)?;
if first_len == 0 {
let cn = chunk_nonce(&base_nonce, 0);
let aad = chunk_aad(0, true);
let cipher = ChaCha20Poly1305::new(Key::from_slice(key_bytes.as_ref()));
let tag = cipher
.encrypt_in_place_detached(Nonce::from_slice(&cn), &aad, &mut [])
.map_err(|_| PqfileError::EncryptionFailure)?;
writer.write_all(tag.as_ref())?;
return Ok(());
}
let mut carry: Option<(Vec<u8>, usize)> = Some((first, first_len));
let mut counter: u32 = 0;
loop {
let mut batch: Vec<(Vec<u8>, usize)> = Vec::with_capacity(batch_size);
if let Some(c) = carry.take() {
batch.push(c);
}
while batch.len() < batch_size {
let mut buf = vec![0u8; chunk_size];
let n = fill_chunk(reader, &mut buf)?;
if n == 0 {
break;
}
batch.push((buf, n));
}
if batch.is_empty() {
break;
}
let mut peek = vec![0u8; chunk_size];
let peek_len = fill_chunk(reader, &mut peek)?;
let batch_is_final = peek_len == 0;
if !batch_is_final {
carry = Some((peek, peek_len));
}
let batch_len = batch.len();
let batch_start = counter;
let results: Vec<Result<_, PqfileError>> = batch
.into_par_iter()
.enumerate()
.map(|(i, (mut chunk, chunk_len))| {
let c = batch_start.checked_add(i as u32).ok_or(PqfileError::EncryptionFailure)?;
let is_last = batch_is_final && i == batch_len - 1;
let cn = chunk_nonce(&base_nonce, c);
let aad = chunk_aad(c, is_last);
let cipher = ChaCha20Poly1305::new(Key::from_slice(key_bytes.as_ref()));
let tag = cipher
.encrypt_in_place_detached(Nonce::from_slice(&cn), &aad, &mut chunk[..chunk_len])
.map_err(|_| PqfileError::EncryptionFailure)?;
let mut tag_arr = [0u8; 16];
tag_arr.copy_from_slice(tag.as_ref());
Ok((chunk, chunk_len, tag_arr))
})
.collect();
for r in results {
let (ct, ct_len, tag): (Vec<u8>, usize, [u8; 16]) = r?;
writer.write_all(&ct[..ct_len])?;
writer.write_all(&tag)?;
}
counter = batch_start
.checked_add(batch_len as u32)
.ok_or(PqfileError::EncryptionFailure)?;
if batch_is_final {
break;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keygen::keygen_bytes;
use tempfile::tempdir;
fn keypair() -> (String, String) {
keygen_bytes(768, None).unwrap()
}
fn keypair_1024() -> (String, String) {
keygen_bytes(1024, None).unwrap()
}
fn keypair_512() -> (String, String) {
keygen_bytes(512, None).unwrap()
}
fn write_pubkey(dir: &Path) -> PathBuf {
let (pub_pem, _) = keypair();
let path = dir.join("pk.pem");
fs::write(&path, pub_pem.as_bytes()).unwrap();
path
}
#[test]
fn encrypt_writes_to_custom_output_path() {
let tmp = tempdir().unwrap();
let pk = write_pubkey(tmp.path());
let input = tmp.path().join("plain.txt");
fs::write(&input, b"hello custom output").unwrap();
let out = tmp.path().join("custom.pqf");
encrypt(&pk, &input, Some(&out)).unwrap();
assert!(out.exists());
}
#[test]
fn encrypt_defaults_to_input_with_pqf_suffix() {
let tmp = tempdir().unwrap();
let pk = write_pubkey(tmp.path());
let input = tmp.path().join("data.txt");
fs::write(&input, b"hello default").unwrap();
encrypt(&pk, &input, None).unwrap();
assert!(tmp.path().join("data.txt.pqf").exists());
}
#[test]
fn encrypt_rejects_malformed_public_key_bytes() {
let bad_key = pem::encode(&pem::Pem::new("ML-KEM-768 PUBLIC KEY", vec![0xFFu8; 1184]));
let result = encrypt_bytes(&bad_key, b"hello");
assert!(matches!(result, Err(PqfileError::InvalidPem(_))));
}
#[test]
fn encrypt_rejects_unrecognised_key_tag() {
let bad_key = pem::encode(&pem::Pem::new("UNKNOWN KEY", vec![0u8; 1184]));
let result = encrypt_bytes(&bad_key, b"hello");
assert!(matches!(result, Err(PqfileError::InvalidPem(_))));
}
#[test]
fn encrypt_stream_rejects_malformed_public_key() {
let bad_key = pem::encode(&pem::Pem::new("ML-KEM-768 PUBLIC KEY", vec![0xFFu8; 1184]));
let mut reader: &[u8] = b"data";
let mut writer = Vec::new();
let result = encrypt_stream(&bad_key, 4, CHUNK_SIZE, &mut reader, &mut writer);
assert!(matches!(result, Err(PqfileError::InvalidPem(_))));
}
#[test]
fn encrypt_stream_rejects_zero_chunk_size() {
let (pub_pem, _) = keypair();
let mut reader: &[u8] = b"data";
let mut writer = Vec::new();
let result = encrypt_stream(&pub_pem, 4, 0, &mut reader, &mut writer);
assert!(matches!(result, Err(PqfileError::EncryptionFailure)));
}
#[test]
fn encrypt_stream_empty_input() {
let (pub_pem, _) = keypair();
let mut reader: &[u8] = &[];
let mut writer = Vec::new();
encrypt_stream(&pub_pem, 0, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
assert_eq!(writer.len(), HEADER_LEN + 16);
}
#[test]
fn encrypt_stream_small_input_produces_header_plus_one_chunk() {
let (pub_pem, _) = keypair();
let plaintext = b"small payload";
let mut reader: &[u8] = plaintext;
let mut writer = Vec::new();
encrypt_stream(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
assert_eq!(writer.len(), HEADER_LEN + plaintext.len() + 16);
}
#[test]
fn encrypt_stream_exact_chunk_boundary() {
let (pub_pem, _) = keypair();
let plaintext = vec![0xABu8; CHUNK_SIZE];
let mut reader: &[u8] = &plaintext;
let mut writer = Vec::new();
encrypt_stream(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
assert_eq!(writer.len(), HEADER_LEN + CHUNK_SIZE + 16);
}
#[test]
fn encrypt_stream_multi_chunk() {
let (pub_pem, _) = keypair();
let plaintext = vec![0x42u8; CHUNK_SIZE * 2 + 1];
let mut reader: &[u8] = &plaintext;
let mut writer = Vec::new();
encrypt_stream(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
let expected = HEADER_LEN + (CHUNK_SIZE + 16) * 2 + (1 + 16);
assert_eq!(writer.len(), expected);
}
#[test]
fn encrypt_stream_writes_v3_version_byte() {
use std::io::Cursor;
use crate::format::VERSION_V3;
let (pub_pem, _) = keypair();
let mut reader: &[u8] = b"test";
let mut writer = Vec::new();
encrypt_stream(&pub_pem, 4, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
let header = PqfHeader::read(&mut Cursor::new(&writer)).unwrap();
assert_eq!(header.version, VERSION_V3);
}
#[test]
fn encrypt_stream_1024_writes_correct_header() {
use std::io::Cursor;
use crate::format::{HEADER_LEN_1024, KEM_VARIANT_1024, VERSION_V3};
let (pub_pem, _) = keypair_1024();
let mut reader: &[u8] = b"test";
let mut writer = Vec::new();
encrypt_stream(&pub_pem, 4, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
let header = PqfHeader::read(&mut Cursor::new(&writer)).unwrap();
assert_eq!(header.version, VERSION_V3);
assert_eq!(header.kem_variant, KEM_VARIANT_1024);
assert_eq!(header.kem_ciphertext.len(), 1568);
assert_eq!(writer.len(), HEADER_LEN_1024 + 4 + 16);
}
#[test]
fn encrypt_stream_512_writes_correct_header() {
use std::io::Cursor;
use crate::format::{HEADER_LEN_512, KEM_VARIANT_512, VERSION_V3};
let (pub_pem, _) = keypair_512();
let mut reader: &[u8] = b"test";
let mut writer = Vec::new();
encrypt_stream(&pub_pem, 4, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
let header = PqfHeader::read(&mut Cursor::new(&writer)).unwrap();
assert_eq!(header.version, VERSION_V3);
assert_eq!(header.kem_variant, KEM_VARIANT_512);
assert_eq!(header.kem_ciphertext.len(), 768);
assert_eq!(writer.len(), HEADER_LEN_512 + 4 + 16);
}
#[test]
fn encrypt_stream_default_chunk_size_emits_v3() {
use std::io::Cursor;
let (pub_pem, _) = keypair();
let mut reader: &[u8] = b"hello";
let mut writer = Vec::new();
encrypt_stream(&pub_pem, 5, CHUNK_SIZE, &mut reader, &mut writer).unwrap();
let header = PqfHeader::read(&mut Cursor::new(&writer)).unwrap();
assert_eq!(header.version, VERSION_V3);
}
#[test]
fn encrypt_stream_custom_chunk_size_emits_v5() {
use std::io::Cursor;
use crate::format::VERSION_V5;
let (pub_pem, _) = keypair();
let mut reader: &[u8] = b"hello";
let mut writer = Vec::new();
encrypt_stream(&pub_pem, 5, 4096, &mut reader, &mut writer).unwrap();
let header = PqfHeader::read(&mut Cursor::new(&writer)).unwrap();
assert_eq!(header.version, VERSION_V5);
assert_eq!(header.chunk_size, 4096);
}
#[test]
fn encrypt_stream_v5_header_is_4_bytes_longer_than_v3() {
use crate::format::V5_CHUNK_SIZE_FIELD_LEN;
let (pub_pem, _) = keypair();
let mut r1: &[u8] = b"x";
let mut v3_out = Vec::new();
encrypt_stream(&pub_pem, 1, CHUNK_SIZE, &mut r1, &mut v3_out).unwrap();
let mut r2: &[u8] = b"x";
let mut v5_out = Vec::new();
encrypt_stream(&pub_pem, 1, 4096, &mut r2, &mut v5_out).unwrap();
assert_eq!(v5_out.len() - v3_out.len(), V5_CHUNK_SIZE_FIELD_LEN);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn encrypt_stream_compressed_writes_v6_version_byte() {
let (pub_pem, _) = keypair();
let plaintext = b"compress me";
let mut out = Vec::new();
encrypt_stream_compressed(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, 3, &mut plaintext.as_slice(), &mut out).unwrap();
let version_pos = crate::format::MAGIC.len();
assert_eq!(out[version_pos], VERSION_V6);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn encrypt_stream_compressed_rejects_zero_chunk_size() {
let (pub_pem, _) = keypair();
let mut reader: &[u8] = b"data";
let mut writer = Vec::new();
let result = encrypt_stream_compressed(&pub_pem, 4, 0, 3, &mut reader, &mut writer);
assert!(matches!(result, Err(PqfileError::EncryptionFailure)));
}
#[test]
fn parallel_encrypt_small_input_decrypts_correctly() {
use crate::decrypt::decrypt_stream;
let (pub_pem, priv_pem) = keypair();
let plaintext = b"parallel small";
let mut ct = Vec::new();
encrypt_stream_parallel(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, 4, &mut plaintext.as_slice(), &mut ct).unwrap();
let mut out = Vec::new();
decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
assert_eq!(out, plaintext);
}
#[test]
fn parallel_encrypt_multi_batch_decrypts_correctly() {
use crate::decrypt::decrypt_stream;
let (pub_pem, priv_pem) = keypair();
let plaintext: Vec<u8> = (0u8..=255).cycle().take(CHUNK_SIZE * 10 + 7).collect();
let mut ct = Vec::new();
encrypt_stream_parallel(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, 4, &mut plaintext.as_slice(), &mut ct).unwrap();
let mut out = Vec::new();
decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
assert_eq!(out, plaintext);
}
#[test]
fn parallel_encrypt_empty_input_decrypts_correctly() {
use crate::decrypt::decrypt_stream;
let (pub_pem, priv_pem) = keypair();
let mut ct = Vec::new();
encrypt_stream_parallel(&pub_pem, 0, CHUNK_SIZE, 4, &mut [].as_slice(), &mut ct).unwrap();
let mut out = Vec::new();
decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
assert!(out.is_empty());
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn encrypt_stream_compressed_v6_header_has_extra_fields() {
let (pub_pem, _) = keypair();
let plaintext = b"v6 header size check";
let mut v3_out = Vec::new();
encrypt_stream(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, &mut plaintext.as_slice(), &mut v3_out).unwrap();
let mut v6_out = Vec::new();
encrypt_stream_compressed(&pub_pem, plaintext.len() as u64, CHUNK_SIZE, 1, &mut plaintext.as_slice(), &mut v6_out).unwrap();
use crate::format::{V5_CHUNK_SIZE_FIELD_LEN, V6_COMPRESSION_FIELD_LEN};
assert!(v6_out.len() > v3_out.len());
let _ = V5_CHUNK_SIZE_FIELD_LEN + V6_COMPRESSION_FIELD_LEN; }
}