Passed
Push — master ( 3bd84c...71d23f )
by Thomas
02:36
created

EncryptHelper::getOldKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace LeKoala\Encrypt;
4
5
use Exception;
6
use League\Csv\InvalidArgument;
7
use SilverStripe\Assets\File;
8
use ParagonIE\ConstantTime\Hex;
9
use SilverStripe\Core\Environment;
10
use ParagonIE\CipherSweet\CipherSweet;
11
use SilverStripe\ORM\FieldType\DBText;
12
use SilverStripe\ORM\FieldType\DBVarchar;
13
use SilverStripe\ORM\FieldType\DBHTMLText;
14
use ParagonIE\CipherSweet\Backend\BoringCrypto;
15
use ParagonIE\CipherSweet\Backend\FIPSCrypto;
16
use ParagonIE\CipherSweet\Backend\ModernCrypto;
17
use ParagonIE\CipherSweet\Contract\BackendInterface;
18
use ParagonIE\CipherSweet\KeyProvider\StringProvider;
19
use SilverStripe\ORM\FieldType\DBComposite;
20
21
/**
22
 * @link https://ciphersweet.paragonie.com/php
23
 * @link https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql
24
 * @link https://paragonie.com/book/pecl-libsodium/read/09-recipes.md
25
 */
