Completed
Branch master (2ce3bf)
by Christoper
01:58
created

FileEncrypter::getOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 12
rs 9.4286
cc 2
eloc 7
nc 2
nop 1
1
<?php namespace Wubbajack\Encryption;
2
3
use Wubbajack\Encryption\Exceptions\EncryptException;
4
use Wubbajack\Encryption\Exceptions\DecryptException;
5
6
/**
7
 * File Encryptor class.
8
 *
9
 * The file encryptor class by default encrypts using the AES standard.
10
 * Please note that it says RIJNDAEL_128 which refers to the block size and not the key size.
11
 * Refer to the following URL's for more details on this
12
 * https://www.leaseweb.com/labs/2014/02/aes-php-mcrypt-key-padding/
13
 * http://stackoverflow.com/questions/6770370/aes-256-encryption-in-php
14
 *
15
 *
16
 * @author  Chris Stolk <[email protected]>
17
 * @package Wubbajack\Encryption
18
 * @license MIT <https://opensource.org/licenses/MIT>
19
 * @since   0.0.1
20
 */
21
class FileEncrypter
22
{
23
24
    const CHUNK_BYTES = 8192;
25
26
    /**
27
     * The encryption key.
28
     *
29
     * @var string
30
     */
31
    protected $key;
32
33
    /**
34
     * The algorithm used for encryption.
35
     *
36
     * @var string
37
     */
38
    protected $cipher = MCRYPT_RIJNDAEL_128;
39
40
    /**
41
     * The mode used for encryption.
42
     *
43
     * @var string
44
     */
45
    protected $mode = MCRYPT_MODE_CBC;
46
47
    /**
48
     * The block size of the cipher.
49
     *
50
     * @var int
51
     */
52
    protected $block = 16;
53
54
    /**
55
     * FileEncrypter constructor.
56
     *
57
     * @param $key
58
     */
59
    public function __construct($key)
60
    {
61
        $this->key = $key;
62
    }
63
64
    /**
65
     * Set the encryption key.
66
     *
67
     * @param  string  $key
68
     * @return void
69
     */
70
    public function setKey($key)
71
    {
72
        $this->key = (string) $key;
73
    }
74
75
    /**
76
     * Set the encryption cipher.
77
     *
78
     * @param  string  $cipher
79
     * @return $this
80
     */
81
    public function setCipher($cipher)
82
    {
83
        $this->cipher = $cipher;
84
        $this->updateBlockSize();
85
86
        return $this;
87
    }
88
89
    /**
90
     * Set the encryption mode.
91
     *
92
     * @param  string $mode
93
     * @return $this
94
     */
95
    public function setMode($mode)
96
    {
97
        $this->mode = $mode;
98
        $this->updateBlockSize();
99
100
        return $this;
101
    }
102
103
    /**
104
     * Encrypts a file and returns the checksum of the encrypted file.
105
     * You can use the checksum to verify integrity as this method of encryption (symmetrical)
106
     * doesn't allow for easy integrity verification.
107
     *
108
     * It's not required but highly recommended as an attacker can shift bytes and thus changes the data
109
     * on the encrypted file.
110
     *
111
     * @param string $source
112
     * @param string $target
113
     * @return string The checksum of the encrypted file
114
     * @throws EncryptException
115
     */
116
    public function encrypt($source, $target)
117
    {
118
        $iv = mcrypt_create_iv($this->getIvSize(), $this->getRandomizer());
119
120
        try {
121
            $this->encryptFile($source, $target, $iv);
122
        } catch (\Exception $e) {
123
            throw new EncryptException('Unable to encrypt file', 0, $e);
124
        }
125
126
        // Returns the encrypted file object, sets the padding and the source file checksum for later checking
127
        return EncryptedFile::create(
128
            $iv,
129
            $this->calculateChecksum($source),
130
            $this->calculatePadding($source, $target),
131
            $target
132
        );
133
    }
134
135
    /**
136
     * Decrypts the source file to a target file. The checksum is an optional parameter
137
     * that can be used to verify integrity of the file some ciphers offer no integrity check of their own.
138
     *
139
     * It's an optional parameter but be warned, the file may have been tampered with by an attacker.
140
     *
141
     * @param EncryptedFile $encryptedFile
142
     * @param string        $target
143
     * @return string Path to the target file
144
     * @throws DecryptException
145
     */
146
    public function decrypt(EncryptedFile $encryptedFile, $target)
147
    {
148
        // Get the path to the source file
149
        $source = $encryptedFile->getFile()->getRealPath();
150
151
        try {
152
            $this->decryptFile($source, $target, $encryptedFile->getIv(), $encryptedFile->getPadding());
153
        } catch (\Exception $e) {
154
            throw new DecryptException('Unable to decrypt file', 0, $e);
155
        }
156
157
        // Verify the integrity of the decrypted file checking the checksum against the checksum of the original source file
158
        if (!$this->verifyChecksum($target, $encryptedFile->getChecksum())) {
159
            unlink($target);
160
            throw new DecryptException('Invalid checksum on decrypted file');
161
        }
162
163
        return $target;
164
    }
165
166
    /**
167
     * Decrypts a file in a stream, performing the callback on each successive decrypted block.
168
     * If the checksum is provided it checks it against the encrypted file for integrity.
169
     *
170
     * The callback can accept two arguments:
171
     *  - $data   - A chunk of decrypted data
172
     *  - $stream - The resource stream that is decrypting
173
     *
174
     * @param EncryptedFile $encryptedFile
175
     * @param \Closure      $callback
176
     * @throws DecryptException
177
     */
178
    public function streamDecrypt(EncryptedFile $encryptedFile, \Closure $callback)
179
    {
180
        // Get the path to the encrypted file
181
        $source = $encryptedFile->getFile()->getRealPath();
182
183
        // Check if callback is a closure
184
        if (!($callback instanceof \Closure)) {
185
            throw new DecryptException('Callback must be callable');
186
        }
187
188
        // Get the decryption stream
189
        $stream = $this->createDecryptionStream($source, $this->getOptions($encryptedFile->getIv()));
190
191
        // Run the callback while the file pointer isn't at the end
192
        while (!feof($stream)) {
193
            $callback(fread($stream, self::CHUNK_BYTES), $stream);
194
        }
195
    }
196
197
    /**
198
     * Encrypts the file
199
     *
200
     * @param string $source_file
201
     * @param string $target_file
202
     * @param string $iv
203
     *
204
     * @return void
205
     */
206 View Code Duplication
    protected function encryptFile($source_file, $target_file, $iv)
1 ignored issue
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...
207
    {
208
        // We start by setting up both the source and target streams
209
        // and applying all the necessary stream filters for encryption.
210
211
        $source  = fopen($source_file, 'r');
212
        $target  = $this->createEncryptionStream($target_file, $this->getOptions($iv));
213
214
        // We copy the source into the target stream, passing through the encryption filter
215
        $this->copyStream($source, $target);
216
217
        // Close the source file and target files
218
        fclose($source);
219
        fclose($target);
220
    }
221
222
    /**
223
     * Decrypts a source file into a target file
224
     *
225
     * @param string $source
226
     * @param string $target
227
     * @param string $iv
228
     * @param int    $padding
229
     *
230
     * @return void
231
     */
232 View Code Duplication
    protected function decryptFile($source, $target, $iv, $padding = 0)
1 ignored issue
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...
233
    {
234
        // We create a stream with a decryption filter appended to it
235
        $source = $this->createDecryptionStream($source, $this->getOptions($iv));
236
        $target = fopen($target, 'w+');
237
238
        // We copy the source into the target, decrypting it in the process
239
        $this->copyStream($source, $target, $padding);
240
241
        // Close both source and target
242
        fclose($source);
243
        fclose($target);
244
    }
245
246
    /**
247
     * Creates a stream that encrypts when written to
248
     *
249
     * @param string $target
250
     * @param array  $options
251
     *
252
     * @return resource
253
     * @throws EncryptException
254
     */
255
    protected function createEncryptionStream($target, array $options)
256
    {
257
        // Open up the resources to both the source and target
258
        $stream = fopen($target, 'w+');
259
260
        // Append the stream write filter with the correct encryption cipher
261
        stream_filter_append($stream, $this->getEncryptionFilterName(), STREAM_FILTER_WRITE, $options);
262
263
        // Returns the target stream
264
        return $stream;
265
    }
266
267
    /**
268
     * Creates a stream that is decrypted when read from
269
     *
270
     * @param string $source
271
     * @param array  $options
272
     *
273
     * @return resource
274
     */
275
    protected function createDecryptionStream($source, array $options)
276
    {
277
        $stream = fopen($source, 'rb');
278
279
        // Append the stream read filter with the decryption cipher
280
        stream_filter_append($stream, $this->getDecryptionFilterName(), STREAM_FILTER_READ, $options);
281
282
        // Returns the target stream
283
        return $stream;
284
    }
285
286
    /**
287
     * Copies a source stream to a target stream.
288
     * If the padding parameter is set it will remove said amount of bytes from the end of the file.
289
     *
290
     * This method does not use stream_copy_to_stream on purpose because this way we have more control
291
     * over the process of moving data from one stream to another.
292
     *
293
     * @param resource $source
294
     * @param resource $target
295
     * @param null|int $padding
296
     *
297
     * @return void
298
     */
299
    protected function copyStream($source, $target, $padding = null)
300
    {
301
        // Ensure that both pointers are at the start of the file
302
        while (!feof($source)) {
303
            $data = fread($source, self::CHUNK_BYTES);
304
305
            // If eof is reached and padding is set, remove it from the file
306
            if (feof($source) && $padding) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $padding of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
307
                $data = substr($data, 0, -$padding);
308
            }
309
310
            fwrite($target, $data);
311
        }
312
    }
