Completed
Push — master ( 20af2b...9f734f )
by Christoper
30:10 queued 14:45
created

FileEncrypter   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 414
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 92.45%

Importance

Changes 4
Bugs 0 Features 2
Metric Value
wmc 33
c 4
b 0
f 2
lcom 1
cbo 3
dl 0
loc 414
ccs 98
cts 106
cp 0.9245
rs 9.4

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setKey() 0 4 1
A setCipher() 0 7 1
A setMode() 0 7 1
A encrypt() 0 18 2
B decrypt() 0 23 4
B streamDecrypt() 0 24 4
A encryptFile() 0 15 1
A decryptFile() 0 13 1
A createEncryptionStream() 0 11 1
A createDecryptionStream() 0 10 1
A copyStream() 0 14 4
A getOptions() 0 8 1
A getIvSize() 0 4 1
A getEncryptionFilterName() 0 4 1
A getDecryptionFilterName() 0 4 1
A getRandomizer() 0 14 3
A verifyChecksum() 0 4 1
A updateBlockSize() 0 4 1
A calculatePadding() 0 4 1
A calculateChecksum() 0 4 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 12
    public function __construct($key)
60
    {
61 12
        $this->key = $key;
62 12
    }
63
64
    /**
65
     * Set the encryption key.
66
     *
67
     * @param  string  $key
68
     * @return void
69
     */
70 12
    public function setKey($key)
71
    {
72 12
        $this->key = (string) $key;
73 12
    }
74
75
    /**
76
     * Set the encryption cipher.
77
     *
78
     * @param  string  $cipher
79
     * @return $this
80
     */
81 12
    public function setCipher($cipher)
82
    {
83 12
        $this->cipher = $cipher;
84 12
        $this->updateBlockSize();
85
86 12
        return $this;
87
    }
88
89
    /**
90
     * Set the encryption mode.
91
     *
92
     * @param  string $mode
93
     * @return $this
94
     */
95 12
    public function setMode($mode)
