Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.72% covered (success)
83.72%
36 / 43
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ObjectCipher
83.72% covered (success)
83.72%
36 / 43
20.00% covered (danger)
20.00%
1 / 5
15.97
0.00% covered (danger)
0.00%
0 / 1
 encryptAes256
71.43% covered (success)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 encryptAes128
75.00% covered (success)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 decryptAes256
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 decryptAes128
81.82% covered (success)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 deriveObjectKey
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Build\Security;
16
17/**
18 * Per-object/per-string AES-CBC encryption for PDF's Standard Security
19 * Handler. A random 16-byte IV is prepended to every encrypted buffer -
20 * that IV is what a decrypting reader (Plan 2) reads back off the front of
21 * the ciphertext.
22 *
23 * @category   Pop
24 * @package    Pop\Pdf
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.2.0
29 */
30class ObjectCipher
31{
32    /**
33     * Encrypt a buffer for revision 6 (AES-256). The file key is used
34     * directly - ISO 32000-2's Algorithm 1.A - unlike revision 4, there is
35     * no separate per-object key derivation step.
36     *
37     * @param  string $fileKey 32 raw bytes
38     * @param  string $data
39     * @return string
40     */
41    public static function encryptAes256(string $fileKey, string $data): string
42    {
43        if (strlen($fileKey) !== 32) {
44            throw new Exception('File key for AES-256 must be exactly 32 bytes');
45        }
46        $iv = random_bytes(16);
47        $ciphertext = openssl_encrypt($data, 'aes-256-cbc', $fileKey, OPENSSL_RAW_DATA, $iv);
48        if ($ciphertext === false) {
49            throw new Exception('AES-256 encryption failed');
50        }
51        return $iv . $ciphertext;
52    }
53
54    /**
55     * Encrypt a buffer for revision 4 (AES-128). ISO 32000-1 Algorithm 1:
56     * the per-object key is derived from the file key mixed with the
57     * object's number/generation (low-order bytes, little-endian) plus a
58     * fixed "sAlT" suffix that signals AES rather than RC4 content
59     * encryption, truncated to min(fileKeyLength + 5, 16) bytes.
60     *
61     * @param  string $fileKey 16 raw bytes
62     * @param  int    $objectNumber
63     * @param  int    $generation
64     * @param  string $data
65     * @return string
66     */
67    public static function encryptAes128(string $fileKey, int $objectNumber, int $generation, string $data): string
68    {
69        if (strlen($fileKey) !== 16) {
70            throw new Exception('File key for AES-128 must be exactly 16 bytes');
71        }
72        $objectKey = self::deriveObjectKey($fileKey, $objectNumber, $generation);
73        $iv        = random_bytes(16);
74        $ciphertext = openssl_encrypt($data, 'aes-128-cbc', $objectKey, OPENSSL_RAW_DATA, $iv);
75        if ($ciphertext === false) {
76            throw new Exception('AES-128 encryption failed');
77        }
78        return $iv . $ciphertext;
79    }
80
81    /**
82     * Decrypt a buffer for revision 6 (AES-256). Inverse of encryptAes256().
83     *
84     * @param  string $fileKey 32 raw bytes
85     * @param  string $data IV-prefixed ciphertext, as produced by encryptAes256()
86     * @throws Exception
87     * @return string
88     */
89    public static function decryptAes256(string $fileKey, string $data): string
90    {
91        if (strlen($fileKey) !== 32) {
92            throw new Exception('File key for AES-256 must be exactly 32 bytes');
93        }
94        if (strlen($data) < 16) {
95            throw new Exception('Encrypted data is too short to contain an IV');
96        }
97
98        $iv         = substr($data, 0, 16);
99        $ciphertext = substr($data, 16);
100        $plaintext  = openssl_decrypt($ciphertext, 'aes-256-cbc', $fileKey, OPENSSL_RAW_DATA, $iv);
101
102        if ($plaintext === false) {
103            throw new Exception('AES-256 decryption failed');
104        }
105
106        return $plaintext;
107    }
108
109    /**
110     * Decrypt a buffer for revision 4 (AES-128). Inverse of encryptAes128().
111     *
112     * @param  string $fileKey 16 raw bytes
113     * @param  int    $objectNumber
114     * @param  int    $generation
115     * @param  string $data IV-prefixed ciphertext, as produced by encryptAes128()
116     * @throws Exception
117     * @return string
118     */
119    public static function decryptAes128(string $fileKey, int $objectNumber, int $generation, string $data): string
120    {
121        if (strlen($fileKey) !== 16) {
122            throw new Exception('File key for AES-128 must be exactly 16 bytes');
123        }
124        if (strlen($data) < 16) {
125            throw new Exception('Encrypted data is too short to contain an IV');
126        }
127
128        $objectKey  = self::deriveObjectKey($fileKey, $objectNumber, $generation);
129        $iv         = substr($data, 0, 16);
130        $ciphertext = substr($data, 16);
131        $plaintext  = openssl_decrypt($ciphertext, 'aes-128-cbc', $objectKey, OPENSSL_RAW_DATA, $iv);
132
133        if ($plaintext === false) {
134            throw new Exception('AES-128 decryption failed');
135        }
136
137        return $plaintext;
138    }
139
140    /**
141     * Derive the AES-128 per-object key from the file key and the object's number/generation
142     *
143     * @param  string $fileKey
144     * @param  int    $objectNumber
145     * @param  int    $generation
146     * @return string
147     */
148    protected static function deriveObjectKey(string $fileKey, int $objectNumber, int $generation): string
149    {
150        $input  = $fileKey;
151        $input .= chr($objectNumber & 0xFF) . chr(($objectNumber >> 8) & 0xFF) . chr(($objectNumber >> 16) & 0xFF);
152        $input .= chr($generation & 0xFF) . chr(($generation >> 8) & 0xFF);
153        $input .= "\x73\x41\x6C\x54"; // "sAlT" - AES content marker, ISO 32000-1 Algorithm 1 step (c)
154
155        $digest    = md5($input, true);
156        $keyLength = min(strlen($fileKey) + 5, 16);
157
158        return substr($digest, 0, $keyLength);
159    }
160}