1036 lines
38 KiB
Rust
1036 lines
38 KiB
Rust
//! PDF security (Save PDF › Security): a password to open the file, a permissions password and
|
||
//! the permissions it guards, written with the standard security handler.
|
||
//!
|
||
//! The export writes a plain file; [`protect`] then encrypts every string and stream in it (the
|
||
//! XMP metadata stays readable with Plaintext Metadata) and adds the encryption dictionary. The
|
||
//! algorithm follows the compatibility ([`Encryption`]): 128-bit RC4 for PDF 1.4 (revision 3) and
|
||
//! 1.5 (revision 4), 128-bit AES for 1.6 (revision 4) and 256-bit AES for 1.7 and 2.0
|
||
//! (revision 6). The random file key, salts and initialisation vectors come from a SHA-256 stream
|
||
//! seeded with the process's random hash keys, the time and the file itself. A linearised file is
|
||
//! encrypted between its two steps ([`protect_keeping`]): what encrypted it then encrypts the hint
|
||
//! stream written last ([`Cipher`]).
|
||
//!
|
||
//! Reading: [`user_password`] turns the permissions (owner) password of a file encrypted with RC4
|
||
//! or 128-bit AES into its open (user) password, so either password opens it.
|
||
|
||
use aes::cipher::{BlockEncrypt, KeyInit};
|
||
use md5::Md5;
|
||
use sha2::{Digest, Sha256, Sha384, Sha512};
|
||
|
||
use crate::lab_spot::{find, rfind, xref_offset};
|
||
use crate::settings::choice;
|
||
use crate::{Changes, Compatibility, PdfError, PdfSettings, Printing, SecuritySettings, Standard};
|
||
|
||
choice! {
|
||
/// The encryption algorithm, which follows the PDF version (Compatibility).
|
||
Encryption {
|
||
Rc4 = "rc4", "128-bit RC4";
|
||
Aes128 = "aes128", "128-bit AES";
|
||
Aes256 = "aes256", "256-bit AES";
|
||
} default Aes256
|
||
}
|
||
|
||
impl Encryption {
|
||
/// The algorithm a file of PDF version `c` is encrypted with.
|
||
pub fn for_compatibility(c: Compatibility) -> Self {
|
||
match c {
|
||
Compatibility::Pdf14 | Compatibility::Pdf15 => Self::Rc4,
|
||
Compatibility::Pdf16 => Self::Aes128,
|
||
Compatibility::Pdf17 | Compatibility::Pdf20 => Self::Aes256,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The standard security handler revision written for PDF version `c`: 3 (RC4, metadata always
|
||
/// encrypted), 4 (crypt filters: RC4 or AES-128) or 6 (AES-256).
|
||
fn revision(c: Compatibility) -> u8 {
|
||
match c {
|
||
Compatibility::Pdf14 => 3,
|
||
Compatibility::Pdf15 | Compatibility::Pdf16 => 4,
|
||
Compatibility::Pdf17 | Compatibility::Pdf20 => 6,
|
||
}
|
||
}
|
||
|
||
/// The longest password (in bytes) revisions 2–4 use: longer ones are cut to this.
|
||
const MAX_LEGACY_PASSWORD: usize = 32;
|
||
/// The longest password (UTF-8 bytes) revision 6 uses.
|
||
const MAX_PASSWORD: usize = 127;
|
||
|
||
impl SecuritySettings {
|
||
/// A password is set: the file is encrypted.
|
||
pub fn protected(&self) -> bool {
|
||
!self.open_password.is_empty() || !self.permissions_password.is_empty()
|
||
}
|
||
|
||
/// Some permission is withheld (only a permissions password applies them).
|
||
pub fn restricts(&self) -> bool {
|
||
self.printing != Printing::High || self.changes != Changes::Any || !self.copy || !self.screen_reader
|
||
}
|
||
|
||
/// The encryption dictionary's `P` entry: the permission bits (1-based bit numbers of the PDF
|
||
/// reference's user access permissions). Everything is allowed without a permissions password.
|
||
pub fn permission_bits(&self) -> i32 {
|
||
if self.permissions_password.is_empty() {
|
||
return !3;
|
||
}
|
||
let bit = |n: u32| 1u32 << (n - 1);
|
||
// Bits 1–2 are clear; the reserved bits 7–8 and 13–32 are set.
|
||
let mut p = 0xFFFF_F0C0u32;
|
||
p |= match self.printing {
|
||
Printing::None => 0,
|
||
Printing::Low => bit(3),
|
||
Printing::High => bit(3) | bit(12),
|
||
};
|
||
p |= match self.changes {
|
||
Changes::None => 0,
|
||
Changes::Pages => bit(11),
|
||
Changes::Forms => bit(9),
|
||
Changes::Comments => bit(6) | bit(9),
|
||
Changes::Any => bit(4) | bit(6) | bit(9) | bit(11),
|
||
};
|
||
if self.copy {
|
||
p |= bit(5);
|
||
}
|
||
// Content copied may always be read out by screen readers.
|
||
if self.copy || self.screen_reader {
|
||
p |= bit(10);
|
||
}
|
||
p as i32
|
||
}
|
||
|
||
/// Refuse passwords the file can't carry: with a PDF standard (PDF/A and PDF/X forbid
|
||
/// encryption), two equal passwords, or passwords the algorithm of `c` can't take.
|
||
pub(crate) fn check(&self, standard: Standard, c: Compatibility) -> Result<(), PdfError> {
|
||
if !self.protected() {
|
||
return Ok(());
|
||
}
|
||
if standard != Standard::None {
|
||
return Err(PdfError::BadSetting(format!(
|
||
"{} files can't be password-protected: remove the passwords or choose no standard",
|
||
standard.label()
|
||
)));
|
||
}
|
||
if self.open_password == self.permissions_password {
|
||
return Err(PdfError::BadSetting("the open and permissions passwords must differ".into()));
|
||
}
|
||
for pw in [&self.open_password, &self.permissions_password] {
|
||
if revision(c) < 6 {
|
||
if !pw.is_ascii() || pw.len() > MAX_LEGACY_PASSWORD {
|
||
return Err(PdfError::BadSetting(format!(
|
||
"{} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any)",
|
||
c.label()
|
||
)));
|
||
}
|
||
} else if pw.len() > MAX_PASSWORD {
|
||
return Err(PdfError::BadSetting(format!("passwords can have up to {MAX_PASSWORD} bytes")));
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl PdfSettings {
|
||
/// The algorithm the file is encrypted with; `None` without a password.
|
||
pub fn encryption(&self) -> Option<Encryption> {
|
||
self.security.protected().then(|| Encryption::for_compatibility(self.compatibility))
|
||
}
|
||
}
|
||
|
||
/// The security options that are accepted but have no effect, one warning each.
|
||
pub(crate) fn warnings(set: &PdfSettings) -> Vec<String> {
|
||
let s = &set.security;
|
||
let mut out = vec![];
|
||
if s.protected() && s.plaintext_metadata && revision(set.compatibility) == 3 {
|
||
out.push(format!("{} encryption covers the metadata too: plaintext metadata needs PDF 1.5 or later", set.compatibility.label()));
|
||
}
|
||
out
|
||
}
|
||
|
||
// ---------- cryptography ----------
|
||
|
||
/// Padding of passwords (revisions 2–4).
|
||
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,
|
||
];
|
||
|
||
fn md5(parts: &[&[u8]]) -> [u8; 16] {
|
||
let mut h = Md5::new();
|
||
for p in parts {
|
||
h.update(p);
|
||
}
|
||
h.finalize().into()
|
||
}
|
||
|
||
/// `pw` cut or padded to 32 bytes.
|
||
fn padded(pw: &[u8]) -> [u8; 32] {
|
||
let mut out = PAD;
|
||
let n = pw.len().min(32);
|
||
out[..n].copy_from_slice(&pw[..n]);
|
||
out[n..].copy_from_slice(&PAD[..32 - n]);
|
||
out
|
||
}
|
||
|
||
fn rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
|
||
if key.is_empty() {
|
||
return data.to_vec();
|
||
}
|
||
let mut s: [u8; 256] = std::array::from_fn(|i| i as u8);
|
||
let mut j = 0u8;
|
||
for (i, k) in (0..256).zip(key.iter().cycle()) {
|
||
j = j.wrapping_add(s[i]).wrapping_add(*k);
|
||
s.swap(i, usize::from(j));
|
||
}
|
||
let (mut i, mut j) = (0u8, 0u8);
|
||
data.iter()
|
||
.map(|b| {
|
||
i = i.wrapping_add(1);
|
||
j = j.wrapping_add(s[usize::from(i)]);
|
||
s.swap(usize::from(i), usize::from(j));
|
||
b ^ s[usize::from(s[usize::from(i)].wrapping_add(s[usize::from(j)]))]
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// `key` with every byte XORed with `i` (the 19 extra RC4 passes of revisions 3–4).
|
||
fn xored(key: &[u8], i: u8) -> Vec<u8> {
|
||
key.iter().map(|b| b ^ i).collect()
|
||
}
|
||
|
||
/// An AES block cipher with a 128- or 256-bit key.
|
||
enum Aes {
|
||
A128(Box<aes::Aes128>),
|
||
A256(Box<aes::Aes256>),
|
||
}
|
||
|
||
impl Aes {
|
||
fn new(key: &[u8]) -> Result<Self, PdfError> {
|
||
let bad = |_| PdfError::Write("bad AES key".into());
|
||
match key.len() {
|
||
16 => aes::Aes128::new_from_slice(key).map(|c| Self::A128(Box::new(c))).map_err(bad),
|
||
_ => aes::Aes256::new_from_slice(key).map(|c| Self::A256(Box::new(c))).map_err(bad),
|
||
}
|
||
}
|
||
|
||
fn block(&self, b: [u8; 16]) -> [u8; 16] {
|
||
let mut block = aes::Block::from(b);
|
||
match self {
|
||
Self::A128(c) => c.encrypt_block(&mut block),
|
||
Self::A256(c) => c.encrypt_block(&mut block),
|
||
}
|
||
block.into()
|
||
}
|
||
|
||
/// CBC from `iv`; `pad`: PKCS #7 padding (else a trailing partial block is dropped).
|
||
fn cbc(&self, iv: [u8; 16], data: &[u8], pad: bool) -> Vec<u8> {
|
||
let mut out = Vec::with_capacity(data.len() + 16);
|
||
let mut prev = iv;
|
||
let mut put = |chunk: [u8; 16], out: &mut Vec<u8>| {
|
||
prev = self.block(std::array::from_fn(|i| chunk[i] ^ prev[i]));
|
||
out.extend_from_slice(&prev);
|
||
};
|
||
let mut chunks = data.chunks_exact(16);
|
||
for c in &mut chunks {
|
||
put(std::array::from_fn(|i| c.get(i).copied().unwrap_or_default()), &mut out);
|
||
}
|
||
if pad {
|
||
let rest = chunks.remainder();
|
||
let n = (16 - rest.len()) as u8;
|
||
put(std::array::from_fn(|i| rest.get(i).copied().unwrap_or(n)), &mut out);
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
/// The first `N` bytes of `b` (zero-filled when shorter).
|
||
fn first<const N: usize>(b: &[u8]) -> [u8; N] {
|
||
std::array::from_fn(|i| b.get(i).copied().unwrap_or_default())
|
||
}
|
||
|
||
/// The revision 6 password hash of `password` with `salt` (and the 48-byte U entry when hashing
|
||
/// the owner password).
|
||
fn hash6(password: &[u8], salt: &[u8], udata: &[u8]) -> Result<[u8; 32], PdfError> {
|
||
let mut k: Vec<u8> = Sha256::new().chain_update(password).chain_update(salt).chain_update(udata).finalize().to_vec();
|
||
let mut round = 0u32;
|
||
loop {
|
||
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 key: [u8; 16] = first(&k);
|
||
let e = Aes::new(&key)?.cbc(first(k.get(16..).unwrap_or_default()), &k1, false);
|
||
// The first 16 bytes of E as a big-endian number, modulo 3 (256 ≡ 1 mod 3).
|
||
k = match e.iter().take(16).map(|b| u32::from(*b)).sum::<u32>() % 3 {
|
||
0 => Sha256::digest(&e).to_vec(),
|
||
1 => Sha384::digest(&e).to_vec(),
|
||
_ => Sha512::digest(&e).to_vec(),
|
||
};
|
||
round += 1;
|
||
if round >= 64 && u32::from(e.last().copied().unwrap_or_default()) <= round - 32 {
|
||
return Ok(first(&k));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Unpredictable bytes: SHA-256 of a seed and a counter.
|
||
struct Entropy {
|
||
seed: [u8; 32],
|
||
counter: u64,
|
||
}
|
||
|
||
impl Entropy {
|
||
/// Seeded with the process's random hash keys (none on the web), the time and `material`.
|
||
fn new(material: &[&[u8]]) -> Self {
|
||
use std::hash::BuildHasher;
|
||
let state = std::hash::RandomState::new();
|
||
let mut h = Sha256::new();
|
||
for i in 0u8..4 {
|
||
h.update(state.hash_one(i).to_le_bytes());
|
||
}
|
||
h.update(vectorcraft_doc::metadata::now_unix().unwrap_or_default().to_le_bytes());
|
||
for m in material {
|
||
h.update((m.len() as u64).to_le_bytes());
|
||
h.update(m);
|
||
}
|
||
Self { seed: h.finalize().into(), counter: 0 }
|
||
}
|
||
|
||
fn bytes<const N: usize>(&mut self) -> [u8; N] {
|
||
let mut out = [0u8; N];
|
||
for chunk in out.chunks_mut(32) {
|
||
self.counter += 1;
|
||
let block: [u8; 32] = Sha256::new().chain_update(self.seed).chain_update(self.counter.to_le_bytes()).finalize().into();
|
||
chunk.copy_from_slice(&block[..chunk.len()]);
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
fn hex(b: &[u8]) -> String {
|
||
let mut s = String::with_capacity(2 * b.len() + 2);
|
||
s.push('<');
|
||
for x in b {
|
||
s.push_str(&format!("{x:02X}"));
|
||
}
|
||
s.push('>');
|
||
s
|
||
}
|
||
|
||
/// What encrypts the strings and streams of one file.
|
||
struct Handler {
|
||
/// The file encryption key.
|
||
key: Vec<u8>,
|
||
cipher: Encryption,
|
||
encrypt_metadata: bool,
|
||
/// The encryption dictionary.
|
||
dict: String,
|
||
}
|
||
|
||
impl Handler {
|
||
/// The handler for `sec` at version `c`, for a file whose first ID is `id`.
|
||
fn new(sec: &SecuritySettings, c: Compatibility, id: &[u8], rng: &mut Entropy) -> Result<Self, PdfError> {
|
||
let r = revision(c);
|
||
let cipher = Encryption::for_compatibility(c);
|
||
let user = sec.open_password.as_bytes();
|
||
// Without a permissions password the open password grants every permission anyway.
|
||
let owner = if sec.permissions_password.is_empty() { user } else { sec.permissions_password.as_bytes() };
|
||
let p = sec.permission_bits();
|
||
let encrypt_metadata = r == 3 || !sec.plaintext_metadata;
|
||
let meta = if encrypt_metadata { "" } else { "/EncryptMetadata false" };
|
||
if r == 6 {
|
||
let key: [u8; 32] = rng.bytes();
|
||
let (uv, uk): ([u8; 8], [u8; 8]) = (rng.bytes(), rng.bytes());
|
||
let u = [&hash6(user, &uv, &[])?[..], &uv[..], &uk[..]].concat();
|
||
let ue = Aes::new(&hash6(user, &uk, &[])?)?.cbc([0; 16], &key, false);
|
||
let (ov, ok): ([u8; 8], [u8; 8]) = (rng.bytes(), rng.bytes());
|
||
let o = [&hash6(owner, &ov, &u)?[..], &ov[..], &ok[..]].concat();
|
||
let oe = Aes::new(&hash6(owner, &ok, &u)?)?.cbc([0; 16], &key, false);
|
||
let mut perms = [0xFFu8; 16];
|
||
perms[..4].copy_from_slice(&p.to_le_bytes());
|
||
perms[8] = if encrypt_metadata { b'T' } else { b'F' };
|
||
perms[9..12].copy_from_slice(b"adb");
|
||
perms[12..].copy_from_slice(&rng.bytes::<4>());
|
||
let perms = Aes::new(&key)?.block(perms);
|
||
let dict = format!(
|
||
"<</Filter/Standard/V 5/R 6/Length 256/CF<</StdCF<</AuthEvent/DocOpen/CFM/AESV3/Length 32>>>>/StmF/StdCF/StrF/StdCF/P {p}/O{}/U{}/OE{}/UE{}/Perms{}{meta}>>",
|
||
hex(&o),
|
||
hex(&u),
|
||
hex(&oe),
|
||
hex(&ue),
|
||
hex(&perms)
|
||
);
|
||
return Ok(Self { key: key.to_vec(), cipher, encrypt_metadata, dict });
|
||
}
|
||
// O (algorithm 3), the file key (algorithm 2) and U (algorithm 5), for a 16-byte key.
|
||
let owner_key = owner_key(owner, r, 16);
|
||
let mut o = rc4(&owner_key, &padded(user));
|
||
for i in 1..=19 {
|
||
o = rc4(&xored(&owner_key, i), &o);
|
||
}
|
||
let no_meta: &[u8] = if encrypt_metadata { &[] } else { &[0xFF; 4] };
|
||
let mut key = md5(&[&padded(user), &o, &p.to_le_bytes(), id, no_meta]);
|
||
for _ in 0..50 {
|
||
key = md5(&[&key]);
|
||
}
|
||
let mut u = rc4(&key, &md5(&[&PAD, id]));
|
||
for i in 1..=19 {
|
||
u = rc4(&xored(&key, i), &u);
|
||
}
|
||
u.extend_from_slice(&rng.bytes::<16>());
|
||
let dict = if r == 3 {
|
||
format!("<</Filter/Standard/V 2/R 3/Length 128/P {p}/O{}/U{}>>", hex(&o), hex(&u))
|
||
} else {
|
||
let cfm = if cipher == Encryption::Aes128 { "AESV2" } else { "V2" };
|
||
format!(
|
||
"<</Filter/Standard/V 4/R 4/Length 128/CF<</StdCF<</AuthEvent/DocOpen/CFM/{cfm}/Length 16>>>>/StmF/StdCF/StrF/StdCF/P {p}/O{}/U{}{meta}>>",
|
||
hex(&o),
|
||
hex(&u)
|
||
)
|
||
};
|
||
Ok(Self { key: key.to_vec(), cipher, encrypt_metadata, dict })
|
||
}
|
||
|
||
/// `data` of object `num generation` encrypted.
|
||
fn encrypt(&self, num: u32, generation: u16, data: &[u8], rng: &mut Entropy) -> Result<Vec<u8>, PdfError> {
|
||
let object_key = || {
|
||
let salt: &[u8] = if self.cipher == Encryption::Aes128 { b"sAlT" } else { &[] };
|
||
md5(&[&self.key, &num.to_le_bytes()[..3], &generation.to_le_bytes(), salt])
|
||
};
|
||
let aes = |key: &[u8], rng: &mut Entropy| -> Result<Vec<u8>, PdfError> {
|
||
let iv: [u8; 16] = rng.bytes();
|
||
Ok([&iv[..], &Aes::new(key)?.cbc(iv, data, true)].concat())
|
||
};
|
||
match self.cipher {
|
||
Encryption::Rc4 => Ok(rc4(&object_key(), data)),
|
||
Encryption::Aes128 => aes(&object_key(), rng),
|
||
Encryption::Aes256 => aes(&self.key, rng),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The RC4 key the owner password encrypts O with (algorithm 3, steps a–d).
|
||
fn owner_key(owner: &[u8], r: u8, n: usize) -> Vec<u8> {
|
||
let mut h = md5(&[&padded(owner)]);
|
||
if r >= 3 {
|
||
for _ in 0..50 {
|
||
h = md5(&[h.get(..n).unwrap_or(&h)]);
|
||
}
|
||
}
|
||
h.get(..n).unwrap_or(&h).to_vec()
|
||
}
|
||
|
||
// ---------- PDF syntax ----------
|
||
|
||
pub(crate) fn is_white(b: u8) -> bool {
|
||
matches!(b, 0 | 9 | 10 | 12 | 13 | 32)
|
||
}
|
||
|
||
fn is_regular(b: u8) -> bool {
|
||
!is_white(b) && !b"()<>[]{}/%".contains(&b)
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub(crate) enum Tok<'a> {
|
||
DictOpen,
|
||
DictClose,
|
||
ArrayOpen,
|
||
ArrayClose,
|
||
Name(&'a [u8]),
|
||
Str(Vec<u8>),
|
||
/// Numbers and keywords (`true`, `R`, `obj`, `stream`…).
|
||
Word(&'a [u8]),
|
||
}
|
||
|
||
/// A token with its byte span.
|
||
pub(crate) type Spanned<'a> = (Tok<'a>, usize, usize);
|
||
|
||
/// Deepest nesting of arrays and dictionaries read.
|
||
const MAX_DEPTH: usize = 64;
|
||
|
||
/// A parsed object, with what encryption needs: the strings and dictionaries with their spans.
|
||
#[derive(Debug)]
|
||
pub(crate) enum Obj<'a> {
|
||
/// `start` is where `<<` is, `end` just after `>>`.
|
||
Dict {
|
||
entries: Vec<(&'a [u8], Obj<'a>)>,
|
||
start: usize,
|
||
end: usize,
|
||
},
|
||
Array(Vec<Obj<'a>>),
|
||
Str {
|
||
bytes: Vec<u8>,
|
||
start: usize,
|
||
end: usize,
|
||
},
|
||
Name(&'a [u8]),
|
||
Int {
|
||
value: i64,
|
||
start: usize,
|
||
end: usize,
|
||
},
|
||
Ref(u32, u16),
|
||
/// Reals, booleans, null.
|
||
Other,
|
||
}
|
||
|
||
impl<'a> Obj<'a> {
|
||
pub(crate) fn get(&self, key: &[u8]) -> Option<&Obj<'a>> {
|
||
match self {
|
||
Self::Dict { entries, .. } => entries.iter().find(|(k, _)| *k == key).map(|(_, v)| v),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn name(&self, key: &[u8]) -> Option<&'a [u8]> {
|
||
match self.get(key)? {
|
||
Self::Name(n) => Some(n),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn int(&self, key: &[u8]) -> Option<i64> {
|
||
match self.get(key)? {
|
||
Self::Int { value, .. } => Some(*value),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn string(&self, key: &[u8]) -> Option<&[u8]> {
|
||
match self.get(key)? {
|
||
Self::Str { bytes, .. } => Some(bytes),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// The first string of the array at `key` (a file's ID).
|
||
fn first_string(&self, key: &[u8]) -> Option<&[u8]> {
|
||
match self.get(key)? {
|
||
Self::Array(items) => items.iter().find_map(|i| match i {
|
||
Self::Str { bytes, .. } => Some(bytes.as_slice()),
|
||
_ => None,
|
||
}),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Every string in this object, in file order.
|
||
fn strings<'s>(&'s self, out: &mut Vec<&'s Obj<'a>>) {
|
||
match self {
|
||
Self::Str { .. } => out.push(self),
|
||
Self::Dict { entries, .. } => entries.iter().for_each(|(_, v)| v.strings(out)),
|
||
Self::Array(items) => items.iter().for_each(|v| v.strings(out)),
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn int(w: &[u8]) -> Option<i64> {
|
||
std::str::from_utf8(w).ok()?.parse().ok()
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub(crate) struct Lexer<'a> {
|
||
buf: &'a [u8],
|
||
pos: usize,
|
||
}
|
||
|
||
impl<'a> Lexer<'a> {
|
||
pub(crate) fn at(buf: &'a [u8], pos: usize) -> Self {
|
||
Self { buf, pos }
|
||
}
|
||
|
||
fn peek_byte(&self, ahead: usize) -> Option<u8> {
|
||
self.buf.get(self.pos + ahead).copied()
|
||
}
|
||
|
||
fn skip_space(&mut self) {
|
||
while let Some(b) = self.peek_byte(0) {
|
||
if is_white(b) {
|
||
self.pos += 1;
|
||
} else if b == b'%' {
|
||
while self.peek_byte(0).is_some_and(|b| b != b'\n' && b != b'\r') {
|
||
self.pos += 1;
|
||
}
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn next(&mut self) -> Option<Spanned<'a>> {
|
||
self.skip_space();
|
||
let start = self.pos;
|
||
let b = self.peek_byte(0)?;
|
||
let tok = match (b, self.peek_byte(1)) {
|
||
(b'<', Some(b'<')) => {
|
||
self.pos += 2;
|
||
Tok::DictOpen
|
||
}
|
||
(b'>', Some(b'>')) => {
|
||
self.pos += 2;
|
||
Tok::DictClose
|
||
}
|
||
(b'<', _) => Tok::Str(self.hex()?),
|
||
(b'(', _) => Tok::Str(self.literal()?),
|
||
(b'[', _) => {
|
||
self.pos += 1;
|
||
Tok::ArrayOpen
|
||
}
|
||
(b']', _) => {
|
||
self.pos += 1;
|
||
Tok::ArrayClose
|
||
}
|
||
(b'/', _) => {
|
||
self.pos += 1;
|
||
self.word();
|
||
Tok::Name(self.buf.get(start + 1..self.pos)?)
|
||
}
|
||
_ if !is_regular(b) => {
|
||
// A stray delimiter.
|
||
self.pos += 1;
|
||
Tok::Word(self.buf.get(start..self.pos)?)
|
||
}
|
||
_ => {
|
||
self.word();
|
||
Tok::Word(self.buf.get(start..self.pos)?)
|
||
}
|
||
};
|
||
Some((tok, start, self.pos))
|
||
}
|
||
|
||
fn word(&mut self) {
|
||
while self.peek_byte(0).is_some_and(is_regular) {
|
||
self.pos += 1;
|
||
}
|
||
}
|
||
|
||
fn hex(&mut self) -> Option<Vec<u8>> {
|
||
self.pos += 1;
|
||
let (mut out, mut high) = (vec![], None);
|
||
loop {
|
||
let b = self.peek_byte(0)?;
|
||
self.pos += 1;
|
||
if b == b'>' {
|
||
out.extend(high.map(|h: u8| h << 4));
|
||
return Some(out);
|
||
}
|
||
if is_white(b) {
|
||
continue;
|
||
}
|
||
let v = char::from(b).to_digit(16)? as u8;
|
||
match high.take() {
|
||
Some(h) => out.push(h << 4 | v),
|
||
None => high = Some(v),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn literal(&mut self) -> Option<Vec<u8>> {
|
||
self.pos += 1;
|
||
let (mut out, mut depth) = (vec![], 1u32);
|
||
loop {
|
||
let b = self.peek_byte(0)?;
|
||
self.pos += 1;
|
||
match b {
|
||
b'\\' => {
|
||
let e = self.peek_byte(0)?;
|
||
self.pos += 1;
|
||
match e {
|
||
b'n' => out.push(b'\n'),
|
||
b'r' => out.push(b'\r'),
|
||
b't' => out.push(b'\t'),
|
||
b'b' => out.push(8),
|
||
b'f' => out.push(12),
|
||
// A backslash before an end of line continues the string.
|
||
b'\r' => {
|
||
if self.peek_byte(0) == Some(b'\n') {
|
||
self.pos += 1;
|
||
}
|
||
}
|
||
b'\n' => {}
|
||
b'0'..=b'7' => {
|
||
let mut v = u32::from(e - b'0');
|
||
for _ in 0..2 {
|
||
match self.peek_byte(0) {
|
||
Some(d @ b'0'..=b'7') => {
|
||
v = v * 8 + u32::from(d - b'0');
|
||
self.pos += 1;
|
||
}
|
||
_ => break,
|
||
}
|
||
}
|
||
out.push((v & 0xFF) as u8);
|
||
}
|
||
// `\(`, `\)`, `\\`; other escapes stand for the character.
|
||
_ => out.push(e),
|
||
}
|
||
}
|
||
b'(' => {
|
||
depth += 1;
|
||
out.push(b);
|
||
}
|
||
b')' => {
|
||
depth -= 1;
|
||
if depth == 0 {
|
||
return Some(out);
|
||
}
|
||
out.push(b);
|
||
}
|
||
// Ends of lines read as `\n`.
|
||
b'\r' => {
|
||
if self.peek_byte(0) == Some(b'\n') {
|
||
self.pos += 1;
|
||
}
|
||
out.push(b'\n');
|
||
}
|
||
_ => out.push(b),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// One object (a reference `n g R` reads as one).
|
||
pub(crate) fn object(&mut self, depth: usize) -> Option<Obj<'a>> {
|
||
if depth > MAX_DEPTH {
|
||
return None;
|
||
}
|
||
let (tok, start, end) = self.next()?;
|
||
Some(match tok {
|
||
Tok::DictOpen => {
|
||
let mut entries = vec![];
|
||
loop {
|
||
let mut look = self.clone();
|
||
match look.next()? {
|
||
(Tok::DictClose, _, end) => {
|
||
*self = look;
|
||
return Some(Obj::Dict { entries, start, end });
|
||
}
|
||
(Tok::Name(k), ..) => {
|
||
*self = look;
|
||
entries.push((k, self.object(depth + 1)?));
|
||
}
|
||
_ => return None,
|
||
}
|
||
}
|
||
}
|
||
Tok::ArrayOpen => {
|
||
let mut items = vec![];
|
||
loop {
|
||
let mut look = self.clone();
|
||
if matches!(look.next()?, (Tok::ArrayClose, ..)) {
|
||
*self = look;
|
||
return Some(Obj::Array(items));
|
||
}
|
||
items.push(self.object(depth + 1)?);
|
||
}
|
||
}
|
||
Tok::Str(bytes) => Obj::Str { bytes, start, end },
|
||
Tok::Name(n) => Obj::Name(n),
|
||
Tok::Word(w) => match int(w) {
|
||
Some(value) => {
|
||
let mut look = self.clone();
|
||
match (look.next(), look.next()) {
|
||
(Some((Tok::Word(g), ..)), Some((Tok::Word(b"R"), ..))) => {
|
||
match (u32::try_from(value), int(g).and_then(|g| u16::try_from(g).ok())) {
|
||
(Ok(n), Some(g)) => {
|
||
*self = look;
|
||
Obj::Ref(n, g)
|
||
}
|
||
_ => Obj::Int { value, start, end },
|
||
}
|
||
}
|
||
_ => Obj::Int { value, start, end },
|
||
}
|
||
}
|
||
None => Obj::Other,
|
||
},
|
||
Tok::DictClose | Tok::ArrayClose => return None,
|
||
})
|
||
}
|
||
|
||
/// The header `num generation obj` of an indirect object.
|
||
pub(crate) fn header(&mut self) -> Option<(u32, u16)> {
|
||
let (Some((Tok::Word(n), ..)), Some((Tok::Word(g), ..)), Some((Tok::Word(b"obj"), ..))) = (self.next(), self.next(), self.next()) else {
|
||
return None;
|
||
};
|
||
Some((u32::try_from(int(n)?).ok()?, u16::try_from(int(g)?).ok()?))
|
||
}
|
||
}
|
||
|
||
// ---------- encrypting a written file ----------
|
||
|
||
fn failed(why: &str) -> PdfError {
|
||
PdfError::Write(format!("can't encrypt the PDF: {why}"))
|
||
}
|
||
|
||
/// A replacement of bytes `start..end` of the file.
|
||
type Edit = (usize, usize, Vec<u8>);
|
||
|
||
/// The catalog entry declaring 256-bit AES in a PDF 1.7 file (extension level 8).
|
||
const AES256_EXTENSION: &[u8] = b"/Extensions<</ADBE<</BaseVersion/1.7/ExtensionLevel 8>>>>";
|
||
|
||
/// An in-use cross-reference entry: object number, generation, offset.
|
||
type Entry = (u32, u16, usize);
|
||
|
||
/// The in-use entries of the cross-reference table at `xref` and the trailer dictionary after it.
|
||
fn xref_table(pdf: &[u8], xref: usize) -> Result<(Vec<Entry>, Obj<'_>), PdfError> {
|
||
let mut lx = Lexer::at(pdf, xref + 4);
|
||
let mut entries = vec![];
|
||
loop {
|
||
let word = |t: Option<Spanned<'_>>| match t {
|
||
Some((Tok::Word(w), ..)) => int(w),
|
||
_ => None,
|
||
};
|
||
let mut look = lx.clone();
|
||
if let Some((Tok::Word(b"trailer"), ..)) = look.next() {
|
||
lx = look;
|
||
break;
|
||
}
|
||
let (first, count) = (word(lx.next()), word(lx.next()));
|
||
let (Some(first), Some(count)) = (first, count) else { return Err(failed("bad cross-reference table")) };
|
||
for i in 0..count {
|
||
let (off, generation, kind) = (word(lx.next()), word(lx.next()), lx.next());
|
||
let (Some(off), Some(generation), Some((Tok::Word(kind), ..))) = (off, generation, kind) else {
|
||
return Err(failed("bad cross-reference entry"));
|
||
};
|
||
if kind == b"n" {
|
||
let num = u32::try_from(first + i).map_err(|_| failed("bad object number"))?;
|
||
entries.push((num, u16::try_from(generation).unwrap_or(0), usize::try_from(off).map_err(|_| failed("bad offset"))?));
|
||
}
|
||
}
|
||
}
|
||
let trailer = lx.object(0).ok_or_else(|| failed("bad trailer"))?;
|
||
Ok((entries, trailer))
|
||
}
|
||
|
||
/// The edits encrypting the object at `off` (its strings and stream data); `catalog`: add the
|
||
/// 256-bit AES extension to it.
|
||
fn object_edits(pdf: &[u8], off: usize, h: &Handler, catalog: bool, rng: &mut Entropy, edits: &mut Vec<Edit>) -> Result<(), PdfError> {
|
||
let mut lx = Lexer::at(pdf, off);
|
||
let (num, generation) = lx.header().ok_or_else(|| failed("an object's header is missing"))?;
|
||
let obj = lx.object(0).ok_or_else(|| failed("unreadable object"))?;
|
||
let mut strings = vec![];
|
||
obj.strings(&mut strings);
|
||
for s in strings {
|
||
if let Obj::Str { bytes, start, end } = s {
|
||
edits.push((*start, *end, hex(&h.encrypt(num, generation, bytes, rng)?).into_bytes()));
|
||
}
|
||
}
|
||
if let (true, Obj::Dict { start, .. }) = (catalog, &obj)
|
||
&& obj.get(b"Extensions").is_none()
|
||
{
|
||
edits.push((start + 2, start + 2, AES256_EXTENSION.to_vec()));
|
||
}
|
||
let Some(((len_start, len_end), (data, data_end))) = stream_spans(pdf, &mut lx, &obj).map_err(failed)? else { return Ok(()) };
|
||
let ty = obj.name(b"Type");
|
||
if matches!(ty, Some(b"XRef" | b"ObjStm")) {
|
||
return Err(failed("cross-reference and object streams aren't supported"));
|
||
}
|
||
if ty == Some(&b"Metadata"[..]) && !h.encrypt_metadata {
|
||
return Ok(());
|
||
}
|
||
let bytes = pdf.get(data..data_end).ok_or_else(|| failed("a stream runs past the end"))?;
|
||
let enc = h.encrypt(num, generation, bytes, rng)?;
|
||
edits.push((len_start, len_end, enc.len().to_string().into_bytes()));
|
||
edits.push((data, data_end, enc));
|
||
Ok(())
|
||
}
|
||
|
||
/// The spans of a stream's length value and of its data.
|
||
pub(crate) type StreamSpans = ((usize, usize), (usize, usize));
|
||
|
||
/// Where the object whose dictionary `dict` the lexer `lx` has just read keeps its stream data:
|
||
/// its [`StreamSpans`]; `None` when it isn't a stream.
|
||
pub(crate) fn stream_spans(pdf: &[u8], lx: &mut Lexer<'_>, dict: &Obj<'_>) -> Result<Option<StreamSpans>, &'static str> {
|
||
let Some((Tok::Word(b"stream"), _, keyword_end)) = lx.next() else { return Ok(None) };
|
||
let Some(Obj::Int { value, start, end }) = dict.get(b"Length") else { return Err("a stream's length isn't a number") };
|
||
// The data starts after the end of line that follows `stream`.
|
||
let mut data = keyword_end;
|
||
if pdf.get(data) == Some(&b'\r') {
|
||
data += 1;
|
||
}
|
||
if pdf.get(data) == Some(&b'\n') {
|
||
data += 1;
|
||
}
|
||
let data_end = usize::try_from(*value).ok().and_then(|n| data.checked_add(n)).ok_or("bad stream length")?;
|
||
if data_end > pdf.len() {
|
||
return Err("a stream runs past the end");
|
||
}
|
||
if !matches!(Lexer::at(pdf, data_end).next(), Some((Tok::Word(b"endstream"), ..))) {
|
||
return Err("a stream's length is wrong");
|
||
}
|
||
Ok(Some(((*start, *end), (data, data_end))))
|
||
}
|
||
|
||
/// What encrypts the streams of a file [`protect_keeping`] encrypted, for those written after it
|
||
/// (the hint stream of a linearised file).
|
||
pub(crate) struct Cipher {
|
||
h: Handler,
|
||
rng: Entropy,
|
||
}
|
||
|
||
impl Cipher {
|
||
/// `data`, the stream of object `num` (generation 0), encrypted.
|
||
pub(crate) fn stream(&mut self, num: u32, data: &[u8]) -> Result<Vec<u8>, PdfError> {
|
||
self.h.encrypt(num, 0, data, &mut self.rng)
|
||
}
|
||
}
|
||
|
||
/// Encrypt `pdf` (as the export writes it: one cross-reference table) as `set`'s Security
|
||
/// section says; without a password it comes back as it is.
|
||
pub(crate) fn protect(pdf: Vec<u8>, set: &PdfSettings) -> Result<Vec<u8>, PdfError> {
|
||
protect_keeping(pdf, set).map(|(pdf, _)| pdf)
|
||
}
|
||
|
||
/// [`protect`], keeping what encrypted the file (`None` when it isn't encrypted). The objects keep
|
||
/// their order and numbers; the encryption dictionary comes after them, numbered last.
|
||
pub(crate) fn protect_keeping(pdf: Vec<u8>, set: &PdfSettings) -> Result<(Vec<u8>, Option<Cipher>), PdfError> {
|
||
let sec = &set.security;
|
||
if !sec.protected() {
|
||
return Ok((pdf, None));
|
||
}
|
||
let xref = xref_offset(&pdf).ok_or_else(|| failed("no cross-reference table"))?;
|
||
let (mut objects, trailer) = xref_table(&pdf, xref)?;
|
||
let Obj::Dict { start: t_start, end: t_end, .. } = &trailer else { return Err(failed("bad trailer")) };
|
||
if trailer.get(b"Prev").is_some() || trailer.get(b"Encrypt").is_some() {
|
||
return Err(failed("the file was updated or encrypted already"));
|
||
}
|
||
let Some(Obj::Int { value: size, start: size_start, end: size_end }) = trailer.get(b"Size") else {
|
||
return Err(failed("the trailer has no size"));
|
||
};
|
||
let size = u32::try_from(*size).ok().filter(|s| *s as usize <= 2 * objects.len() + 16).ok_or_else(|| failed("bad trailer size"))?;
|
||
let root = match trailer.get(b"Root") {
|
||
Some(Obj::Ref(n, _)) => Some(*n),
|
||
_ => None,
|
||
};
|
||
let mut rng = Entropy::new(&[sec.open_password.as_bytes(), sec.permissions_password.as_bytes(), &pdf]);
|
||
let (id, new_id) = match trailer.first_string(b"ID") {
|
||
Some(id) => (id.to_vec(), None),
|
||
None => {
|
||
let id: [u8; 16] = rng.bytes();
|
||
(id.to_vec(), Some(id))
|
||
}
|
||
};
|
||
let h = Handler::new(sec, set.compatibility, &id, &mut rng)?;
|
||
let extension = h.cipher == Encryption::Aes256 && set.compatibility == Compatibility::Pdf17;
|
||
|
||
objects.sort_by_key(|o| o.2);
|
||
let mut edits = vec![];
|
||
for (num, _, off) in &objects {
|
||
object_edits(&pdf, *off, &h, extension && root == Some(*num), &mut rng, &mut edits)?;
|
||
}
|
||
edits.sort_by_key(|e| (e.0, e.1));
|
||
|
||
// The objects with their edits, recording where each one now starts.
|
||
let mut out = Vec::with_capacity(pdf.len() + pdf.len() / 8 + 1024);
|
||
let mut offsets = vec![None; size as usize + 1];
|
||
let (mut at, mut pending) = (0, edits.iter().peekable());
|
||
let mut copy_to = |to: usize, out: &mut Vec<u8>, at: &mut usize| -> Result<(), PdfError> {
|
||
while let Some((s, e, new)) = pending.next_if(|e| e.0 < to) {
|
||
out.extend_from_slice(pdf.get(*at..*s).ok_or_else(|| failed("overlapping edits"))?);
|
||
out.extend_from_slice(new);
|
||
*at = *e;
|
||
}
|
||
out.extend_from_slice(pdf.get(*at..to).ok_or_else(|| failed("overlapping edits"))?);
|
||
*at = to;
|
||
Ok(())
|
||
};
|
||
for (num, generation, off) in &objects {
|
||
copy_to(*off, &mut out, &mut at)?;
|
||
let slot = offsets.get_mut(*num as usize).filter(|_| *num < size).ok_or_else(|| failed("bad object number"))?;
|
||
*slot = Some((out.len(), *generation));
|
||
}
|
||
copy_to(xref, &mut out, &mut at)?;
|
||
if let Some(slot) = offsets.get_mut(size as usize) {
|
||
*slot = Some((out.len(), 0));
|
||
}
|
||
out.extend_from_slice(format!("{size} 0 obj\n{}\nendobj\n", h.dict).as_bytes());
|
||
|
||
// A new cross-reference table and the trailer naming the encryption dictionary.
|
||
let table = out.len();
|
||
out.extend_from_slice(format!("xref\n0 {}\n", size + 1).as_bytes());
|
||
for o in &offsets {
|
||
let entry = match o {
|
||
Some((off, generation)) => format!("{off:010} {generation:05} n\r\n"),
|
||
None => "0000000000 65535 f\r\n".to_string(),
|
||
};
|
||
out.extend_from_slice(entry.as_bytes());
|
||
}
|
||
out.extend_from_slice(b"trailer\n");
|
||
let dict_body = t_end.checked_sub(2).filter(|b| *b >= *size_end).ok_or_else(|| failed("bad trailer"))?;
|
||
out.extend_from_slice(pdf.get(*t_start..*size_start).ok_or_else(|| failed("bad trailer"))?);
|
||
out.extend_from_slice((size + 1).to_string().as_bytes());
|
||
out.extend_from_slice(pdf.get(*size_end..dict_body).ok_or_else(|| failed("bad trailer"))?);
|
||
if let Some(id) = new_id {
|
||
out.extend_from_slice(format!("/ID[{}{}]", hex(&id), hex(&id)).as_bytes());
|
||
}
|
||
out.extend_from_slice(format!("/Encrypt {size} 0 R>>\nstartxref\n{table}\n%%EOF").as_bytes());
|
||
Ok((out, Some(Cipher { h, rng })))
|
||
}
|
||
|
||
// ---------- reading ----------
|
||
|
||
/// The trailer dictionary of `pdf`: after its last cross-reference table, or the dictionary of
|
||
/// its last cross-reference stream.
|
||
fn trailer(pdf: &[u8]) -> Option<Obj<'_>> {
|
||
let sx = rfind(pdf, b"startxref")?;
|
||
let mut lx = Lexer::at(pdf, sx + 9);
|
||
let off = match lx.next()? {
|
||
(Tok::Word(w), ..) => usize::try_from(int(w)?).ok()?,
|
||
_ => return None,
|
||
};
|
||
if pdf.get(off..)?.starts_with(b"xref") {
|
||
let at = find(pdf, b"trailer", off)?;
|
||
return Lexer::at(pdf, at + 7).object(0);
|
||
}
|
||
let mut lx = Lexer::at(pdf, off);
|
||
lx.header()?;
|
||
lx.object(0)
|
||
}
|
||
|
||
/// The object `num generation` of `pdf`, found by its header (the last one in the file).
|
||
fn find_object(pdf: &[u8], num: u32, generation: u16) -> Option<Obj<'_>> {
|
||
let head = format!("{num} {generation} obj");
|
||
let mut end = pdf.len();
|
||
while let Some(at) = rfind(pdf.get(..end)?, head.as_bytes()) {
|
||
if at == 0 || pdf.get(at - 1).is_some_and(|b| is_white(*b)) {
|
||
let mut lx = Lexer::at(pdf, at);
|
||
lx.header()?;
|
||
return lx.object(0);
|
||
}
|
||
end = at + head.len() - 1;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The open (user) password of `pdf`, from its permissions (owner) password `owner`: for files
|
||
/// encrypted with RC4 or 128-bit AES (revisions 2–4), whose user password is in their O entry.
|
||
/// `None` for other files, a wrong password or a user password that isn't UTF-8.
|
||
pub(crate) fn user_password(pdf: &[u8], owner: &str) -> Option<String> {
|
||
let trailer = trailer(pdf)?;
|
||
let found;
|
||
let enc = match trailer.get(b"Encrypt")? {
|
||
Obj::Ref(n, g) => {
|
||
found = find_object(pdf, *n, *g)?;
|
||
&found
|
||
}
|
||
d @ Obj::Dict { .. } => d,
|
||
_ => return None,
|
||
};
|
||
let r = u8::try_from(enc.int(b"R")?).ok().filter(|r| (2..=4).contains(r))?;
|
||
let n = if r == 2 { 5 } else { usize::try_from(enc.int(b"Length").unwrap_or(40) / 8).ok()?.clamp(5, 16) };
|
||
let key = owner_key(owner.as_bytes(), r, n);
|
||
let mut user = enc.string(b"O")?.get(..32)?.to_vec();
|
||
if r == 2 {
|
||
user = rc4(&key, &user);
|
||
} else {
|
||
for i in (0..=19).rev() {
|
||
user = rc4(&xored(&key, i), &user);
|
||
}
|
||
}
|
||
// The password is what comes before the padding.
|
||
let len = (0..=32).find(|l| user.get(*l..) == PAD.get(..32 - l))?;
|
||
String::from_utf8(user.get(..len)?.to_vec()).ok()
|
||
}
|