313
314
    /**
315
     * Returns the options for the stream filter
316
     * @param null $iv If no IV is set, one will be created
317
     *
318
     * @return array Returns an array with 'mode','key' and 'iv'
319
     */
320
    protected function getOptions($iv = null)
321
    {
322
        if ($iv === null) {
323
            mcrypt_create_iv($this->getIvSize(), $this->getRandomizer());
324
        }
325
326
        return [
327
            'mode' => $this->mode,
328
            'key'  => $this->key,
329
            'iv'   => $iv,
330
        ];
331
    }
332
333
    /**
334
     * Get the IV size for the cipher.
335
     *
336
     * @return int
337
     */
338
    protected function getIvSize()
339
    {
340
        return mcrypt_get_iv_size($this->cipher, $this->mode);
341
    }
342
343
    /**
344
     * Returns the encryption cipher for the stream filter
345
     *
346
     * @return string
347
     */
348
    protected function getEncryptionFilterName()
349
    {
350
        return 'mcrypt.'. $this->cipher;
351
    }
352
353
    /**
354
     * Returns the decryption cipher for the stream filter
355
     *
356
     * @return string
357
     */
358
    protected function getDecryptionFilterName()
359
    {
360
        return 'mdecrypt.'. $this->cipher;
361
    }
362
363
    /**
364
     * Get the random data source available for the OS.
365
     *
366
     * @return int
367
     */
