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) |
|
|
|
|
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) |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.