Passed
Push — master ( de7816...ce3a1e )
by Nikolaos
07:17
created

Crypt::checkDecryptKey()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 13
rs 10
cc 3
nc 4
nop 1
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Phalcon;
15
16
use Phalcon\Crypt\CryptInterface;
17
use Phalcon\Crypt\Exception as CryptException;
18
use Phalcon\Crypt\MismatchException;
19
use Phalcon\Helper\Str;
20
21
use function base64_decode;
22
use function base64_encode;
23
use function chr;
24
use function function_exists;
25
use function hash;
26
use function hash_algos;
27
use function hash_hmac;
28
use function hash_hmac_algos;
29
use function in_array;
30
use function openssl_cipher_iv_length;
31
use function openssl_decrypt;
32
use function openssl_encrypt;
33
use function openssl_get_cipher_methods;
34
use function openssl_random_pseudo_bytes;
35
use function ord;
36
use function rand;
37
use function range;
38
use function rtrim;
39
use function sprintf;
40
use function str_ireplace;
41
use function str_repeat;
42
use function strlen;
43
use function strrpos;
44
use function strtolower;
45
use function strtoupper;
46
use function substr;
47
48
use const OPENSSL_RAW_DATA;
49
50
/**
51
 * Provides encryption capabilities to Phalcon applications.
52
 *
53
 * ```php
54
 * use Phalcon\Crypt;
55
 *
56
 * $crypt = new Crypt();
57
 *
58
 * $crypt->setCipher('aes-256-ctr');
59
 *
60
 * $key  =
61
 * "T4\xb1\x8d\xa9\x98\x05\\\x8c\xbe\x1d\x07&[\x99\x18\xa4~Lc1\xbeW\xb3";
62
 * $text = "The message to be encrypted";
63
 *
64
 * $encrypted = $crypt->encrypt($text, $key);
65
 *
66
 * echo $crypt->decrypt($encrypted, $key);
67
 * ```
68
 *
69
 * @property string $authData
70
 * @property string $authTag
71
 * @property int    $authTagLength
72
 * @property array  $availableCiphers
73
 * @property string $cipher
74
 * @property string $hashAlgo
75
 * @property int    $ivLength
76
 * @property string $key
77
 * @property int    $padding
78
 * @property bool   $useSigning
79
 */
