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
|
|
View Code Duplication |
if (version_compare($pdfVersion, '1.6', '>=')) { |
|
|
|
|
112
|
|
|
return new Pdf16Encryption( |
113
|
|
|
$permanentFileIdentifier, |
114
|
|
|
$options->getUserPassword(), |
115
|
|
|
$options->getOwnerPassword(), |
116
|
|
|
$options->getUserPermissions() |
117
|
|
|
); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
View Code Duplication |
if (version_compare($pdfVersion, '1.4', '>=')) { |
|
|
|
|
121
|
|
|
return new Pdf14Encryption( |
122
|
|
|
$permanentFileIdentifier, |
123
|
|
|
$options->getUserPassword(), |
124
|
|
|
$options->getOwnerPassword(), |
125
|
|
|
$options->getUserPermissions() |
126
|
|
|
); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
return new Pdf11Encryption( |
130
|
|
|
$permanentFileIdentifier, |
131
|
|
|
$options->getUserPassword(), |
132
|
|
|
$options->getOwnerPassword(), |
133
|
|
|
$options->getUserPermissions() |
134
|
|
|
); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* {@inheritdoc} |
139
|
|
|
*/ |
140
|
|
|
public function writeEncryptDictionary(ObjectWriter $objectWriter) |
141
|
|
|
{ |
142
|
|
|
$objectWriter->writeName('Encrypt'); |
143
|
|
|
$objectWriter->startDictionary(); |
144
|
|
|
|
145
|
|
|
$objectWriter->writeName('Filter'); |
146
|
|
|
$objectWriter->writeName('Standard'); |
147
|
|
|
|
148
|
|
|
$objectWriter->writeName('V'); |
149
|
|
|
$objectWriter->writeNumber($this->getAlgorithm()); |
150
|
|
|
|
151
|
|
|
$objectWriter->writeName('R'); |
152
|
|
|
$objectWriter->writeNumber($this->getRevision()); |
153
|
|
|
|
154
|
|
|
$objectWriter->writeName('O'); |
155
|
|
|
$objectWriter->writeHexadecimalString($this->ownerEntry); |
156
|
|
|
|
157
|
|
|
$objectWriter->writeName('U'); |
158
|
|
|
$objectWriter->writeHexadecimalString($this->userEntry); |
159
|
|
|
|
160
|
|
|
$objectWriter->writeName('P'); |
161
|
|
|
$objectWriter->writeNumber($this->userPermissions->toInt($this->getRevision())); |
162
|
|
|
|
163
|
|
|
$this->writeAdditionalEncryptDictionaryEntries($objectWriter); |
164
|
|
|
|
165
|
|
|
$objectWriter->endDictionary(); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Adds additional entries to the encrypt dictionary if required. |
170
|
|
|
* |
171
|
|
|
* @param ObjectWriter $objectWriter |
172
|
|
|
*/ |
173
|
|
|
protected function writeAdditionalEncryptDictionaryEntries(ObjectWriter $objectWriter) |
174
|
|
|
{ |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Returns the revision number of the encryption. |
179
|
|
|
* |
180
|
|
|
* @return int |
181
|
|
|
*/ |
182
|
|
|
abstract protected function getRevision(); |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Returns the algorithm number of the encryption. |
186
|
|
|
* |
187
|
|
|
* @return int |
188
|
|
|
*/ |
189
|
|
|
abstract protected function getAlgorithm(); |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Returns the key length to be used. |
193
|
|
|
* |
194
|
|
|
* The returned value must be either 40 or 128. |
195
|
|
|
* |
196
|
|
|
* @return int |
197
|
|
|
*/ |
198
|
|
|
abstract protected function getKeyLength(); |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Computes an individual ecryption key for an object. |
202
|
|
|
* |
203
|
|
|
* @param string $objectNumber |
204
|
|
|
* @param string $generationNumber |
205
|
|
|
* @return string |
206
|
|
|
*/ |
207
|
|
|
protected function computeIndividualEncryptionKey($objectNumber, $generationNumber) |
208
|
|
|
{ |
209
|
|
|
return substr(md5( |
210
|
|
|
$this->encryptionKey |
211
|
|
|
. substr(pack('V', $objectNumber), 0, 3) |
212
|
|
|
. substr(pack('V', $generationNumber), 0, 2), |
213
|
|
|
true |
214
|
|
|
), 0, min(16, strlen($this->encryptionKey) + 5)); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* Encodes a given password into latin-1 and performs length check. |
219
|
|
|
* |
220
|
|
|
* @param string $password |
221
|
|
|
* @return string |
222
|
|
|
* @throws UnsupportedPasswordException |
223
|
|
|
*/ |
224
|
|
|
private function encodePassword($password) |
225
|
|
|
{ |
226
|
|
|
set_error_handler(function () { |
227
|
|
|
}, E_NOTICE); |
228
|
|
|
$encodedPassword = iconv('UTF-8', 'ISO-8859-1', $password); |
229
|
|
|
restore_error_handler(); |
230
|
|
|
|
231
|
|
|
if (false === $encodedPassword) { |
232
|
|
|
throw new UnsupportedPasswordException('Password contains non-latin-1 characters'); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if (strlen($encodedPassword) > 32) { |
236
|
|
|
throw new UnsupportedPasswordException('Password is longer than 32 characters'); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return $encodedPassword; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Computes the encryption key as defined by algorithm 3.2 in 3.5.2. |
244
|
|
|
* |
245
|
|
|
* @param string $password |
246
|
|
|
* @param int $revision |
247
|
|
|
* @param int $keyLength |
248
|
|
|
* @param string $ownerEntry |
249
|
|
|
* @param int $permissions |
250
|
|
|
* @param string $permanentFileIdentifier |
251
|
|
|
* @param bool $encryptMetadata |
252
|
|
|
* @return string |
253
|
|
|
*/ |
254
|
|
|
private function computeEncryptionKey( |
255
|
|
|
$password, |
256
|
|
|
$revision, |
257
|
|
|
$keyLength, |
258
|
|
|
$ownerEntry, |
259
|
|
|
$permissions, |
260
|
|
|
$permanentFileIdentifier, |
261
|
|
|
$encryptMetadata = true |
262
|
|
|
) { |
263
|
|
|
$string = substr($password . self::ENCRYPTION_PADDING, 0, 32) |
264
|
|
|
. $ownerEntry |
265
|
|
|
. pack('V', $permissions) |
266
|
|
|
. $permanentFileIdentifier; |
267
|
|
|
|
268
|
|
|
if ($revision >= 4 && $encryptMetadata) { |
269
|
|
|
$string .= "\0xff\0xff\0xff\0xff"; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
$hash = md5($string, true); |
273
|
|
|
|
274
|
|
|
if ($revision >= 3) { |
275
|
|
|
for ($i = 0; $i < 50; ++$i) { |
276
|
|
|
$hash = md5(substr($hash, 0, $keyLength), true); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
return substr($hash, 0, $keyLength); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
return substr($hash, 0, 5); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Computes the owner entry as defined by algorithm 3.3 in 3.5.2. |
287
|
|
|
* |
288
|
|
|
* @param string $ownerPassword |
289
|
|
|
* @param string $userPassword |
290
|
|
|
* @param int $revision |
291
|
|
|
* @param int $keyLength |
292
|
|
|
* @return string |
293
|
|
|
*/ |
294
|
|
|
private function computeOwnerEntry($ownerPassword, $userPassword, $revision, $keyLength) |
295
|
|
|
{ |
296
|
|
|
$hash = md5(substr($ownerPassword . self::ENCRYPTION_PADDING, 0, 32), true); |
297
|
|
|
|
298
|
|
|
if ($revision >= 3) { |
299
|
|
|
for ($i = 0; $i < 50; ++$i) { |
300
|
|
|
$hash = md5($hash, true); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
$key = substr($hash, 0, $keyLength); |
304
|
|
|
} else { |
305
|
|
|
$key = substr($hash, 0, 5); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
$value = openssl_encrypt(substr($userPassword . self::ENCRYPTION_PADDING, 0, 32), 'rc4', $key, true); |
309
|
|
|
|
310
|
|
|
if ($revision >= 3) { |
311
|
|
|
$value = self::applyRc4Loop($value, $key, $keyLength); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
return $value; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Computes the user entry (rev 2) as defined by algorithm 3.4 in 3.5.2. |
319
|
|
|
* |
320
|
|
|
* @param string $userPassword |
321
|
|
|
* @param string $ownerEntry |
322
|
|
|
* @param int $userPermissionFlags |
323
|
|
|
* @param string $permanentFileIdentifier |
324
|
|
|
* @return string[] |
325
|
|
|
*/ |
326
|
|
|
private function computeUserEntryRev2($userPassword, $ownerEntry, $userPermissionFlags, $permanentFileIdentifier) |
327
|
|
|
{ |
328
|
|
|
$key = self::computeEncryptionKey( |
329
|
|
|
$userPassword, |
330
|
|
|
2, |
331
|
|
|
5, |
332
|
|
|
$ownerEntry, |
333
|
|
|
$userPermissionFlags, |
334
|
|
|
$permanentFileIdentifier |
335
|
|
|
); |
336
|
|
|
|
337
|
|
|
return [ |
338
|
|
|
openssl_encrypt(self::ENCRYPTION_PADDING, 'rc4', $key, true), |
339
|
|
|
$key |
340
|
|
|
]; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Computes the user entry (rev 3 or greater) as defined by algorithm 3.5 in 3.5.2. |
345
|
|
|
* |
346
|
|
|
* @param string $userPassword |
347
|
|
|
* @param int $revision |
348
|
|
|
* @param int $keyLength |
349
|
|
|
* @param string $ownerEntry |
350
|
|
|
* @param int $permissions |
351
|
|
|
* @param string $permanentFileIdentifier |
352
|
|
|
* @return string[] |
353
|
|
|
*/ |
354
|
|
|
private function computeUserEntryRev3OrGreater( |
355
|
|
|
$userPassword, |
356
|
|
|
$revision, |
357
|
|
|
$keyLength, |
358
|
|
|
$ownerEntry, |
359
|
|
|
$permissions, |
360
|
|
|
$permanentFileIdentifier |
361
|
|
|
) { |
362
|
|
|
$key = self::computeEncryptionKey( |
363
|
|
|
$userPassword, |
364
|
|
|
$revision, |
365
|
|
|
$keyLength, |
366
|
|
|
$ownerEntry, |
367
|
|
|
$permissions, |
368
|
|
|
$permanentFileIdentifier |
369
|
|
|
); |
370
|
|
|
|
371
|
|
|
$hash = md5(self::ENCRYPTION_PADDING . $permanentFileIdentifier, true); |
372
|
|
|
$value = self::applyRc4Loop(openssl_encrypt($hash, 'rc4', $key, true), $key, $keyLength); |
373
|
|
|
$value .= openssl_random_pseudo_bytes(16); |
374
|
|
|
|
375
|
|
|
return [ |
376
|
|
|
$value, |
377
|
|
|
$key |
378
|
|
|
]; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Applies loop RC4 encryption. |
383
|
|
|
* |
384
|
|
|
* @param string $value |
385
|
|
|
* @param string $key |
386
|
|
|
* @param int $keyLength |
387
|
|
|
* @return string |
388
|
|
|
*/ |
389
|
|
|
private function applyRc4Loop($value, $key, $keyLength) |
390
|
|
|
{ |
391
|
|
|
for ($i = 1; $i <= 19; ++$i) { |
392
|
|
|
$newKey = ''; |
393
|
|
|
|
394
|
|
|
for ($j = 0; $j < $keyLength; ++$j) { |
395
|
|
|
$newKey .= chr(ord($key[$j]) ^ $i); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
$value = openssl_encrypt($value, 'rc4', $newKey, true); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
return $value; |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.