Completed
Push — master ( bc5c42...47bad9 )
by Ben
02:53
created

AbstractEncryption::encodePassword()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
rs 9.2
cc 3
eloc 13
nc 3
nop 1
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(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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(
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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