#![cfg(not(target_arch = "wasm32"))]
use proptest::prelude::*;
use pqfile::decrypt::decrypt_stream;
use pqfile::encrypt::encrypt_stream;
use pqfile::format::CHUNK_SIZE;
use pqfile::keygen::keygen_bytes;
use pqfile::shamir::{reconstruct_key, split_key};
proptest! {
#[test]
fn prop_encrypt_decrypt_roundtrip(
plaintext in proptest::collection::vec(any::<u8>(), 0..=(3 * CHUNK_SIZE + 512))
) {
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let mut ct = Vec::new();
encrypt_stream(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
).unwrap();
let mut out = Vec::new();
decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
prop_assert_eq!(out, plaintext);
}
}
proptest! {
#[test]
fn prop_encrypt_decrypt_small_chunks(
plaintext in proptest::collection::vec(any::<u8>(), 0..=2048),
chunk_size in 16usize..=256,
) {
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let mut ct = Vec::new();
encrypt_stream(
&pub_pem,
plaintext.len() as u64,
chunk_size,
&mut plaintext.as_slice(),
&mut ct,
).unwrap();
let mut out = Vec::new();
decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
prop_assert_eq!(out, plaintext);
}
}
#[test]
fn prop_chunk_boundary_sizes() {
const KIB: usize = 1024;
let boundaries = [
16 * KIB - 1,
16 * KIB,
16 * KIB + 1,
64 * KIB - 1,
64 * KIB,
64 * KIB + 1,
256 * KIB - 1,
256 * KIB,
256 * KIB + 1,
];
let (pub_pem, priv_pem) = pqfile::keygen::keygen_bytes(768, None).unwrap();
for &size in &boundaries {
let plaintext: Vec<u8> = (0u8..=255).cycle().take(size).collect();
let mut ct = Vec::new();
pqfile::encrypt::encrypt_stream(
&pub_pem,
size as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
)
.unwrap_or_else(|e| panic!("encrypt failed at size {size}: {e}"));
let mut out = Vec::new();
pqfile::decrypt::decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None)
.unwrap_or_else(|e| panic!("decrypt failed at size {size}: {e}"));
assert_eq!(out, plaintext, "roundtrip mismatch at boundary size {size}");
}
}
proptest! {
#[test]
fn prop_single_byte_flip_fails(
plaintext in proptest::collection::vec(any::<u8>(), 1..=64),
flip_offset in 0usize..64,
) {
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let mut ct = Vec::new();
encrypt_stream(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
).unwrap();
let header_len = ct.len().saturating_sub(plaintext.len() + 16);
let payload_len = ct.len() - header_len;
if payload_len == 0 {
return Ok(());
}
let flip_pos = header_len + (flip_offset % payload_len);
ct[flip_pos] ^= 0xFF;
let result = decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut Vec::new(), None);
prop_assert!(result.is_err(), "tampered ciphertext must not decrypt successfully");
}
}
proptest! {
#[test]
fn prop_shamir_split_reconstruct(
threshold in 2u8..=5,
extra in 0u8..=3, ) {
let total = threshold + extra;
let (_, priv_pem) = keygen_bytes(768, None).unwrap();
let result = split_key(&priv_pem, threshold, total, None).unwrap();
prop_assert_eq!(result.share_pems.len(), total as usize);
let subset: Vec<&str> = result.share_pems[..threshold as usize]
.iter().map(|s| s.as_str()).collect();
let (_, reconstructed_priv) = reconstruct_key(&subset).unwrap();
prop_assert_eq!(reconstructed_priv, priv_pem);
}
}
proptest! {
#[test]
fn prop_shamir_insufficient_shares_gives_wrong_key(
threshold in 3u8..=5,
) {
let total = threshold;
let (_, priv_pem) = keygen_bytes(768, None).unwrap();
let result = split_key(&priv_pem, threshold, total, None).unwrap();
let subset: Vec<&str> = result.share_pems[..threshold as usize - 1]
.iter().map(|s| s.as_str()).collect();
match reconstruct_key(&subset) {
Err(_) => {} Ok((_, recovered_priv)) => prop_assert_ne!(recovered_priv, priv_pem,
"reconstructing with too few shares must not recover the key"),
}
}
}