use pdfrum_object::{Dict, Name, NoResolve, Object, PdfString};
use zeroize::Zeroize;
use crate::Error;
use crate::SecurityHandler;
use crate::permissions::Permissions;
use crate::primitives::aes_cbc_encrypt;
use crate::standard::{r6_prepared, revision6_hash};
pub const ENTROPY_LEN: usize = 68;
pub struct KeyMaterial([u8; ENTROPY_LEN]);
impl KeyMaterial {
pub fn from_os() -> Result<Self, Error> {
let mut bytes = [0u8; ENTROPY_LEN];
getrandom::fill(&mut bytes).map_err(|_| Error::NoEntropy)?;
Ok(Self(bytes))
}
}
impl Drop for KeyMaterial {
fn drop(&mut self) {
self.0.zeroize();
}
}
const NAMED_PERMISSION_BITS: u32 = 0x0F3C;
pub fn standard_r6(
user: &[u8],
owner: &[u8],
permissions: Permissions,
encrypt_metadata: bool,
key_material: &KeyMaterial,
) -> Result<(Dict, SecurityHandler), Error> {
let entropy = &key_material.0;
let owner = if owner.is_empty() { user } else { owner };
let user_prepared = r6_prepared(6, user).ok_or(Error::WrongPassword)?;
let owner_prepared = r6_prepared(6, owner).ok_or(Error::WrongPassword)?;
let mut file_key: [u8; 32] = slice(entropy, 0)?;
let user_validation: [u8; 8] = slice(entropy, 32)?;
let user_key_salt: [u8; 8] = slice(entropy, 40)?;
let owner_validation: [u8; 8] = slice(entropy, 48)?;
let owner_key_salt: [u8; 8] = slice(entropy, 56)?;
let perms_random: [u8; 4] = slice(entropy, 64)?;
let mut u = [0u8; 48];
u[..32].copy_from_slice(&revision6_hash(&user_prepared, user_validation, None));
u[32..40].copy_from_slice(&user_validation);
u[40..48].copy_from_slice(&user_key_salt);
let ue = wrap(
&revision6_hash(&user_prepared, user_key_salt, None),
&file_key,
)?;
let mut o = [0u8; 48];
o[..32].copy_from_slice(&revision6_hash(&owner_prepared, owner_validation, Some(&u)));
o[32..40].copy_from_slice(&owner_validation);
o[40..48].copy_from_slice(&owner_key_salt);
let oe = wrap(
&revision6_hash(&owner_prepared, owner_key_salt, Some(&u)),
&file_key,
)?;
let p = permissions.bits() | !NAMED_PERMISSION_BITS;
let mut perms = [0u8; 16];
perms[..4].copy_from_slice(&p.to_le_bytes());
perms[4..8].copy_from_slice(&[0xFF; 4]);
perms[8] = if encrypt_metadata { b'T' } else { b'F' };
perms[9..12].copy_from_slice(b"adb");
perms[12..16].copy_from_slice(&perms_random);
aes_cbc_encrypt(&file_key, &[0u8; 16], &mut perms).map_err(|_| Error::WrongPassword)?;
file_key.zeroize();
let name = |s: &str| Object::Name(Name::from(s));
let bytes = |b: &[u8]| Object::Str(PdfString::hex(b));
let std_cf = Dict::from_pairs([
(Name::from("CFM"), name("AESV3")),
(Name::from("AuthEvent"), name("DocOpen")),
(Name::from("Length"), Object::Int(32)),
]);
let cf = Dict::from_pairs([(Name::from("StdCF"), Object::Dict(std_cf))]);
#[expect(
clippy::cast_possible_wrap,
reason = "/P is the same 32 bits read as a signed integer, per the standard"
)]
let p_signed = i64::from(p as i32);
let dict = Dict::from_pairs([
(Name::from("Filter"), name("Standard")),
(Name::from("V"), Object::Int(5)),
(Name::from("R"), Object::Int(6)),
(Name::from("Length"), Object::Int(256)),
(Name::from("P"), Object::Int(p_signed)),
(Name::from("O"), bytes(&o)),
(Name::from("U"), bytes(&u)),
(Name::from("OE"), bytes(&oe)),
(Name::from("UE"), bytes(&ue)),
(Name::from("Perms"), bytes(&perms)),
(Name::from("CF"), Object::Dict(cf)),
(Name::from("StmF"), name("StdCF")),
(Name::from("StrF"), name("StdCF")),
(
Name::from("EncryptMetadata"),
Object::Bool(encrypt_metadata),
),
]);
let handler = SecurityHandler::from_encrypt_dict(&dict, &[], owner, &NoResolve)?;
Ok((dict, handler))
}
fn wrap(intermediate: &[u8; 32], key: &[u8; 32]) -> Result<[u8; 32], Error> {
let mut wrapped = *key;
aes_cbc_encrypt(intermediate, &[0u8; 16], &mut wrapped).map_err(|_| Error::WrongPassword)?;
Ok(wrapped)
}
fn slice<const N: usize>(entropy: &[u8; ENTROPY_LEN], at: usize) -> Result<[u8; N], Error> {
entropy
.get(at..at + N)
.and_then(|s| <[u8; N]>::try_from(s).ok())
.ok_or(Error::WrongPassword)
}
#[cfg(test)]
mod tests {
use super::{KeyMaterial, standard_r6};
use crate::permissions::Permissions;
use crate::{CryptClass, SecurityHandler};
use pdfrum_object::{NoResolve, ObjRef};
fn entropy() -> KeyMaterial {
KeyMaterial::from_os().unwrap()
}
#[test]
fn the_file_opens_with_either_password_and_not_with_a_wrong_one() {
let perms = Permissions {
print: true,
..Permissions::NONE
};
let (dict, handler) = standard_r6(b"user", b"owner", perms, true, &entropy()).unwrap();
let as_user = SecurityHandler::from_encrypt_dict(&dict, &[], b"user", &NoResolve).unwrap();
assert!(!as_user.owner_unlocked());
assert!(as_user.permissions().print && !as_user.permissions().copy);
let as_owner =
SecurityHandler::from_encrypt_dict(&dict, &[], b"owner", &NoResolve).unwrap();
assert!(as_owner.owner_unlocked());
assert!(SecurityHandler::from_encrypt_dict(&dict, &[], b"nope", &NoResolve).is_err());
assert!(
handler.owner_unlocked(),
"the handler this function hands back is the owner's"
);
}
#[test]
fn what_the_handler_enciphers_the_opened_one_deciphers() {
let (dict, handler) =
standard_r6(b"", b"secret", Permissions::ALL, true, &entropy()).unwrap();
let obj = ObjRef::new(7, 0);
let iv = crate::Iv([3u8; 16]);
let enciphered = handler.encrypt(obj, CryptClass::Stream, iv, b"hello, cipher");
assert_ne!(enciphered, b"hello, cipher");
let reader = SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve).unwrap();
assert_eq!(
reader.decrypt(obj, CryptClass::Stream, &enciphered),
b"hello, cipher"
);
}
#[test]
fn an_empty_owner_password_falls_back_to_the_user_password() {
let (dict, _) = standard_r6(b"pw", b"", Permissions::ALL, false, &entropy()).unwrap();
let opened = SecurityHandler::from_encrypt_dict(&dict, &[], b"pw", &NoResolve).unwrap();
assert!(opened.owner_unlocked());
assert!(!opened.encrypt_metadata());
}
#[test]
fn perms_random_bytes_are_not_the_file_key_prefix() {
let mut bytes = [0u8; super::ENTROPY_LEN];
bytes[..32].fill(0xAA);
bytes[32..64].fill(0x11);
bytes[64..68].fill(0xBB);
let material = super::KeyMaterial(bytes);
let (dict, _) = standard_r6(b"user", b"owner", Permissions::ALL, true, &material).unwrap();
let perms = dict
.string(&pdfrum_object::Name::from("Perms"))
.expect("standard_r6 writes /Perms");
let mut block = [0u8; 16];
block.copy_from_slice(perms.bytes.as_ref());
crate::primitives::aes_cbc_decrypt(&[0xAA; 32], &[0u8; 16], &mut block)
.expect("the file key decrypts /Perms");
assert_eq!(&block[12..16], &[0xBB; 4], "Algorithm 10's four bytes");
assert_ne!(
&block[12..16],
&bytes[..4],
"must not reuse the file-key prefix"
);
}
}