26
class EncryptHelper
27
{
28
    const BORING = "brng";
29
    const MODERN = "nacl";
30
    const FIPS = "fips";
31
32
    /**
33
     * @var CipherSweet
34
     */
35
    protected static $ciphersweet;
36
37
    /**
38
     * @var array
39
     */
40
    protected static $field_cache = [];
41
42
    /**
43
     * @var string
44
     */
45
    protected static $forcedEncryption = null;
46
47
    /**
48
     * @var bool
49
     */
50
    protected static $automaticRotation = true;
51
52
    /**
53
     * @return string
54
     */
55
    public static function getForcedEncryption()
56
    {
57
        return self::$forcedEncryption;
58
    }
59
60
    /**
61
     * @param string $forcedEncryption brng|nacl|fips
62
     * @return void
63
     */
64
    public static function setForcedEncryption($forcedEncryption)
65
    {
66
        if (!in_array($forcedEncryption, ["brng", "nacl", "fips"])) {
67
            throw new InvalidArgument("$forcedEncryption is not supported");
68
        }
69
        self::$forcedEncryption = $forcedEncryption;
70
    }
71
72
    /**
73
     * This would only work if you changed from algorithm
74
     * @return bool
75
     */
76
    public static function getAutomaticRotation()
77
    {
78
        return self::$automaticRotation;
79
    }
80
81
    /**
82
     * @param bool $setAutomaticRotation
83
     * @return void
84
     */
85
    public static function setAutomaticRotation($automaticRotation)
86
    {
87
        self::$automaticRotation = $automaticRotation;
88
    }
89
90
    /**
91
     * @link https://github.com/paragonie/ciphersweet/issues/62
92
     * @param array $ciphertext
93
     * @return array
94
     */
95
    public static function removeNulls($ciphertext)
96
    {
97
        foreach ($ciphertext as $k => $v) {
98
            if ($v === null) {
99
                $ciphertext[$k] = '';
100
            }
101
        }
102
        return $ciphertext;
103
    }
104
105
    /**
106
     * Attempting to pass a key of an invalid size (i.e. not 256-bit) will result in a CryptoOperationException being thrown.
107
     * The recommended way to generate a key is to use this method
108
     *
109
     * @return string Something like 4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc
110
     */
111
    public static function generateKey()
112
    {
113
        return Hex::encode(random_bytes(32));
114
    }
115
116
    /**
117
     * Get app encryption key
118
     * Encryption key should be provided in your $_ENV or .env file
119
     *
120
     * @return string
121
     */
122
    public static function getKey()
123
    {
124
        $key = Environment::getEnv('ENCRYPTION_KEY');
125
        if (!$key) {
126
            $key = self::generateKey();
127
            throw new Exception("Please define an ENCRYPTION_KEY in your environment. You can use this one: $key");
128
        }
129
        return $key;
130
    }
131
132
    /**
133
     * @return string
134
     */
135
    public static function getOldKey()
136
    {
137
        return Environment::getEnv('OLD_ENCRYPTION_KEY');
138
    }
139
140
    /**
141
     * @param string $key
142
     * @return StringProvider
143
     */
144
    public static function getProviderWithKey($key = null)
145
    {
146
        if ($key === null) {
147
            $key = self::getKey();
148
        }
149
        return new StringProvider($key);
150
    }
151
152
    /**
153
     * @return BackendInterface
154
     */
155
    public static function getRecommendedBackend()
156
    {
157
        if (version_compare(phpversion(), '7.2', '<')) {
158
            return new FIPSCrypto();
159
        }
160
        return new BoringCrypto();
161
    }
162
163
    /**
164
     * @param string $encryption
165
     * @return BackendInterface
166
     */
167
    public static function getBackendForEncryption($encryption = null)
168
    {
169
        if (!$encryption) {
170
            return self::getRecommendedBackend();
171
        }
172
        switch ($encryption) {
173
            case self::BORING:
174
                return new BoringCrypto();
175
            case self::MODERN:
176
                return new ModernCrypto();
177
            case self::FIPS:
178
                return new FIPSCrypto();
179
        }
180
        throw new Exception("Unsupported encryption $encryption");
181
    }
182
183
    /**
184
     * @param BackendInterface $backend
185
     * @param string $key
186
     * @return CipherSweet
187
     */
188
    public static function getEngineForEncryption($encryption = null, $key = null)
189
    {
190
        return self::getEngine(self::getBackendForEncryption($encryption), $key);
191
    }
192
193
    /**
194
     * @param BackendInterface $backend
195
     * @param string $key
196
     * @return CipherSweet
197
     */
198
    public static function getEngine(BackendInterface $backend, $key = null)
199
    {
200
        $provider = self::getProviderWithKey($key);
201
        return new CipherSweet($provider, $backend);
202
    }
203
204
    /**
205
     * @return CipherSweet
206
     */
207
    public static function getCipherSweet()
208
    {
209
        if (self::$ciphersweet) {
210
            return self::$ciphersweet;
211
        }
212
        $provider = self::getProviderWithKey();
213
        if (self::$forcedEncryption) {
214
            $backend = self::getBackendForEncryption(self::$forcedEncryption);
215
        } else {
216
            $backend = self::getRecommendedBackend();
217
        }
218
        self::$ciphersweet = new CipherSweet($provider, $backend);
219
        return self::$ciphersweet;
220
    }
221
222
    /**
223
     * @return void
224
     */
225
    public static function clearCipherSweet()
226
    {
227
        self::$ciphersweet = null;
228
    }
229
230
    /**
231
     * @return BackendInterface
232
     */
233
    public static function getCipherSweetBackend()
234
    {
235
        return self::getCipherSweet()->getBackend();
236
    }
237
238
    /**
239
     * Check if a value is encrypted
240
     *
241
     * @param string $value
242
     * @return boolean
243
     */
244
    public static function isEncrypted($value)
245
    {
246
        $prefix = substr($value, 0, 5);
247
        return in_array($prefix, ["brng:", "nacl:", "fips:"]);
248
    }
249
250
    /**
251
     * @param string $value
252
     * @return boolean
253
     */
254
    public static function isFips($value)
255
    {
256
        if (strpos($value, 'fips:') === 0) {
257
            return true;
258
        }
259
        return false;
260
    }
261
262
    /**
263
     * @param string $value
264
     * @return boolean
265
     */
266
    public static function isNacl($value)
267
    {
268
        if (strpos($value, 'nacl:') === 0) {
269
            return true;
270
        }
271
        return false;
272
    }
273
274
    /**
275
     * @param string $value
276
     * @return boolean
277
     */
278
    public static function isBoring($value)
279
    {
280
        if (strpos($value, 'brng:') === 0) {
281
            return true;
282
        }
283
        return false;
284
    }
285
286
    /**
287
     * @param string $value
288
     * @return string
289
     */
290
    public static function getEncryption($value)
291
    {
292
        if (self::isBoring($value)) {
293
            return self::BORING;
294
        }
295
        if (self::isNacl($value)) {
296
            return self::MODERN;
297
        }
298
        if (self::isFips($value)) {
299
            return self::FIPS;
300
        }
301
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
302
    }
303
304
    /**
305
     * Check if a field is encrypted on a class
306
     * This relies on a field class starting with Encrypted
307
     *
308
     * @param string $class
309
     * @param string $field
310
     * @return boolean
311
     */
312
    public static function isEncryptedField($class, $field)
313
    {
314
        $key = $class . '_' . $field;
315
        if (isset(self::$field_cache[$key])) {
316
            return self::$field_cache[$key];
317
        }
318
319
        $fields = $class::config()->db;
320
321
        if (isset($fields[$field])) {
322
            $dbClass = $fields[$field];
323
            self::$field_cache[$key] = strpos($dbClass, 'Encrypted') !== false;
324
        } else {
325
            self::$field_cache[$key] = false;
326
        }
327
        return self::$field_cache[$key];
328
    }
329
330
    /**
331
     * @param string $class
332
     * @param bool $dbFields
333
     * @return array
334
     */
335
    public static function getEncryptedFields($class, $dbFields = false)
336
    {
337
        $fields = $class::config()->db;
338
        $list = [];
339
        foreach ($fields as $field => $dbClass) {
340
            $key = $class . '_' . $field;
341
            if (isset($fields[$field])) {
342
                self::$field_cache[$key] = strpos($dbClass, 'Encrypted') !== false;
343
                if (self::$field_cache[$key]) {
344
                    if ($dbFields && is_subclass_of($dbClass, DBComposite::class)) {
345
                        $list[] = $field . "Value";
346
                    } else {
347
                        $list[] = $field;
348
                    }
349
                }
350
            } else {
351
                self::$field_cache[$key] = false;
352
            }
353
        }
354
        return $list;
355
    }
356
357
    /**
358
     * A simple encryption
359
     * @param string $value
360
     * @return string
361
     */
362
    public static function encrypt($value)
363
    {
364
        // Do not encrypt twice
365
        $encryption = self::getEncryption($value);
366
        if ($encryption) {
367
            return $value;
368
        }
369
        $provider = self::getProviderWithKey();
370
        $backend = self::getBackendForEncryption($encryption);
371
        return $backend->encrypt($value, $provider->getSymmetricKey());
372
    }
373
374
    /**
375
     * A simple decryption
376
     * @param string $value
377
     * @return string
378
     */
379
    public static function decrypt($value)
380
    {
381
        // Only decrypt what we can decrypt
382
        $encryption = self::getEncryption($value);
383
        if (!$encryption) {
384
            return $value;
385
        }
386
        $provider = self::getProviderWithKey();
387
        $backend =  self::getBackendForEncryption($encryption);
388
        return $backend->decrypt($value, $provider->getSymmetricKey());
389
    }
390
391
    /**
392
     * Return a map of fields with their encrypted counterpart
393
     *
394
     * @return array
395
     */
396
    public static function mapEncryptionDBField()
397
    {
398
        return [
399
            DBHTMLText::class => EncryptedDBHTMLText::class,
400
            DBText::class => EncryptedDBText::class,
401
            DBVarchar::class => EncryptedDBVarchar::class,
402
        ];
403
    }
404
405
    /**
406
     * Compute Blind Index Information Leaks
407
     *
408
     * @link https://ciphersweet.paragonie.com/security
409
     * @param array $indexes
410
     * @param int $R
411
     * @return float
412
     */
413
    public static function coincidenceCount(array $indexes, $R)
414
    {
415
        $exponent = 0;
416
        $count = count($indexes);
417
        for ($i = 0; $i < $count; ++$i) {
418
            $exponent += min($indexes[$i]['L'], $indexes[$i]['K']);
419
        }
420
        return (float) max(1, $R) / pow(2, $exponent);
421
    }
422
423
    /**
424
     * Send a decrypted file
425
     *
426
     * @param File $file
427
     * @return void
428
     */
429
    public static function sendEncryptedFile(File $file)
430
    {
431
        header('Content-disposition: attachment; filename="' . basename($file->getFilename()) . '"');
432
        header('Content-type: application/octetstream');
433
        header('Pragma: no-cache');
434
        header('Expires: 0');
435
        $file->sendDecryptedFile();
436
    }
437
}
438