Completed
Push — master ( 1a4c19...c1db7b )
by Ben
02:15
created

AbstractEncryption   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 386
Duplicated Lines 4.15 %

Coupling/Cohesion

Components 3
Dependencies 9

Importance

Changes 5
Bugs 0 Features 1
Metric Value
wmc 27
c 5
b 0
f 1
lcom 3
cbo 9
dl 16
loc 386
rs 10

14 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 49 4
B forPdfVersion() 16 27 3
B writeEncryptDictionary() 0 27 1
A writeAdditionalEncryptDictionaryEntries() 0 3 1
getRevision() 0 1 ?
getAlgorithm() 0 1 ?
getKeyLength() 0 1 ?
A computeIndividualEncryptionKey() 0 9 1
A encodePassword() 0 17 3
B computeEncryptionKey() 0 30 5
B computeOwnerEntry() 0 22 4
A computeUserEntryRev2() 0 16 1
B computeUserEntryRev3OrGreater() 0 26 1
A applyRc4Loop() 0 14 3

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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', '>=')) {
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...
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', '>=')) {
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...
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