Completed
Push — master ( c1db7b...040cf4 )
by Ben
02:21
created

AbstractEncryption::forPdfVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 17
rs 9.4286
cc 3
eloc 12
nc 3
nop 3
1
<?php
2
/**
3
 * BaconPdf
4
 *
5
 * @link      http://github.com/Bacon/BaconPdf For the canonical source repository
6
 * @copyright 2015 Ben Scholzen (DASPRiD)
7
 * @license   http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
8
 */
9
10
namespace Bacon\Pdf\Encryption;
11
12
use Bacon\Pdf\Exception\RuntimeException;
13
use Bacon\Pdf\Exception\UnexpectedValueException;
14
use Bacon\Pdf\Exception\UnsupportedPasswordException;
15
use Bacon\Pdf\Options\EncryptionOptions;
16
use Bacon\Pdf\Writer\ObjectWriter;
17
18
abstract class AbstractEncryption implements EncryptionInterface
19
{
20
    // @codingStandardsIgnoreStart
21
    const ENCRYPTION_PADDING = "\x28\xbf\x4e\x5e\x4e\x75\x8a\x41\x64\x00\x4e\x56\xff\xfa\x01\x08\x2e\x2e\x00\xb6\xd0\x68\x3e\x80\x2f\x0c\xa9\xfe\x64\x53\x69\x7a";
22
    // @codingStandardsIgnoreEnd
23
24
    /**
25
     * @var string
26
     */
27
    private $encryptionKey;
28
29
    /**
30
     * @var string
31
     */
32
    private $userEntry;
33
34
    /**
35
     * @var string
36
     */
37
    private $ownerEntry;
38
39
    /**
40
     * @var Permissions
41
     */
42
    private $userPermissions;
43
44
    /**
45
     * @param  string      $permanentFileIdentifier
46
     * @param  string      $userPassword
47
     * @param  string      $ownerPassword
48
     * @param  Permissions $userPermissions
49
     * @throws UnexpectedValueException
50
     */
51
    public function __construct(
52
        $permanentFileIdentifier,
53
        $userPassword,
54
        $ownerPassword,
55
        Permissions $userPermissions
56
    ) {
57
        // @codeCoverageIgnoreStart
58
        if (!extension_loaded('openssl')) {
59
            throw new RuntimeException('The OpenSSL extension is required for encryption');
60
        }
61
        // @codeCoverageIgnoreEnd
62
63
        $encodedUserPassword  = $this->encodePassword($userPassword);
64
        $encodedOwnerPassword = $this->encodePassword($ownerPassword);
65
66
        $revision  = $this->getRevision();
67
        $keyLength = $this->getKeyLength() / 8;
68
69
        if (!in_array($keyLength, [40 / 8, 128 / 8])) {
70
            throw new UnexpectedValueException('Key length must be either 40 or 128');
71
        }
72
73
        $this->ownerEntry = $this->computeOwnerEntry(
74
            $encodedOwnerPassword,
75
            $encodedUserPassword,
76
            $revision,
77
            $keyLength
78
        );
79
80
        if (2 === $revision) {
81
            list($this->userEntry, $this->encryptionKey) = $this->computeUserEntryRev2(
82
                $encodedUserPassword,
83
                $this->ownerEntry,
84
                $revision,
85
                $permanentFileIdentifier
86
            );
87
        } else {
88
            list($this->userEntry, $this->encryptionKey) = $this->computeUserEntryRev3OrGreater(
89
                $encodedUserPassword,
90
                $revision,
91
                $keyLength,
92
                $this->ownerEntry,
93
                $userPermissions->toInt($revision),
94
                $permanentFileIdentifier
95
            );
96
        }
97
98
        $this->userPermissions = $userPermissions;
99
    }
100
101
    /**
102
     * Returns an encryption fitting for a specific PDF version.
103
     *
104
     * @param  string            $pdfVersion
105
     * @param  string            $permanentFileIdentifier
106
     * @param  EncryptionOptions $options
107
     * @return EncryptionInterface
108
     */
109
    public static function forPdfVersion($pdfVersion, $permanentFileIdentifier, EncryptionOptions $options)
110
    {
111
        if (version_compare($pdfVersion, '1.6', '>=')) {
112
            $encryptionClassName = 'Pdf16Encryption';
113
        } elseif (version_compare($pdfVersion, '1.4', '>=')) {
114
            $encryptionClassName = 'Pdf14Encryption';
115
        } else {
116
            $encryptionClassName = 'Pdf11Encryption';
117
        }
118
119
        return new $encryptionClassName(
120
            $permanentFileIdentifier,
121
            $options->getUserPassword(),
122
            $options->getOwnerPassword(),
123
            $options->getUserPermissions()
124
        );
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function writeEncryptDictionary(ObjectWriter $objectWriter)
131
    {
132
        $objectWriter->writeName('Encrypt');
133
        $objectWriter->startDictionary();
134
135
        $objectWriter->writeName('Filter');
136
        $objectWriter->writeName('Standard');
137
138
        $objectWriter->writeName('V');
139
        $objectWriter->writeNumber($this->getAlgorithm());
140
141
        $objectWriter->writeName('R');
142
        $objectWriter->writeNumber($this->getRevision());
143
144
        $objectWriter->writeName('O');
145
        $objectWriter->writeHexadecimalString($this->ownerEntry);
146
147
        $objectWriter->writeName('U');
148
        $objectWriter->writeHexadecimalString($this->userEntry);
149
150
        $objectWriter->writeName('P');
151
        $objectWriter->writeNumber($this->userPermissions->toInt($this->getRevision()));
152
153
        $this->writeAdditionalEncryptDictionaryEntries($objectWriter);
154
155
        $objectWriter->endDictionary();
156
    }
157
158
    /**
159
     * Adds additional entries to the encrypt dictionary if required.
160
     *
161
     * @param ObjectWriter $objectWriter
162
     */
163
    protected function writeAdditionalEncryptDictionaryEntries(ObjectWriter $objectWriter)
164
    {
165
    }
166
167
    /**
168
     * Returns the revision number of the encryption.
169
     *
170
     * @return int
171
     */
172
    abstract protected function getRevision();
173
174
    /**
175
     * Returns the algorithm number of the encryption.
176
     *
177
     * @return int
178
     */
179
    abstract protected function getAlgorithm();
180
181
    /**
182
     * Returns the key length to be used.
183
     *
184
     * The returned value must be either 40 or 128.
185
     *
186
     * @return int
187
     */
188
    abstract protected function getKeyLength();
189
190
    /**
191
     * Computes an individual ecryption key for an object.
192
     *
193
     * @param  string $objectNumber
194
     * @param  string $generationNumber
195
     * @return string
196
     */
197
    protected function computeIndividualEncryptionKey($objectNumber, $generationNumber)
198
    {
199
        return substr(md5(
200
            $this->encryptionKey
201
            . substr(pack('V', $objectNumber), 0, 3)
202
            . substr(pack('V', $generationNumber), 0, 2),
203
            true
204
        ), 0, min(16, strlen($this->encryptionKey) + 5));
205
    }
206
207
    /**
208
     * Encodes a given password into latin-1 and performs length check.
209
     *
210
     * @param  string $password
211
     * @return string
212
     * @throws UnsupportedPasswordException
213
     */
214
    private function encodePassword($password)
215
    {
216
        set_error_handler(function () {
217
        }, E_NOTICE);
218
        $encodedPassword = iconv('UTF-8', 'ISO-8859-1', $password);
219
        restore_error_handler();
220
221
        if (false === $encodedPassword) {
222
            throw new UnsupportedPasswordException('Password contains non-latin-1 characters');
223
        }
224
225
        if (strlen($encodedPassword) > 32) {
226
            throw new UnsupportedPasswordException('Password is longer than 32 characters');
227
        }
228
229
        return $encodedPassword;
230
    }
231
232
    /**
233
     * Computes the encryption key as defined by algorithm 3.2 in 3.5.2.
234
     *
235
     * @param  string $password
236
     * @param  int    $revision
237
     * @param  int    $keyLength
238
     * @param  string $ownerEntry
239
     * @param  int    $permissions
240
     * @param  string $permanentFileIdentifier
241
     * @param  bool   $encryptMetadata
242
     * @return string
243
     */
244
    private function computeEncryptionKey(
245
        $password,
246
        $revision,
247
        $keyLength,
248
        $ownerEntry,
249
        $permissions,
250
        $permanentFileIdentifier,
251
        $encryptMetadata = true
252
    ) {
253
        $string = substr($password . self::ENCRYPTION_PADDING, 0, 32)
254
                . $ownerEntry
255
                . pack('V', $permissions)
256
                . $permanentFileIdentifier;
257
258
        if ($revision >= 4 && $encryptMetadata) {
259
            $string .= "\0xff\0xff\0xff\0xff";
260
        }
261
262
        $hash = md5($string, true);
263
264
        if ($revision >= 3) {
265
            for ($i = 0; $i < 50; ++$i) {
266
                $hash = md5(substr($hash, 0, $keyLength), true);
267
            }
268
269
            return substr($hash, 0, $keyLength);
270
        }
271
272
        return substr($hash, 0, 5);
273
    }
274
275
    /**
276
     * Computes the owner entry as defined by algorithm 3.3 in 3.5.2.
277
     *
278
     * @param  string $ownerPassword
279
     * @param  string $userPassword
280
     * @param  int    $revision
281
     * @param  int    $keyLength
282
     * @return string
283
     */
284
    private function computeOwnerEntry($ownerPassword, $userPassword, $revision, $keyLength)
285
    {
286
        $hash = md5(substr($ownerPassword . self::ENCRYPTION_PADDING, 0, 32), true);
287
288
        if ($revision >= 3) {
289
            for ($i = 0; $i < 50; ++$i) {
290
                $hash = md5($hash, true);
291
            }
292
293
            $key = substr($hash, 0, $keyLength);
294
        } else {
295
            $key = substr($hash, 0, 5);
296
        }
297
298
        $value = openssl_encrypt(substr($userPassword . self::ENCRYPTION_PADDING, 0, 32), 'rc4', $key, true);
299
300
        if ($revision >= 3) {
301
            $value = self::applyRc4Loop($value, $key, $keyLength);
302
        }
303
304
        return $value;
305
    }
306
307
    /**
308
     * Computes the user entry (rev 2) as defined by algorithm 3.4 in 3.5.2.
309
     *
310
     * @param  string $userPassword
311
     * @param  string $ownerEntry
312
     * @param  int    $userPermissionFlags
313
     * @param  string $permanentFileIdentifier
314
     * @return string[]
315
     */
316
    private function computeUserEntryRev2($userPassword, $ownerEntry, $userPermissionFlags, $permanentFileIdentifier)
317
    {
318
        $key = self::computeEncryptionKey(
319
            $userPassword,
320
            2,
321
            5,
322
            $ownerEntry,
323
            $userPermissionFlags,
324
            $permanentFileIdentifier
325
        );
326
327
        return [
328
            openssl_encrypt(self::ENCRYPTION_PADDING, 'rc4', $key, true),
329
            $key
330
        ];
331
    }
332
333
    /**
334
     * Computes the user entry (rev 3 or greater) as defined by algorithm 3.5 in 3.5.2.
335
     *
336
     * @param  string $userPassword
337
     * @param  int    $revision
338
     * @param  int    $keyLength
339
     * @param  string $ownerEntry
340
     * @param  int    $permissions
341
     * @param  string $permanentFileIdentifier
342
     * @return string[]
343
     */
344
    private function computeUserEntryRev3OrGreater(
345
        $userPassword,
346
        $revision,
347
        $keyLength,
348
        $ownerEntry,
349
        $permissions,
350
        $permanentFileIdentifier
351
    ) {
352
        $key = self::computeEncryptionKey(
353
            $userPassword,
354
            $revision,
355
            $keyLength,
356
            $ownerEntry,
357
            $permissions,
358
            $permanentFileIdentifier
359
        );
360
361
        $hash  = md5(self::ENCRYPTION_PADDING . $permanentFileIdentifier, true);
362
        $value = self::applyRc4Loop(openssl_encrypt($hash, 'rc4', $key, true), $key, $keyLength);
363
        $value .= openssl_random_pseudo_bytes(16);
364
365
        return [
366
            $value,
367
            $key
368
        ];
369
    }
370
371
    /**
372
     * Applies loop RC4 encryption.
373
     *
374
     * @param  string $value
375
     * @param  string $key
376
     * @param  int    $keyLength
377
     * @return string
378
     */
379
    private function applyRc4Loop($value, $key, $keyLength)
380
    {
381
        for ($i = 1; $i <= 19; ++$i) {
382
            $newKey = '';
383
384
            for ($j = 0; $j < $keyLength; ++$j) {
385
                $newKey .= chr(ord($key[$j]) ^ $i);
386
            }
387
388
            $value = openssl_encrypt($value, 'rc4', $newKey, true);
389
        }
390
391
        return $value;
392
    }
393
}
394