80
class Crypt implements CryptInterface
81
{
82
    public const PADDING_ANSI_X_923     = 1;
83
    public const PADDING_DEFAULT        = 0;
84
    public const PADDING_ISO_10126      = 3;
85
    public const PADDING_ISO_IEC_7816_4 = 4;
86
    public const PADDING_PKCS7          = 2;
87
    public const PADDING_SPACE          = 6;
88
    public const PADDING_ZERO           = 5;
89
90
    /**
91
     * @var string
92
     */
93
    protected $authData = "";
94
95
    /**
96
     * @var string
97
     */
98
    protected $authTag;
99
100
    /**
101
     * @var int
102
     */
103
    protected $authTagLength = 16;
104
105
    /**
106
     * Available cipher methods.
107
     *
108
     * @var array
109
     */
110
    protected $availableCiphers = [];
111
112
    /**
113
     * @var string
114
     */
115
    protected $cipher = "aes-256-cfb";
116
117
    /**
118
     * The name of hashing algorithm.
119
     *
120
     * @var string
121
     */
122
    protected $hashAlgo = "sha512";
123
124
    /**
125
     * The cipher iv length.
126
     *
127
     * @var int
128
     */
129
    protected $ivLength = 16;
130
131
    /**
132
     * @var string
133
     */
134
    protected $key = "";
135
136
    /**
137
     * @var int
138
     */
139
    protected $padding = 0;
140
141
    /**
142
     * Whether calculating message digest enabled or not.
143
     *
144
     * @var bool
145
     */
146
    protected $useSigning = true;
147
148
    /**
149
     * Crypt constructor.
150
     *
151
     * @param string $cipher
152
     * @param bool   $useSigning
153
     *
154
     * @throws CryptException
155
     */
156
    public function __construct(
157
        string $cipher = "aes-256-cfb",
158
        bool $useSigning = false
159
    ) {
160
        $this->initializeAvailableCiphers();
161
162
        $this->setCipher($cipher);
163
        $this->useSigning($useSigning);
164
    }
165
166
    /**
167
     * Decrypts an encrypted text.
168
     *
169
     * ```php
170
     * $encrypted = $crypt->decrypt(
171
     *     $encrypted,
172
     *     "T4\xb1\x8d\xa9\x98\x05\\\x8c\xbe\x1d\x07&[\x99\x18\xa4~Lc1\xbeW\xb3"
173
     * );
174
     * ```
175
     *
176
     * @param string      $text
177
     * @param string|null $key
178
     *
179
     * @return string
180
     * @throws CryptException
181
     */
182
    public function decrypt(string $text, string $key = null): string
183
    {
184
        $decryptKey = $this->checkDecryptKey($key);
185
186
        $cipher   = $this->cipher;
187
        $mode     = strtolower(
188
            substr($cipher, strrpos($cipher, "-") - strlen($cipher))
189
        );
190
        $authData = $this->authData;
191
        $authTag  = $this->authTag;
192
193
        $this->assertCipherIsAvailable($cipher);
194
195
        $ivLength = $this->ivLength;
196
197
        if ($ivLength > 0) {
198
            $blockSize = $ivLength;
199
        } else {
200
            $blockSize = $this->getIvLength(
201
                str_ireplace("-" . $mode, "", $cipher)
202
            );
203
        }
204
205
        $iv = mb_substr($text, 0, $ivLength, "8bit");
206
207
        if ($this->useSigning) {
208
            return $this->calculateWithSigning(
209
                $text,
210
                $ivLength,
211
                $mode,
212
                $cipher,
213
                $decryptKey,
214
                $iv,
215
                $authTag,
216
                $authData,
217
                $blockSize
218
            );
219
        }
220
221
        $cipherText = mb_substr($text, $ivLength, null, "8bit");
222
        $decrypted  = $this->calculateGcmCcm(
223
            $mode,
224
            $cipherText,
225
            $cipher,
226
            $decryptKey,
227
            $iv,
228
            $authTag,
229
            $authData
230
        );
231
232
        if ($mode == "-cbc" || $mode == "-ecb") {
233
            $decrypted = $this->cryptUnpadText(
234
                $decrypted,
235
                $mode,
236
                $blockSize,
237
                $this->padding
238
            );
239
        }
240
241
        return $decrypted;
242
    }
243
244
    /**
245
     * Decrypt a text that is coded as a base64 string.
246
     *
247
     * @param string      $text
248
     * @param string|null $key
249
     * @param bool        $safe
250
     *
251
     * @return string
252
     * @throws CryptException
253
     */
254
    public function decryptBase64(
255
        string $text,
256
        $key = null,
257
        bool $safe = false
258
    ): string {
259
        if ($safe) {
260
            $text = strtr($text, "-_", "+/") .
261
                substr("===", (strlen($text) + 3) % 4);
262
        }
263
264
        return $this->decrypt(
265
            base64_decode($text),
266
            $key
267
        );
268
    }
269
270
    /**
271
     * Encrypts a text.
272
     *
273
     * ```php
274
     * $encrypted = $crypt->encrypt(
275
     *     "Top secret",
276
     *     "T4\xb1\x8d\xa9\x98\x05\\\x8c\xbe\x1d\x07&[\x99\x18\xa4~Lc1\xbeW\xb3"
277
     * );
278
     * ```
279
     *
280
     * @param string      $text
281
     * @param string|null $key
282
     *
283
     * @return string
284
     * @throws CryptException
285
     */
286
    public function encrypt(string $text, string $key = null): string
287
    {
288
        if (empty($key)) {
289
            $encryptKey = $this->key;
290
        } else {
291
            $encryptKey = $key;
292
        }
293
294
        if (empty($encryptKey)) {
295
            throw new CryptException("Encryption key cannot be empty");
296
        }
297
298
        $cipher = $this->cipher;
299
        $mode   = strtolower(
300
            substr(
301
                $cipher,
302
                strrpos($cipher, "-") - strlen($cipher)
303
            )
304
        );
305
306
        $this->assertCipherIsAvailable($cipher);
307
308
        $ivLength = $this->ivLength;
309
310
        if ($ivLength > 0) {
311
            $blockSize = $ivLength;
312
        } else {
313
            $blockSize = $this->getIvLength(
314
                str_ireplace(
315
                    "-" . $mode,
316
                    "",
317
                    $cipher
318
                )
319
            );
320
        }
321
322
        $iv          = openssl_random_pseudo_bytes($ivLength);
323
        $paddingType = $this->padding;
324
325
        if ($paddingType != 0 && ($mode == "-cbc" || $mode == "-ecb")) {
326
            $padded = $this->cryptPadText($text, $mode, $blockSize, $paddingType);
327
        } else {
328
            $padded = $text;
329
        }
330
331
        /**
332
         * If the mode is "gcm" or "ccm" and auth data has been passed call it
333
         * with that data
334
         */
335
        if (("-gcm" === $mode || "-ccm" === $mode) && !empty($this->authData)) {
336
            $authData      = $this->authData;
337
            $authTag       = $this->authTag;
338
            $authTagLength = $this->authTagLength;
339
340
            $encrypted = openssl_encrypt(
341
                $padded,
342
                $cipher,
343
                $encryptKey,
344
                OPENSSL_RAW_DATA,
345
                $iv,
346
                $authTag,
347
                $authData,
348
                $authTagLength
349
            );
350
351
            $this->authTag = $authTag;
352
        } else {
353
            $encrypted = openssl_encrypt(
354
                $padded,
355
                $cipher,
356
                $encryptKey,
357
                OPENSSL_RAW_DATA,
358
                $iv
359
            );
360
        }
361
362
        if ($this->useSigning) {
363
            $hashAlgo = $this->getHashAlgo();
364
            $digest   = hash_hmac($hashAlgo, $padded, $encryptKey, true);
365
366
            return $iv . $digest . $encrypted;
367
        }
368
369
        return $iv . $encrypted;
370
    }
371
372
    /**
373
     * Encrypts a text returning the result as a base64 string.
374
     *
375
     * @param string     $text
376
     * @param mixed|null $key
377
     * @param bool       $safe
378
     *
379
     * @return string
380
     * @throws CryptException
381
     */
382
    public function encryptBase64(
383
        string $text,
384
        $key = null,
385
        bool $safe = false
386
    ): string {
387
        if ($safe === true) {
388
            return rtrim(
389
                strtr(
390
                    base64_encode(
391
                        $this->encrypt($text, $key)
392
                    ),
393
                    "+/",
394
                    "-_"
395
                ),
396
                "="
397
            );
398
        }
399
400
        return base64_encode(
401
            $this->encrypt($text, $key)
402
        );
403
    }
404
405
    /**
406
     * Returns a list of available ciphers.
407
     *
408
     * @return array
409
     * @throws CryptException
410
     */
411
    public function getAvailableCiphers(): array
412
    {
413
        $availableCiphers = $this->availableCiphers;
414
415
        if (empty($availableCiphers)) {
416
            $this->initializeAvailableCiphers();
417
418
            $availableCiphers = $this->availableCiphers;
419
        }
420
421
        $allowedCiphers = [];
422
        foreach ($availableCiphers as $cipher) {
423
            if (
424
                !Str::endsWith(strtolower($cipher), "des") &&
425
                !Str::endsWith(strtolower($cipher), "rc2") &&
426
                !Str::endsWith(strtolower($cipher), "rc4") &&
427
                !Str::endsWith(strtolower($cipher), "des") &&
428
                !Str::endsWith(strtolower($cipher), "ecb")
429
            ) {
430
                $allowedCiphers[] = $cipher;
431
            }
432
        }
433
434
        return $allowedCiphers;
435
    }
436
437
    /**
438
     * Return a list of registered hashing algorithms suitable for hash_hmac.
439
     */
440
    public function getAvailableHashAlgos(): array
441
    {
442
        if (function_exists("hash_hmac_algos")) {
443
            return hash_hmac_algos();
444
        }
445
446
        return hash_algos();
447
    }
448
449
    /**
450
     * @return string
451
     */
452
    public function getAuthData(): string
453
    {
454
        return $this->authData;
455
    }
456
457
    /**
458
     * @return string
459
     */
460
    public function getAuthTag(): string
461
    {
462
        return $this->authTag;
463
    }
464
465
    /**
466
     * @return int
467
     */
468
    public function getAuthTagLength(): int
469
    {
470
        return $this->authTagLength;
471
    }
472
473
    /**
474
     * Returns the current cipher
475
     */
476
    public function getCipher(): string
477
    {
478
        return $this->cipher;
479
    }
480
481
    /**
482
     * Get the name of hashing algorithm.
483
     */
484
    public function getHashAlgo(): string
485
    {
486
        return $this->hashAlgo;
487
    }
488
489
    /**
490
     * Returns the encryption key
491
     */
492
    public function getKey(): string
493
    {
494
        return $this->key;
495
    }
496
497
    /**
498
     * @param string $data
499
     *
500
     * @return CryptInterface
501
     */
502
    public function setAuthData(string $data): CryptInterface
503
    {
504
        $this->authData = $data;
505
506
        return $this;
507
    }
508
509
    /**
510
     * @param string $tag
511
     *
512
     * @return CryptInterface
513
     */
514
    public function setAuthTag(string $tag): CryptInterface
515
    {
516
        $this->authTag = $tag;
517
518
        return $this;
519
    }
520
521
    /**
522
     * @param int $length
523
     *
524
     * @return CryptInterface
525
     */
526
    public function setAuthTagLength(int $length): CryptInterface
527
    {
528
        $this->authTagLength = $length;
529
530
        return $this;
531
    }
532
533
    /**
534
     * Sets the cipher algorithm for data encryption and decryption.
535
     *
536
     * The `aes-256-gcm' is the preferable cipher, but it is not usable
537
     * until the openssl library is upgraded, which is available in PHP 7.1.
538
     *
539
     * The `aes-256-ctr' is arguably the best choice for cipher
540
     * algorithm for current openssl library version.
541
     *
542
     * @param string $cipher
543
     *
544
     * @return CryptInterface
545
     * @throws CryptException
546
     */
547
    public function setCipher(string $cipher): CryptInterface
548
    {
549
        $this->assertCipherIsAvailable($cipher);
550
551
        $this->ivLength = $this->getIvLength($cipher);
552
        $this->cipher   = $cipher;
553
554
        return $this;
555
    }
556
557
    /**
558
     * Set the name of hashing algorithm.
559
     *
560
     * @param string $hashAlgo
561
     *
562
     * @return CryptInterface
563
     * @throws CryptException
564
     */
565
    public function setHashAlgo(string $hashAlgo): CryptInterface
566
    {
567
        $this->assertHashAlgorithmAvailable($hashAlgo);
568
569
        $this->hashAlgo = $hashAlgo;
570
571
        return $this;
572
    }
573
574
    /**
575
     * Sets the encryption key.
576
     *
577
     * The `$key' should have been previously generated in a cryptographically
578
     * safe way.
579
     *
580
     * Bad key:
581
     * "le password"
582
     *
583
     * Better (but still unsafe):
584
     * "#1dj8$=dp?.ak//j1V$~%*0X"
585
     *
586
     * Good key:
587
     * "T4\xb1\x8d\xa9\x98\x05\\\x8c\xbe\x1d\x07&[\x99\x18\xa4~Lc1\xbeW\xb3"
588
     *
589
     * @param string $key
590
     *
591
     * @return CryptInterface
592
     */
593
    public function setKey(string $key): CryptInterface
594
    {
595
        $this->key = $key;
596
597
        return $this;
598
    }
599
600
    /**
601
     * Changes the padding scheme used.
602
     *
603
     * @param int $scheme
604
     *
605
     * @return CryptInterface
606
     */
607
    public function setPadding(int $scheme): CryptInterface
608
    {
609
        $this->padding = $scheme;
610
611
        return $this;
612
    }
613
614
    /**
615
     * Sets if the calculating message digest must used.
616
     *
617
     * @param bool $useSigning
618
     *
619
     * @return CryptInterface
620
     */
621
    public function useSigning(bool $useSigning): CryptInterface
622
    {
623
        $this->useSigning = $useSigning;
624
625
        return $this;
626
    }
627
628
    /**
629
     * Assert the cipher is available.
630
     *
631
     * @param string $cipher
632
     *
633
     * @throws CryptException
634
     */
635
    protected function assertCipherIsAvailable(string $cipher): void
636
    {
637
        $availableCiphers = $this->getAvailableCiphers();
638
639
        if (!in_array(strtoupper($cipher), $availableCiphers)) {
640
            throw new CryptException(
641
                sprintf(
642
                    "The cipher algorithm \"%s\" is not supported on this system.",
643
                    $cipher
644
                )
645
            );
646
        }
647
    }
648
649
    /**
650
     * Assert the hash algorithm is available.
651
     *
652
     * @param string $hashAlgo
653
     *
654
     * @throws CryptException
655
     */
656
    protected function assertHashAlgorithmAvailable(string $hashAlgo): void
657
    {
658
        $availableAlgorithms = $this->getAvailableHashAlgos();
659
660
        if (!in_array($hashAlgo, $availableAlgorithms)) {
661
            throw new CryptException(
662
                sprintf(
663
                    "The hash algorithm \"%s\" is not supported on this system.",
664
                    $hashAlgo
665
                )
666
            );
667
        }
668
    }
669
670
    /**
671
     * Pads texts before encryption.
672
     *
673
     * @see http://www.di-mgt.com.au/cryptopad.html
674
     *
675
     * @param string $text
676
     * @param string $mode
677
     * @param int    $blockSize
678
     * @param int    $paddingType
679
     *
680
     * @return string
681
     * @throws CryptException
682
     */
683
    protected function cryptPadText(
684
        string $text,
685
        string $mode,
686
        int $blockSize,
687
        int $paddingType
688
    ): string {
689
690
        $paddingSize = 0;
691
        $padding     = null;
692
693
        if ($mode == "cbc" || $mode == "ecb") {
694
            $paddingSize = $blockSize - (strlen($text) % $blockSize);
695
696
            if ($paddingSize >= 256) {
697
                throw new CryptException("Block size is bigger than 256");
698
            }
699
700
            switch ($paddingType) {
701
                case self::PADDING_ANSI_X_923:
702
                    $padding = str_repeat(chr(0), $paddingSize - 1) . chr($paddingSize);
703
                    break;
704
705
                case self::PADDING_PKCS7:
706
                    $padding = str_repeat(chr($paddingSize), $paddingSize);
707
                    break;
708
709
                case self::PADDING_ISO_10126:
710
                    $padding = "";
711
712
                    foreach (range(0, $paddingSize - 2) as $counter) {
713
                        $padding .= chr(rand());
714
                    }
715
716
                    $padding .= chr($paddingSize);
717
718
                    break;
719
720
                case self::PADDING_ISO_IEC_7816_4:
721
                    $padding = chr(0x80) . str_repeat(chr(0), $paddingSize - 1);
722
                    break;
723
724
                case self::PADDING_ZERO:
725
                    $padding = str_repeat(chr(0), $paddingSize);
726
                    break;
727
728
                case self::PADDING_SPACE:
729
                    $padding = str_repeat(" ", $paddingSize);
730
                    break;
731
732
                default:
733
                    $paddingSize = 0;
734
                    break;
735
            }
736
        }
737
738
        if (!$paddingSize) {
739
            return $text;
740
        }
741
742
        if ($paddingSize > $blockSize) {
743
            throw new CryptException("Invalid padding size");
744
        }
745
746
        return $text . substr($padding, 0, $paddingSize);
747
    }
748
749
    /**
750
     * Removes a padding from a text.
751
     *
752
     * If the function detects that the text was not padded, it will return it
753
     * unmodified.
754
     *
755
     * @param string $text
756
     * @param string $mode
757
     * @param int    $blockSize
758
     * @param int    $paddingType
759
     *
760
     * @return false|string
761
     */
762
    protected function cryptUnpadText(
763
        string $text,
764
        string $mode,
765
        int $blockSize,
766
        int $paddingType
767
    ) {
768
        $paddingSize = 0;
769
        $length      = strlen($text);
770
771
        if (
772
            $length > 0 &&
773
            ($length % $blockSize == 0) &&
774
            ($mode == "cbc" || $mode == "ecb")
775
        ) {
776
            switch ($paddingType) {
777
                case self::PADDING_ANSI_X_923:
778
                    $paddingSize = $this->getPaddingSizeAnsiX923(
779
                        $text,
780
                        $length,
781
                        $blockSize
782
                    );
783
                    break;
784
785
                case self::PADDING_PKCS7:
786
                    $paddingSize = $this->getPaddingSizePkcs7(
787
                        $text,
788
                        $length,
789
                        $blockSize
790
                    );
791
                    break;
792
793
                case self::PADDING_ISO_10126:
794
                    return $this->getPaddingSizeIso10126($text, $length);
795
796
                case self::PADDING_ISO_IEC_7816_4:
797
                    return $this->getPaddingSizeIsoIec78164(
798
                        $text,
799
                        $length,
800
                        $blockSize
801
                    );
802
803
                case self::PADDING_ZERO:
804
                    return $this->getPaddingSizeZero(
805
                        $text,
806
                        $length,
807
                        $blockSize
808
                    );
809
810
                case self::PADDING_SPACE:
811
                    return $this->getPaddingSizeSpace(
812
                        $text,
813
                        $length,
814
                        $blockSize
815
                    );
816
817
                default:
818
                    break;
819
            }
820
821
            if ($paddingSize && $paddingSize <= $blockSize) {
822
                if ($paddingSize < $length) {
823
                    return substr($text, 0, $length - $paddingSize);
824
                }
825
826
                return "";
827
            } else {
828
                $paddingSize = 0;
829
            }
830
        }
831
832
        if (!$paddingSize) {
833
            return $text;
834
        }
835
    }
836
837
    /**
838
     * Initialize available cipher algorithms.
839
     *
840
     * @param string $cipher
841
     *
842
     * @return int
843
     * @throws CryptException
844
     */
845
    protected function getIvLength(string $cipher): int
846
    {
847
        if (!function_exists("openssl_cipher_iv_length")) {
848
            throw new CryptException("openssl extension is required");
849
        }
850
851
        return openssl_cipher_iv_length($cipher);
852
    }
853
854
    /**
855
     * Initialize available cipher algorithms.
856
     *
857
     * @throws CryptException
858
     */
859
    protected function initializeAvailableCiphers(): void
860
    {
861
        if (!function_exists("openssl_get_cipher_methods")) {
862
            throw new CryptException("openssl extension is required");
863
        }
864
865
        $availableCiphers = openssl_get_cipher_methods(true);
866
867
        foreach ($availableCiphers as $key => $cipher) {
868
            $availableCiphers[$key] = strtoupper($cipher);
869
        }
870
871
        $this->availableCiphers = $availableCiphers;
872
    }
873
874
    /**
875
     * @param string $mode
876
     * @param string $cipherText
877
     * @param string $cipher
878
     * @param string $decryptKey
879
     * @param string $iv
880
     * @param string $authTag
881
     * @param string $authData
882
     *
883
     * @return string
884
     */
885
    private function calculateGcmCcm(
886
        string $mode,
887
        string $cipherText,
888
        string $cipher,
889
        string $decryptKey,
890
        string $iv,
891
        string $authTag,
892
        string $authData
893
    ): string {
894
        if (("-gcm" === $mode || "-ccm" === $mode) && !empty($this->authData)) {
895
            $decrypted = openssl_decrypt(
896
                $cipherText,
897
                $cipher,
898
                $decryptKey,
899
                OPENSSL_RAW_DATA,
900
                $iv,
901
                $authTag,
902
                $authData
903
            );
904
        } else {
905
            $decrypted = openssl_decrypt(
906
                $cipherText,
907
                $cipher,
908
                $decryptKey,
909
                OPENSSL_RAW_DATA,
910
                $iv
911
            );
912
        }
913
914
        return $decrypted;
915
    }
916
917
    /**
918
     * @param string $text
919
     * @param int    $ivLength
920
     * @param string $mode
921
     * @param string $cipher
922
     * @param string $decryptKey
923
     * @param string $iv
924
     * @param string $authTag
925
     * @param string $authData
926
     * @param int    $blockSize
927
     *
928
     * @return string
929
     * @throws MismatchException
930
     */
931
    private function calculateWithSigning(
932
        string $text,
933
        int $ivLength,
934
        string $mode,
935
        string $cipher,
936
        string $decryptKey,
937
        string $iv,
938
        string $authTag,
939
        string $authData,
940
        int $blockSize
941
    ): string {
942
        $hashAlgo   = $this->getHashAlgo();
943
        $hashLength = strlen(hash($hashAlgo, "", true));
944
        $hash       = mb_substr($text, $ivLength, $hashLength, "8bit");
945
        $cipherText = mb_substr($text, $ivLength + $hashLength, null, "8bit");
946
947
        if (
948
            ("-gcm" === $mode || "-ccm" === $mode) &&
949
            !empty($this->authData)
950
        ) {
951
            $decrypted = openssl_decrypt(
952
                $cipherText,
953
                $cipher,
954
                $decryptKey,
955
                OPENSSL_RAW_DATA,
956
                $iv,
957
                $authTag,
958
                $authData
959
            );
960
        } else {
961
            $decrypted = openssl_decrypt(
962
                $cipherText,
963
                $cipher,
964
                $decryptKey,
965
                OPENSSL_RAW_DATA,
966
                $iv
967
            );
968
        }
969
970
        if ($mode == "-cbc" || $mode == "-ecb") {
971
            $decrypted = $this->cryptUnpadText(
972
                $decrypted,
973
                $mode,
974
                $blockSize,
975
                $this->padding
976
            );
977
        }
978
979
        /**
980
         * Checks on the decrypted's message digest using the HMAC method.
981
         */
982
        if (hash_hmac($hashAlgo, $decrypted, $decryptKey, true) !== $hash) {
983
            throw new MismatchException("Hash does not match.");
984
        }
985
986
        return $decrypted;
987
    }
988
989
990
    /**
991
     * @param string|null $key
992
     *
993
     * @return string
994
     * @throws CryptException
995
     */
996
    private function checkDecryptKey(string $key = null): string
997
    {
998
        if (empty($key)) {
999
            $decryptKey = $this->key;
1000
        } else {
1001
            $decryptKey = $key;
1002
        }
1003
1004
        if (empty($decryptKey)) {
1005
            throw new CryptException("Decryption key cannot be empty");
1006
        }
1007
1008
        return $decryptKey;
1009
    }
1010
1011
    /**
1012
     * @param string $text
1013
     * @param int    $length
1014
     * @param int    $blockSize
1015
     *
1016
     * @return int
1017
     */
1018
    private function getPaddingSizeAnsiX923(
1019
        string $text,
1020
        int $length,
1021
        int $blockSize
1022
    ): int {
1023
        $paddingSize = 0;
1024
        $last        = substr($text, $length - 1, 1);
1025
        $ord         = (int) ord($last);
1026
1027
        if ($ord <= $blockSize) {
1028
            $paddingSize = $ord;
1029
            $padding     = str_repeat(chr(0), $paddingSize - 1) . $last;
1030
1031
            if (substr($text, $length - $paddingSize) != $padding) {
1032
                $paddingSize = 0;
1033
            }
1034
        }
1035
1036
        return $paddingSize;
1037
    }
1038
1039
    /**
1040
     * @param string $text
1041
     * @param int    $length
1042
     * @param int    $blockSize
1043
     *
1044
     * @return int
1045
     */
1046
    private function getPaddingSizePkcs7(
1047
        string $text,
1048
        int $length,
1049
        int $blockSize
1050
    ): int {
1051
        $paddingSize = 0;
1052
        $last = substr($text, $length - 1, 1);
1053
        $ord  = (int) ord($last);
1054
1055
        if ($ord <= $blockSize) {
1056
            $paddingSize = $ord;
1057
            $padding     = str_repeat(chr($paddingSize), $paddingSize);
1058
1059
            if (substr($text, $length - $paddingSize) != $padding) {
1060
                $paddingSize = 0;
1061
            }
1062
        }
1063
1064
        return $paddingSize;
1065
    }
1066
1067
    /**
1068
     * @param string $text
1069
     * @param int    $length
1070
     *
1071
     * @return int
1072
     */
1073
    private function getPaddingSizeIso10126(
1074
        string $text,
1075
        int $length
1076
    ): int {
1077
        $last = substr($text, $length - 1, 1);
1078
1079
        return (int) ord($last);
1080
    }
1081
1082
    /**
1083
     * @param string $text
1084
     * @param int    $length
1085
     * @param int    $blockSize
1086
     *
1087
     * @return int
1088
     */
1089
    private function getPaddingSizeIsoIec78164(
1090
        string $text,
1091
        int $length,
1092
        int $blockSize
1093
    ): int {
1094
        $paddingSize = 0;
1095
        $i = $length - 1;
1096
1097
        while ($i > 0 && $text[$i] == 0x00 && $paddingSize < $blockSize) {
1098
            $paddingSize++;
1099
            $i--;
1100
        }
1101
1102
        if ($text[$i] == 0x80) {
1103
            $paddingSize++;
1104
        } else {
1105
            $paddingSize = 0;
1106
        }
1107
1108
        return $paddingSize;
1109
    }
1110
1111
    /**
1112
     * @param string $text
1113
     * @param int    $length
1114
     * @param int    $blockSize
1115
     *
1116
     * @return int
1117
     */
1118
    private function getPaddingSizeZero(
1119
        string $text,
1120
        int $length,
1121
        int $blockSize
1122
    ): int {
1123
        $paddingSize = 0;
1124
        $i = $length - 1;
1125
1126
        while ($i >= 0 && $text[$i] == 0x00 && $paddingSize <= $blockSize) {
1127
            $paddingSize++;
1128
            $i--;
1129
        }
1130
1131
        return $paddingSize;
1132
    }
1133
1134
1135
1136
    /**
1137
     * @param string $text
1138
     * @param int    $length
1139
     * @param int    $blockSize
1140
     *
1141
     * @return int
1142
     */
1143
    private function getPaddingSizeSpace(
1144
        string $text,
1145
        int $length,
1146
        int $blockSize
1147
    ): int {
1148
        $paddingSize = 0;
1149
        $i = $length - 1;
1150
1151
        while ($i >= 0 && $text[$i] == 0x20 && $paddingSize <= $blockSize) {
1152
            $paddingSize++;
1153
            $i--;
1154
        }
1155
1156
        return $paddingSize;
1157
    }
1158
}
1159