368
    protected function getRandomizer()
369
    {
370
        if (defined('MCRYPT_DEV_URANDOM')) {
371
            return MCRYPT_DEV_URANDOM;
372
        }
373
374
        if (defined('MCRYPT_DEV_RANDOM')) {
375
            return MCRYPT_DEV_RANDOM;
376
        }
377
378
        mt_srand();
379
380
        return MCRYPT_RAND;
381
    }
382
383
    /**
384
     * Compares the given checksum with the actual file checksum.
385
     * Returns true if they match, false if not
386
     *
387
     * @param string $file
388
     * @param string $checksum
389
     * @return bool
390
     */
391
    protected function verifyChecksum($file, $checksum)
392
    {
393
        return strcmp($checksum, $this->calculateChecksum($file)) === 0;
394
    }
395
396
    /**
397
     * Update the block size for the current cipher and mode.
398
     *
399
     * @return void
400
     */
401
    protected function updateBlockSize()
402
    {
403
        $this->block = mcrypt_get_iv_size($this->cipher, $this->mode);
404
    }
405
406
    /**
407
     * Calculates the padding that was added during encryption
408
     *
409
     * @param string $source Path the the source file
410
     * @param string $target Path to the target file
411
     * @return int
412
     */
413
    protected function calculatePadding($source, $target)
414
    {
415
        return filesize($target) - filesize($source);
416
    }
417
418
    /**
419
     * Calculates the checksum of the file
420
     *
421
     * @param string $file
422
     * @return string
423
     */
424
    protected function calculateChecksum($file)
425
    {
426
        return sha1_file($file);
427
    }
428
}
429