use aes::cipher::{generic_array::GenericArray, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use sha2::Digest;
use std::sync::Arc;
use zpdf_core::{ObjectId, PdfDict, PdfObject, PdfString};
const PAD: [u8; 32] = [
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Algo {
Identity,
Rc4,
AesV2,
AesV3,
}
pub enum BuildResult {
Decryptor(Decryptor),
Degrade,
WrongPassword,
}
pub struct Decryptor {
key: Vec<u8>,
stm_algo: Algo,
str_algo: Algo,
encrypt_id: Option<ObjectId>,
encrypt_metadata: bool,
}
impl Decryptor {
pub fn from_encrypt_dict(
dict: &PdfDict,
id_first: &[u8],
encrypt_id: Option<ObjectId>,
password: &[u8],
) -> BuildResult {
let filter = dict.get_name("Filter").unwrap_or("");
if filter != "Standard" {
tracing::warn!(
"unsupported security handler /Filter {filter}; document will not decrypt"
);
return BuildResult::Degrade;
}
let v = dict.get_i64("V").unwrap_or(0);
let r = dict.get_i64("R").unwrap_or(0);
let (stm_algo, str_algo) = if v >= 4 {
(
algo_for_filter(dict, dict.get_name("StmF").unwrap_or("Identity")),
algo_for_filter(dict, dict.get_name("StrF").unwrap_or("Identity")),
)
} else {
(Algo::Rc4, Algo::Rc4)
};
let encrypt_metadata = match dict.get("EncryptMetadata") {
Some(PdfObject::Bool(b)) => *b,
_ => true,
};
let key = if v >= 5 {
match compute_key_v5(dict, r, password) {
Some(k) => k,
None if password.is_empty() => return BuildResult::Degrade,
None => return BuildResult::WrongPassword,
}
} else {
let o = string_bytes(dict, "O");
let u = string_bytes(dict, "U");
let p = match dict.get("P") {
Some(PdfObject::Integer(n)) => *n as i32,
Some(PdfObject::Real(n)) => *n as i32,
_ => 0,
};
let length_bits = if stm_algo == Algo::AesV2 || str_algo == Algo::AesV2 {
128
} else {
key_length_bits(dict, v)
};
match authenticate_rc4(
password,
&o,
&u,
p,
id_first,
r,
length_bits,
encrypt_metadata,
) {
Ok(key) => key,
Err(_) if !password.is_empty() && !u.is_empty() => {
return BuildResult::WrongPassword;
}
Err(best_effort_key) => {
if u.is_empty() && !password.is_empty() {
tracing::warn!(
"encrypted document has no /U to authenticate the password against \
(V={v} R={r}); proceeding with the supplied password unverified"
);
} else {
tracing::warn!(
"encryption key did not validate against /U (V={v} R={r}); the PDF \
may require a password — decrypted content may be garbage"
);
}
best_effort_key
}
}
};
BuildResult::Decryptor(Self {
key,
stm_algo,
str_algo,
encrypt_id,
encrypt_metadata,
})
}
pub fn decrypt_object(&self, obj: &mut PdfObject, id: ObjectId) {
if Some(id) == self.encrypt_id {
return;
}
self.walk(obj, id);
}
pub fn decrypt_stream_bytes(&self, id: ObjectId, data: &[u8]) -> Vec<u8> {
self.decrypt(id, data, self.stm_algo)
}
fn walk(&self, obj: &mut PdfObject, id: ObjectId) {
match obj {
PdfObject::String(s) if self.str_algo != Algo::Identity => {
*s = PdfString(self.decrypt(id, &s.0, self.str_algo));
}
PdfObject::String(_) => {}
PdfObject::Array(a) => {
for o in a.iter_mut() {
self.walk(o, id);
}
}
PdfObject::Dict(d) => {
for v in d.0.values_mut() {
self.walk(v, id);
}
}
PdfObject::Stream(s) => {
let typ = s.dict.get_name("Type").unwrap_or("");
let is_xref = typ == "XRef";
let plain_meta = !self.encrypt_metadata && typ == "Metadata";
if !is_xref && !plain_meta && self.stm_algo != Algo::Identity {
let dec = self.decrypt(id, &s.data, self.stm_algo);
s.data = Arc::from(dec);
}
for v in s.dict.0.values_mut() {
self.walk(v, id);
}
}
_ => {}
}
}
fn decrypt(&self, id: ObjectId, data: &[u8], algo: Algo) -> Vec<u8> {
match algo {
Algo::Identity => data.to_vec(),
Algo::Rc4 => rc4(&self.object_key(id, algo), data),
Algo::AesV2 | Algo::AesV3 => aes_cbc_decrypt(&self.object_key(id, algo), data),
}
}
fn object_key(&self, id: ObjectId, algo: Algo) -> Vec<u8> {
if algo == Algo::AesV3 {
return self.key.clone();
}
let mut input = Vec::with_capacity(self.key.len() + 9);
input.extend_from_slice(&self.key);
let num = id.0.to_le_bytes();
input.extend_from_slice(&num[..3]);
let gen = id.1.to_le_bytes();
input.extend_from_slice(&gen[..2]);
if algo == Algo::AesV2 {
input.extend_from_slice(b"sAlT");
}
let hash = md5(&input);
let n = (self.key.len() + 5).min(16);
hash[..n].to_vec()
}
}
fn key_length_bits(dict: &PdfDict, v: i64) -> i64 {
if v >= 4 {
let stmf = dict.get_name("StmF").unwrap_or("Identity");
if stmf != "Identity" {
if let Some(len) = dict
.get_dict("CF")
.ok()
.and_then(|cf| cf.get_dict(stmf).ok())
.and_then(|f| f.get_i64("Length").ok())
{
return if len <= 32 { len * 8 } else { len };
}
}
}
dict.get_i64("Length").unwrap_or(40)
}
fn validate_user_password(key: &[u8], u: &[u8], id_first: &[u8], r: i64) -> bool {
if u.is_empty() {
return false;
}
if r == 2 {
return rc4(key, &PAD) == u;
}
let mut input = Vec::with_capacity(PAD.len() + id_first.len());
input.extend_from_slice(&PAD);
input.extend_from_slice(id_first);
let mut x = rc4(key, &md5(&input));
for i in 1u8..=19 {
let step_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
x = rc4(&step_key, &x);
}
u.len() >= 16 && x.len() >= 16 && x[..16] == u[..16]
}
fn algo_for_filter(dict: &PdfDict, filter_name: &str) -> Algo {
if filter_name == "Identity" {
return Algo::Identity;
}
let cfm = dict
.get_dict("CF")
.ok()
.and_then(|cf| cf.get_dict(filter_name).ok())
.and_then(|f| f.get_name("CFM").ok())
.unwrap_or("V2");
match cfm {
"AESV2" => Algo::AesV2,
"AESV3" => Algo::AesV3,
"None" => Algo::Identity,
_ => Algo::Rc4,
}
}
fn pad_password(password: &[u8]) -> [u8; 32] {
let mut out = [0u8; 32];
let take = password.len().min(32);
out[..take].copy_from_slice(&password[..take]);
out[take..].copy_from_slice(&PAD[..32 - take]);
out
}
fn rc4_key_len(r: i64, length_bits: i64) -> usize {
if r == 2 {
5
} else {
(length_bits / 8).clamp(5, 16) as usize
}
}
#[allow(clippy::too_many_arguments)]
fn authenticate_rc4(
password: &[u8],
o: &[u8],
u: &[u8],
p: i32,
id_first: &[u8],
r: i64,
length_bits: i64,
encrypt_metadata: bool,
) -> std::result::Result<Vec<u8>, Vec<u8>> {
let key = compute_key_rc4(password, o, p, id_first, r, length_bits, encrypt_metadata);
if validate_user_password(&key, u, id_first, r) {
return Ok(key);
}
let recovered = recover_user_password_rc4(password, o, r, length_bits);
let owner_key = compute_key_rc4(&recovered, o, p, id_first, r, length_bits, encrypt_metadata);
if validate_user_password(&owner_key, u, id_first, r) {
return Ok(owner_key);
}
Err(key)
}
fn recover_user_password_rc4(owner_password: &[u8], o: &[u8], r: i64, length_bits: i64) -> Vec<u8> {
let n = rc4_key_len(r, length_bits);
let mut hash = md5(&pad_password(owner_password));
if r >= 3 {
for _ in 0..50 {
hash = md5(&hash[..n]);
}
}
let owner_key = &hash[..n];
let mut user = o.to_vec();
if r == 2 {
user = rc4(owner_key, &user);
} else {
for i in (0..=19u8).rev() {
let step_key: Vec<u8> = owner_key.iter().map(|b| b ^ i).collect();
user = rc4(&step_key, &user);
}
}
user
}
fn compute_key_rc4(
password: &[u8],
o: &[u8],
p: i32,
id_first: &[u8],
r: i64,
length_bits: i64,
encrypt_metadata: bool,
) -> Vec<u8> {
let n = rc4_key_len(r, length_bits);
let mut input = Vec::with_capacity(32 + 32 + 4 + id_first.len() + 4);
input.extend_from_slice(&pad_password(password));
let mut o32 = [0u8; 32];
let take = o.len().min(32);
o32[..take].copy_from_slice(&o[..take]);
input.extend_from_slice(&o32);
input.extend_from_slice(&(p as u32).to_le_bytes());
input.extend_from_slice(id_first);
if r >= 4 && !encrypt_metadata {
input.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
}
let mut hash = md5(&input);
if r >= 3 {
for _ in 0..50 {
hash = md5(&hash[..n]);
}
}
hash[..n].to_vec()
}
fn compute_key_v5(dict: &PdfDict, r: i64, password: &[u8]) -> Option<Vec<u8>> {
let o = string_bytes(dict, "O");
let u = string_bytes(dict, "U");
let oe = string_bytes(dict, "OE");
let ue = string_bytes(dict, "UE");
let password = &password[..password.len().min(127)];
if u.len() >= 48 {
let (vsalt, ksalt) = (&u[32..40], &u[40..48]);
if hash_v5(r, password, vsalt, &[])[..] == u[..32] {
let ik = hash_v5(r, password, ksalt, &[]);
if let Some(key) = decrypt_file_key(&ik, &ue, "UE") {
return Some(key);
}
}
}
if o.len() >= 48 && u.len() >= 48 {
let u48 = &u[..48];
let (vsalt, ksalt) = (&o[32..40], &o[40..48]);
if hash_v5(r, password, vsalt, u48)[..] == o[..32] {
let ik = hash_v5(r, password, ksalt, u48);
if let Some(key) = decrypt_file_key(&ik, &oe, "OE") {
return Some(key);
}
}
}
tracing::warn!(
"V5/R{r} password validation failed (the PDF likely requires a password); \
document will not decrypt"
);
None
}
fn decrypt_file_key(intermediate: &[u8; 32], encrypted: &[u8], which: &str) -> Option<Vec<u8>> {
if encrypted.len() != 32 {
tracing::warn!(
"/{which} must be 32 bytes, got {}; document will not decrypt",
encrypted.len()
);
return None;
}
let mut buf = encrypted.to_vec();
if !cbc_decrypt_in_place(intermediate, &[0u8; 16], &mut buf) {
return None;
}
Some(buf)
}
fn hash_v5(r: i64, password: &[u8], salt: &[u8], udata: &[u8]) -> [u8; 32] {
let mut input = Vec::with_capacity(password.len() + salt.len() + udata.len());
input.extend_from_slice(password);
input.extend_from_slice(salt);
input.extend_from_slice(udata);
let initial: [u8; 32] = sha2::Sha256::digest(&input).into();
if r >= 6 {
hash_r6(initial, password, udata)
} else {
initial
}
}
fn hash_r6(initial: [u8; 32], password: &[u8], udata: &[u8]) -> [u8; 32] {
let mut k: Vec<u8> = initial.to_vec();
let mut e_last: u8 = 0;
let mut round: i64 = 0;
while round < 64 || i64::from(e_last) > round - 32 {
let mut k1 = Vec::with_capacity(64 * (password.len() + k.len() + udata.len()));
for _ in 0..64 {
k1.extend_from_slice(password);
k1.extend_from_slice(&k);
k1.extend_from_slice(udata);
}
let e = aes128_cbc_encrypt_nopad(&k[..16], &k[16..32], &k1);
e_last = *e.last().unwrap_or(&0);
let m = e[..16].iter().map(|&b| u32::from(b)).sum::<u32>() % 3;
k = match m {
0 => sha2::Sha256::digest(&e).to_vec(),
1 => sha2::Sha384::digest(&e).to_vec(),
_ => sha2::Sha512::digest(&e).to_vec(),
};
round += 1;
}
let mut out = [0u8; 32];
out.copy_from_slice(&k[..32]);
out
}
fn string_bytes(dict: &PdfDict, key: &str) -> Vec<u8> {
match dict.get(key) {
Some(PdfObject::String(s)) => s.0.clone(),
_ => Vec::new(),
}
}
fn aes_cbc_decrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
if data.is_empty() {
return Vec::new();
}
if data.len() < 16 || !(data.len() - 16).is_multiple_of(16) {
tracing::warn!(
"AES-CBC payload length {} is not 16+16k; leaving data unmodified",
data.len()
);
return data.to_vec();
}
let (iv, ct) = data.split_at(16);
let mut buf = ct.to_vec();
if !cbc_decrypt_in_place(key, iv, &mut buf) {
tracing::warn!(
"invalid AES key length {}; leaving data unmodified",
key.len()
);
return data.to_vec();
}
strip_pkcs5_padding(buf)
}
fn strip_pkcs5_padding(mut buf: Vec<u8>) -> Vec<u8> {
let Some(&last) = buf.last() else { return buf };
let pad = last as usize;
if (1..=16).contains(&pad)
&& pad <= buf.len()
&& buf[buf.len() - pad..].iter().all(|&b| b == last)
{
buf.truncate(buf.len() - pad);
} else {
tracing::warn!("invalid PKCS#5 padding byte {last}; keeping unpadded data");
}
buf
}
fn cbc_decrypt_in_place(key: &[u8], iv: &[u8], buf: &mut [u8]) -> bool {
debug_assert_eq!(buf.len() % 16, 0);
match key.len() {
16 => {
let Ok(mut dec) = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv) else {
return false;
};
for block in buf.chunks_exact_mut(16) {
dec.decrypt_block_mut(GenericArray::from_mut_slice(block));
}
true
}
32 => {
let Ok(mut dec) = cbc::Decryptor::<aes::Aes256>::new_from_slices(key, iv) else {
return false;
};
for block in buf.chunks_exact_mut(16) {
dec.decrypt_block_mut(GenericArray::from_mut_slice(block));
}
true
}
_ => false,
}
}
fn aes128_cbc_encrypt_nopad(key: &[u8], iv: &[u8], data: &[u8]) -> Vec<u8> {
debug_assert_eq!(data.len() % 16, 0);
let mut buf = data.to_vec();
let Ok(mut enc) = cbc::Encryptor::<aes::Aes128>::new_from_slices(key, iv) else {
return buf; };
for block in buf.chunks_exact_mut(16) {
enc.encrypt_block_mut(GenericArray::from_mut_slice(block));
}
buf
}
fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
if key.is_empty() {
return data.to_vec();
}
let mut s: [u8; 256] = [0; 256];
for (i, b) in s.iter_mut().enumerate() {
*b = i as u8;
}
let mut j: u8 = 0;
for i in 0..256 {
j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
s.swap(i, j as usize);
}
let mut out = Vec::with_capacity(data.len());
let mut i: u8 = 0;
let mut j: u8 = 0;
for &byte in data {
i = i.wrapping_add(1);
j = j.wrapping_add(s[i as usize]);
s.swap(i as usize, j as usize);
let k = s[(s[i as usize].wrapping_add(s[j as usize])) as usize];
out.push(byte ^ k);
}
out
}
const MD5_S: [u32; 64] = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
const MD5_K: [u32; 64] = [
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
];
pub fn md5(data: &[u8]) -> [u8; 16] {
let mut a0: u32 = 0x67452301;
let mut b0: u32 = 0xefcdab89;
let mut c0: u32 = 0x98badcfe;
let mut d0: u32 = 0x10325476;
let bit_len = (data.len() as u64).wrapping_mul(8);
let mut msg = data.to_vec();
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&bit_len.to_le_bytes());
for chunk in msg.chunks_exact(64) {
let mut m = [0u32; 16];
for (i, word) in m.iter_mut().enumerate() {
*word = u32::from_le_bytes([
chunk[i * 4],
chunk[i * 4 + 1],
chunk[i * 4 + 2],
chunk[i * 4 + 3],
]);
}
let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
for i in 0..64 {
let (f, g) = match i {
0..=15 => ((b & c) | (!b & d), i),
16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
_ => (c ^ (b | !d), (7 * i) % 16),
};
let f = f.wrapping_add(a).wrapping_add(MD5_K[i]).wrapping_add(m[g]);
a = d;
d = c;
c = b;
b = b.wrapping_add(f.rotate_left(MD5_S[i]));
}
a0 = a0.wrapping_add(a);
b0 = b0.wrapping_add(b);
c0 = c0.wrapping_add(c);
d0 = d0.wrapping_add(d);
}
let mut out = [0u8; 16];
out[0..4].copy_from_slice(&a0.to_le_bytes());
out[4..8].copy_from_slice(&b0.to_le_bytes());
out[8..12].copy_from_slice(&c0.to_le_bytes());
out[12..16].copy_from_slice(&d0.to_le_bytes());
out
}
#[cfg(test)]
mod tests {
use super::*;
use zpdf_core::{PdfName, PdfStream};
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn aes256_cbc_encrypt_nopad(key: &[u8], iv: &[u8], data: &[u8]) -> Vec<u8> {
let mut buf = data.to_vec();
let mut enc = cbc::Encryptor::<aes::Aes256>::new_from_slices(key, iv).unwrap();
for block in buf.chunks_exact_mut(16) {
enc.encrypt_block_mut(GenericArray::from_mut_slice(block));
}
buf
}
fn content_stream_bytes(file: &crate::PdfFile) -> Vec<u8> {
let root = file.trailer.get_ref("Root").expect("trailer /Root");
let cat = file.resolve(root).expect("resolve catalog");
let pages_ref = cat.as_dict().unwrap().get_ref("Pages").unwrap();
let pages = file.resolve(pages_ref).expect("resolve pages");
let kids = pages.as_dict().unwrap().get_array("Kids").unwrap().to_vec();
let PdfObject::Ref(page_ref) = kids[0] else {
panic!("Kids[0] is not a reference")
};
let page = file.resolve(page_ref).expect("resolve page");
let contents_ref = page.as_dict().unwrap().get_ref("Contents").unwrap();
file.resolve_stream_data(contents_ref)
.expect("decode content stream")
}
#[test]
fn test4_rc4_key_derivation_oracle() {
let o = unhex("c5e5cd078ac4b56637f8a5d03a1ecd261ecf59fdcd8b50944ba1bb0e9e95ebfb");
let u = unhex("ffe4a8e86d2951800946f19d21089e1a71ca3d813608e586339bab72aa28206a");
let id0 = unhex("1a6dd6c3b3c1957a915bb98dbf691ce0");
let p: i32 = -64;
let key = compute_key_rc4(b"", &o, p, &id0, 2, 40, true);
assert_eq!(hex(&key), "b374aaeaf4", "file key (Algorithm 2)");
assert_eq!(rc4(&key, &PAD), u, "user-password validation (Algorithm 4)");
assert!(
validate_user_password(&key, &u, &id0, 2),
"validate_user_password should accept the correct R2 key"
);
let wrong = compute_key_rc4(b"", &o, p, &[], 2, 40, true);
assert!(
!validate_user_password(&wrong, &u, &id0, 2),
"validate_user_password should reject a wrong key"
);
let dec = Decryptor {
key,
stm_algo: Algo::Rc4,
str_algo: Algo::Rc4,
encrypt_id: None,
encrypt_metadata: true,
};
let objkey = dec.object_key(ObjectId(1652, 0), Algo::Rc4);
assert_eq!(
hex(&objkey),
"30dadd6463d5f9765abc",
"per-object key (Algorithm 1)"
);
}
#[test]
fn md5_known_answers() {
assert_eq!(hex(&md5(b"")), "d41d8cd98f00b204e9800998ecf8427e");
assert_eq!(hex(&md5(b"a")), "0cc175b9c0f1b6a831c399e269772661");
assert_eq!(hex(&md5(b"abc")), "900150983cd24fb0d6963f7d28e17f72");
assert_eq!(
hex(&md5(b"message digest")),
"f96b697d7cb7938d525a2f31aaf161d0"
);
assert_eq!(
hex(&md5(b"abcdefghijklmnopqrstuvwxyz")),
"c3fcd3d76192e4007dfb496cca67e13b"
);
assert_eq!(
hex(&md5(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
)),
"d174ab98d277d9f5a5611c2c9f419d9f"
);
}
#[test]
fn v4_rc4_key_length_from_crypt_filter() {
let mut stdcf = PdfDict::new();
stdcf.insert(PdfName::new("CFM"), PdfObject::Name(PdfName::new("V2")));
stdcf.insert(PdfName::new("Length"), PdfObject::Integer(16)); let mut cf = PdfDict::new();
cf.insert(PdfName::new("StdCF"), PdfObject::Dict(stdcf));
let mut dict = PdfDict::new();
dict.insert(PdfName::new("CF"), PdfObject::Dict(cf));
dict.insert(PdfName::new("StmF"), PdfObject::Name(PdfName::new("StdCF")));
dict.insert(PdfName::new("V"), PdfObject::Integer(4));
assert_eq!(key_length_bits(&dict, 4), 128);
assert_eq!(algo_for_filter(&dict, "StdCF"), Algo::Rc4);
assert_eq!(algo_for_filter(&dict, "Identity"), Algo::Identity);
}
#[test]
fn rc4_known_answers() {
let ct = rc4(b"Key", b"Plaintext");
assert_eq!(hex(&ct), "bbf316e8d940af0ad3");
assert_eq!(rc4(b"Key", &ct), b"Plaintext");
let ct = rc4(b"Wiki", b"pedia");
assert_eq!(hex(&ct), "1021bf0420");
let ct = rc4(b"Secret", b"Attack at dawn");
assert_eq!(hex(&ct), "45a01f645fc35b383552544b9bf5");
}
#[test]
fn aes_cbc_known_answers() {
let key = unhex("2b7e151628aed2a6abf7158809cf4f3c");
let iv = unhex("000102030405060708090a0b0c0d0e0f");
let pt = unhex("6bc1bee22e409f96e93d7e117393172a");
let ct = aes128_cbc_encrypt_nopad(&key, &iv, &pt);
assert_eq!(hex(&ct), "7649abac8119b246cee98e9b12e9197d");
let mut buf = ct.clone();
assert!(cbc_decrypt_in_place(&key, &iv, &mut buf));
assert_eq!(buf, pt);
let key = unhex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4");
let ct256 = aes256_cbc_encrypt_nopad(&key, &iv, &pt);
assert_eq!(hex(&ct256), "f58c4c04d6e5f1ba779eabfb5f7bfbd6");
let mut buf = ct256.clone();
assert!(cbc_decrypt_in_place(&key, &iv, &mut buf));
assert_eq!(buf, pt);
}
#[test]
fn aes_iv_prefix_and_padding() {
let key = unhex("000102030405060708090a0b0c0d0e0f");
let iv = [0x42u8; 16];
let plaintext = b"attack at dawn".to_vec();
let mut padded = plaintext.clone();
padded.extend_from_slice(&[2, 2]);
let mut payload = iv.to_vec();
payload.extend_from_slice(&aes128_cbc_encrypt_nopad(&key, &iv, &padded));
assert_eq!(aes_cbc_decrypt(&key, &payload), plaintext);
let mut bad = plaintext.clone();
bad.extend_from_slice(&[2, 0]);
let mut payload = iv.to_vec();
payload.extend_from_slice(&aes128_cbc_encrypt_nopad(&key, &iv, &bad));
assert_eq!(aes_cbc_decrypt(&key, &payload), bad);
assert_eq!(aes_cbc_decrypt(&key, &[1, 2, 3]), vec![1, 2, 3]);
assert_eq!(
aes_cbc_decrypt(&key, &payload[..17]),
payload[..17].to_vec()
);
assert_eq!(aes_cbc_decrypt(&key, b""), Vec::<u8>::new());
assert_eq!(aes_cbc_decrypt(&key, &iv), Vec::<u8>::new());
}
#[test]
fn v5_key_derivation_roundtrip() {
for r in [5i64, 6] {
let file_key = [0xA5u8; 32];
let (uvsalt, uksalt) = ([0x11u8; 8], [0x22u8; 8]);
let mut u = hash_v5(r, b"", &uvsalt, &[]).to_vec();
u.extend_from_slice(&uvsalt);
u.extend_from_slice(&uksalt);
let ik = hash_v5(r, b"", &uksalt, &[]);
let ue = aes256_cbc_encrypt_nopad(&ik, &[0u8; 16], &file_key);
let (ovsalt, oksalt) = ([0x33u8; 8], [0x44u8; 8]);
let mut o = hash_v5(r, b"", &ovsalt, &u[..48]).to_vec();
o.extend_from_slice(&ovsalt);
o.extend_from_slice(&oksalt);
let oik = hash_v5(r, b"", &oksalt, &u[..48]);
let oe = aes256_cbc_encrypt_nopad(&oik, &[0u8; 16], &file_key);
let mut dict = PdfDict::new();
dict.insert(PdfName::new("U"), PdfObject::String(PdfString(u.clone())));
dict.insert(PdfName::new("UE"), PdfObject::String(PdfString(ue)));
dict.insert(PdfName::new("O"), PdfObject::String(PdfString(o.clone())));
dict.insert(PdfName::new("OE"), PdfObject::String(PdfString(oe.clone())));
assert_eq!(
compute_key_v5(&dict, r, b"").as_deref(),
Some(&file_key[..]),
"user-password path, R{r}"
);
let mut dict2 = PdfDict::new();
dict2.insert(PdfName::new("U"), PdfObject::String(PdfString(u)));
dict2.insert(PdfName::new("O"), PdfObject::String(PdfString(o)));
dict2.insert(PdfName::new("OE"), PdfObject::String(PdfString(oe)));
assert_eq!(
compute_key_v5(&dict2, r, b"").as_deref(),
Some(&file_key[..]),
"owner-password fallback, R{r}"
);
}
}
#[test]
fn v4_identity_stream_filter_leaves_streams_alone() {
let mut stdcf = PdfDict::new();
stdcf.insert(PdfName::new("CFM"), PdfObject::Name(PdfName::new("V2")));
stdcf.insert(PdfName::new("Length"), PdfObject::Integer(16));
let mut cf = PdfDict::new();
cf.insert(PdfName::new("StdCF"), PdfObject::Dict(stdcf));
let mut dict = PdfDict::new();
dict.insert(
PdfName::new("Filter"),
PdfObject::Name(PdfName::new("Standard")),
);
dict.insert(PdfName::new("V"), PdfObject::Integer(4));
dict.insert(PdfName::new("R"), PdfObject::Integer(4));
dict.insert(PdfName::new("CF"), PdfObject::Dict(cf));
dict.insert(
PdfName::new("StmF"),
PdfObject::Name(PdfName::new("Identity")),
);
dict.insert(PdfName::new("StrF"), PdfObject::Name(PdfName::new("StdCF")));
let dec = match Decryptor::from_encrypt_dict(&dict, &[], None, b"") {
BuildResult::Decryptor(d) => d,
_ => panic!("decryptor"),
};
assert_eq!(dec.stm_algo, Algo::Identity);
assert_eq!(dec.str_algo, Algo::Rc4);
let stream_data = b"stream payload".to_vec();
let string_data = b"string payload".to_vec();
let mut arr = PdfObject::Array(vec![
PdfObject::Stream(PdfStream::new(PdfDict::new(), stream_data.clone())),
PdfObject::String(PdfString(string_data.clone())),
]);
dec.decrypt_object(&mut arr, ObjectId(9, 0));
let PdfObject::Array(items) = &arr else {
unreachable!()
};
let PdfObject::Stream(s) = &items[0] else {
unreachable!()
};
assert_eq!(
&s.data[..],
&stream_data[..],
"Identity /StmF must not touch streams"
);
let PdfObject::String(st) = &items[1] else {
unreachable!()
};
assert_ne!(st.0, string_data, "/StrF StdCF must decrypt strings");
assert_eq!(
dec.decrypt_stream_bytes(ObjectId(9, 0), b"objstm"),
b"objstm"
);
}
#[test]
fn encrypt_metadata_false_skips_metadata_stream() {
let dec = Decryptor {
key: vec![1, 2, 3, 4, 5],
stm_algo: Algo::Rc4,
str_algo: Algo::Rc4,
encrypt_id: None,
encrypt_metadata: false,
};
let xmp = b"<x:xmpmeta/>".to_vec();
let mut meta_dict = PdfDict::new();
meta_dict.insert(
PdfName::new("Type"),
PdfObject::Name(PdfName::new("Metadata")),
);
let mut meta = PdfObject::Stream(PdfStream::new(meta_dict, xmp.clone()));
dec.decrypt_object(&mut meta, ObjectId(7, 0));
let PdfObject::Stream(s) = &meta else {
unreachable!()
};
assert_eq!(
&s.data[..],
&xmp[..],
"plaintext metadata must not be corrupted"
);
let mut other = PdfObject::Stream(PdfStream::new(PdfDict::new(), xmp.clone()));
dec.decrypt_object(&mut other, ObjectId(7, 0));
let PdfObject::Stream(s) = &other else {
unreachable!()
};
assert_ne!(&s.data[..], &xmp[..]);
}
const AES_MARKER: &[u8] = b"(Hello AES zpdf fixture) Tj";
fn assert_fixture_decrypts(bytes: &[u8]) {
let file = crate::PdfFile::parse(bytes.to_vec()).expect("parse encrypted fixture");
let content = content_stream_bytes(&file);
assert!(
content.windows(AES_MARKER.len()).any(|w| w == AES_MARKER),
"decrypted content stream should contain the known marker, got: {:?}",
String::from_utf8_lossy(&content)
);
}
#[test]
fn aesv2_r4_decrypts_end_to_end() {
assert_fixture_decrypts(include_bytes!("../tests/fixtures/aesv2_r4.pdf"));
}
#[test]
fn aesv3_r5_decrypts_end_to_end() {
assert_fixture_decrypts(include_bytes!("../tests/fixtures/aesv3_r5.pdf"));
}
#[test]
fn aesv3_r6_decrypts_end_to_end() {
assert_fixture_decrypts(include_bytes!("../tests/fixtures/aesv3_r6.pdf"));
}
fn hexstr(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn build_rc4_direct_encrypt_pdf(content_plain: &[u8]) -> Vec<u8> {
let okey = md5(&PAD);
let o = rc4(&okey[..5], &PAD);
let id0: Vec<u8> = (0u8..16).collect();
let p: i32 = -1;
let key = compute_key_rc4(b"", &o, p, &id0, 2, 40, true);
let u = rc4(&key, &PAD);
let enc = Decryptor {
key,
stm_algo: Algo::Rc4,
str_algo: Algo::Rc4,
encrypt_id: None,
encrypt_metadata: true,
};
let content_enc = enc.decrypt_stream_bytes(ObjectId(5, 0), content_plain);
let mut stream_obj = format!("<< /Length {} >>\nstream\n", content_enc.len()).into_bytes();
stream_obj.extend_from_slice(&content_enc);
stream_obj.extend_from_slice(b"\nendstream");
let bodies: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>".to_vec(),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
stream_obj,
];
let mut out: Vec<u8> = b"%PDF-1.4\n".to_vec();
let mut offsets = Vec::new();
for (i, body) in bodies.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
}
let xref_pos = out.len();
out.extend_from_slice(b"xref\n0 6\n0000000000 65535 f \n");
for off in &offsets {
out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(
format!(
"trailer\n<< /Size 6 /Root 1 0 R /ID [<{id}> <{id}>] /Encrypt << /Filter \
/Standard /V 1 /R 2 /Length 40 /O <{o}> /U <{u}> /P {p} >> >>\nstartxref\n\
{xref_pos}\n%%EOF\n",
id = hexstr(&id0),
o = hexstr(&o),
u = hexstr(&u),
)
.as_bytes(),
);
out
}
#[test]
fn rc4_direct_encrypt_dict_in_trailer() {
let plain = b"BT /F1 12 Tf (direct encrypt dict) Tj ET";
let pdf = build_rc4_direct_encrypt_pdf(plain);
let file = crate::PdfFile::parse(pdf).expect("parse hand-built encrypted PDF");
assert_eq!(content_stream_bytes(&file), plain);
}
fn build_rc4_password_pdf(
user_pw: &[u8],
owner_pw: &[u8],
content_plain: &[u8],
omit_u: bool,
) -> Vec<u8> {
let (r, bits, n) = (3i64, 128i64, 16usize);
let id0: Vec<u8> = (0u8..16).collect();
let p: i32 = -44;
let mut okey = md5(&pad_password(owner_pw));
for _ in 0..50 {
okey = md5(&okey[..n]);
}
let owner_key = &okey[..n];
let mut o = pad_password(user_pw).to_vec();
for i in 0..=19u8 {
let step_key: Vec<u8> = owner_key.iter().map(|b| b ^ i).collect();
o = rc4(&step_key, &o);
}
let key = compute_key_rc4(user_pw, &o, p, &id0, r, bits, true);
let mut u_input = Vec::new();
u_input.extend_from_slice(&PAD);
u_input.extend_from_slice(&id0);
let mut x = rc4(&key, &md5(&u_input));
for i in 1..=19u8 {
let step_key: Vec<u8> = key.iter().map(|b| b ^ i).collect();
x = rc4(&step_key, &x);
}
let mut u = x;
u.extend_from_slice(&[0u8; 16]);
let enc = Decryptor {
key,
stm_algo: Algo::Rc4,
str_algo: Algo::Rc4,
encrypt_id: None,
encrypt_metadata: true,
};
let content_enc = enc.decrypt_stream_bytes(ObjectId(5, 0), content_plain);
let mut stream_obj = format!("<< /Length {} >>\nstream\n", content_enc.len()).into_bytes();
stream_obj.extend_from_slice(&content_enc);
stream_obj.extend_from_slice(b"\nendstream");
let bodies: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>".to_vec(),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
stream_obj,
];
let mut out: Vec<u8> = b"%PDF-1.6\n".to_vec();
let mut offsets = Vec::new();
for (i, body) in bodies.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
}
let xref_pos = out.len();
out.extend_from_slice(b"xref\n0 6\n0000000000 65535 f \n");
for off in &offsets {
out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
let u_entry = if omit_u {
String::new()
} else {
format!("/U <{}> ", hexstr(&u))
};
out.extend_from_slice(
format!(
"trailer\n<< /Size 6 /Root 1 0 R /ID [<{id}> <{id}>] /Encrypt << /Filter \
/Standard /V 2 /R 3 /Length 128 /O <{o}> {u_entry}/P {p} >> >>\nstartxref\n\
{xref_pos}\n%%EOF\n",
id = hexstr(&id0),
o = hexstr(&o),
)
.as_bytes(),
);
out
}
#[test]
fn user_password_decrypts() {
let plain = b"BT (user password works) Tj ET";
let pdf = build_rc4_password_pdf(b"secret", b"master", plain, false);
let file =
crate::PdfFile::parse_with_password(pdf, b"secret").expect("user password opens");
assert_eq!(content_stream_bytes(&file), plain);
}
#[test]
fn owner_password_decrypts_via_recovery() {
let plain = b"BT (owner password works) Tj ET";
let pdf = build_rc4_password_pdf(b"secret", b"master", plain, false);
let file =
crate::PdfFile::parse_with_password(pdf, b"master").expect("owner password opens");
assert_eq!(content_stream_bytes(&file), plain);
}
#[test]
fn wrong_password_is_rejected() {
let pdf = build_rc4_password_pdf(b"secret", b"master", b"BT (x) Tj ET", false);
match crate::PdfFile::parse_with_password(pdf, b"nope") {
Err(zpdf_core::Error::WrongPassword) => {}
Err(e) => panic!("expected WrongPassword, got error {e:?}"),
Ok(_) => panic!("expected WrongPassword, but the document opened"),
}
}
#[test]
fn empty_password_open_degrades_without_erroring() {
let plain = b"BT (needs a password) Tj ET";
let pdf = build_rc4_password_pdf(b"secret", b"master", plain, false);
let file = crate::PdfFile::parse(pdf).expect("default open still succeeds");
assert!(file.is_encrypted());
assert_ne!(content_stream_bytes(&file), plain);
}
#[test]
fn missing_u_opens_best_effort_not_wrong_password() {
let plain = b"BT (no /U to check) Tj ET";
let pdf = build_rc4_password_pdf(b"secret", b"master", plain, true);
let file =
crate::PdfFile::parse_with_password(pdf, b"secret").expect("correct password opens");
assert_eq!(content_stream_bytes(&file), plain);
let pdf = build_rc4_password_pdf(b"secret", b"master", plain, true);
let file = crate::PdfFile::parse_with_password(pdf, b"nope")
.expect("wrong password still opens best-effort (no /U to reject against)");
assert_ne!(content_stream_bytes(&file), plain);
}
}