Completed
Push — master ( 7f9ddb...20af2b )
by Christoper
04:26 queued 02:19
created

FileEncrypter   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 408
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 91.67%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 32
c 1
b 0
f 1
lcom 1
cbo 3
dl 0
loc 408
ccs 88
cts 96
cp 0.9167
rs 9.6

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 encryptFile() 0 15 1
A decryptFile() 0 13 1
A createEncryptionStream() 0 11 1
A createDecryptionStream() 0 10 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
A encrypt() 0 18 2
A decrypt() 0 19 3
A streamDecrypt() 0 18 3
A copyStream() 0 14 4
A getOptions() 0 12 2
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 2
        } 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
            $iv,
129 6
            $this->calculateChecksum($source),
130 6
            $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 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
        } 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 2
        if (!$this->verifyChecksum($target, $encryptedFile->getChecksum())) {
159 2
            unlink($target);
160 2
            throw new DecryptException('Invalid checksum on decrypted file');
161
        }
162
163 2
        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 2
    public function streamDecrypt(EncryptedFile $encryptedFile, \Closure $callback)
179
    {
180
        // Get the path to the encrypted file
181 2
        $source = $encryptedFile->getFile()->getRealPath();
182
183
        // Check if callback is a closure
184 2
        if (!($callback instanceof \Closure)) {
185
            throw new DecryptException('Callback must be callable');
186
        }
187
188
        // Get the decryption stream
189 2
        $stream = $this->createDecryptionStream($source, $this->getOptions($encryptedFile->getIv()));
190
191
        // Run the callback while the file pointer isn't at the end
192 2
        while (!feof($stream)) {
193 2
            $callback(fread($stream, self::CHUNK_BYTES), $stream);
194
        }
195 2
    }
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 6
    protected function encryptFile($source_file, $target_file, $iv)
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 6
        $source  = fopen($source_file, 'r');
212 6
        $target  = $this->createEncryptionStream($target_file, $this->getOptions($iv));
213
214
        // We copy the source into the target stream, passing through the encryption filter
215 6
        $this->copyStream($source, $target);
216
217
        // Close the source file and target files
218 6
        fclose($source);
219 6
        fclose($target);
220 6
    }
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 2
    protected function decryptFile($source, $target, $iv, $padding = 0)
233
    {
234
        // We create a stream with a decryption filter appended to it
235 2
        $source = $this->createDecryptionStream($source, $this->getOptions($iv));
236 2
        $target = fopen($target, 'w+');
237
238
        // We copy the source into the target, decrypting it in the process
239 2
        $this->copyStream($source, $target, $padding);
240
241
        // Close both source and target
242 2
        fclose($source);
243 2
        fclose($target);
244 2
    }
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 6
    protected function createEncryptionStream($target, array $options)
256
    {
257
        // Open up the resources to both the source and target
258 6
        $stream = fopen($target, 'w+');
259
260
        // Append the stream write filter with the correct encryption cipher
261 6
        stream_filter_append($stream, $this->getEncryptionFilterName(), STREAM_FILTER_WRITE, $options);
262
263
        // Returns the target stream
264 6
        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 4
    protected function createDecryptionStream($source, array $options)
276
    {
277 4
        $stream = fopen($source, 'rb');
278
279
        // Append the stream read filter with the decryption cipher
280 4
        stream_filter_append($stream, $this->getDecryptionFilterName(), STREAM_FILTER_READ, $options);
281
282
        // Returns the target stream
283 4
        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 6
    protected function copyStream($source, $target, $padding = null)
300
    {
301
        // Ensure that both pointers are at the start of the file
302 6
        while (!feof($source)) {
303 6
            $data = fread($source, self::CHUNK_BYTES);
304
305
            // If eof is reached and padding is set, remove it from the file
306 6
            if (feof($source) && (int) $padding > 0) {
307 2
                $data = substr($data, 0, -$padding);
308
            }
309
310 6
            fwrite($target, $data);
311
        }
312 6
    }
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 6
    protected function getOptions($iv = null)
321
    {
322 6
        if ($iv === null) {
323
            mcrypt_create_iv($this->getIvSize(), $this->getRandomizer());
324
        }
325
326
        return [
327 6
            'mode' => $this->mode,
328 6
            'key'  => $this->key,
329 6
            'iv'   => $iv,
330
        ];
331
    }
332
333
    /**
334
     * Get the IV size for the cipher.
335
     *
336
     * @return int
337
     */
338 6
    protected function getIvSize()
339
    {
340 6
        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 6
    protected function getEncryptionFilterName()
349
    {
350 6
        return 'mcrypt.'. $this->cipher;
351
    }
352
353
    /**
354
     * Returns the decryption cipher for the stream filter
355
     *
356
     * @return string
357
     */
358 4
    protected function getDecryptionFilterName()
359
    {
360 4
        return 'mdecrypt.'. $this->cipher;
361
    }
362
363
    /**
364
     * Get the random data source available for the OS.
365
     *
366
     * @return int
367
     */
368 6
    protected function getRandomizer()
369
    {
370 6
        if (defined('MCRYPT_DEV_URANDOM')) {
371 6
            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 2
    protected function verifyChecksum($file, $checksum)
392
    {
393 2
        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 12
    protected function updateBlockSize()
402
    {
403 12
        $this->block = mcrypt_get_iv_size($this->cipher, $this->mode);
404 12
    }
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 6
    protected function calculatePadding($source, $target)
414
    {
415 6
        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 6
    protected function calculateChecksum($file)
425
    {
426 6
        return sha1_file($file);
427
    }
428
}
429