Passed
Push — master ( 82755f...3bd84c )
by Thomas
01:56
created

EncryptHelper::clearCipherSweet()   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
     * @return bool
74
     */
75
    public static function getAutomaticRotation()
76
    {
77
        return self::$automaticRotation;
78
    }
79
80
    /**
81
     * @param bool $setAutomaticRotation
82
     * @return void
83
     */
84
    public static function setAutomaticRotation($automaticRotation)
85
    {
86
        self::$automaticRotation = $automaticRotation;
87
    }
88
89
    /**
90
     * @link https://github.com/paragonie/ciphersweet/issues/62
91
     * @param array $ciphertext
92
     * @return array
93
     */
94
    public static function removeNulls($ciphertext)
95
    {
96
        foreach ($ciphertext as $k => $v) {
97
            if ($v === null) {
98
                $ciphertext[$k] = '';
99
            }
100
        }
101
        return $ciphertext;
102
    }
103
104
    /**
105
     * Attempting to pass a key of an invalid size (i.e. not 256-bit) will result in a CryptoOperationException being thrown.
106
     * The recommended way to generate a key is to use this method
107
     *
108
     * @return string Something like 4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc
109
     */
110
    public static function generateKey()
111
    {
112
        return Hex::encode(random_bytes(32));
113
    }
114
115
    /**
116
     * Get app encryption key
117
     * Encryption key should be provided in your $_ENV or .env file
118
     *
119
     * @return string
120
     */
121
    public static function getKey()
122
    {
123
        $key = Environment::getEnv('ENCRYPTION_KEY');
124
        if (!$key) {
125
            $key = self::generateKey();
126
            throw new Exception("Please define an ENCRYPTION_KEY in your environment. You can use this one: $key");
127
        }
128
        return $key;
129
    }
130
131
    /**
132
     * @return StringProvider
133
     */
134
    public static function getProviderWithKey()
135
    {
136
        return new StringProvider(
137
            self::getKey()
138
        );
139
    }
140
141
    /**
142
     * @return BackendInterface
143
     */
144
    public static function getRecommendedBackend()
145
    {
146
        if (version_compare(phpversion(), '7.2', '<')) {
147
            return new FIPSCrypto();
148
        }
149
        return new BoringCrypto();
150
    }
151
152
    /**
153
     * @param string $encryption
154
     * @return BackendInterface
155
     */
156
    public static function getBackendForEncryption($encryption = null)
157
    {
158
        if (!$encryption) {
159
            return self::getRecommendedBackend();
160
        }
161
        switch ($encryption) {
162
            case self::BORING:
163
                return new BoringCrypto();
164
            case self::MODERN:
165
                return new ModernCrypto();
166
            case self::FIPS:
167
                return new FIPSCrypto();
168
        }
169
        throw new Exception("Unsupported encryption $encryption");
170
    }
171
172
    /**
173
     * @param BackendInterface $backend
174
     * @return CipherSweet
175
     */
176
    public static function getEngineForEncryption($encryption = null)
177
    {
178
        return self::getEngine(self::getBackendForEncryption($encryption));
179
    }
180
181
    /**
182
     * @param BackendInterface $backend
183
     * @return CipherSweet
184
     */
185
    public static function getEngine(BackendInterface $backend)
186
    {
187
        $provider = self::getProviderWithKey();
188
        return new CipherSweet($provider, $backend);
189
    }
190
191
    /**
192
     * @return CipherSweet
193
     */
194
    public static function getCipherSweet()
195
    {
196
        if (self::$ciphersweet) {
197
            return self::$ciphersweet;
198
        }
199
        $provider = self::getProviderWithKey();
200
        if (self::$forcedEncryption) {
201
            $backend = self::getBackendForEncryption(self::$forcedEncryption);
202
        } else {
203
            $backend = self::getRecommendedBackend();
204
        }
205
        self::$ciphersweet = new CipherSweet($provider, $backend);
206
        return self::$ciphersweet;
207
    }
208
209
    /**
210
     * @return void
211
     */
212
    public static function clearCipherSweet()
213
    {
214
        self::$ciphersweet = null;
215
    }
216
217
    /**
218
     * @return BackendInterface
219
     */
220
    public static function getCipherSweetBackend()
221
    {
222
        return self::getCipherSweet()->getBackend();
223
    }
224
225
    /**
226
     * Check if a value is encrypted
227
     *
228
     * @param string $value
229
     * @return boolean
230
     */
231
    public static function isEncrypted($value)
232
    {
233
        $prefix = substr($value, 0, 5);
234
        return in_array($prefix, ["brng:", "nacl:", "fips:"]);
235
    }
236
237
    /**
238
     * @param string $value
239
     * @return boolean
240
     */
241
    public static function isFips($value)
242
    {
243
        if (strpos($value, 'fips:') === 0) {
244
            return true;
245
        }
246
        return false;
247
    }
248
249
    /**
250
     * @param string $value
251
     * @return boolean
252
     */
