Passed
Push — master ( f8669d...f4fa40 )
by Thomas
03:19
created

EncryptHelper::coincidenceCount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 5
c 1
b 0
f 1
dl 0
loc 8
rs 10
cc 2
nc 2
nop 2
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
     * @param string $forcedEncryption brng|nacl|fips
47
     * @return void
48
     */
49
    public static function setForcedEncryption($forcedEncryption)
50
    {
51
        self::$forcedEncryption = $forcedEncryption;
52
    }
53
54
    /**
55
     * Attempting to pass a key of an invalid size (i.e. not 256-bit) will result in a CryptoOperationException being thrown.
56
     * The recommended way to generate a key is to use this method
57
     *
58
     * @return string Something like 4e1c44f87b4cdf21808762970b356891db180a9dd9850e7baf2a79ff3ab8a2fc
59
     */
60
    public static function generateKey()
61
    {
62
        return Hex::encode(random_bytes(32));
63
    }
64
65
    /**
66
     * Get app encryption key
67
     * Encryption key should be provided in your $_ENV or .env file
68
     *
69
     * @return string
70
     */
71
    public static function getKey()
72
    {
73
        $key = Environment::getEnv('ENCRYPTION_KEY');
74
        if (!$key) {
75
            $key = self::generateKey();
76
            throw new Exception("Please define an ENCRYPTION_KEY in your environment. You can use this one: $key");
77
        }
78
        return $key;
79
    }
80
81
    /**
82
     * @return StringProvider
83
     */
84
    public static function getProviderWithKey()
85
    {
86
        return new StringProvider(
87
            self::getKey()
88
        );
89
    }
90
91
    /**
92
     * @return BackendInterface
93
     */
94
    public static function getRecommendedBackend()
95
    {
96
        if (version_compare(phpversion(), '7.2', '<')) {
97
            return new FIPSCrypto();
98
        }
99
        return new BoringCrypto();
100
    }
101
102
    /**
103
     * @param string $encryption
104
     * @return BackendInterface
105
     */
106
    public static function getBackendForEncryption($encryption = null)
107
    {
108
        if (!$encryption) {
109
            return self::getRecommendedBackend();
110
        }
111
        switch ($encryption) {
112
            case self::BORING:
113
                return new BoringCrypto();
114
            case self::MODERN:
115
                return new ModernCrypto();
116
            case self::FIPS:
117
                return new FIPSCrypto();
118
        }
119
        throw new Exception("Unsupported encryption $encryption");
120
    }
121
122
123
    /**
124
     * @return CipherSweet
125
     */
126
    public static function getCipherSweet()
127
    {
128
        if (self::$ciphersweet) {
129
            return self::$ciphersweet;
130
        }
131
        $provider = self::getProviderWithKey();
132
        if (self::$forcedEncryption) {
133
            $backend = self::getBackendForEncryption(self::$forcedEncryption);
134
        } else {
135
            $backend = self::getRecommendedBackend();
136
        }
137
        self::$ciphersweet = new CipherSweet($provider, $backend);
138
        return self::$ciphersweet;
139
    }
140
141
    /**
142
     * @return BackendInterface
143
     */
144
    public static function getCipherSweetBackend()
145
    {
146
        return self::getCipherSweet()->getBackend();
147
    }
148
149
    /**
150
     * Check if a value is encrypted
151
     *
152
     * @param string $value
153
     * @return boolean
154
     */
155
    public static function isEncrypted($value)
156
    {
157
        $prefix = substr($value, 0, 5);
158
        return in_array($prefix, ["brng:", "nacl:", "fips:"]);
159
    }
160
161
    /**
162
     * @param string $value
163
     * @return boolean
164
     */
165
    public static function isFips($value)
166
    {
167
        if (strpos($value, 'fips:') === 0) {
168
            return true;
169
        }
170
        return false;
171
    }
172
173
    /**
174
     * @param string $value
175
     * @return boolean
176
     */
177
    public static function isNacl($value)
