Completed
Branch barista (01af75)
by
unknown
60:31 queued 51:15
created

EE_Encryption::acme_decrypt()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 1
dl 0
loc 26
rs 9.1928
c 0
b 0
f 0
1
<?php
2
3
use EventEspresso\core\interfaces\InterminableInterface;
4
5
// phpcs:disable PHPCompatibility.PHP.DeprecatedFunctions.mcrypt_get_iv_sizeDeprecatedRemoved
6
// phpcs:disable PHPCompatibility.PHP.RemovedConstants.mcrypt_rijndael_256DeprecatedRemoved
7
// phpcs:disable PHPCompatibility.PHP.RemovedConstants.mcrypt_mode_ecbDeprecatedRemoved
8
// phpcs:disable PHPCompatibility.PHP.DeprecatedFunctions.mcrypt_create_ivDeprecatedRemoved
9
// phpcs:disable PHPCompatibility.PHP.RemovedConstants.mcrypt_randDeprecatedRemoved
10
// phpcs:disable PHPCompatibility.PHP.DeprecatedFunctions.mcrypt_encryptDeprecatedRemoved
11
// phpcs:disable PHPCompatibility.PHP.DeprecatedFunctions.mcrypt_decryptDeprecatedRemoved
12
13
// mcrypt methods are removed in php7.2 but we have a condition in this class that only uses them if they are available.
14
15
/**
16
 * EE_Encryption class
17
 * class for applying low-grade string encryption/decryption
18
 * really only good for hiding content from simple bots and script kiddies
19
 * but better for solving encoding issues with databases
20
 *
21
 * @package    Event Espresso
22
 * @subpackage includes/functions
23
 * @author     Brent Christensen
24
 */
