use pdfrum_object::ObjRef;
use crate::key::SmallKey;
use crate::primitives::{BLOCK, aes_cbc_decrypt, aes_cbc_encrypt, md5};
use crate::rc4::rc4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CryptClass {
Stream,
String,
Embedded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Iv(pub [u8; BLOCK]);
impl Iv {
#[must_use]
pub const fn bytes(&self) -> &[u8; BLOCK] {
&self.0
}
}
const AES_SALT: [u8; 4] = *b"sAlT";
fn salted(key: &SmallKey, obj: ObjRef) -> ([u8; 48], usize) {
let mut scratch = [0u8; 48];
let key_len = key.len().min(scratch.len());
if let (Some(head), Some(from)) = (scratch.get_mut(..key_len), key.bytes().get(..key_len)) {
head.copy_from_slice(from);
}
let num = obj.num.to_le_bytes();
let generation = obj.generation.to_le_bytes();
for (offset, byte) in num
.iter()
.take(3)
.chain(generation.iter().take(2))
.enumerate()
{
if let Some(slot) = scratch.get_mut(key_len + offset) {
*slot = *byte;
}
}
(scratch, key_len)
}
#[must_use]
fn rc4_object_key(key: &SmallKey, obj: ObjRef) -> Vec<u8> {
let (scratch, key_len) = salted(key, obj);
let digest = md5(scratch.get(..key_len + 5).unwrap_or(&scratch));
let len = (key_len + 5).min(digest.len());
digest.get(..len).unwrap_or(&digest).to_vec()
}
#[must_use]
fn aes_v4_object_key(key: &SmallKey, obj: ObjRef) -> [u8; 16] {
let (mut scratch, key_len) = salted(key, obj);
for (offset, byte) in AES_SALT.iter().enumerate() {
if let Some(slot) = scratch.get_mut(key_len + 5 + offset) {
*slot = *byte;
}
}
md5(scratch.get(..key_len + 9).unwrap_or(&scratch))
}
#[must_use]
pub(crate) fn decrypt_rc4(key: &SmallKey, obj: ObjRef, data: &[u8]) -> Vec<u8> {
rc4(&rc4_object_key(key, obj), data)
}
#[must_use]
pub(crate) fn decrypt_aes_v4(key: &SmallKey, obj: ObjRef, data: &[u8]) -> Vec<u8> {
decrypt_aes_cbc(&aes_v4_object_key(key, obj), data)
}
#[must_use]
pub(crate) fn decrypt_aes_v5(key: &[u8; 32], data: &[u8]) -> Vec<u8> {
decrypt_aes_cbc(key, data)
}
#[must_use]
pub(crate) fn encrypt_rc4(key: &SmallKey, obj: ObjRef, data: &[u8]) -> Vec<u8> {
rc4(&rc4_object_key(key, obj), data)
}
#[must_use]
pub(crate) fn encrypt_aes_v4(
key: &SmallKey,
obj: ObjRef,
iv: &[u8; BLOCK],
data: &[u8],
) -> Vec<u8> {
encrypt_aes_cbc(&aes_v4_object_key(key, obj), iv, data)
}
#[must_use]
pub(crate) fn encrypt_aes_v5(key: &[u8; 32], iv: &[u8; BLOCK], data: &[u8]) -> Vec<u8> {
encrypt_aes_cbc(key, iv, data)
}
#[must_use]
fn encrypt_aes_cbc(key: &[u8], iv: &[u8; BLOCK], data: &[u8]) -> Vec<u8> {
let pad = BLOCK - data.len() % BLOCK;
let mut body = data.to_vec();
body.extend(std::iter::repeat_n(u8::try_from(pad).unwrap_or(0), pad));
if aes_cbc_encrypt(key, iv, &mut body).is_err() {
return Vec::new();
}
let mut out = Vec::with_capacity(BLOCK.saturating_add(body.len()));
out.extend_from_slice(iv);
out.append(&mut body);
out
}
#[must_use]
fn decrypt_aes_cbc(key: &[u8], data: &[u8]) -> Vec<u8> {
let Some(iv) = data
.get(..BLOCK)
.and_then(|s| <[u8; BLOCK]>::try_from(s).ok())
else {
return Vec::new();
};
let body = data.get(BLOCK..).unwrap_or_default();
let whole = body.len() - body.len() % BLOCK;
let Some(mut out) = body.get(..whole).map(<[u8]>::to_vec) else {
return Vec::new();
};
if aes_cbc_decrypt(key, &iv, &mut out).is_err() {
return Vec::new();
}
if whole < body.len() {
return out;
}
let Some(pad) = out.last().copied() else {
return out;
};
if usize::from(pad) >= BLOCK {
out.truncate(out.len() - BLOCK);
} else {
out.truncate(out.len() - usize::from(pad));
}
out
}
#[cfg(test)]
mod tests {
use super::{
BLOCK, CryptClass, Iv, aes_v4_object_key, decrypt_aes_cbc, decrypt_aes_v4, decrypt_aes_v5,
decrypt_rc4, encrypt_aes_cbc, encrypt_aes_v4, encrypt_aes_v5, encrypt_rc4, rc4_object_key,
};
use crate::key::SmallKey;
use crate::primitives::aes_cbc_encrypt;
use pdfrum_object::ObjRef;
fn key(len: usize) -> SmallKey {
SmallKey::from_prefix(&(0..32u8).collect::<Vec<_>>(), len)
}
#[test]
fn rc4_object_key_length_is_capped_at_sixteen() {
assert_eq!(rc4_object_key(&key(5), ObjRef::new(1, 0)).len(), 10);
assert_eq!(rc4_object_key(&key(10), ObjRef::new(1, 0)).len(), 15);
assert_eq!(rc4_object_key(&key(16), ObjRef::new(1, 0)).len(), 16);
}
#[test]
fn object_numbers_contribute_three_bytes() {
let k = key(16);
assert_eq!(
rc4_object_key(&k, ObjRef::new(1, 0)),
rc4_object_key(&k, ObjRef::new(0x0100_0001, 0))
);
assert_ne!(
rc4_object_key(&k, ObjRef::new(1, 0)),
rc4_object_key(&k, ObjRef::new(2, 0))
);
assert_ne!(
rc4_object_key(&k, ObjRef::new(1, 0)),
rc4_object_key(&k, ObjRef::new(1, 1))
);
}
#[test]
fn generation_contributes_two_bytes() {
let k = key(16);
assert_ne!(
rc4_object_key(&k, ObjRef::new(1, 0x0100)),
rc4_object_key(&k, ObjRef::new(1, 0))
);
}
#[test]
fn aes_v4_object_key_is_a_full_digest() {
let derived = aes_v4_object_key(&key(16), ObjRef::new(1, 0));
assert_eq!(derived.len(), 16);
let mut scratch = Vec::new();
scratch.extend_from_slice(key(16).bytes());
scratch.extend_from_slice(&[1, 0, 0, 0, 0]);
scratch.extend_from_slice(b"sAlT");
assert_eq!(scratch.len(), 25);
assert_eq!(derived, crate::primitives::md5(&scratch));
}
#[test]
fn aes_v5_uses_the_file_key_verbatim() {
let file_key = [7u8; 32];
let payload = encrypted(&file_key, b"hello");
let first = decrypt_aes_v5(&file_key, &payload);
assert_eq!(first, b"hello");
assert_eq!(decrypt_aes_v5(&file_key, &payload), first);
}
fn encrypted(key: &[u8], plaintext: &[u8]) -> Vec<u8> {
let iv = [0x5Au8; BLOCK];
let pad = BLOCK - plaintext.len() % BLOCK;
let mut body = plaintext.to_vec();
body.extend(std::iter::repeat_n(u8::try_from(pad).unwrap_or(0), pad));
aes_cbc_encrypt(key, &iv, &mut body).expect("valid key");
let mut out = iv.to_vec();
out.extend_from_slice(&body);
out
}
#[test]
fn aes_lengths_under_seventeen_bytes_yield_nothing() {
let k = [0u8; 16];
for len in 0..=BLOCK {
assert!(
decrypt_aes_cbc(&k, &vec![0xAA; len]).is_empty(),
"{len} bytes"
);
}
}
#[test]
fn a_round_trip_recovers_the_plaintext() {
let k = [3u8; 16];
for len in [0usize, 1, 15, 16, 17, 31, 32, 100] {
let plaintext: Vec<u8> = (0..len)
.map(|i| u8::try_from(i % 251).unwrap_or(0))
.collect();
assert_eq!(
decrypt_aes_cbc(&k, &encrypted(&k, &plaintext)),
plaintext,
"{len} bytes"
);
}
}
fn one_block_with_last(key: &[u8], last: u8) -> Vec<u8> {
let iv = [0u8; BLOCK];
let mut block = [0u8; BLOCK];
if let Some(slot) = block.last_mut() {
*slot = last;
}
let mut body = block.to_vec();
aes_cbc_encrypt(key, &iv, &mut body).expect("valid key");
let mut out = iv.to_vec();
out.extend_from_slice(&body);
out
}
#[test]
fn the_final_block_pad_byte_decides_how_much_survives() {
let k = [9u8; 16];
assert!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 0x10)).is_empty());
assert!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 0xFF)).is_empty());
assert_eq!(
decrypt_aes_cbc(&k, &one_block_with_last(&k, 0)).len(),
BLOCK
);
assert_eq!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 1)).len(), 15);
assert_eq!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 15)).len(), 1);
}
#[test]
fn inconsistent_padding_is_accepted() {
let k = [9u8; 16];
assert_eq!(decrypt_aes_cbc(&k, &one_block_with_last(&k, 4)).len(), 12);
}
#[test]
fn a_partial_tail_is_dropped_and_the_block_before_it_is_kept() {
let k = [4u8; 16];
let mut payload = one_block_with_last(&k, 3);
assert_eq!(decrypt_aes_cbc(&k, &payload).len(), 13);
payload.extend_from_slice(&[0xEE; 5]);
assert_eq!(decrypt_aes_cbc(&k, &payload).len(), BLOCK);
}
#[test]
fn two_blocks_plus_a_tail_keep_both_blocks() {
let k = [4u8; 16];
let iv = [0u8; BLOCK];
let mut body = vec![0u8; 2 * BLOCK];
aes_cbc_encrypt(&k, &iv, &mut body).expect("valid key");
let mut payload = iv.to_vec();
payload.extend_from_slice(&body);
payload.extend_from_slice(&[0x11; 7]);
assert_eq!(decrypt_aes_cbc(&k, &payload).len(), 2 * BLOCK);
}
#[test]
fn rc4_preserves_length_and_round_trips() {
let k = key(16);
let obj = ObjRef::new(5, 0);
assert!(decrypt_rc4(&k, obj, &[]).is_empty());
let data: Vec<u8> = (0..77u8).collect();
let once = decrypt_rc4(&k, obj, &data);
assert_eq!(once.len(), data.len());
assert_eq!(decrypt_rc4(&k, obj, &once), data);
}
#[test]
fn a_key_aes_cannot_accept_yields_empty_output_rather_than_a_panic() {
for len in [0usize, 1, 15, 17, 31, 33] {
let bad = vec![0u8; len];
assert!(
decrypt_aes_cbc(&bad, &[0xAA; 48]).is_empty(),
"{len}-byte key"
);
}
}
#[test]
fn aes_encryption_grows_a_payload_by_a_vector_and_a_pad() {
let k = [0x2Bu8; 16];
for (plain, expected) in [
(0usize, 32),
(1, 32),
(15, 32),
(16, 48),
(17, 48),
(31, 48),
] {
let out = encrypt_aes_cbc(&k, &[0u8; BLOCK], &vec![0xA5; plain]);
assert_eq!(out.len(), expected, "{plain} bytes of plaintext");
}
}
#[test]
fn aes_round_trips_through_the_quirky_decoder_at_every_length() {
let k = [0x3Cu8; 16];
for len in 0..96usize {
let plaintext: Vec<u8> = (0..len)
.map(|i| u8::try_from(i % 251).unwrap_or(0))
.collect();
let iv = [u8::try_from(len % 256).unwrap_or(0); BLOCK];
let sealed = encrypt_aes_cbc(&k, &iv, &plaintext);
assert_eq!(
decrypt_aes_cbc(&k, &sealed),
plaintext,
"{len} bytes did not survive"
);
}
}
#[test]
fn the_vector_is_the_prefix_and_changes_every_block() {
let k = [0x11u8; 32];
let plaintext = vec![0u8; 3 * BLOCK];
let first = encrypt_aes_cbc(&k, &[1u8; BLOCK], &plaintext);
let second = encrypt_aes_cbc(&k, &[2u8; BLOCK], &plaintext);
assert_eq!(first.get(..BLOCK), Some(&[1u8; BLOCK][..]));
assert_eq!(second.get(..BLOCK), Some(&[2u8; BLOCK][..]));
assert_ne!(first.get(BLOCK..), second.get(BLOCK..));
}
#[test]
fn the_object_keyed_ciphers_round_trip_under_the_same_reference() {
let k = key(16);
let obj = ObjRef::new(12, 3);
let payload: Vec<u8> = (0..70u8).collect();
assert_eq!(
decrypt_rc4(&k, obj, &encrypt_rc4(&k, obj, &payload)),
payload
);
let sealed = encrypt_aes_v4(&k, obj, &Iv([9; BLOCK]).0, &payload);
assert_eq!(decrypt_aes_v4(&k, obj, &sealed), payload);
assert_ne!(decrypt_aes_v4(&k, ObjRef::new(13, 3), &sealed), payload);
let file_key = [0x5Au8; 32];
let sealed = encrypt_aes_v5(&file_key, &[3; BLOCK], &payload);
assert_eq!(decrypt_aes_v5(&file_key, &sealed), payload);
}
#[test]
fn rc4_encryption_is_its_own_inverse() {
let k = key(10);
let obj = ObjRef::new(3, 0);
let payload: Vec<u8> = (0..40u8).map(|i| i.wrapping_mul(7)).collect();
assert_eq!(
encrypt_rc4(&k, obj, &payload),
decrypt_rc4(&k, obj, &payload)
);
assert!(encrypt_rc4(&k, obj, &[]).is_empty());
}
#[test]
fn an_impossible_key_encrypts_to_nothing_rather_than_panicking() {
for len in [0usize, 1, 15, 17, 31, 33] {
assert!(encrypt_aes_cbc(&vec![0u8; len], &[0; BLOCK], b"payload").is_empty());
}
}
#[test]
fn crypt_classes_are_distinct_values() {
assert_ne!(CryptClass::Stream, CryptClass::String);
assert_ne!(CryptClass::String, CryptClass::Embedded);
}
}