178
    {
179
        if (strpos($value, 'nacl:') === 0) {
180
            return true;
181
        }
182
        return false;
183
    }
184
185
    /**
186
     * @param string $value
187
     * @return boolean
188
     */
189
    public static function isBoring($value)
190
    {
191
        if (strpos($value, 'brng:') === 0) {
192
            return true;
193
        }
194
        return false;
195
    }
196
197
    /**
198
     * @param string $value
199
     * @return string
200
     */
201
    public static function getEncryption($value)
202
    {
203
        if (self::isBoring($value)) {
204
            return self::BORING;
205
        }
206
        if (self::isNacl($value)) {
207
            return self::MODERN;
208
        }
209
        if (self::isFips($value)) {
210
            return self::FIPS;
211
        }
212
        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...
213
    }
214
215
    /**
216
     * Check if a field is encrypted on a class
217
     * This relies on a field class starting with Encrypted
218
     *
219
     * @param string $class
220
     * @param string $field
221
     * @return boolean
222
     */
223
    public static function isEncryptedField($class, $field)
224
    {
225
        $key = $class . '_' . $field;
226
        if (isset(self::$field_cache[$key])) {
227
            return self::$field_cache[$key];
228
        }
229
230
        $fields = $class::config()->db;
231
232
        if (isset($fields[$field])) {
233
            $dbClass = $fields[$field];
234
            self::$field_cache[$key] = strpos($dbClass, 'Encrypted') !== false;
235
        } else {
236
            self::$field_cache[$key] = false;
237
        }
238
        return self::$field_cache[$key];
239
    }
240
241
    /**
242
     * A simple encryption
243
     * @param string $value
244
     * @return string
245
     */
246
    public static function encrypt($value)
247
    {
248
        // Do not encrypt twice
249
        $encryption = self::getEncryption($value);
250
        if ($encryption) {
251
            return $value;
252
        }
253
        $provider = self::getProviderWithKey();
254
        $backend = self::getBackendForEncryption($encryption);
255
        return $backend->encrypt($value, $provider->getSymmetricKey());
256
    }
257
258
    /**
259
     * A simple decryption
260
     * @param string $value
261
     * @return string
262
     */
263
    public static function decrypt($value)
264
    {
265
        // Only decrypt what we can decrypt
266
        $encryption = self::getEncryption($value);
267
        if (!$encryption) {
268
            return $value;
269
        }
270
        $provider = self::getProviderWithKey();
271
        $backend =  self::getBackendForEncryption($encryption);
272
        return $backend->decrypt($value, $provider->getSymmetricKey());
273
    }
274
275
    /**
276
     * Return a map of fields with their encrypted counterpart
277
     *
278
     * @return array
279
     */
280
    public static function mapEncryptionDBField()
281
    {
282
        return [
283
            DBHTMLText::class => EncryptedDBHTMLText::class,
284
            DBText::class => EncryptedDBText::class,
285
            DBVarchar::class => EncryptedDBVarchar::class,
286
        ];
287
    }
288
289
    /**
290
     * Compute Blind Index Information Leaks
291
     *
292
     * @link https://ciphersweet.paragonie.com/security
293
     * @param array $indexes
294
     * @param int $R
295
     * @return float
296
     */
297
    public static function coincidenceCount(array $indexes, $R)
298
    {
299
        $exponent = 0;
300
        $count = count($indexes);
301
        for ($i = 0; $i < $count; ++$i) {
302
            $exponent += min($indexes[$i]['L'], $indexes[$i]['K']);
303
        }
304
        return (float) max(1, $R) / pow(2, $exponent);
305
    }
306
307
    /**
308
     * Send a decrypted file
309
     *
310
     * @param File $file
311
     * @return void
312
     */
313
    public static function sendEncryptedFile(File $file)
314
    {
315
        header('Content-disposition: attachment; filename="' . basename($file->getFilename()) . '"');
316
        header('Content-type: application/octetstream');
317
        header('Pragma: no-cache');
318
        header('Expires: 0');
319
        $file->sendDecryptedFile();
320
    }
321
}
322