#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(clippy::indexing_slicing)]
mod create;
mod handler;
mod key;
mod object;
mod permissions;
mod primitives;
mod rc4;
mod saslprep;
mod standard;
#[cfg(test)]
mod test_fixtures;
pub use create::{ENTROPY_LEN, KeyMaterial, standard_r6};
pub use handler::{Error, SecurityHandler, is_signature_dict};
pub use key::SmallKey;
pub use object::{CryptClass, Iv};
pub use permissions::Permissions;
pub use primitives::{md5, sha1};
pub use rc4::rc4;
pub use standard::{Cipher, EncryptParams, PAD, PasswordEncoding, parse_encrypt_dict};
#[cfg(test)]
mod tests {
use super::{CryptClass, Error, Iv, Permissions, SecurityHandler, is_signature_dict};
use crate::standard::Cipher;
use crate::test_fixtures::{self, unhex};
use pdfrum_object::{Dict, Name, NoResolve, ObjRef, Object, PdfString, names};
fn cipher_of(dict: &Dict) -> Result<(Cipher, usize), Error> {
super::parse_encrypt_dict(dict, &NoResolve).map(|p| (p.cipher, p.key_len))
}
#[test]
fn aes_v2_fixture_promotes_a_byte_length_to_bits() {
let dict = test_fixtures::encrypted_pdf_dict();
assert_eq!(cipher_of(&dict), Ok((Cipher::Aes, 16)));
}
#[test]
fn aes_v2_fixture_opens_with_either_password() {
let dict = test_fixtures::encrypted_pdf_dict();
let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
let user = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
.expect("the user password");
assert!(matches!(user, SecurityHandler::AesV4 { .. }));
assert!(!user.owner_unlocked());
assert_eq!(user.permission_word(false), 0xFFFF_F2C0);
assert_eq!(user.permission_word(true), 0xFFFF_F2C0);
assert_eq!(user.revision(), 4);
let owner = SecurityHandler::from_encrypt_dict(&dict, &id, b"5678", &NoResolve)
.expect("the owner password");
assert!(owner.owner_unlocked());
assert_eq!(owner.permission_word(true), 0xFFFF_FFFC);
assert_eq!(owner.permission_word(false), 0xFFFF_F2C0);
}
#[test]
fn aes_v2_fixture_rejects_the_wrong_password() {
let dict = test_fixtures::encrypted_pdf_dict();
let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
for password in [&b""[..], b"tiger"] {
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &id, password, &NoResolve).unwrap_err(),
Error::WrongPassword,
"password {password:?}"
);
}
}
#[test]
fn revision_5_fixture_opens_with_all_four_spellings() {
let dict = test_fixtures::r5_dict();
let id = unhex("7ca64129d20fc9745f1bfc0e4166590a");
let owner_keys: Vec<_> = [&b"\xe2ge"[..], b"\xc3\xa2ge"]
.iter()
.map(|password| {
let handler = SecurityHandler::from_encrypt_dict(&dict, &id, password, &NoResolve)
.unwrap_or_else(|_| panic!("owner {password:?}"));
assert!(handler.owner_unlocked());
assert_eq!(handler.revision(), 5);
match &handler {
SecurityHandler::AesV5 { key, .. } => **key,
_ => panic!("expected AesV5"),
}
})
.collect();
assert_eq!(owner_keys.first(), owner_keys.last());
for password in [&b"h\xf4tel"[..], b"h\xc3\xb4tel"] {
let handler = SecurityHandler::from_encrypt_dict(&dict, &id, password, &NoResolve)
.unwrap_or_else(|_| panic!("user {password:?}"));
assert!(!handler.owner_unlocked());
}
}
#[test]
fn revision_5_ignores_the_file_id() {
let dict = test_fixtures::r5_dict();
assert!(SecurityHandler::from_encrypt_dict(&dict, &[], b"h\xf4tel", &NoResolve).is_ok());
assert!(SecurityHandler::from_encrypt_dict(&dict, &[], b"\xe2ge", &NoResolve).is_ok());
}
#[test]
fn a_tampered_perms_block_rejects_the_password() {
let mut dict = test_fixtures::r5_dict();
let mut perms = unhex("c954c264d796dfd131ddb784f5a8b1bf");
if let Some(byte) = perms.get_mut(9) {
*byte ^= 0xFF;
}
dict.push(
names::PERMS.clone(),
Object::Str(PdfString::literal(&perms)),
);
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], b"h\xf4tel", &NoResolve).unwrap_err(),
Error::WrongPassword
);
}
#[test]
fn revision_6_fixture_opens_with_all_four_spellings() {
let dict = test_fixtures::r6_dict();
for password in [&b"\xe2ge"[..], b"\xc3\xa2ge"] {
let handler = SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve)
.unwrap_or_else(|_| panic!("owner {password:?}"));
assert!(handler.owner_unlocked());
assert_eq!(handler.revision(), 6);
}
for password in [&b"h\xf4tel"[..], b"h\xc3\xb4tel"] {
let handler = SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve)
.unwrap_or_else(|_| panic!("user {password:?}"));
assert!(!handler.owner_unlocked());
}
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], b"tiger", &NoResolve).unwrap_err(),
Error::WrongPassword
);
}
#[test]
fn revision_5_alternate_fixture() {
let dict = test_fixtures::bug_644_dict();
let owner = SecurityHandler::from_encrypt_dict(&dict, &[], b"b", &NoResolve)
.expect("the owner password");
assert!(owner.owner_unlocked());
assert_eq!(owner.permission_word(true), 0xFFFF_FFFC);
assert_eq!(owner.permission_word(false), 0xFFFF_FFFC);
let user = SecurityHandler::from_encrypt_dict(&dict, &[], b"a", &NoResolve)
.expect("the user password");
assert!(!user.owner_unlocked());
assert_eq!(user.permission_word(false), 0xFFFF_FFFC);
assert_eq!(
format!("{:?}", (owner.revision(), user.revision())),
"(5, 5)"
);
for password in [&b""[..], b"tiger"] {
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve).unwrap_err(),
Error::WrongPassword,
"password {password:?}"
);
}
assert_eq!(
owner.password_encoding(),
crate::PasswordEncoding::AsGiven,
"an ASCII password never converts"
);
}
type KeyLengthRow = (
i64,
Option<i64>,
Option<i64>,
Option<&'static str>,
Result<(Cipher, usize), Error>,
);
#[test]
fn key_length_resolution_table() {
use test_fixtures::encrypt_dict;
let cases: [KeyLengthRow; 14] = [
(1, None, None, None, Ok((Cipher::Rc4, 5))),
(1, Some(128), None, None, Ok((Cipher::Rc4, 5))),
(2, None, None, None, Ok((Cipher::Rc4, 5))),
(2, Some(40), None, None, Ok((Cipher::Rc4, 5))),
(2, Some(128), None, None, Ok((Cipher::Rc4, 16))),
(
2,
Some(256),
None,
None,
Err(Error::CipherKeyLength {
cipher: "RC4",
len: 32,
}),
),
(
2,
Some(8),
None,
None,
Err(Error::CipherKeyLength {
cipher: "RC4",
len: 1,
}),
),
(4, Some(128), None, Some("V2"), Ok((Cipher::Rc4, 16))),
(4, Some(128), Some(16), Some("AESV2"), Ok((Cipher::Aes, 16))),
(
4,
Some(128),
Some(128),
Some("AESV2"),
Ok((Cipher::Aes, 16)),
),
(4, None, None, Some("AESV2"), Ok((Cipher::Aes, 16))),
(
4,
Some(128),
Some(40),
Some("AESV2"),
Err(Error::CipherKeyLength {
cipher: "AES",
len: 5,
}),
),
(5, Some(256), Some(32), Some("AESV3"), Ok((Cipher::Aes, 32))),
(5, None, None, Some("AESV3"), Ok((Cipher::Aes, 32))),
];
for (version, length, filter_length, method, expected) in cases {
let dict = encrypt_dict(version, length, filter_length, method);
assert_eq!(
cipher_of(&dict),
expected,
"/V {version} /Length {length:?} /CF Length {filter_length:?} /CFM {method:?}"
);
}
}
#[test]
fn a_negative_filter_length_is_malformed() {
let dict = test_fixtures::encrypt_dict(4, Some(128), Some(-8), Some("AESV2"));
assert!(matches!(
cipher_of(&dict),
Err(Error::MalformedEncryptDict(_))
));
}
#[test]
fn an_identity_crypt_filter_yields_the_identity_handler() {
let dict = test_fixtures::identity_dict();
assert_eq!(cipher_of(&dict), Ok((Cipher::None, 0)));
let handler = SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve)
.expect("identity needs no password");
assert!(matches!(handler, SecurityHandler::Identity));
assert_eq!(
handler.decrypt(ObjRef::new(3, 0), CryptClass::Stream, b"plain"),
b"plain"
);
}
#[test]
fn differing_stream_and_string_filters_open_rather_than_refusing() {
let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
dict.push(names::STR_F.clone(), Object::Name(Name::from("Other")));
assert_eq!(cipher_of(&dict), Ok((Cipher::Aes, 16)));
}
#[test]
fn an_absent_stream_filter_defaults_to_identity_beside_a_named_string_filter() {
let mut dict = test_fixtures::bare_v4_dict();
dict.push(names::STR_F.clone(), Object::Name(Name::from("StdCF")));
let params =
super::parse_encrypt_dict(&dict, &NoResolve).expect("a defaulted /StmF is conformant");
assert_eq!(params.cipher, Cipher::None, "/StmF defaults to /Identity");
assert_eq!(params.string_cipher, Cipher::Aes, "/StrF names StdCF");
}
#[test]
fn both_class_filters_absent_default_to_identity() {
let dict = test_fixtures::bare_v4_dict();
assert_eq!(cipher_of(&dict), Ok((Cipher::None, 0)));
let handler = SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve)
.expect("a document with neither class filter opens");
assert!(matches!(handler, SecurityHandler::Identity));
}
#[test]
fn a_missing_crypt_filter_dictionary_defaults_to_identity() {
let dict = Dict::from_pairs([
(names::FILTER.clone(), Object::Name(names::STANDARD.clone())),
(names::V.clone(), Object::Int(4)),
(names::R.clone(), Object::Int(4)),
]);
assert_eq!(cipher_of(&dict), Ok((Cipher::None, 0)));
}
#[test]
fn an_identity_string_filter_leaves_strings_plaintext_beside_an_enciphering_stream() {
let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
dict.push(names::STR_F.clone(), Object::Name(names::IDENTITY.clone()));
let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("conformant per §7.6.5");
assert_eq!(params.cipher, Cipher::Aes);
assert_eq!(params.string_cipher, Cipher::None);
}
#[test]
fn a_non_standard_filter_is_unsupported() {
for spelling in ["Adobe.PubSec", "Nonesuch"] {
let dict =
Dict::from_pairs([(names::FILTER.clone(), Object::Name(Name::from(spelling)))]);
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve).unwrap_err(),
Error::UnsupportedHandler(spelling.as_bytes().into())
);
}
}
#[test]
fn a_string_valued_filter_is_not_the_standard_handler() {
let dict = Dict::from_pairs([(
names::FILTER.clone(),
Object::Str(PdfString::literal(b"Standard")),
)]);
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve).unwrap_err(),
Error::UnsupportedHandler(Box::default())
);
}
#[test]
fn encrypt_metadata_reads_only_a_boolean() {
let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
dict.push(names::ENCRYPT_METADATA.clone(), Object::Int(0));
let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("parses");
assert!(params.encrypt_metadata, "an integer is not a boolean");
let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
dict.push(names::ENCRYPT_METADATA.clone(), Object::Bool(false));
let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("parses");
assert!(!params.encrypt_metadata);
}
#[test]
fn identity_reports_no_restrictions_and_no_revision() {
let handler = SecurityHandler::Identity;
assert_eq!(handler.permission_word(false), 0xFFFF_FFFF);
assert_eq!(handler.permission_word(true), 0xFFFF_FFFF);
assert_eq!(handler.revision(), 0);
assert!(handler.encrypt_metadata());
assert!(!handler.owner_unlocked());
}
#[test]
fn the_permission_mask_clears_reserved_bits_and_forces_the_high_ones() {
let dict = test_fixtures::encrypted_pdf_dict();
let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
let handler = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
.expect("the user password");
let reported = handler.permission_word(false);
assert_eq!(reported & 0b11, 0, "the two reserved bits are cleared");
assert_eq!(
reported & 0xFFFF_F0C0,
0xFFFF_F0C0,
"the forced bits are set"
);
}
#[test]
fn the_public_methods_decode_the_word_the_private_one_reports() {
let dict = test_fixtures::encrypted_pdf_dict();
let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
let user = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
.expect("the user password");
let granted = user.permissions();
assert_eq!(granted, Permissions::from_bits(user.permission_word(false)));
assert_eq!(
granted,
Permissions {
extract: true,
..Permissions::NONE
}
);
assert_eq!(user.owner_permissions(), granted);
let owner = SecurityHandler::from_encrypt_dict(&dict, &id, b"5678", &NoResolve)
.expect("the owner password");
assert_eq!(owner.owner_permissions(), Permissions::ALL);
assert_eq!(owner.permissions(), granted);
}
#[test]
fn a_short_user_entry_rejects_rather_than_reading_out_of_bounds() {
for len in 0..16usize {
let dict = test_fixtures::r3_dict_with_user_entry(&vec![0xCD; len]);
let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &id, b"h\xf4tel", &NoResolve)
.unwrap_err(),
Error::WrongPassword,
"/U of {len} bytes"
);
}
}
#[test]
fn a_partial_user_entry_is_zero_padded_and_still_compared() {
for len in 16..32usize {
let dict = test_fixtures::r3_dict_with_user_entry(&vec![0xCD; len]);
let id = unhex("9b744068bb5efbe920baaba6da63c2bf");
assert!(
SecurityHandler::from_encrypt_dict(&dict, &id, b"h\xf4tel", &NoResolve).is_err(),
"/U of {len} bytes"
);
}
}
#[test]
fn short_version_five_entries_reject_rather_than_panicking() {
for len in [0usize, 1, 31, 47] {
let short = vec![0xEFu8; len];
for key in [names::O, names::U, names::PERMS] {
let mut dict = test_fixtures::r5_dict();
dict.push(key.clone(), Object::Str(PdfString::literal(&short)));
for password in [&b"h\xf4tel"[..], b"\xe2ge"] {
assert!(
SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve)
.is_err(),
"{key:?} of {len} bytes with {password:?}"
);
}
}
for (key, password) in [(names::UE, &b"h\xf4tel"[..]), (names::OE, b"\xe2ge")] {
let mut dict = test_fixtures::r5_dict();
dict.push(key.clone(), Object::Str(PdfString::literal(&short)));
assert!(
SecurityHandler::from_encrypt_dict(&dict, &[], password, &NoResolve).is_err(),
"{key:?} of {len} bytes"
);
}
}
}
#[test]
fn an_empty_perms_entry_rejects() {
let mut dict = test_fixtures::r5_dict();
dict.push(names::PERMS.clone(), Object::Str(PdfString::literal(b"")));
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], b"h\xf4tel", &NoResolve).unwrap_err(),
Error::WrongPassword
);
}
#[test]
fn a_truncated_owner_entry_fails_the_open() {
for revision in [2i64, 3] {
for len in [0usize, 1, 31] {
let dict = test_fixtures::rc4_dict_with_owner_entry(revision, &vec![0x11; len]);
assert_eq!(
SecurityHandler::from_encrypt_dict(&dict, &[], b"a", &NoResolve).unwrap_err(),
Error::WrongPassword,
"/R {revision} with /O of {len} bytes"
);
}
}
}
#[test]
fn signature_dictionaries_are_recognised_by_type_then_field_type() {
let sig_type = Dict::from_pairs([(names::TYPE.clone(), Object::Name(names::SIG.clone()))]);
assert!(is_signature_dict(&sig_type));
let sig_field = Dict::from_pairs([(names::FT.clone(), Object::Name(names::SIG.clone()))]);
assert!(is_signature_dict(&sig_field));
let annot = Dict::from_pairs([
(names::TYPE.clone(), Object::Name(Name::from("Annot"))),
(names::FT.clone(), Object::Name(names::SIG.clone())),
]);
assert!(!is_signature_dict(&annot));
assert!(!is_signature_dict(&Dict::new()));
}
#[test]
fn a_string_valued_type_still_names_a_signature() {
let dict =
Dict::from_pairs([(names::TYPE.clone(), Object::Str(PdfString::literal(b"Sig")))]);
assert!(is_signature_dict(&dict));
}
fn opened_handlers() -> Vec<(&'static str, SecurityHandler)> {
let aes_v2_id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
let rc4_id = unhex("9b744068bb5efbe920baaba6da63c2bf");
vec![
(
"RC4 (/R 3)",
SecurityHandler::from_encrypt_dict(
&test_fixtures::r3_dict(),
&rc4_id,
b"h\xf4tel",
&NoResolve,
)
.expect("the r3 user password"),
),
(
"AESV2 (/R 4)",
SecurityHandler::from_encrypt_dict(
&test_fixtures::encrypted_pdf_dict(),
&aes_v2_id,
b"1234",
&NoResolve,
)
.expect("the encrypted.pdf user password"),
),
(
"AESV3 (/R 6)",
SecurityHandler::from_encrypt_dict(
&test_fixtures::r6_dict(),
&[],
b"h\xf4tel",
&NoResolve,
)
.expect("the r6 user password"),
),
]
}
#[test]
fn rc4_decrypt_round_trips_through_the_public_api() {
let rc4_id = unhex("9b744068bb5efbe920baaba6da63c2bf");
let handler = SecurityHandler::from_encrypt_dict(
&test_fixtures::r3_dict(),
&rc4_id,
b"h\xf4tel",
&NoResolve,
)
.expect("the r3 user password");
let obj = ObjRef::new(12, 3);
let payload = b"Hello, encrypted world.".to_vec();
for class in [CryptClass::Stream, CryptClass::String, CryptClass::Embedded] {
let once = handler.decrypt(obj, class, &payload);
assert_ne!(once, payload, "{class:?} actually enciphered");
assert_eq!(handler.decrypt(obj, class, &once), payload, "{class:?}");
}
}
#[test]
fn every_crypt_class_decrypts_the_same_way() {
for (name, handler) in opened_handlers() {
let obj = ObjRef::new(7, 0);
let payload: Vec<u8> = (0..64u8).collect();
let stream = handler.decrypt(obj, CryptClass::Stream, &payload);
assert_eq!(
handler.decrypt(obj, CryptClass::String, &payload),
stream,
"{name}"
);
assert_eq!(
handler.decrypt(obj, CryptClass::Embedded, &payload),
stream,
"{name}"
);
}
}
fn eff_dict(embedded_method: &str) -> Dict {
use pdfrum_object::Name;
let mut dict = test_fixtures::encrypted_pdf_dict();
let std_cf = Dict::from_pairs([
(names::CFM.clone(), Object::Name(Name::from("AESV2"))),
(names::LENGTH.clone(), Object::Int(16)),
]);
let emb_cf = Dict::from_pairs([
(
names::CFM.clone(),
Object::Name(Name::from(embedded_method)),
),
(names::LENGTH.clone(), Object::Int(16)),
]);
dict.push(
names::CF.clone(),
Object::Dict(Dict::from_pairs([
(Name::from("StdCF"), Object::Dict(std_cf)),
(Name::from("EmbCF"), Object::Dict(emb_cf)),
])),
);
dict.push(names::EFF.clone(), Object::Name(Name::from("EmbCF")));
dict
}
fn eff_handler(dict: &Dict) -> SecurityHandler {
SecurityHandler::from_encrypt_dict(
dict,
&unhex("1B0FD0F5E29AD84DBF67775E9E3B009F"),
b"1234",
&NoResolve,
)
.expect("the encrypted.pdf user password")
}
#[test]
fn an_eff_naming_another_filter_decrypts_embedded_files_with_it() {
let dict = eff_dict("V2");
let params = super::parse_encrypt_dict(&dict, &NoResolve).unwrap();
assert_eq!(params.cipher, Cipher::Aes);
assert_eq!(params.embedded_cipher, Some(Cipher::Rc4));
let handler = eff_handler(&dict);
assert_eq!(handler.embedded_cipher(), Some(Cipher::Rc4));
let obj = ObjRef::new(9, 0);
let payload: Vec<u8> = (0..48u8).collect();
let as_stream = handler.decrypt(obj, CryptClass::Stream, &payload);
let as_embedded = handler.decrypt(obj, CryptClass::Embedded, &payload);
assert_eq!(as_embedded.len(), payload.len());
assert_ne!(as_embedded, as_stream);
assert_eq!(
handler.decrypt(obj, CryptClass::String, &payload),
as_stream
);
}
#[test]
fn the_embedded_class_round_trips_under_its_own_cipher() {
let dict = eff_dict("V2");
let handler = eff_handler(&dict);
let obj = ObjRef::new(9, 0);
let payload = b"an attachment".to_vec();
let sealed = handler.encrypt(obj, CryptClass::Embedded, Iv([3; 16]), &payload);
assert_eq!(handler.decrypt(obj, CryptClass::Embedded, &sealed), payload);
assert_ne!(
sealed,
handler.encrypt(obj, CryptClass::Stream, Iv([3; 16]), &payload)
);
}
#[test]
fn an_absent_eff_leaves_every_class_on_the_stream_cipher() {
for (name, handler) in opened_handlers() {
assert_eq!(handler.embedded_cipher(), None, "{name}");
}
let dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
let params = super::parse_encrypt_dict(&dict, &NoResolve).unwrap();
assert_eq!(params.embedded_cipher, None);
}
#[test]
fn an_eff_that_agrees_with_the_stream_filter_is_no_override() {
use pdfrum_object::Name;
let mut same = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
same.push(names::EFF.clone(), Object::Name(Name::from("StdCF")));
assert_eq!(
super::parse_encrypt_dict(&same, &NoResolve)
.unwrap()
.embedded_cipher,
None
);
let agreeing = eff_dict("AESV3");
assert_eq!(
super::parse_encrypt_dict(&agreeing, &NoResolve)
.unwrap()
.embedded_cipher,
None
);
}
#[test]
fn an_identity_eff_leaves_embedded_files_unenciphered() {
let mut dict = test_fixtures::encrypted_pdf_dict();
dict.push(names::EFF.clone(), Object::Name(names::IDENTITY.clone()));
let handler = eff_handler(&dict);
assert_eq!(handler.embedded_cipher(), Some(Cipher::None));
let obj = ObjRef::new(9, 0);
let payload: Vec<u8> = (0..48u8).collect();
assert_eq!(
handler.decrypt(obj, CryptClass::Embedded, &payload),
payload
);
assert_ne!(handler.decrypt(obj, CryptClass::Stream, &payload), payload);
}
#[test]
fn an_eff_naming_a_missing_filter_falls_back_rather_than_failing() {
use pdfrum_object::Name;
let mut dict = test_fixtures::encrypt_dict(4, Some(128), Some(16), Some("AESV2"));
dict.push(names::EFF.clone(), Object::Name(Name::from("NoSuchCF")));
let params = super::parse_encrypt_dict(&dict, &NoResolve).expect("still opens");
assert_eq!(params.embedded_cipher, None);
}
#[test]
fn only_the_pre_version_five_handlers_key_by_object() {
let payload: Vec<u8> = (0..48u8).map(|i| i.wrapping_mul(5)).collect();
for (name, handler) in opened_handlers() {
let first = handler.decrypt(ObjRef::new(1, 0), CryptClass::Stream, &payload);
let second = handler.decrypt(ObjRef::new(2, 0), CryptClass::Stream, &payload);
if handler.revision() >= 5 {
assert_eq!(second, first, "{name} uses the file key verbatim");
} else {
assert_ne!(second, first, "{name} salts by object number");
}
}
}
#[test]
fn decrypt_never_panics_and_never_grows_a_payload() {
for (name, handler) in opened_handlers() {
for len in 0..80usize {
let payload = vec![0xA5u8; len];
let out = handler.decrypt(ObjRef::new(9, 1), CryptClass::Stream, &payload);
assert!(out.len() <= len, "{name} at {len} bytes");
}
}
for len in 0..40usize {
let payload = vec![0x5Au8; len];
assert_eq!(
SecurityHandler::Identity.decrypt(ObjRef::new(0, 0), CryptClass::String, &payload),
payload
);
}
}
fn every_revision() -> Vec<(&'static str, SecurityHandler)> {
let r2_id = unhex("2b778de1bcef1733b35e680882812409");
let mut all = vec![(
"RC4 (/R 2)",
SecurityHandler::from_encrypt_dict(
&test_fixtures::r2_dict(),
&r2_id,
b"h\xf4tel",
&NoResolve,
)
.expect("the r2 user password"),
)];
all.extend(opened_handlers());
all.push((
"AESV3 (/R 5)",
SecurityHandler::from_encrypt_dict(
&test_fixtures::r5_dict(),
&[],
b"h\xf4tel",
&NoResolve,
)
.expect("the r5 user password"),
));
all
}
#[test]
fn every_revision_round_trips_encrypt_then_decrypt() {
for (name, handler) in every_revision() {
let obj = ObjRef::new(11, 0);
for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 64, 127] {
let payload: Vec<u8> = (0..len)
.map(|i| u8::try_from(i % 253).unwrap_or(0))
.collect();
for class in [CryptClass::Stream, CryptClass::String, CryptClass::Embedded] {
let iv = Iv([u8::try_from(len % 256).unwrap_or(0); 16]);
let sealed = handler.encrypt(obj, class, iv, &payload);
assert_eq!(
handler.decrypt(obj, class, &sealed),
payload,
"{name} {class:?} at {len} bytes"
);
}
}
}
}
#[test]
fn an_encrypted_payload_is_not_its_own_plaintext() {
let payload = b"Hello, encrypted world.".to_vec();
for (name, handler) in every_revision() {
let sealed = handler.encrypt(
ObjRef::new(4, 0),
CryptClass::Stream,
Iv([0x5A; 16]),
&payload,
);
assert_ne!(sealed, payload, "{name}");
}
assert_eq!(
SecurityHandler::Identity.encrypt(
ObjRef::new(4, 0),
CryptClass::Stream,
Iv([0; 16]),
&payload
),
payload
);
}
#[test]
fn only_the_pre_version_five_handlers_key_an_encryption_by_object() {
let payload = b"payload".to_vec();
for (name, handler) in every_revision() {
let iv = Iv([1; 16]);
let first = handler.encrypt(ObjRef::new(1, 0), CryptClass::Stream, iv, &payload);
let second = handler.encrypt(ObjRef::new(2, 0), CryptClass::Stream, iv, &payload);
if handler.revision() >= 5 {
assert_eq!(second, first, "{name} uses the file key verbatim");
} else {
assert_ne!(second, first, "{name} salts by object number");
}
}
}
#[test]
fn the_same_vector_produces_the_same_ciphertext() {
for (name, handler) in every_revision() {
let obj = ObjRef::new(6, 0);
let once = handler.encrypt(obj, CryptClass::Stream, Iv([2; 16]), b"stable");
let again = handler.encrypt(obj, CryptClass::Stream, Iv([2; 16]), b"stable");
assert_eq!(once, again, "{name}");
let other = handler.encrypt(obj, CryptClass::Stream, Iv([3; 16]), b"stable");
if handler.revision() >= 4 {
assert_ne!(other, once, "{name} mixes the vector in");
}
}
}
#[test]
fn encrypt_never_panics_and_grows_by_the_documented_amount() {
for (name, handler) in every_revision() {
for len in 0..80usize {
let out = handler.encrypt(
ObjRef::new(9, 1),
CryptClass::Stream,
Iv([0xC3; 16]),
&vec![0xA5u8; len],
);
let expected = if len == 0 || matches!(handler, SecurityHandler::Rc4V2 { .. }) {
len
} else {
32 + (len / 16) * 16
};
assert_eq!(out.len(), expected, "{name} at {len} bytes");
}
}
}
#[test]
fn an_empty_payload_stays_empty_at_every_revision() {
for (name, handler) in every_revision() {
for class in [CryptClass::Stream, CryptClass::String, CryptClass::Embedded] {
assert!(
handler
.encrypt(ObjRef::new(2, 0), class, Iv([0xFF; 16]), b"")
.is_empty(),
"{name} {class:?}"
);
}
}
}
fn pseudo_random(seed: u64, len: usize) -> Vec<u8> {
let mut state = seed.wrapping_mul(0x2545_F491_4F6C_DD1D) | 1;
(0..len)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
u8::try_from(state >> 56).unwrap_or(0)
})
.collect()
}
#[test]
fn arbitrary_encrypt_dictionaries_never_panic() {
for seed in 0..8u64 {
for version in [-1i64, 0, 1, 2, 3, 4, 5, 6, 99] {
for revision in [0i64, 2, 3, 4, 5, 6, 7] {
for length in [
*pdfrum_object::INT_RANGE.start(),
-8,
0,
8,
40,
128,
256,
4096,
*pdfrum_object::INT_RANGE.end(),
] {
let mut dict = test_fixtures::encrypt_dict(
version,
Some(length),
Some(length),
Some("AESV2"),
);
dict.push(names::R.clone(), Object::Int(revision));
for key in [names::O, names::U, names::OE, names::UE, names::PERMS] {
let entry = pseudo_random(
seed.wrapping_add(key.as_bytes().len() as u64),
usize::try_from(seed % 60).unwrap_or(0),
);
dict.push(key.clone(), Object::Str(PdfString::literal(entry)));
}
let password = pseudo_random(seed, usize::try_from(seed % 9).unwrap_or(0));
let file_id =
pseudo_random(seed + 1, usize::try_from(seed % 20).unwrap_or(0));
let _ = SecurityHandler::from_encrypt_dict(
&dict, &file_id, &password, &NoResolve,
);
}
}
}
}
}
#[test]
fn arbitrary_version_five_entries_terminate() {
for seed in 0..4u64 {
let mut dict = test_fixtures::r6_dict();
for key in [names::O, names::U, names::OE, names::UE, names::PERMS] {
let entry = pseudo_random(seed, 48);
dict.push(key.clone(), Object::Str(PdfString::literal(entry)));
}
let password = pseudo_random(seed, 64);
let _ = SecurityHandler::from_encrypt_dict(&dict, &[], &password, &NoResolve);
}
}
#[test]
fn every_password_entry_length_is_survivable() {
for len in 0..64usize {
let entry = pseudo_random(len as u64, len);
for base in [
test_fixtures::r2_dict(),
test_fixtures::r3_dict(),
test_fixtures::encrypted_pdf_dict(),
test_fixtures::r5_dict(),
] {
for key in [names::O, names::U, names::OE, names::UE, names::PERMS] {
let mut dict = base.clone();
dict.push(key.clone(), Object::Str(PdfString::literal(&entry)));
let _ = SecurityHandler::from_encrypt_dict(&dict, &[], b"pw", &NoResolve);
}
}
}
}
#[test]
fn handlers_are_send_and_sync() {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SecurityHandler>();
assert_send_sync::<Error>();
assert_send_sync::<crate::EncryptParams>();
}
#[test]
fn a_handler_debug_dump_never_shows_key_material() {
let dict = test_fixtures::encrypted_pdf_dict();
let id = unhex("1B0FD0F5E29AD84DBF67775E9E3B009F");
let handler = SecurityHandler::from_encrypt_dict(&dict, &id, b"1234", &NoResolve)
.expect("the user password");
let dump = format!("{handler:?}");
assert!(dump.contains("redacted"), "{dump}");
}
}