96
    {
97 12
        $this->mode = $mode;
98 12
        $this->updateBlockSize();
99
100 12
        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 EncryptedFile An encrypted file object containing information about the IV, checksum and padding
114
     * @throws EncryptException
115
     */
116 6
    public function encrypt($source, $target)
117
    {
118 6
        $iv = mcrypt_create_iv($this->getIvSize(), $this->getRandomizer());
119
120
        try {
121 6
            $this->encryptFile($source, $target, $iv);
122 4
        } catch (\Exception $e) {
123 2
            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 6
        return EncryptedFile::create(
128 3
            $iv,
129 6
            $this->calculateChecksum($source),
130 6
            $this->calculatePadding($source, $target),
131
            $target
132 3
        );
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 2
    public function decrypt(EncryptedFile $encryptedFile, $target)
147
    {
148
        // Get the path to the source file
149 2
        $source = $encryptedFile->getFile()->getRealPath();
150
151
        try {
152 2
            $this->decryptFile($source, $target, $encryptedFile->getIv(), $encryptedFile->getPadding());
153 1
        } catch (DecryptException $e) {
154
            // Cascade Decrypt exceptions
155
            throw $e;
156
        } catch (\Exception $e) {
157
            // "wrap" other exceptions and add them to the previous stack
158
            throw new DecryptException('Unable to decrypt file', 0, $e);
159
        }
160
161
        // Verify the integrity of the decrypted file checking the checksum against the checksum of the original source file
162 2
        if (!$this->verifyChecksum($target, $encryptedFile->getChecksum())) {
163 2
            unlink($target);
164 2
            throw new DecryptException('Invalid checksum on decrypted file');
165
        }
166
167 2
        return $target;
168
    }
169
170
    /**
171
     * Decrypts a file in a stream, performing the callback on each successive decrypted block.
172
     * If the checksum is provided it checks it against the encrypted file for integrity.
173
     *
174
     * The callback can accept two arguments:
175
     *  - $data   - A chunk of decrypted data
176
     *  - $stream - The resource stream that is decrypting
177
     *
178
     * @param EncryptedFile $encryptedFile
179
     * @param \Closure      $callback
180
     * @throws DecryptException
181
     */
182 2
    public function streamDecrypt(EncryptedFile $encryptedFile, \Closure $callback)
183
    {
184
        // Get the path to the encrypted file
185 2
        $source = $encryptedFile->getFile()->getRealPath();
186
187
        // Get the decryption stream
188
        try {
189 2
            $stream = $this->createDecryptionStream($source, $this->getOptions($encryptedFile->getIv()));
190 1
        } catch (\Exception $e) {
191
            throw new DecryptException('Unable to create decryption stream', 0, $e);
192
        }
193
194
        // Run the callback while the file pointer isn't at the end
195 2
        while (!feof($stream)) {
196 2
            $data = fread($stream, self::CHUNK_BYTES);
197
198
            // Trim the padded bytes off of the data once EOF has been reached
199 2
            if (feof($stream)) {
200 2
                $data = substr($data, 0, -$encryptedFile->getPadding());
201 1
            }
202
203 2
            $callback($data, $stream);
204 1
        }
205 2
    }
206
207
    /**
208
     * Encrypts the file
209
     *
210
     * @param string $source_file
211
     * @param string $target_file
212
     * @param string $iv
213
     *
214
     * @return void
215
     */
216 6
    protected function encryptFile($source_file, $target_file, $iv)
217
    {
218
        // We start by setting up both the source and target streams
219
        // and applying all the necessary stream filters for encryption.
220
221 6
        $source  = fopen($source_file, 'r');
222 6
        $target  = $this->createEncryptionStream($target_file, $this->getOptions($iv));
223
224
        // We copy the source into the target stream, passing through the encryption filter
225 6
        $this->copyStream($source, $target);
226
227
        // Close the source file and target files
228 6
        fclose($source);
229 6
        fclose($target);
230 6
    }
231
232
    /**
233
     * Decrypts a source file into a target file
234
     *
235
     * @param string $source
236
     * @param string $target
237
     * @param string $iv
238
     * @param int    $padding
239
     *
240
     * @return void
241
     */
242 2
    protected function decryptFile($source, $target, $iv, $padding = 0)
243
    {
244
        // We create a stream with a decryption filter appended to it
245 2
        $source = $this->createDecryptionStream($source, $this->getOptions($iv));
246 2
        $target = fopen($target, 'w+');
247
248
        // We copy the source into the target, decrypting it in the process
249 2
        $this->copyStream($source, $target, $padding);
250
251
        // Close both source and target
252 2
        fclose($source);
253 2
        fclose($target);
254 2
    }
255
256
    /**
257
     * Creates a stream that encrypts when written to
258
     *
259
     * @param string $target
260
     * @param array  $options
261
     *
262
     * @return resource
263
     * @throws EncryptException
264
     */
265 6
    protected function createEncryptionStream($target, array $options)
266
    {
267
        // Open up the resources to both the source and target
268 6
        $stream = fopen($target, 'w+');
269
270
        // Append the stream write filter with the correct encryption cipher
271 6
        stream_filter_append($stream, $this->getEncryptionFilterName(), STREAM_FILTER_WRITE, $options);
272
273
        // Returns the target stream
274 6
        return $stream;
275
    }
276
277
    /**
278
     * Creates a stream that is decrypted when read from
279
     *
280
     * @param string $source
281
     * @param array  $options
282
     *
283
     * @return resource
284
     */
285 4
    protected function createDecryptionStream($source, array $options)
286
    {
287 4
        $stream = fopen($source, 'rb');
288
289
        // Append the stream read filter with the decryption cipher
290 4
        stream_filter_append($stream, $this->getDecryptionFilterName(), STREAM_FILTER_READ, $options);
291
292
        // Returns the target stream
293 4
        return $stream;
294
    }
295
296
    /**
297
     * Copies a source stream to a target stream.
298
     * If the padding parameter is set it will remove said amount of bytes from the end of the file.
299
     *
300
     * This method does not use stream_copy_to_stream on purpose because this way we have more control
301
     * over the process of moving data from one stream to another.
302
     *
303
     * @param resource $source
304
     * @param resource $target
305
     * @param null|int $padding
306
     *
307
     * @return void
308
     */
309 6
    protected function copyStream($source, $target, $padding = null)
310
    {
311
        // Ensure that both pointers are at the start of the file
312 6
        while (!feof($source)) {
313 6
            $data = fread($source, self::CHUNK_BYTES);
314
315
            // If eof is reached and padding is set, remove it from the file
316 6
            if (feof($source) && (int) $padding > 0) {
317 2
                $data = substr($data, 0, -$padding);
318 1
            }
319
320 6
            fwrite($target, $data);
321 3
        }
322 6
    }
323
324
    /**
325
     * Returns the options for the stream filter
326
     * @param string $iv
327
     *
328
     * @return array Returns an array with 'mode','key' and 'iv'
329
     */
330 6
    protected function getOptions($iv)
331
    {
332
        return [
333 6
            'mode' => $this->mode,
334 6
            'key'  => $this->key,
335 6
            'iv'   => $iv,
336 3
        ];
337
    }
338
339
    /**
340
     * Get the IV size for the cipher.
341
     *
342
     * @return int
343
     */
344 6
    protected function getIvSize()
345
    {
346 6
        return mcrypt_get_iv_size($this->cipher, $this->mode);
347
    }
348
349
    /**
350
     * Returns the encryption cipher for the stream filter
351
     *
352
     * @return string
353
     */
354 6
    protected function getEncryptionFilterName()
355
    {
356 6
        return 'mcrypt.'. $this->cipher;
357
    }
358
359
    /**
360
     * Returns the decryption cipher for the stream filter
361
     *
362
     * @return string
363
     */
364 4
    protected function getDecryptionFilterName()
365
    {
366 4
        return 'mdecrypt.'. $this->cipher;
367
    }
368
369
    /**
370
     * Get the random data source available for the OS.
371
     *
372
     * @return int
373
     */
374 6
    protected function getRandomizer()
375
    {
376 6
        if (defined('MCRYPT_DEV_URANDOM')) {
377 6
            return MCRYPT_DEV_URANDOM;
378
        }
379
380
        if (defined('MCRYPT_DEV_RANDOM')) {
381
            return MCRYPT_DEV_RANDOM;
382
        }
383
384
        mt_srand();
385
386
        return MCRYPT_RAND;
387
    }
388
389
    /**
390
     * Compares the given checksum with the actual file checksum.
391
     * Returns true if they match, false if not
392
     *
393
     * @param string $file
394
     * @param string $checksum
395
     * @return bool
396
     */
397 2
    protected function verifyChecksum($file, $checksum)
398
    {
399 2
        return strcmp($checksum, $this->calculateChecksum($file)) === 0;
400
    }
401
402
    /**
403
     * Update the block size for the current cipher and mode.
404
     *
405
     * @return void
406
     */
407 12
    protected function updateBlockSize()
408
    {
409 12
        $this->block = mcrypt_get_iv_size($this->cipher, $this->mode);
410 12
    }
411
412
    /**
413
     * Calculates the padding that was added during encryption
414
     *
415
     * @param string $source Path the the source file
416
     * @param string $target Path to the target file
417
     * @return int
418
     */
419 6
    protected function calculatePadding($source, $target)
420
    {
421 6
        return filesize($target) - filesize($source);
422
    }
423
424
    /**
425
     * Calculates the checksum of the file
426
     *
427
     * @param string $file
428
     * @return string
429
     */
430 6
    protected function calculateChecksum($file)
431
    {
432 6
        return sha1_file($file);
433
    }
434
}
435