Skip to main content

pdfrum_edit/
encrypt.rs

1//! The output-side security seam.
2//!
3//! An encrypted document saves **encrypted**, under the handler and file key
4//! the original password opened it with, so the saved file opens with that
5//! same password. Objects arrive here plaintext — the parser peeled the
6//! cipher off at fetch time — and this is where it goes back on.
7//!
8//! [`SaveOptions::remove_security`](crate::SaveOptions::remove_security)
9//! remains the explicit way to drop it: it suppresses `/Encrypt` from the
10//! trailer and passes `None` here, so nothing is enciphered.
11//!
12//! # Three things are never enciphered
13//!
14//! - **The `/Encrypt` dictionary itself.** ISO 32000-1 §7.6.1: a reader has
15//!   to read `/O`, `/U` and `/Perms` before it has a key. The C++ tests for it
16//!   by pointer identity against the object it is writing; we test by object
17//!   number, which is the same test once the dictionary has one.
18//! - **A signature's `/Contents`.** It covers a byte range of the finished
19//!   file, so re-enciphering it would invalidate the signature.
20//! - **An XMP metadata stream's payload.** ISO 32000-1 §14.3.2 requires the
21//!   packet be readable by a consumer holding neither the file key nor a
22//!   flate decoder. Note this is unconditional in the C++ writer — it never
23//!   consults `/EncryptMetadata`, so a document declaring `true` still gets a
24//!   plaintext metadata stream on save. The stream's *dictionary* is still
25//!   enciphered; only the payload is exempt.
26//!
27//! The trailer is a fourth, but it never reaches this type: it is not an
28//! indirect object, and `write_classic` passes `None` outright.
29//!
30//! # Where the initialisation vectors come from
31//!
32//! AES-CBC needs a vector per payload, and under AESV3 (ISO 32000-2 §7.6.5.3)
33//! the file key enciphers every object verbatim, with no per-object
34//! derivation. The vector is therefore the only thing separating two
35//! ciphertexts under one key, and it has to satisfy both of CBC's
36//! requirements:
37//!
38//! - **Unique within a save.** Two payloads sharing a key and a vector leak
39//!   the XOR of their first blocks to anyone holding the ciphertext, key or
40//!   no key. [`IvSource`] mixes a per-save secret with the object number and
41//!   a counter, so each payload gets its own vector however the writer
42//!   enumerates objects.
43//! - **Unpredictable.** The per-save secret is 32 bytes drawn from the
44//!   operating system, so a vector cannot be recomputed from anything the
45//!   file itself exposes — its length, its head, its tail, or a sibling file
46//!   built from the same template.
47//!
48//! An encrypted save is therefore not byte-reproducible: reproducible
49//! ciphertext is reproducible secrets. [`crate::IdSource::Fixed`] pins
50//! everything a save writes in the clear, which is what reproducibility is
51//! for.
52
53use std::cell::Cell;
54
55use pdfrum_crypt::{CryptClass, Iv, SecurityHandler};
56use pdfrum_object::ObjRef;
57use sha2::{Digest, Sha256};
58use zeroize::Zeroize;
59
60use crate::Error;
61
62/// Where a save's AES initialisation vectors come from.
63///
64/// Deliberately neither `Copy` nor `Clone`: it holds a counter, and a copy
65/// would hand two call sites the same vector sequence. Its `Debug` shows the
66/// counter and withholds the secret, which a log has no use for.
67pub struct IvSource {
68    /// Thirty-two bytes from the operating system, drawn once per save. The
69    /// vectors are unpredictable exactly because this is.
70    secret: [u8; 32],
71    /// Advanced once per vector handed out.
72    counter: Cell<u64>,
73}
74
75impl std::fmt::Debug for IvSource {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("IvSource")
78            .field("counter", &self.counter.get())
79            .finish_non_exhaustive()
80    }
81}
82
83impl Drop for IvSource {
84    fn drop(&mut self) {
85        self.secret.zeroize();
86    }
87}
88
89impl IvSource {
90    /// A source keyed by fresh operating-system randomness.
91    ///
92    /// # Errors
93    ///
94    /// [`Error::NoEntropy`] when the platform's generator is unavailable.
95    pub fn from_os() -> Result<Self, Error> {
96        let mut secret = [0u8; 32];
97        getrandom::fill(&mut secret).map_err(|_| Error::NoEntropy)?;
98        Ok(Self {
99            secret,
100            counter: Cell::new(0),
101        })
102    }
103
104    /// The next vector, advancing the counter.
105    ///
106    /// `obj` goes into the mix as well as the counter, so two saves that
107    /// enumerate objects in different orders still give each object its own
108    /// vector rather than one that depends on its position.
109    fn next(&self, obj: ObjRef) -> Iv {
110        let index = self.counter.get();
111        self.counter.set(index.wrapping_add(1));
112
113        // SHA-256 over the secret and the pair that makes this payload
114        // unique. Absorbing before emitting is what carries every input into
115        // every output byte; the vector is the leading half of the digest.
116        let mut hash = Sha256::new();
117        hash.update(self.secret);
118        hash.update(index.to_le_bytes());
119        hash.update(obj.num.to_le_bytes());
120        hash.update(obj.generation.to_le_bytes());
121        let digest = hash.finalize();
122        let mut out = [0u8; 16];
123        for (slot, byte) in out.iter_mut().zip(digest) {
124            *slot = byte;
125        }
126        Iv(out)
127    }
128}
129
130/// Enciphers the strings and stream payloads of one indirect object.
131///
132/// Per ISO 32000-1 §7.6.2 the key is derived per object from its number and
133/// generation; the generation is always 0 here because that is what the
134/// writer emits, which is also what the C++ passes.
135///
136/// Borrowed rather than owned so one handler and one vector source serve a
137/// whole save, with only the object number changing per object.
138#[derive(Debug, Clone, Copy)]
139pub struct Encryptor<'a> {
140    handler: &'a SecurityHandler,
141    ivs: &'a IvSource,
142    object_number: u32,
143}
144
145impl<'a> Encryptor<'a> {
146    /// An encryptor for the object numbered `num`.
147    #[must_use]
148    pub const fn new(handler: &'a SecurityHandler, ivs: &'a IvSource, object_number: u32) -> Self {
149        Self {
150            handler,
151            ivs,
152            object_number,
153        }
154    }
155
156    /// Whether this document's metadata stream is enciphered along with
157    /// everything else (`/EncryptMetadata`, default true).
158    ///
159    /// Read by the stream encoder, which owns the exemption; see its docs for
160    /// why we consult the flag where the C++ writer does not.
161    #[must_use]
162    pub fn encrypts_metadata(&self) -> bool {
163        self.handler.encrypt_metadata()
164    }
165
166    /// Encipher one run of bytes as a string or a stream payload.
167    ///
168    /// The class never changes the result — PDFium refuses a document whose
169    /// `/StmF` and `/StrF` differ — but naming it keeps the call site honest
170    /// about what it is writing.
171    #[must_use]
172    pub fn encrypt(&self, class: CryptClass, data: &[u8]) -> Vec<u8> {
173        let obj = ObjRef::new(self.object_number, 0);
174        self.handler.encrypt(obj, class, self.ivs.next(obj), data)
175    }
176}
177
178/// Everything a save needs to re-encipher what it writes.
179///
180/// Held by [`crate::save`] for the length of one save and handed to each
181/// object in turn. `None` for an unencrypted document and for a
182/// `remove_security` save, which is what makes "pass `None` and nothing is
183/// enciphered" the whole of the plaintext path.
184#[derive(Debug)]
185pub(crate) struct Security<'a> {
186    /// The handler the original password opened the document with.
187    pub(crate) handler: &'a SecurityHandler,
188    /// The vector source for this save.
189    pub(crate) ivs: IvSource,
190    /// The object number the `/Encrypt` dictionary will be written as — the
191    /// one object that is never enciphered.
192    pub(crate) encrypt_object: Option<u32>,
193}
194
195impl Security<'_> {
196    /// The encryptor for object `num`, or `None` when that object is the
197    /// `/Encrypt` dictionary.
198    #[must_use]
199    pub(crate) fn for_object(&self, num: u32) -> Option<Encryptor<'_>> {
200        if self.encrypt_object == Some(num) {
201            return None;
202        }
203        Some(Encryptor::new(self.handler, &self.ivs, num))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::{Encryptor, IvSource, Security};
210    use pdfrum_crypt::{CryptClass, SecurityHandler};
211    use pdfrum_object::ObjRef;
212
213    fn handler() -> SecurityHandler {
214        SecurityHandler::AesV5 {
215            key: Box::new([0x42; 32]),
216            revision: 6,
217            permissions: 0xFFFF_FFFC,
218            owner_unlocked: false,
219            encrypt_metadata: true,
220            encoding: pdfrum_crypt::PasswordEncoding::AsGiven,
221            // No /EFF, so the embedded class takes the stream cipher —
222            // ISO 32000-1 §7.6.5 table 20's own default.
223            embedded_cipher: None,
224            strings_identity: false,
225        }
226    }
227
228    // The property CBC actually needs: no two payloads in one save share a
229    // vector, whether they belong to one object or to many.
230    #[test]
231    fn vectors_never_repeat_within_one_save() {
232        let ivs = IvSource::from_os().unwrap();
233        let mut seen = std::collections::BTreeSet::new();
234        for num in 1..40u32 {
235            for _ in 0..4 {
236                assert!(seen.insert(ivs.next(ObjRef::new(num, 0)).0), "{num}");
237            }
238        }
239        assert_eq!(seen.len(), 39 * 4);
240    }
241
242    // Unpredictability: two sources drawn from the OS share nothing, so a
243    // vector cannot be recovered from another save of the same document.
244    #[test]
245    fn two_sources_never_agree() {
246        let a = IvSource::from_os().unwrap();
247        let b = IvSource::from_os().unwrap();
248        for num in 1..8u32 {
249            assert_ne!(a.next(ObjRef::new(num, 0)), b.next(ObjRef::new(num, 0)));
250        }
251    }
252
253    // The round trip through the encryptor's own object numbering.
254    #[test]
255    fn an_encryptor_round_trips_under_its_object_number() {
256        let h = handler();
257        let ivs = IvSource::from_os().unwrap();
258        let enc = Encryptor::new(&h, &ivs, 12);
259        let payload = b"the quick brown fox".to_vec();
260        let sealed = enc.encrypt(CryptClass::String, &payload);
261        assert_ne!(sealed, payload);
262        assert_eq!(
263            h.decrypt(ObjRef::new(12, 0), CryptClass::String, &sealed),
264            payload
265        );
266    }
267
268    // The encrypt dictionary is the one object that gets no encryptor.
269    #[test]
270    fn the_encrypt_dictionary_is_never_given_an_encryptor() {
271        let h = handler();
272        let security = Security {
273            handler: &h,
274            ivs: IvSource::from_os().unwrap(),
275            encrypt_object: Some(9),
276        };
277        assert!(security.for_object(9).is_none());
278        assert!(security.for_object(8).is_some());
279        assert!(security.for_object(10).is_some());
280
281        // With no encrypt object named, every object gets one.
282        let security = Security {
283            handler: &h,
284            ivs: IvSource::from_os().unwrap(),
285            encrypt_object: None,
286        };
287        assert!(security.for_object(9).is_some());
288    }
289
290    // An unencrypted document's seam is the identity, which is what makes
291    // "pass `None`" and "pass an Identity handler" agree.
292    #[test]
293    fn the_identity_handler_writes_what_it_was_given() {
294        let h = SecurityHandler::Identity;
295        let ivs = IvSource::from_os().unwrap();
296        let enc = Encryptor::new(&h, &ivs, 4);
297        let payload = vec![0xABu8; 33];
298        assert_eq!(enc.encrypt(CryptClass::Stream, &payload), payload);
299    }
300}