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\DomainException; |
13
|
|
|
use Bacon\Pdf\Exception\UnexpectedValueException; |
14
|
|
|
use Bacon\Pdf\Exception\UnsupportedPasswordException; |
15
|
|
|
use Bacon\Pdf\PdfWriter; |
16
|
|
|
use Bacon\Pdf\Utils\EncryptionUtils; |
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|null $ownerPassword |
48
|
|
|
* @param Permissions|null $userPermissions |
49
|
|
|
* @throws UnexpectedValueException |
50
|
|
|
*/ |
51
|
|
|
public function __construct( |
52
|
|
|
$permanentFileIdentifier, |
53
|
|
|
$userPassword, |
54
|
|
|
$ownerPassword = null, |
55
|
|
|
Permissions $userPermissions = null |
56
|
|
|
) { |
57
|
|
|
if (null === $ownerPassword) { |
58
|
|
|
$ownerPassword = $userPassword; |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
$encodedUserPassword = $this->encodePassword($userPassword); |
62
|
|
|
$encodedOwnerPassword = $this->encodePassword($ownerPassword); |
63
|
|
|
|
64
|
|
|
$revision = $this->getRevision(); |
65
|
|
|
$keyLength = $this->getKeyLength(); |
66
|
|
|
|
67
|
|
|
if ($revision < 3 && null !== $userPermissions) { |
68
|
|
|
throw new DomainException('This encryption does not support permissions'); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
if (!in_array($keyLength, [40, 128])) { |
72
|
|
|
throw new UnexpectedValueException('Key length must be either 40 or 128'); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$this->ownerEntry = $this->computeOwnerEntry( |
76
|
|
|
$encodedOwnerPassword, |
77
|
|
|
$encodedUserPassword, |
78
|
|
|
$revision, |
79
|
|
|
$keyLength |
80
|
|
|
); |
81
|
|
|
|
82
|
|
|
if (2 === $revision) { |
83
|
|
|
list($this->userEntry, $this->encryptionKey) = EncryptionUtils::computeUserEntryRev2( |
84
|
|
|
$encodedUserPassword, |
85
|
|
|
$this->ownerEntry, |
86
|
|
|
$revision, |
87
|
|
|
$permanentFileIdentifier |
88
|
|
|
); |
89
|
|
|
} else { |
90
|
|
|
list($this->userEntry, $this->encryptionKey) = EncryptionUtils::computeUserEntryRev3OrGreater( |
91
|
|
|
$encodedUserPassword, |
92
|
|
|
$revision, |
93
|
|
|
$keyLength, |
94
|
|
|
$this->ownerEntry, |
95
|
|
|
$userPermissions->toInt($revision), |
96
|
|
|
$permanentFileIdentifier |
97
|
|
|
); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
$this->userPermissions = $userPermissions; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* {@inheritdoc} |
105
|
|
|
*/ |
106
|
|
|
public function writeEncryptDictionary(PdfWriter $pdfWriter) |
107
|
|
|
{ |
108
|
|
|
$pdfWriter->startDictionary(); |
109
|
|
|
|
110
|
|
|
$pdfWriter->writeName('Filter'); |
111
|
|
|
$pdfWriter->writeName('Standard'); |
112
|
|
|
|
113
|
|
|
$pdfWriter->writeName('V'); |
114
|
|
|
$pdfWriter->writeNumber($this->getAlgorithm()); |
115
|
|
|
|
116
|
|
|
$pdfWriter->writeName('R'); |
117
|
|
|
$pdfWriter->writeNumber($this->getRevision()); |
118
|
|
|
|
119
|
|
|
$pdfWriter->writeName('O'); |
120
|
|
|
$pdfWriter->writeNumber($this->ownerEntry); |
121
|
|
|
|
122
|
|
|
$pdfWriter->writeName('U'); |
123
|
|
|
$pdfWriter->writeNumber($this->userEntry); |
124
|
|
|
|
125
|
|
|
$pdfWriter->writeName('P'); |
126
|
|
|
|
127
|
|
|
if (null === $this->userPermissions) { |
128
|
|
|
$pdfWriter->writeNumber(0); |
129
|
|
|
} else { |
130
|
|
|
$pdfWriter->writeNumber($this->userPermissions->toInt($this->getRevision())); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
$this->writeAdditionalEncryptDictionaryEntries($pdfWriter); |
134
|
|
|
|
135
|
|
|
$pdfWriter->endDictionary(); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Adds additional entries to the encrypt dictionary if required. |
140
|
|
|
* |
141
|
|
|
* @param PdfWriter $pdfWriter |
142
|
|
|
*/ |
143
|
|
|
protected function writeAdditionalEncryptDictionaryEntries(PdfWriter $pdfWriter) |
144
|
|
|
{ |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Returns the revision number of the encryption. |
149
|
|
|
* |
150
|
|
|
* @return int |
151
|
|
|
*/ |
152
|
|
|
abstract protected function getRevision(); |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Returns the algorithm number of the encryption. |
156
|
|
|
* |
157
|
|
|
* @return int |
158
|
|
|
*/ |
159
|
|
|
abstract protected function getAlgorithm(); |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Returns the key length to be used. |
163
|
|
|
* |
164
|
|
|
* The returned value must be either 40 or 128. |
165
|
|
|
* |
166
|
|
|
* @return int |
167
|
|
|
*/ |
168
|
|
|
abstract protected function getKeyLength(); |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Computes an individual ecryption key for an object. |
172
|
|
|
* |
173
|
|
|
* @param string $objectNumber |
174
|
|
|
* @param string $generationNumber |
175
|
|
|
* @return string |
176
|
|
|
*/ |
177
|
|
|
protected function computeIndividualEncryptionKey($objectNumber, $generationNumber) |
178
|
|
|
{ |
179
|
|
|
return substr(hex2bin(md5( |
180
|
|
|
$this->encryptionKey |
181
|
|
|
. substr(pack('V', $objectNumber), 0, 3) |
182
|
|
|
. substr(pack('V', $generationNumber), 0, 2) |
183
|
|
|
)), 0, min(16, strlen($this->encryptionKey) + 5)); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Encodes a given password into latin-1 and performs length check. |
188
|
|
|
* |
189
|
|
|
* @param string $password |
190
|
|
|
* @return string |
191
|
|
|
* @throws UnsupportedPasswordException |
192
|
|
|
*/ |
193
|
|
|
private function encodePassword($password) |
194
|
|
|
{ |
195
|
|
|
set_error_handler(function () {}, E_NOTICE); |
196
|
|
|
$encodedPassword = iconv('UTF-8', 'ISO-8859-1', $password); |
197
|
|
|
restore_error_handler(); |
198
|
|
|
|
199
|
|
|
if (false === $encodedPassword) { |
200
|
|
|
throw new UnsupportedPasswordException(sprintf( |
201
|
|
|
'The password "%s" contains non-latin-1 characters', |
202
|
|
|
$password |
203
|
|
|
)); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
if (strlen($encodedPassword) > 32) { |
207
|
|
|
throw new UnsupportedPasswordException(sprintf( |
208
|
|
|
'The password "%s" is longer than 32 characters', |
209
|
|
|
$password |
210
|
|
|
)); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
return $encodedPassword; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Computes the encryption key as defined by algorithm 3.2 in 3.5.2. |
218
|
|
|
* |
219
|
|
|
* @param string $password |
220
|
|
|
* @param int $revision |
221
|
|
|
* @param int $keyLength |
222
|
|
|
* @param string $ownerEntry |
223
|
|
|
* @param int $permissions |
224
|
|
|
* @param string $idEntry |
225
|
|
|
* @param bool $encryptMetadata |
226
|
|
|
* @return string |
227
|
|
|
*/ |
228
|
|
View Code Duplication |
private function computeEncryptionKey( |
|
|
|
|
229
|
|
|
$password, |
230
|
|
|
$revision, |
231
|
|
|
$keyLength, |
232
|
|
|
$ownerEntry, |
233
|
|
|
$permissions, |
234
|
|
|
$idEntry, |
235
|
|
|
$encryptMetadata = true |
236
|
|
|
) { |
237
|
|
|
$string = substr($password . self::ENCRYPTION_PADDING, 0, 32) |
238
|
|
|
. $ownerEntry |
239
|
|
|
. pack('V', $permissions) |
240
|
|
|
. $idEntry; |
241
|
|
|
|
242
|
|
|
if ($revision >= 4 && $encryptMetadata) { |
243
|
|
|
$string .= "\0xff\0xff\0xff\0xff"; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$hash = hex2bin(md5($string)); |
247
|
|
|
|
248
|
|
|
if ($revision >= 3) { |
249
|
|
|
for ($i = 0; $i < 50; ++$i) { |
250
|
|
|
$hash = hex2bin(md5(substr($hash, 0, $keyLength))); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return substr($hash, 0, $keyLength); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
return substr($hash, 0, 5); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Computes the owner entry as defined by algorithm 3.3 in 3.5.2. |
261
|
|
|
* |
262
|
|
|
* @param string $ownerPassword |
263
|
|
|
* @param string $userPassword |
264
|
|
|
* @param int $revision |
265
|
|
|
* @param int $keyLength |
266
|
|
|
* @return string |
267
|
|
|
*/ |
268
|
|
View Code Duplication |
private function computeOwnerEntry($ownerPassword, $userPassword, $revision, $keyLength) |
|
|
|
|
269
|
|
|
{ |
270
|
|
|
$hash = hex2bin(md5(substr($ownerPassword . self::ENCRYPTION_PADDING, 0, 32))); |
271
|
|
|
|
272
|
|
|
if ($revision >= 3) { |
273
|
|
|
for ($i = 0; $i < 50; ++$i) { |
274
|
|
|
$hash = hex2bin(md5($hash)); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
$key = substr($hash, 0, $keyLength); |
278
|
|
|
} else { |
279
|
|
|
$key = substr($hash, 0, 5); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
$value = openssl_encrypt(substr($userPassword . self::ENCRYPTION_PADDING, 0, 32), 'rc-4', $key); |
283
|
|
|
|
284
|
|
|
if ($revision >= 3) { |
285
|
|
|
$value = self::applyRc4Loop($value, $key, $keyLength); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
return $value; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Computes the user entry (rev 2) as defined by algorithm 3.4 in 3.5.2. |
293
|
|
|
* |
294
|
|
|
* @param string $userPassword |
295
|
|
|
* @param string $ownerEntry |
296
|
|
|
* @param int $userPermissionFlags |
297
|
|
|
* @param string $idEntry |
298
|
|
|
* @return string[] |
299
|
|
|
*/ |
300
|
|
|
private function computeUserEntryRev2($userPassword, $ownerEntry, $userPermissionFlags, $idEntry) |
|
|
|
|
301
|
|
|
{ |
302
|
|
|
$key = self::computeEncryptionKey($userPassword, 2, 5, $ownerEntry, $userPermissionFlags, $idEntry); |
303
|
|
|
|
304
|
|
|
return [ |
305
|
|
|
openssl_encrypt(self::ENCRYPTION_PADDING, 'rc4', $key), |
306
|
|
|
$key |
307
|
|
|
]; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Computes the user entry (rev 3 or greater) as defined by algorithm 3.5 in 3.5.2. |
312
|
|
|
* |
313
|
|
|
* @param string $userPassword |
314
|
|
|
* @param int $revision |
315
|
|
|
* @param int $keyLength |
316
|
|
|
* @param string $ownerEntry |
317
|
|
|
* @param int $permissions |
318
|
|
|
* @param string $idEntry |
319
|
|
|
* @return string[] |
320
|
|
|
*/ |
321
|
|
|
private function computeUserEntryRev3OrGreater( |
|
|
|
|
322
|
|
|
$userPassword, |
323
|
|
|
$revision, |
324
|
|
|
$keyLength, |
325
|
|
|
$ownerEntry, |
326
|
|
|
$permissions, |
327
|
|
|
$idEntry |
328
|
|
|
) { |
329
|
|
|
$key = self::computeEncryptionKey($userPassword, $revision, $keyLength, $ownerEntry, $permissions, $idEntry); |
330
|
|
|
$hash = hex2bin(md5(self::ENCRYPTION_PADDING . $idEntry)); |
331
|
|
|
$value = self::applyRc4Loop(openssl_encrypt($hash, 'rc4', $key), $key, $keyLength); |
332
|
|
|
|
333
|
|
View Code Duplication |
if (function_exists('random_bytes')) { |
|
|
|
|
334
|
|
|
// As of PHP 7 |
335
|
|
|
$value .= random_bytes(16); |
336
|
|
|
} else { |
337
|
|
|
mt_srand(); |
338
|
|
|
|
339
|
|
|
for ($i = 0; $i < 16; ++$i) { |
340
|
|
|
$value .= chr(mt_rand(0, 255)); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
return [ |
345
|
|
|
$value, |
346
|
|
|
$key |
347
|
|
|
]; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Applies loop RC4 encryption. |
352
|
|
|
* |
353
|
|
|
* @param string $value |
354
|
|
|
* @param string $key |
355
|
|
|
* @param int $keyLength |
356
|
|
|
* @return string |
357
|
|
|
*/ |
358
|
|
View Code Duplication |
private function applyRc4Loop($value, $key, $keyLength) |
|
|
|
|
359
|
|
|
{ |
360
|
|
|
for ($i = 1; $i <= 19; ++$i) { |
361
|
|
|
$newKey = ''; |
362
|
|
|
|
363
|
|
|
for ($j = 0; $j < $keyLength; ++$j) { |
364
|
|
|
$newKey = chr(ord($key[$j]) ^ $i); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
$value = openssl_encrypt($value, 'rc4', $newKey); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
return $value; |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
|
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.