25
class EE_Encryption implements InterminableInterface
26
{
27
28
    /**
29
     * key used for saving the encryption key to the wp_options table
30
     */
31
    const ENCRYPTION_OPTION_KEY = 'ee_encryption_key';
32
33
    /**
34
     * the OPENSSL cipher method used
35
     */
36
    const OPENSSL_CIPHER_METHOD = 'AES-128-CBC';
37
38
    /**
39
     * WP "options_name" used to store a verified available cipher method
40
     */
41
    const OPENSSL_CIPHER_METHOD_OPTION_NAME = 'ee_openssl_cipher_method';
42
43
    /**
44
     * the OPENSSL digest method used
45
     */
46
    const OPENSSL_DIGEST_METHOD = 'sha512';
47
48
    /**
49
     * separates the encrypted text from the initialization vector
50
     */
51
    const OPENSSL_IV_DELIMITER = ':iv:';
52
53
    /**
54
     * appended to text encrypted using the acme encryption
55
     */
56
    const ACME_ENCRYPTION_FLAG = '::ae';
57
58
59
    /**
60
     * instance of the EE_Encryption object
61
     */
62
    protected static $_instance;
63
64
    /**
65
     * @var string $_encryption_key
66
     */
67
    protected $_encryption_key;
68
69
    /**
70
     * @var string $cipher_method
71
     */
72
    private $cipher_method = '';
73
74
    /**
75
     * @var array $cipher_methods
76
     */
77
    private $cipher_methods = array();
78
79
    /**
80
     * @var array $digest_methods
81
     */
82
    private $digest_methods = array();
83
84
    /**
85
     * @var boolean $_use_openssl_encrypt
86
     */
87
    protected $_use_openssl_encrypt = false;
88
89
    /**
90
     * @var boolean $_use_base64_encode
91
     */
92
    protected $_use_base64_encode = false;
93
94
95
    /**
96
     * protected constructor to prevent direct creation
97
     */
98
    protected function __construct()
99
    {
100
        if (! defined('ESPRESSO_ENCRYPT')) {
101
            define('ESPRESSO_ENCRYPT', true);
102
        }
103
        if (extension_loaded('openssl')) {
104
            $this->_use_openssl_encrypt = true;
105
        }
106
        if (function_exists('base64_encode')) {
107
            $this->_use_base64_encode = true;
108
        }
109
    }
110
111
112
    /**
113
     * singleton method used to instantiate class object
114
     *
115
     * @return EE_Encryption
116
     */
117
    public static function instance()
118
    {
119
        // check if class object is instantiated
120
        if (! EE_Encryption::$_instance instanceof EE_Encryption) {
121
            EE_Encryption::$_instance = new self();
122
        }
123
        return EE_Encryption::$_instance;
124
    }
125
126
127
    /**
128
     * get encryption key
129
     *
130
     * @return string
131
     */
132
    public function get_encryption_key()
133
    {
134
        // if encryption key has not been set
135
        if (empty($this->_encryption_key)) {
136
            // retrieve encryption_key from db
137
            $this->_encryption_key = get_option(EE_Encryption::ENCRYPTION_OPTION_KEY, '');
138
            // WHAT?? No encryption_key in the db ??
139
            if ($this->_encryption_key === '') {
140
                // let's make one. And md5 it to make it just the right size for a key
141
                $new_key = md5($this->generate_random_string());
142
                // now save it to the db for later
143
                add_option(EE_Encryption::ENCRYPTION_OPTION_KEY, $new_key);
144
                // here's the key - FINALLY !
145
                $this->_encryption_key = $new_key;
146
            }
147
        }
148
        return $this->_encryption_key;
149
    }
150
151
152
    /**
153
     * encrypts data
154
     *
155
     * @param string $text_string - the text to be encrypted
156
     * @return string
157
     * @throws RuntimeException
158
     */
159
    public function encrypt($text_string = '')
160
    {
161
        // you give me nothing??? GET OUT !
162
        if (empty($text_string)) {
163
            return $text_string;
164
        }
165
        if ($this->_use_openssl_encrypt) {
166
            $encrypted_text = $this->openssl_encrypt($text_string);
167
        } else {
168
            $encrypted_text = $this->acme_encrypt($text_string);
169
        }
170
        return $encrypted_text;
171
    }
172
173
174
    /**
175
     * decrypts data
176
     *
177
     * @param string $encrypted_text - the text to be decrypted
178
     * @return string
179
     * @throws RuntimeException
180
     */
181
    public function decrypt($encrypted_text = '')
182
    {
183
        // you give me nothing??? GET OUT !
184
        if (empty($encrypted_text)) {
185
            return $encrypted_text;
186
        }
187
        // if PHP's mcrypt functions are installed then we'll use them
188
        if ($this->_use_openssl_encrypt) {
189
            $decrypted_text = $this->openssl_decrypt($encrypted_text);
190
        } else {
191
            $decrypted_text = $this->acme_decrypt($encrypted_text);
192
        }
193
        return $decrypted_text;
194
    }
195
196
197
    /**
198
     * encodes string with PHP's base64 encoding
199
     *
200
     * @see http://php.net/manual/en/function.base64-encode.php
201
     * @param string $text_string the text to be encoded
202
     * @return string
203
     */
204
    public function base64_string_encode($text_string = '')
205
    {
206
        // you give me nothing??? GET OUT !
207
        if (empty($text_string) || ! $this->_use_base64_encode) {
208
            return $text_string;
209
        }
210
        // encode
211
        return base64_encode($text_string);
212
    }
213
214
215
    /**
216
     * decodes string that has been encoded with PHP's base64 encoding
217
     *
218
     * @see http://php.net/manual/en/function.base64-encode.php
219
     * @param string $encoded_string the text to be decoded
220
     * @return string
221
     * @throws RuntimeException
222
     */
223
    public function base64_string_decode($encoded_string = '')
224
    {
225
        // you give me nothing??? GET OUT !
226
        if (empty($encoded_string) || ! $this->valid_base_64($encoded_string)) {
227
            return $encoded_string;
228
        }
229
        // decode
230
        $decoded_string = base64_decode($encoded_string);
231
        if ($decoded_string === false) {
232
            throw new RuntimeException(
233
                esc_html__('Base 64 decoding failed.', 'event_espresso')
234
            );
235
        }
236
        return $decoded_string;
237
    }
238
239
240
    /**
241
     * encodes  url string with PHP's base64 encoding
242
     *
243
     * @see http://php.net/manual/en/function.base64-encode.php
244
     * @param string $text_string the text to be encoded
245
     * @return string
246
     */
247
    public function base64_url_encode($text_string = '')
248
    {
249
        // you give me nothing??? GET OUT !
250
        if (empty($text_string) || ! $this->_use_base64_encode) {
251
            return $text_string;
252
        }
253
        // encode
254
        $encoded_string = base64_encode($text_string);
255
        // remove chars to make encoding more URL friendly
256
        return strtr($encoded_string, '+/=', '-_,');
257
    }
258
259
260
    /**
261
     * decodes  url string that has been encoded with PHP's base64 encoding
262
     *
263
     * @see http://php.net/manual/en/function.base64-encode.php
264
     * @param string $encoded_string the text to be decoded
265
     * @return string
266
     * @throws RuntimeException
267
     */
268
    public function base64_url_decode($encoded_string = '')
269
    {
270
        // you give me nothing??? GET OUT !
271
        if (empty($encoded_string) || ! $this->valid_base_64($encoded_string)) {
272
            return $encoded_string;
273
        }
274
        // replace previously removed characters
275
        $encoded_string = strtr($encoded_string, '-_,', '+/=');
276
        // decode
277
        $decoded_string = base64_decode($encoded_string);
278
        if ($decoded_string === false) {
279
            throw new RuntimeException(
280
                esc_html__('Base 64 decoding failed.', 'event_espresso')
281
            );
282
        }
283
        return $decoded_string;
284
    }
285
286
287
    /**
288
     * encrypts data using PHP's openssl functions
289
     *
290
     * @param string $text_string the text to be encrypted
291
     * @param string $cipher_method
292
     * @param string $encryption_key
293
     * @return string
294
     * @throws RuntimeException
295
     */
296
    protected function openssl_encrypt(
297
        $text_string = '',
298
        $cipher_method = EE_Encryption::OPENSSL_CIPHER_METHOD,
299
        $encryption_key = ''
300
    ) {
301
        // you give me nothing??? GET OUT !
302
        if (empty($text_string)) {
303
            return $text_string;
304
        }
305
        $this->cipher_method = $this->getCipherMethod($cipher_method);
306
        // get initialization vector size
307
        $iv_size = openssl_cipher_iv_length($this->cipher_method);
308
        // generate initialization vector.
309
        // The second parameter ("crypto_strong") is passed by reference,
310
        // and is used to determines if the algorithm used was "cryptographically strong"
311
        // openssl_random_pseudo_bytes() will toggle it to either true or false
312
        $iv = openssl_random_pseudo_bytes($iv_size, $is_strong);
313
        if ($iv === false || $is_strong === false) {
314
            throw new RuntimeException(
315
                esc_html__('Failed to generate OpenSSL initialization vector.', 'event_espresso')
316
            );
317
        }
318
        // encrypt it
319
        $encrypted_text = openssl_encrypt(
320
            $text_string,
321
            $this->cipher_method,
322
            $this->getDigestHashValue(EE_Encryption::OPENSSL_DIGEST_METHOD, $encryption_key),
323
            0,
324
            $iv
325
        );
326
        // append the initialization vector
327
        $encrypted_text .= EE_Encryption::OPENSSL_IV_DELIMITER . $iv;
328
        // trim and maybe encode
329
        return $this->_use_base64_encode
330
            ? trim(base64_encode($encrypted_text))
331
            : trim($encrypted_text);
332
    }
333
334
335
    /**
336
     * Returns a cipher method that has been verified to work.
337
     * First checks if the cached cipher has been set already and if so, returns that.
338
     * Then tests the incoming default and returns that if it's good.
339
     * If not, then it retrieves the previously tested and saved cipher method.
340
     * But if that doesn't exist, then calls getAvailableCipherMethod()
341
     * to see what is available on the server, and returns the results.
342
     *
343
     * @param string $cipher_method
344
     * @return string
345
     * @throws RuntimeException
346
     */
347
    protected function getCipherMethod($cipher_method = EE_Encryption::OPENSSL_CIPHER_METHOD)
348
    {
349
        if ($this->cipher_method !== '') {
350
            return $this->cipher_method;
351
        }
352
        // verify that the default cipher method can produce an initialization vector
353
        if (openssl_cipher_iv_length($cipher_method) === false) {
354
            // nope? okay let's get what we found in the past to work
355
            $cipher_method = get_option(EE_Encryption::OPENSSL_CIPHER_METHOD_OPTION_NAME, '');
356
            // oops... haven't tested available cipher methods yet
357
            if ($cipher_method === '' || openssl_cipher_iv_length($cipher_method) === false) {
358
                $cipher_method = $this->getAvailableCipherMethod($cipher_method);
359
            }
360
        }
361
        return $cipher_method;
362
    }
363
364
365
    /**
366
     * @param string $cipher_method
367
     * @return string
368
     * @throws \RuntimeException
369
     */
370
    protected function getAvailableCipherMethod($cipher_method)
371
    {
372
        // verify that the incoming cipher method can produce an initialization vector
373
        if (openssl_cipher_iv_length($cipher_method) === false) {
374
            // nope? then check the next cipher in the list of available cipher methods
375
            $cipher_method = next($this->cipher_methods);
376
            // what? there's no list? then generate that list and cache it,
377
            if (empty($this->cipher_methods)) {
378
                $this->cipher_methods = openssl_get_cipher_methods();
379
                // then grab the first item from the list
380
                $cipher_method = reset($this->cipher_methods);
381
            }
382
            if ($cipher_method === false) {
383
                throw new RuntimeException(
384
                    esc_html__(
385
                        'OpenSSL support appears to be enabled on the server, but no cipher methods are available. Please contact the server administrator.',
386
                        'event_espresso'
387
                    )
388
                );
389
            }
390
            // verify that the next cipher method works
391
            return $this->getAvailableCipherMethod($cipher_method);
392
        }
393
        // if we've gotten this far, then we found an available cipher method that works
394
        // so save that for next time
395
        update_option(
396
            EE_Encryption::OPENSSL_CIPHER_METHOD_OPTION_NAME,
397
            $cipher_method
398
        );
399
        return $cipher_method;
400
    }
401
402
403
    /**
404
     * decrypts data that has been encrypted with PHP's openssl functions
405
     *
406
     * @param string $encrypted_text the text to be decrypted
407
     * @param string $cipher_method
408
     * @param string $encryption_key
409
     * @return string
410
     * @throws RuntimeException
411
     */
412
    protected function openssl_decrypt(
413
        $encrypted_text = '',
414
        $cipher_method = EE_Encryption::OPENSSL_CIPHER_METHOD,
415
        $encryption_key = ''
416
    ) {
417
        // you give me nothing??? GET OUT !
418
        if (empty($encrypted_text)) {
419
            return $encrypted_text;
420
        }
421
        // decode
422
        $encrypted_text = $this->valid_base_64($encrypted_text)
423
            ? $this->base64_url_decode($encrypted_text)
424
            : $encrypted_text;
425
        $encrypted_components = explode(
426
            EE_Encryption::OPENSSL_IV_DELIMITER,
427
            $encrypted_text,
428
            2
429
        );
430
        // decrypt it
431
        $decrypted_text = openssl_decrypt(
432
            $encrypted_components[0],
433
            $this->getCipherMethod($cipher_method),
434
            $this->getDigestHashValue(EE_Encryption::OPENSSL_DIGEST_METHOD, $encryption_key),
435
            0,
436
            $encrypted_components[1]
437
        );
438
        $decrypted_text = trim($decrypted_text);
439
        return $decrypted_text;
440
    }
441
442
443
    /**
444
     * Computes the digest hash value using the specified digest method.
445
     * If that digest method fails to produce a valid hash value,
446
     * then we'll grab the next digest method and recursively try again until something works.
447
     *
448
     * @param string $digest_method
449
     * @param string $encryption_key
450
     * @return string
451
     * @throws RuntimeException
452
     */
453
    protected function getDigestHashValue($digest_method = EE_Encryption::OPENSSL_DIGEST_METHOD, $encryption_key = '')
454
    {
455
        $encryption_key = $encryption_key !== ''
456
            ? $encryption_key
457
            : $this->get_encryption_key();
458
        $digest_hash_value = openssl_digest($encryption_key, $digest_method);
459
        if ($digest_hash_value === false) {
460
            return $this->getDigestHashValue($this->getDigestMethod());
461
        }
462
        return $digest_hash_value;
463
    }
464
465
466
    /**
467
     * Returns the NEXT element in the $digest_methods array.
468
     * If the $digest_methods array is empty, then we populate it
469
     * with the available values returned from openssl_get_md_methods().
470
     *
471
     * @return string
472
     * @throws \RuntimeException
473
     */
474
    protected function getDigestMethod()
475
    {
476
        $digest_method = prev($this->digest_methods);
477
        if (empty($this->digest_methods)) {
478
            $this->digest_methods = openssl_get_md_methods();
479
            $digest_method = end($this->digest_methods);
480
        }
481
        if ($digest_method === false) {
482
            throw new RuntimeException(
483
                esc_html__(
484
                    'OpenSSL support appears to be enabled on the server, but no digest methods are available. Please contact the server administrator.',
485
                    'event_espresso'
486
                )
487
            );
488
        }
489
        return $digest_method;
490
    }
491
492
493
    /**
494
     * encrypts data for acme servers that didn't bother to install PHP mcrypt
495
     *
496
     * @see http://stackoverflow.com/questions/800922/how-to-encrypt-string-without-mcrypt-library-in-php
497
     * @param string $text_string the text to be decrypted
498
     * @return string
499
     */
500
    protected function acme_encrypt($text_string = '')
501
    {
502
        // you give me nothing??? GET OUT !
503
        if (empty($text_string)) {
504
            return $text_string;
505
        }
506
        $key_bits = str_split(
507
            str_pad(
508
                '',
509
                strlen($text_string),
510
                $this->get_encryption_key(),
511
                STR_PAD_RIGHT
512
            )
513
        );
514
        $string_bits = str_split($text_string);
515
        foreach ($string_bits as $k => $v) {
516
            $temp = ord($v) + ord($key_bits[ $k ]);
517
            $string_bits[ $k ] = chr($temp > 255 ? ($temp - 256) : $temp);
518
        }
519
        $encrypted_text = implode('', $string_bits);
520
        $encrypted_text .= EE_Encryption::ACME_ENCRYPTION_FLAG;
521
        return $this->_use_base64_encode
522
            ? base64_encode($encrypted_text)
523
            : $encrypted_text;
524
    }
525
526
527
    /**
528
     * decrypts data for acme servers that didn't bother to install PHP mcrypt
529
     *
530
     * @see http://stackoverflow.com/questions/800922/how-to-encrypt-string-without-mcrypt-library-in-php
531
     * @param string $encrypted_text the text to be decrypted
532
     * @return string
533
     * @throws RuntimeException
534
     */
535
    protected function acme_decrypt($encrypted_text = '')
536
    {
537
        // you give me nothing??? GET OUT !
538
        if (empty($encrypted_text)) {
539
            return $encrypted_text;
540
        }
541
        // decode the data ?
542
        $encrypted_text = $this->valid_base_64($encrypted_text)
543
            ? $this->base64_url_decode($encrypted_text)
544
            : $encrypted_text;
545
        $encrypted_text = substr($encrypted_text, 0, -4);
546
        $key_bits = str_split(
547
            str_pad(
548
                '',
549
                strlen($encrypted_text),
550
                $this->get_encryption_key(),
551
                STR_PAD_RIGHT
552
            )
553
        );
554
        $string_bits = str_split($encrypted_text);
555
        foreach ($string_bits as $k => $v) {
556
            $temp = ord($v) - ord($key_bits[ $k ]);
557
            $string_bits[ $k ] = chr($temp < 0 ? ($temp + 256) : $temp);
558
        }
559
        return implode('', $string_bits);
560
    }
561
562
563
    /**
564
     * @see http://stackoverflow.com/questions/2556345/detect-base64-encoding-in-php#30231906
565
     * @param $string
566
     * @return bool
567
     */
568
    protected function valid_base_64($string)
569
    {
570
        // ensure data is a string
571
        if (! is_string($string) || ! $this->_use_base64_encode) {
572
            return false;
573
        }
574
        $decoded = base64_decode($string, true);
575
        // Check if there is no invalid character in string
576
        if (! preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $string)) {
577
            return false;
578
        }
579
        // Decode the string in strict mode and send the response
580
        if (! base64_decode($string, true)) {
581
            return false;
582
        }
583
        // Encode and compare it to original one
584
        return base64_encode($decoded) === $string;
585
    }
586
587
588
    /**
589
     * generate random string
590
     *
591
     * @see http://stackoverflow.com/questions/637278/what-is-the-best-way-to-generate-a-random-key-within-php
592
     * @param int $length number of characters for random string
593
     * @return string
594
     */
595
    public function generate_random_string($length = 40)
596
    {
597
        $iterations = ceil($length / 40);
598
        $random_string = '';
599
        for ($i = 0; $i < $iterations; $i++) {
600
            $random_string .= sha1(microtime(true) . mt_rand(10000, 90000));
601
        }
602
        $random_string = substr($random_string, 0, $length);
603
        return $random_string;
604
    }
605
}
606