Completed
Push — develop ( c76e7d...dfe9a5 )
by Christoper
08:43
created

FileEncrypter::streamDecrypt()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0073

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 0
loc 25
ccs 12
cts 13
cp 0.9231
rs 8.5806
cc 4
eloc 10
nc 4
nop 2
crap 4.0073
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 (\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
            $data = fread($stream, self::CHUNK_BYTES);
194
195
            // Trim the padded bytes off of the data once EOF has been reached
196 2
            if (feof($stream)) {
197 2
                $data = substr($data, 0, -$encryptedFile->getPadding());
198 1
            }
199
200 2
            $callback($data, $stream);
201 1
        }
202 2
    }
203
204
    /**
205
     * Encrypts the file
206
     *
207
     * @param string $source_file
208
     * @param string $target_file
209
     * @param string $iv
210
     *
211
     * @return void
212
     */
213 6
    protected function encryptFile($source_file, $target_file, $iv)
214
    {
215
        // We start by setting up both the source and target streams
216
        // and applying all the necessary stream filters for encryption.
217
218 6
        $source  = fopen($source_file, 'r');
219 6
        $target  = $this->createEncryptionStream($target_file, $this->getOptions($iv));
220
221
        // We copy the source into the target stream, passing through the encryption filter
222 6
        $this->copyStream($source, $target);
223
224
        // Close the source file and target files
225 6
        fclose($source);
226 6
        fclose($target);
227 6
    }
228
229
    /**
230
     * Decrypts a source file into a target file
231
     *
232
     * @param string $source
233
     * @param string $target
234
     * @param string $iv
235
     * @param int    $padding
236
     *
237
     * @return void
238
     */
239 2
    protected function decryptFile($source, $target, $iv, $padding = 0)
240
    {
241
        // We create a stream with a decryption filter appended to it
242 2
        $source = $this->createDecryptionStream($source, $this->getOptions($iv));
243 2
        $target = fopen($target, 'w+');
244
245
        // We copy the source into the target, decrypting it in the process
246 2
        $this->copyStream($source, $target, $padding);
247
248
        // Close both source and target
249 2
        fclose($source);
250 2
        fclose($target);
251 2
    }
252
253
    /**
254
     * Creates a stream that encrypts when written to
255
     *
256
     * @param string $target
257
     * @param array  $options
258
     *
259
     * @return resource
260
     * @throws EncryptException
261
     */
262 6
    protected function createEncryptionStream($target, array $options)
263
    {
264
        // Open up the resources to both the source and target
265 6
        $stream = fopen($target, 'w+');
266
267
        // Append the stream write filter with the correct encryption cipher
268 6
        stream_filter_append($stream, $this->getEncryptionFilterName(), STREAM_FILTER_WRITE, $options);
269
270
        // Returns the target stream
271 6
        return $stream;
272
    }
273
274
    /**
275
     * Creates a stream that is decrypted when read from
276
     *
277
     * @param string $source
278
     * @param array  $options
279
     *
280
     * @return resource
281
     */
282 4
    protected function createDecryptionStream($source, array $options)
283
    {
284 4
        $stream = fopen($source, 'rb');
285
286
        // Append the stream read filter with the decryption cipher
287 4
        stream_filter_append($stream, $this->getDecryptionFilterName(), STREAM_FILTER_READ, $options);
288
289
        // Returns the target stream
290 4
        return $stream;
291
    }
292
293
    /**
294
     * Copies a source stream to a target stream.
295
     * If the padding parameter is set it will remove said amount of bytes from the end of the file.
296
     *
297
     * This method does not use stream_copy_to_stream on purpose because this way we have more control
298
     * over the process of moving data from one stream to another.
299
     *
300
     * @param resource $source
301
     * @param resource $target
302
     * @param null|int $padding
303
     *
304
     * @return void
305
     */
306 6
    protected function copyStream($source, $target, $padding = null)
307
    {
308
        // Ensure that both pointers are at the start of the file
309 6
        while (!feof($source)) {
310 6
            $data = fread($source, self::CHUNK_BYTES);
311
312
            // If eof is reached and padding is set, remove it from the file
313 6
            if (feof($source) && (int) $padding > 0) {
314 2
                $data = substr($data, 0, -$padding);
315 1
            }
316
317 6
            fwrite($target, $data);
318 3
        }
319 6
    }
320
321
    /**
322
     * Returns the options for the stream filter
323
     * @param null $iv If no IV is set, one will be created
324
     *
325
     * @return array Returns an array with 'mode','key' and 'iv'
326
     */
327 6
    protected function getOptions($iv = null)
328
    {
329 6
        if ($iv === null) {
330
            mcrypt_create_iv($this->getIvSize(), $this->getRandomizer());
331
        }
332
333
        return [
334 6
            'mode' => $this->mode,
335 6
            'key'  => $this->key,
336 6
            'iv'   => $iv,
337 3
        ];
338
    }
339
340
    /**
341
     * Get the IV size for the cipher.
342
     *
343
     * @return int
344
     */
345 6
    protected function getIvSize()
346
    {
347 6
        return mcrypt_get_iv_size($this->cipher, $this->mode);
348
    }
349
350
    /**
351
     * Returns the encryption cipher for the stream filter
352
     *
353
     * @return string
354
     */
355 6
    protected function getEncryptionFilterName()
356
    {
357 6
        return 'mcrypt.'. $this->cipher;
358
    }
359
360
    /**
361
     * Returns the decryption cipher for the stream filter
362
     *
363
     * @return string
364
     */
365 4
    protected function getDecryptionFilterName()
366
    {
367 4
        return 'mdecrypt.'. $this->cipher;
368
    }
369
370
    /**
371
     * Get the random data source available for the OS.
372
     *
373
     * @return int
374
     */
375 6
    protected function getRandomizer()
376
    {
377 6
        if (defined('MCRYPT_DEV_URANDOM')) {
378 6
            return MCRYPT_DEV_URANDOM;
379
        }
380
381
        if (defined('MCRYPT_DEV_RANDOM')) {
382
            return MCRYPT_DEV_RANDOM;
383
        }
384
385
        mt_srand();
386
387
        return MCRYPT_RAND;
388
    }
389
390
    /**
391
     * Compares the given checksum with the actual file checksum.
392
     * Returns true if they match, false if not
393
     *
394
     * @param string $file
395
     * @param string $checksum
396
     * @return bool
397
     */
398 2
    protected function verifyChecksum($file, $checksum)
399
    {
400 2
        return strcmp($checksum, $this->calculateChecksum($file)) === 0;
401
    }
402
403
    /**
404
     * Update the block size for the current cipher and mode.
405
     *
406
     * @return void
407
     */
408 12
    protected function updateBlockSize()
409
    {
410 12
        $this->block = mcrypt_get_iv_size($this->cipher, $this->mode);
411 12
    }
412
413
    /**
414
     * Calculates the padding that was added during encryption
415
     *
416
     * @param string $source Path the the source file
417
     * @param string $target Path to the target file
418
     * @return int
419
     */
420 6
    protected function calculatePadding($source, $target)
421
    {
422 6
        return filesize($target) - filesize($source);
423
    }
424
425
    /**
426
     * Calculates the checksum of the file
427
     *
428
     * @param string $file
429
     * @return string
430
     */
431 6
    protected function calculateChecksum($file)
432
    {
433 6
        return sha1_file($file);
434
    }
435
}
436