Skip to main content

pdfrum_type1/
eexec.rs

1//! The two Type 1 stream ciphers (Type 1 specification ยง7).
2//!
3//! Both are the same three-line feedback cipher differing only in their seed:
4//! `eexec` (seed 55665) wraps the whole private dictionary, and each individual
5//! charstring is separately encrypted with seed 4330. Neither offers any
6//! security โ€” the seeds are published constants โ€” so this module is a codec,
7//! not cryptography, and lives here rather than in `pdfrum-crypt`.
8//!
9//! Both forms discard a number of leading plaintext bytes that were random
10//! padding at encryption time: 4 for `eexec`, and `lenIV` (default 4, but the
11//! Private dictionary may set anything including 0) for charstrings.
12
13/// Seed for the `eexec` envelope around the private dictionary.
14pub const EEXEC_SEED: u16 = 55665;
15/// Seed for an individual charstring or subroutine.
16pub const CHARSTRING_SEED: u16 = 4330;
17/// Plaintext bytes `eexec` discards; fixed by the specification.
18pub const EEXEC_SKIP: usize = 4;
19/// Default `lenIV` when the Private dictionary does not say.
20pub const DEFAULT_LEN_IV: i32 = 4;
21
22const MULT: u16 = 52845;
23const ADD: u16 = 22719;
24
25/// Decrypt `cipher` with `seed`, dropping the first `skip` plaintext bytes.
26///
27/// A `skip` larger than the plaintext yields an empty result rather than
28/// panicking โ€” a `lenIV` of 16 against a 3-byte charstring is a real thing
29/// broken fonts do.
30///
31/// ```
32/// use pdfrum_type1::{decrypt, CHARSTRING_SEED};
33///
34/// // Round-trip: encrypting then decrypting with the same seed is identity.
35/// let plain = b"\0\0\0\0hsbw-ish payload";
36/// let cipher = pdfrum_type1::encrypt(plain, CHARSTRING_SEED);
37/// assert_eq!(decrypt(&cipher, CHARSTRING_SEED, 4), b"hsbw-ish payload");
38/// ```
39#[must_use]
40pub fn decrypt(cipher: &[u8], seed: u16, skip: usize) -> Vec<u8> {
41    let mut r = seed;
42    let mut out = Vec::with_capacity(cipher.len().saturating_sub(skip));
43    for (i, &c) in cipher.iter().enumerate() {
44        let plain = c ^ (r >> 8) as u8;
45        r = (u16::from(c).wrapping_add(r))
46            .wrapping_mul(MULT)
47            .wrapping_add(ADD);
48        if i >= skip {
49            out.push(plain);
50        }
51    }
52    out
53}
54
55/// The inverse of [`decrypt`], without the skip: `plain` is emitted as-is
56/// through the cipher.
57///
58/// Exists so tests can build encrypted fixtures from readable charstrings
59/// instead of hand-assembled byte tables. It is also the operation an encoder
60/// would need, which is why it is public rather than `#[cfg(test)]`.
61#[must_use]
62pub fn encrypt(plain: &[u8], seed: u16) -> Vec<u8> {
63    let mut r = seed;
64    let mut out = Vec::with_capacity(plain.len());
65    for &p in plain {
66        let c = p ^ (r >> 8) as u8;
67        out.push(c);
68        r = (u16::from(c).wrapping_add(r))
69            .wrapping_mul(MULT)
70            .wrapping_add(ADD);
71    }
72    out
73}
74
75/// Turn a Private-dictionary `lenIV` into a byte count.
76///
77/// The value is signed in the source and fonts do write negatives; a negative
78/// or absurd `lenIV` means "discard nothing", which is FreeType's reading and
79/// the only one that keeps such a font renderable.
80#[must_use]
81pub fn len_iv_skip(len_iv: i32) -> usize {
82    usize::try_from(len_iv).unwrap_or(0)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::{CHARSTRING_SEED, DEFAULT_LEN_IV, EEXEC_SEED, EEXEC_SKIP, decrypt, encrypt};
88
89    #[test]
90    fn known_eexec_vector() {
91        // Encrypting 32 zero bytes with the eexec seed is a fully determined
92        // sequence; recomputing it here from the published recurrence pins the
93        // constants against a typo in either direction.
94        let cipher = encrypt(&[0u8; 32], EEXEC_SEED);
95        let mut r: u16 = EEXEC_SEED;
96        let expected: Vec<u8> = (0..32)
97            .map(|_| {
98                let c = (r >> 8) as u8; // plaintext byte is 0
99                r = (u16::from(c).wrapping_add(r))
100                    .wrapping_mul(52845)
101                    .wrapping_add(22719);
102                c
103            })
104            .collect();
105        assert_eq!(cipher, expected);
106        assert_eq!(cipher.len(), 32);
107        // And the first four bytes are the padding eexec throws away.
108        assert_eq!(decrypt(&cipher, EEXEC_SEED, EEXEC_SKIP), vec![0u8; 28]);
109    }
110
111    #[test]
112    fn len_iv_of_zero_four_and_eight() {
113        let plain: Vec<u8> = (0u8..24).collect();
114        let cipher = encrypt(&plain, CHARSTRING_SEED);
115        for skip in [0usize, DEFAULT_LEN_IV as usize, 8] {
116            assert_eq!(
117                decrypt(&cipher, CHARSTRING_SEED, skip),
118                plain.get(skip..).unwrap_or_default(),
119                "lenIV {skip}"
120            );
121        }
122    }
123
124    #[test]
125    fn skip_past_the_end_is_empty_not_a_panic() {
126        let cipher = encrypt(b"abc", CHARSTRING_SEED);
127        assert!(decrypt(&cipher, CHARSTRING_SEED, 99).is_empty());
128    }
129
130    #[test]
131    fn negative_len_iv_discards_nothing() {
132        assert_eq!(super::len_iv_skip(-1), 0);
133        assert_eq!(super::len_iv_skip(4), 4);
134    }
135}