253
    public static function isNacl($value)
254
    {
255
        if (strpos($value, 'nacl:') === 0) {
256
            return true;
257
        }
258
        return false;
259
    }
260
261
    /**
262
     * @param string $value
263
     * @return boolean
264
     */
265
    public static function isBoring($value)
266
    {
267
        if (strpos($value, 'brng:') === 0) {
268
            return true;
269
        }
270
        return false;
271
    }
272
273
    /**
274
     * @param string $value
275
     * @return string
276
     */
277
    public static function getEncryption($value)
278
    {
279
        if (self::isBoring($value)) {
280
            return self::BORING;
281
        }
282
        if (self::isNacl($value)) {
283
            return self::MODERN;
284
        }
285
        if (self::isFips($value)) {
286
            return self::FIPS;
287
        }
288
        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...
289
    }
290
291
    /**
292
     * Check if a field is encrypted on a class
293
     * This relies on a field class starting with Encrypted
294
     *
295
     * @param string $class
296
     * @param string $field
297
     * @return boolean
298
     */
299
    public static function isEncryptedField($class, $field)
300
    {
301
        $key = $class . '_' . $field;
302
        if (isset(self::$field_cache[$key])) {
303
            return self::$field_cache[$key];
304
        }
305
306
        $fields = $class::config()->db;
307
308
        if (isset($fields[$field])) {
309
            $dbClass = $fields[$field];
310
            self::$field_cache[$key] = strpos($dbClass, 'Encrypted') !== false;
311
        } else {
312
            self::$field_cache[$key] = false;
313
        }
314
        return self::$field_cache[$key];
315
    }
316
317
    /**
318
     * @param string $class
319
     * @param bool $dbFields
320
     * @return array
321
     */
322
    public static function getEncryptedFields($class, $dbFields = false)
323
    {
324
        $fields = $class::config()->db;
325
        $list = [];
326
        foreach ($fields as $field => $dbClass) {
327
            $key = $class . '_' . $field;
328
            if (isset($fields[$field])) {
329
                self::$field_cache[$key] = strpos($dbClass, 'Encrypted') !== false;
330
                if (self::$field_cache[$key]) {
331
                    if ($dbFields && is_subclass_of($dbClass, DBComposite::class)) {
332
                        $list[] = $field . "Value";
333
                    } else {
334
                        $list[] = $field;
335
                    }
336
                }
337
            } else {
338
                self::$field_cache[$key] = false;
339
            }
340
        }
341
        return $list;
342
    }
343
344
    /**
345
     * A simple encryption
346
     * @param string $value
347
     * @return string
348
     */
349
    public static function encrypt($value)
350
    {
351
        // Do not encrypt twice
352
        $encryption = self::getEncryption($value);
353
        if ($encryption) {
354
            return $value;
355
        }
356
        $provider = self::getProviderWithKey();
357
        $backend = self::getBackendForEncryption($encryption);
358
        return $backend->encrypt($value, $provider->getSymmetricKey());
359
    }
360
361
    /**
362
     * A simple decryption
363
     * @param string $value
364
     * @return string
365
     */
366
    public static function decrypt($value)
367
    {
368
        // Only decrypt what we can decrypt
369
        $encryption = self::getEncryption($value);
370
        if (!$encryption) {
371
            return $value;
372
        }
373
        $provider = self::getProviderWithKey();
374
        $backend =  self::getBackendForEncryption($encryption);
375
        return $backend->decrypt($value, $provider->getSymmetricKey());
376
    }
377
378
    /**
379
     * Return a map of fields with their encrypted counterpart
380
     *
381
     * @return array
382
     */
383
    public static function mapEncryptionDBField()
384
    {
385
        return [
386
            DBHTMLText::class => EncryptedDBHTMLText::class,
387
            DBText::class => EncryptedDBText::class,
388
            DBVarchar::class => EncryptedDBVarchar::class,
389
        ];
390
    }
391
392
    /**
393
     * Compute Blind Index Information Leaks
394
     *
395
     * @link https://ciphersweet.paragonie.com/security
396
     * @param array $indexes
397
     * @param int $R
398
     * @return float
399
     */
400
    public static function coincidenceCount(array $indexes, $R)
401
    {
402
        $exponent = 0;
403
        $count = count($indexes);
404
        for ($i = 0; $i < $count; ++$i) {
405
            $exponent += min($indexes[$i]['L'], $indexes[$i]['K']);
406
        }
407
        return (float) max(1, $R) / pow(2, $exponent);
408
    }
409
410
    /**
411
     * Send a decrypted file
412
     *
413
     * @param File $file
414
     * @return void
415
     */
416
    public static function sendEncryptedFile(File $file)
417
    {
418
        header('Content-disposition: attachment; filename="' . basename($file->getFilename()) . '"');
419
        header('Content-type: application/octetstream');
420
        header('Pragma: no-cache');
421
        header('Expires: 0');
422
        $file->sendDecryptedFile();
423
    }
424
}
425