Passed
Push — master ( f4fa40...82755f )
by Thomas
02:30
created

EncryptHelper   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 380
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
wmc 50
eloc 115
c 3
b 0
f 1
dl 0
loc 380
rs 8.4

26 Methods

Rating   Name   Duplication   Size   Complexity  
A generateKey() 0 3 1
A getEngineForEncryption() 0 3 1
A setForcedEncryption() 0 3 1
A isNacl() 0 6 2
A getProviderWithKey() 0 4 1
A getBackendForEncryption() 0 14 5
A setAutomaticRotation() 0 3 1
A getEncryptedFields() 0 16 4
A isFips() 0 6 2
A getCipherSweet() 0 13 3
A isEncrypted() 0 4 1
A removeNulls() 0 8 3
A isBoring() 0 6 2
A isEncryptedField() 0 16 3
A getKey() 0 8 2
A decrypt() 0 10 2
A encrypt() 0 10 2
A coincidenceCount() 0 8 2
A getRecommendedBackend() 0 6 2
A sendEncryptedFile() 0 7 1
A getEncryption() 0 12 4
A mapEncryptionDBField() 0 6 1
A getForcedEncryption() 0 3 1
A getEngine() 0 4 1
A getCipherSweetBackend() 0 3 1
A getAutomaticRotation() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like EncryptHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EncryptHelper, and based on these observations, apply Extract Interface, too.

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