use pqfile::decrypt::{decrypt_stream, decrypt_stream_passphrase};
use pqfile::encrypt::{encrypt_stream, encrypt_stream_passphrase};
use pqfile::format::{
is_header_authenticated, version_layout, CHUNK_SIZE, COMPRESSION_NONE, KEM_CT_LEN_768,
NONCE_LEN, VERSION_AUTH_BIT, VERSION_V3, VERSION_V4,
};
use pqfile::keygen::keygen_bytes;
const PLAINTEXT: &[u8] = b"authenticated header test payload";
const VERSION_OFF: usize = 4;
fn single_header_base(ct_len: usize) -> usize {
4 + 1 + 2 + ct_len + NONCE_LEN + 8
}
fn encrypt_768(chunk_size: usize) -> (Vec<u8>, String) {
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 },
&mut ct,
)
.unwrap();
(ct, priv_pem)
}
#[test]
fn new_files_carry_the_auth_bit() {
let (ct, priv_pem) = encrypt_768(CHUNK_SIZE);
assert!(is_header_authenticated(ct[VERSION_OFF]));
assert_eq!(version_layout(ct[VERSION_OFF]), VERSION_V3);
let mut out = Vec::new();
decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
assert_eq!(out, PLAINTEXT);
}
#[test]
fn auth_bit_strip_is_rejected() {
let (mut ct, priv_pem) = encrypt_768(CHUNK_SIZE);
ct[VERSION_OFF] &= !VERSION_AUTH_BIT;
let mut out = Vec::new();
let result = decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None);
assert!(result.is_err(), "stripped auth bit must fail decryption");
}
#[test]
fn auth_bit_added_to_legacy_file_is_rejected() {
let mut ct = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/compat/v3_512.pqf"
))
.unwrap();
let priv_pem = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/compat/v3_512.priv.pem"
))
.unwrap();
assert!(!is_header_authenticated(ct[VERSION_OFF]));
ct[VERSION_OFF] |= VERSION_AUTH_BIT;
let mut out = Vec::new();
let result = decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None);
assert!(
result.is_err(),
"adding the auth bit to a legacy file must fail decryption"
);
}
#[test]
fn v5_chunk_size_tamper_is_rejected() {
let (mut ct, priv_pem) = encrypt_768(4096);
let chunk_size_off = single_header_base(KEM_CT_LEN_768);
assert_eq!(
u32::from_le_bytes(ct[chunk_size_off..chunk_size_off + 4].try_into().unwrap()),
4096
);
ct[chunk_size_off..chunk_size_off + 4].copy_from_slice(&8192u32.to_le_bytes());
let mut out = Vec::new();
let result = decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None);
assert!(result.is_err(), "tampered chunk_size must fail decryption");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn v6_compression_flag_flip_is_rejected() {
use pqfile::encrypt::encrypt_stream_compressed;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext: Vec<u8> = (0u8..=63).cycle().take(CHUNK_SIZE + 100).collect();
let mut ct = Vec::new();
encrypt_stream_compressed(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
3,
&mut plaintext.as_slice(),
&mut ct,
)
.unwrap();
let algo_off = single_header_base(KEM_CT_LEN_768) + 4;
assert_ne!(ct[algo_off], COMPRESSION_NONE, "fixture must be compressed");
ct[algo_off] = COMPRESSION_NONE;
let mut out = Vec::new();
let result = decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None);
assert!(
result.is_err(),
"flipped compression flag must fail decryption, not emit compressed bytes"
);
assert_ne!(out, plaintext);
}
#[test]
fn v10_kdf_param_tamper_is_rejected() {
let mut ct = Vec::new();
encrypt_stream_passphrase(
"hunter2",
PLAINTEXT.len() as u64,
&mut { PLAINTEXT },
&mut ct,
)
.unwrap();
assert!(is_header_authenticated(ct[VERSION_OFF]));
let t_off = 4 + 1 + 16 + 4;
let t = u32::from_le_bytes(ct[t_off..t_off + 4].try_into().unwrap());
ct[t_off..t_off + 4].copy_from_slice(&(t - 1).to_le_bytes());
let mut out = Vec::new();
let result = decrypt_stream_passphrase("hunter2", &mut ct.as_slice(), &mut out);
assert!(
result.is_err(),
"tampered Argon2 t_cost must fail decryption"
);
}
#[test]
fn rekey_preserves_auth_bit_and_decrypts() {
let (pub_old, priv_old) = keygen_bytes(768, None).unwrap();
let (pub_new, priv_new) = keygen_bytes(768, None).unwrap();
let mut ct = Vec::new();
encrypt_stream(
&pub_old,
PLAINTEXT.len() as u64,
CHUNK_SIZE,
&mut { PLAINTEXT },
&mut ct,
)
.unwrap();
assert!(is_header_authenticated(ct[VERSION_OFF]));
let mut rekeyed = Vec::new();
pqfile::rekey::rekey_stream(&priv_old, &pub_new, &mut ct.as_slice(), &mut rekeyed, None)
.unwrap();
assert!(is_header_authenticated(rekeyed[VERSION_OFF]));
assert_eq!(version_layout(rekeyed[VERSION_OFF]), VERSION_V4);
let mut out = Vec::new();
decrypt_stream(&priv_new, &mut rekeyed.as_slice(), &mut out, None).unwrap();
assert_eq!(out, PLAINTEXT);
}
#[test]
fn add_recipient_preserves_auth_bit_and_decrypts() {
let (pub1, priv1) = keygen_bytes(768, None).unwrap();
let (pub2, priv2) = keygen_bytes(768, None).unwrap();
let mut ct = Vec::new();
pqfile::encrypt::encrypt_stream_multi(
&[pub1.as_str()],
PLAINTEXT.len() as u64,
&mut { PLAINTEXT },
&mut ct,
)
.unwrap();
assert!(is_header_authenticated(ct[VERSION_OFF]));
let mut with_new = Vec::new();
pqfile::add_recipient::add_recipient_stream(
&priv1,
&pub2,
&mut ct.as_slice(),
&mut with_new,
None,
)
.unwrap();
assert!(is_header_authenticated(with_new[VERSION_OFF]));
for priv_pem in [&priv1, &priv2] {
let mut out = Vec::new();
decrypt_stream(priv_pem, &mut with_new.as_slice(), &mut out, None).unwrap();
assert_eq!(out, PLAINTEXT);
}
}
#[test]
fn legacy_fixture_with_bit_variants_reports_clean_errors() {
let mut ct = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/compat/v2_768.pqf"
))
.unwrap();
let priv_pem = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/compat/v2_768.priv.pem"
))
.unwrap();
ct[VERSION_OFF] |= VERSION_AUTH_BIT;
let mut out = Vec::new();
let result = decrypt_stream(&priv_pem, &mut ct.as_slice(), &mut out, None);
assert!(matches!(
result,
Err(pqfile::error::PqfileError::UnsupportedVersion(0x82))
));
}