Skip to main content

pdfrum_crypt/
create.rs

1//! Making a document encrypted: the `/Encrypt` dictionary and the handler
2//! for a fresh AES-256 (revision 6) file.
3//!
4//! ISO 32000-2 §7.6.4.4.7–8, algorithms 8, 9 and 10 — the inverse of what
5//! [`crate::standard`] checks when such a file is opened. Only revision 6:
6//! RC4 and the 128-bit AES of revisions 2–4 are deprecated by the same
7//! standard, and a file this crate writes should be one its reader would
8//! choose. Opening covers every revision regardless.
9//!
10//! The file key and the four salts are 64 bytes of operating-system
11//! randomness, held by [`KeyMaterial`]. That type is the only way to reach
12//! [`standard_r6`], and its only constructor draws from the OS, so no derived
13//! or reproducible byte sequence can stand in for a key.
14
15use pdfrum_object::{Dict, Name, NoResolve, Object, PdfString};
16
17use zeroize::Zeroize;
18
19use crate::Error;
20use crate::SecurityHandler;
21use crate::permissions::Permissions;
22use crate::primitives::aes_cbc_encrypt;
23use crate::standard::{r6_prepared, revision6_hash};
24
25/// The bytes of a new file's secrets: the 32-byte file key, then the
26/// user validation salt, user key salt, owner validation salt and owner key
27/// salt, 8 bytes each, then four random bytes for `/Perms` (ISO 32000-2
28/// Algorithm 10).
29pub const ENTROPY_LEN: usize = 68;
30
31/// The secret bytes behind one encrypted file: the AES-256 file key, the
32/// four revision-6 salts (ISO 32000-2 §7.6.4.4.7, algorithms 8 and 9), and
33/// four random bytes for `/Perms` (Algorithm 10).
34///
35/// The bytes come from the operating system's cryptographic generator and
36/// from nowhere else. There is no constructor taking a seed, a slice or a
37/// byte array, so a caller cannot substitute a derived sequence: an
38/// unguessable file key is a property of the type, not of the call site.
39/// Neither `Clone` nor `Debug`, so the bytes are neither duplicated across
40/// two files nor printed. `Drop` wipes them.
41pub struct KeyMaterial([u8; ENTROPY_LEN]);
42
43impl KeyMaterial {
44    /// Sixty-eight fresh bytes from the operating system.
45    ///
46    /// # Errors
47    ///
48    /// [`Error::NoEntropy`] when the platform's generator is unavailable —
49    /// the only outcome besides success, since a partial read is not one the
50    /// underlying interface reports.
51    pub fn from_os() -> Result<Self, Error> {
52        let mut bytes = [0u8; ENTROPY_LEN];
53        getrandom::fill(&mut bytes).map_err(|_| Error::NoEntropy)?;
54        Ok(Self(bytes))
55    }
56}
57
58impl Drop for KeyMaterial {
59    fn drop(&mut self) {
60        self.0.zeroize();
61    }
62}
63
64/// The `/P` bits this crate names; every other bit is reserved and written
65/// as 1, which is what ISO 32000-2 Table 22 asks for.
66const NAMED_PERMISSION_BITS: u32 = 0x0F3C;
67
68/// The `/Encrypt` dictionary and the handler that enciphers under it, for a
69/// document protected by `user` (opens with reading rights) and `owner`
70/// (opens with every right). An empty user password means anyone can open
71/// the file; an empty owner password is replaced by the user password, as
72/// Acrobat does, so there is always a way to unlock it.
73///
74/// # Errors
75///
76/// [`Error::WrongPassword`] only in the impossible case that the handler
77/// built from the dictionary does not accept the password it was built for
78/// — which would be a defect in this function, not in the caller's input;
79/// a password that is not valid UTF-8 or does not survive `SASLprep`.
80pub fn standard_r6(
81    user: &[u8],
82    owner: &[u8],
83    permissions: Permissions,
84    encrypt_metadata: bool,
85    key_material: &KeyMaterial,
86) -> Result<(Dict, SecurityHandler), Error> {
87    let entropy = &key_material.0;
88    let owner = if owner.is_empty() { user } else { owner };
89    let user_prepared = r6_prepared(6, user).ok_or(Error::WrongPassword)?;
90    let owner_prepared = r6_prepared(6, owner).ok_or(Error::WrongPassword)?;
91
92    let mut file_key: [u8; 32] = slice(entropy, 0)?;
93    let user_validation: [u8; 8] = slice(entropy, 32)?;
94    let user_key_salt: [u8; 8] = slice(entropy, 40)?;
95    let owner_validation: [u8; 8] = slice(entropy, 48)?;
96    let owner_key_salt: [u8; 8] = slice(entropy, 56)?;
97    let perms_random: [u8; 4] = slice(entropy, 64)?;
98
99    // Algorithm 8: /U is the hash of the user password and its validation
100    // salt, then the two salts; /UE is the file key enciphered under the hash
101    // of the password and its key salt.
102    let mut u = [0u8; 48];
103    u[..32].copy_from_slice(&revision6_hash(&user_prepared, user_validation, None));
104    u[32..40].copy_from_slice(&user_validation);
105    u[40..48].copy_from_slice(&user_key_salt);
106    let ue = wrap(
107        &revision6_hash(&user_prepared, user_key_salt, None),
108        &file_key,
109    )?;
110
111    // Algorithm 9: the same for the owner, with /U folded into both hashes.
112    let mut o = [0u8; 48];
113    o[..32].copy_from_slice(&revision6_hash(&owner_prepared, owner_validation, Some(&u)));
114    o[32..40].copy_from_slice(&owner_validation);
115    o[40..48].copy_from_slice(&owner_key_salt);
116    let oe = wrap(
117        &revision6_hash(&owner_prepared, owner_key_salt, Some(&u)),
118        &file_key,
119    )?;
120
121    // Algorithm 10: /Perms is the permissions word, four 0xFF bytes, T or F
122    // for metadata, "adb", and four random bytes, under the file key.
123    // The four bytes are their own slice of entropy, not a prefix of the
124    // file key: ISO 32000-2 asks for random bytes, and putting key material
125    // in the plaintext (then encrypting it under that key with a zero IV)
126    // would hand four key bytes to anyone who recovered the block.
127    let p = permissions.bits() | !NAMED_PERMISSION_BITS;
128    let mut perms = [0u8; 16];
129    perms[..4].copy_from_slice(&p.to_le_bytes());
130    perms[4..8].copy_from_slice(&[0xFF; 4]);
131    perms[8] = if encrypt_metadata { b'T' } else { b'F' };
132    perms[9..12].copy_from_slice(b"adb");
133    perms[12..16].copy_from_slice(&perms_random);
134    aes_cbc_encrypt(&file_key, &[0u8; 16], &mut perms).map_err(|_| Error::WrongPassword)?;
135    file_key.zeroize();
136
137    let name = |s: &str| Object::Name(Name::from(s));
138    let bytes = |b: &[u8]| Object::Str(PdfString::hex(b));
139    let std_cf = Dict::from_pairs([
140        (Name::from("CFM"), name("AESV3")),
141        (Name::from("AuthEvent"), name("DocOpen")),
142        (Name::from("Length"), Object::Int(32)),
143    ]);
144    let cf = Dict::from_pairs([(Name::from("StdCF"), Object::Dict(std_cf))]);
145    #[expect(
146        clippy::cast_possible_wrap,
147        reason = "/P is the same 32 bits read as a signed integer, per the standard"
148    )]
149    let p_signed = i64::from(p as i32);
150    let dict = Dict::from_pairs([
151        (Name::from("Filter"), name("Standard")),
152        (Name::from("V"), Object::Int(5)),
153        (Name::from("R"), Object::Int(6)),
154        (Name::from("Length"), Object::Int(256)),
155        (Name::from("P"), Object::Int(p_signed)),
156        (Name::from("O"), bytes(&o)),
157        (Name::from("U"), bytes(&u)),
158        (Name::from("OE"), bytes(&oe)),
159        (Name::from("UE"), bytes(&ue)),
160        (Name::from("Perms"), bytes(&perms)),
161        (Name::from("CF"), Object::Dict(cf)),
162        (Name::from("StmF"), name("StdCF")),
163        (Name::from("StrF"), name("StdCF")),
164        (
165            Name::from("EncryptMetadata"),
166            Object::Bool(encrypt_metadata),
167        ),
168    ]);
169
170    // The handler is built the way an opened file's is, from the dictionary
171    // and the owner password, so what this function wrote is what the
172    // reader will check.
173    let handler = SecurityHandler::from_encrypt_dict(&dict, &[], owner, &NoResolve)?;
174    Ok((dict, handler))
175}
176
177/// `key` enciphered under `intermediate` with AES-256, no IV, no padding —
178/// the /UE and /OE wrapping.
179fn wrap(intermediate: &[u8; 32], key: &[u8; 32]) -> Result<[u8; 32], Error> {
180    let mut wrapped = *key;
181    aes_cbc_encrypt(intermediate, &[0u8; 16], &mut wrapped).map_err(|_| Error::WrongPassword)?;
182    Ok(wrapped)
183}
184
185fn slice<const N: usize>(entropy: &[u8; ENTROPY_LEN], at: usize) -> Result<[u8; N], Error> {
186    entropy
187        .get(at..at + N)
188        .and_then(|s| <[u8; N]>::try_from(s).ok())
189        .ok_or(Error::WrongPassword)
190}
191
192#[cfg(test)]
193mod tests {
194    use super::{KeyMaterial, standard_r6};
195    use crate::permissions::Permissions;
196    use crate::{CryptClass, SecurityHandler};
197    use pdfrum_object::{NoResolve, ObjRef};
198
199    fn entropy() -> KeyMaterial {
200        KeyMaterial::from_os().unwrap()
201    }
202
203    #[test]
204    fn the_file_opens_with_either_password_and_not_with_a_wrong_one() {
205        let perms = Permissions {
206            print: true,
207            ..Permissions::NONE
208        };
209        let (dict, handler) = standard_r6(b"user", b"owner", perms, true, &entropy()).unwrap();
210        let as_user = SecurityHandler::from_encrypt_dict(&dict, &[], b"user", &NoResolve).unwrap();
211        assert!(!as_user.owner_unlocked());
212        assert!(as_user.permissions().print && !as_user.permissions().copy);
213        let as_owner =
214            SecurityHandler::from_encrypt_dict(&dict, &[], b"owner", &NoResolve).unwrap();
215        assert!(as_owner.owner_unlocked());
216        assert!(SecurityHandler::from_encrypt_dict(&dict, &[], b"nope", &NoResolve).is_err());
217        assert!(
218            handler.owner_unlocked(),
219            "the handler this function hands back is the owner's"
220        );
221    }
222
223    #[test]
224    fn what_the_handler_enciphers_the_opened_one_deciphers() {
225        let (dict, handler) =
226            standard_r6(b"", b"secret", Permissions::ALL, true, &entropy()).unwrap();
227        let obj = ObjRef::new(7, 0);
228        let iv = crate::Iv([3u8; 16]);
229        let enciphered = handler.encrypt(obj, CryptClass::Stream, iv, b"hello, cipher");
230        assert_ne!(enciphered, b"hello, cipher");
231        let reader = SecurityHandler::from_encrypt_dict(&dict, &[], b"", &NoResolve).unwrap();
232        assert_eq!(
233            reader.decrypt(obj, CryptClass::Stream, &enciphered),
234            b"hello, cipher"
235        );
236    }
237
238    #[test]
239    fn an_empty_owner_password_falls_back_to_the_user_password() {
240        let (dict, _) = standard_r6(b"pw", b"", Permissions::ALL, false, &entropy()).unwrap();
241        let opened = SecurityHandler::from_encrypt_dict(&dict, &[], b"pw", &NoResolve).unwrap();
242        assert!(opened.owner_unlocked());
243        assert!(!opened.encrypt_metadata());
244    }
245
246    #[test]
247    fn perms_random_bytes_are_not_the_file_key_prefix() {
248        let mut bytes = [0u8; super::ENTROPY_LEN];
249        bytes[..32].fill(0xAA);
250        bytes[32..64].fill(0x11);
251        bytes[64..68].fill(0xBB);
252        let material = super::KeyMaterial(bytes);
253        let (dict, _) = standard_r6(b"user", b"owner", Permissions::ALL, true, &material).unwrap();
254        let perms = dict
255            .string(&pdfrum_object::Name::from("Perms"))
256            .expect("standard_r6 writes /Perms");
257        let mut block = [0u8; 16];
258        block.copy_from_slice(perms.bytes.as_ref());
259        crate::primitives::aes_cbc_decrypt(&[0xAA; 32], &[0u8; 16], &mut block)
260            .expect("the file key decrypts /Perms");
261        assert_eq!(&block[12..16], &[0xBB; 4], "Algorithm 10's four bytes");
262        assert_ne!(
263            &block[12..16],
264            &bytes[..4],
265            "must not reuse the file-key prefix"
266        );
267    }
268}