SymmetricCrypt::getAdditionalAuthenticationData()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Crypt;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Limoncello\Crypt\Contracts\DecryptInterface;
22
use Limoncello\Crypt\Contracts\EncryptInterface;
23
use Limoncello\Crypt\Exceptions\CryptException;
24
use function assert;
25
use function in_array;
26
use function openssl_cipher_iv_length;
27
use function openssl_decrypt;
28
use function openssl_encrypt;
29
use function openssl_get_cipher_methods;
30
use function openssl_random_pseudo_bytes;
31
use function strlen;
32
use function substr;
33
34
/**
35
 * @package Limoncello\Crypt
36
 *
37
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
38
 */
39
class SymmetricCrypt extends BaseCrypt implements EncryptInterface, DecryptInterface
40
{
41
    /**
42
     * @var string
43
     */
44
    private $method;
45
46
    /**
47
     * @var string
48
     */
49
    private $password;
50
51
    /**
52
     * Such as OPENSSL_RAW_DATA, OPENSSL_ZERO_PADDING.
53
     *
54
     * @var int
55
     */
56
    private $options = 0;
57
58
    /**
59
     * @var string
60
     */
61
    private $initializationVector = '';
62
63
    // Authenticated Encryption with Associated Data options (since PHP 7.1)
64
65
    /**
66
     * Use Authenticated Encryption with Associated Data (since PHP 7.1)
67
     *
68
     * @var bool
69
     */
70
    private $useAuthentication = false;
71
72
    /**
73
     * Additional authentication data.
74
     *
75
     * @var string
76
     */
77
    private $aad = '';
78
79
    /**
80
     * The length of the authentication tag. Its value can be between 4 and 16 for GCM (Galois/Counter Mode) mode.
81
     *
82
     * @var int
83
     */
84
    private $tagLength = 16;
85
86
    /**
87
     * @param string $method
88
     * @param string $password
89
     */
90 8
    public function __construct(string $method, string $password)
91
    {
92 8
        $this->setMethod($method)->setPassword($password)->asRaw();
93
    }
94
95
    /**
96
     * @inheritdoc
97
     *
98
     * @SuppressWarnings(PHPMD.ElseExpression)
99
     */
100 5
    public function decrypt(string $data): string
101
    {
102 5
        $this->clearErrors();
103
104 5
        $vector = $this->getIV();
105 5
        if (empty($vector) === true) {
106 5
            $ivLength = $this->openSslIvLength($this->getMethod());
107 5
            $vector   = $this->readIV($data, $ivLength);
108 4
            $data     = $this->extractData($data, $ivLength);
109
        }
110
111 3
        if ($this->isUseAuthentication() === true) {
112 2
            $tagLength = $this->getTagLength();
113 2
            $tag       = $this->readTag($data, $tagLength);
114 1
            $data      = $this->extractData($data, $tagLength);
115
116 1
            $decrypted = $this->openSslDecryptAuthenticated(
117 1
                $data,
118 1
                $this->getMethod(),
119 1
                $this->getPassword(),
120 1
                $this->getOptions(),
121 1
                $vector,
122 1
                $this->getAdditionalAuthenticationData(),
123 1
                $tag
124
            );
125
        } else {
126 1
            $decrypted = $this->openSslDecrypt(
127 1
                $data,
128 1
                $this->getMethod(),
129 1
                $this->getPassword(),
130 1
                $this->getOptions(),
131 1
                $vector
132
            );
133
        }
134
135 2
        return $decrypted;
136
    }
137
138
    /**
139
     * @inheritdoc
140
     *
141
     * @SuppressWarnings(PHPMD.ElseExpression)
142
     */
143 4
    public function encrypt(string $data): string
144
    {
145 4
        $this->clearErrors();
146
147 4
        $isAddIvToOutput = false;
148 4
        $vector          = $this->getIV();
149 4
        if (empty($vector) === true) {
150 3
            $vector          = $this->generateIV();
151 3
            $isAddIvToOutput = true;
152
        }
153
154 4
        if ($this->isUseAuthentication() === true) {
155 1
            $encrypted = $this->openSslEncryptAuthenticated(
156 1
                $data,
157 1
                $this->getMethod(),
158 1
                $this->getPassword(),
159 1
                $this->getOptions(),
160 1
                $vector,
161 1
                $this->getAdditionalAuthenticationData(),
162 1
                $tag,
163 1
                $this->getTagLength()
164
            );
165
166
            // Tag/Message authentication code should be sent with the encrypted message
167
            // otherwise it won't be possible to validate and encrypt the message.
168
            // Though https://tools.ietf.org/html/rfc5084 do not directly says it should
169
            // be passed along with the encrypted message adding it is one of the possible
170
            // solutions.
171 1
            $encrypted = $tag . $encrypted;
172
        } else {
173 3
            $encrypted = $this->openSslEncrypt(
174 3
                $data,
175 3
                $this->getMethod(),
176 3
                $this->getPassword(),
177 3
                $this->getOptions(),
178 3
                $vector
179
            );
180
        }
181
182
        // Add initialization vector (IV) if it was generated otherwise it won't be possible to encrypt the message.
183
        //
184
        // Also @see http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf
185
        //
186
        // Appendix C: Generation of Initialization Vectors
187
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
188
        // ...
189
        // The IV need not be secret, so the IV, or information sufficient to determine the IV, may be
190
        // transmitted with the ciphertext.
191
        // ...
192 3
        $result = $isAddIvToOutput === false ? $encrypted : $vector . $encrypted;
193
194 3
        return $result;
195
    }
196
197
    /**
198
     * @return string
199
     */
200 4
    public function getPassword(): string
201
    {
202 4
        return $this->password;
203
    }
204
205
    /**
206
     * @return string
207
     */
208 7
    public function getMethod(): string
209
    {
210 7
        return $this->method;
211
    }
212
213
    /**
214
     * @param string $method
215
     *
216
     * @return self
217
     */
218 8
    public function setMethod(string $method): self
219
    {
220 8
        assert(
221 8
            ($availableMethods = openssl_get_cipher_methods(true)) !== false &&
222 8
            in_array($method, $availableMethods) === true
223
        );
224
225 8
        $this->method = $method;
226
227 8
        return $this;
228
    }
229
230
    /**
231
     * @param string $password
232
     *
233
     * @return self
234
     */
235 8
    public function setPassword(string $password): self
236
    {
237 8
        assert(empty($password) === false);
238
239 8
        $this->password = $password;
240
241 8
        return $this;
242
    }
243
244
    /**
245
     * @param string $value
246
     *
247
     * @return self
248
     */
249 1
    public function setIV(string $value): self
250
    {
251 1
        $this->initializationVector = $value;
252
253 1
        return $this;
254
    }
255
256
    /**
257
     * @return string
258
     */
259 7
    public function getIV(): string
260
    {
261 7
        return $this->initializationVector;
262
    }
263
264
    /**
265
     * @return self
266
     */
267 1
    public function withZeroPadding(): self
268
    {
269 1
        return $this->setOption(OPENSSL_ZERO_PADDING);
270
    }
271
272
    /**
273
     * @return self
274
     */
275 5
    public function withoutZeroPadding(): self
276
    {
277 5
        return $this->clearOption(OPENSSL_ZERO_PADDING);
278
    }
279
280
    /**
281
     * @return bool
282
     */
283 5
    public function isUseAuthentication(): bool
284
    {
285 5
        return $this->useAuthentication;
286
    }
287
288
    /**
289
     * Authenticated Encryption with Associated Data available for certain methods since PHP 7.1.
290
     *
291
     * @return self
292
     */
293 3
    public function enableAuthentication(): self
294
    {
295 3
        $this->useAuthentication = true;
296
297 3
        return $this;
298
    }
299
300
    /**
301
     * Authenticated Encryption with Associated Data available for certain methods since PHP 7.1.
302
     *
303
     * @return self
304
     */
305 1
    public function disableAuthentication(): self
306
    {
307 1
        $this->useAuthentication = false;
308
309 1
        return $this;
310
    }
311
312
    /**
313
     * @return string
314
     */
315 1
    public function getAdditionalAuthenticationData(): string
316
    {
317 1
        return $this->aad;
318
    }
319
320
    /**
321
     * @param string $data
322
     *
323
     * @return self
324
     */
325 1
    public function setAdditionalAuthenticationData(string $data): self
326
    {
327 1
        $this->aad = $data;
328
329 1
        return $this;
330
    }
331
332
    /**
333
     * @return int
334
     */
335 2
    public function getTagLength(): int
336
    {
337 2
        return $this->tagLength;
338
    }
339
340
    /**
341
     * @param int $length
342
     *
343
     * @return self
344
     */
345 2
    public function setTagLength(int $length): self
346
    {
347 2
        assert($this->isTagLengthMightBeValid($length));
348
349 2
        $this->tagLength = $length;
350
351 2
        return $this;
352
    }
353
354
    /**
355
     * @return self
356
     */
357 8
    protected function asRaw(): self
358
    {
359 8
        return $this->setOption(OPENSSL_RAW_DATA);
360
    }
361
362
    /**
363
     * @return int
364
     */
365 8
    protected function getOptions(): int
366
    {
367 8
        return $this->options;
368
    }
369
370
    /**
371
     * @param int $options
372
     *
373
     * @return self
374
     */
375 8
    protected function setOptions(int $options): self
376
    {
377 8
        $this->options = $options;
378
379 8
        return $this;
380
    }
381
382
    /**
383
     * @return string
384
     */
385 3
    protected function generateIV(): string
386
    {
387 3
        $ivLength = $this->openSslIvLength($this->getMethod());
388 3
        $ivLength !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
389
390 3
        $vector = openssl_random_pseudo_bytes($ivLength);
391
392 3
        return $vector;
393
    }
394
395
    /**
396
     * @param int $option
397
     *
398
     * @return self
399
     */
400 8
    protected function setOption(int $option): self
401
    {
402 8
        $this->setOptions($this->getOptions() | $option);
403
404 8
        return $this;
405
    }
406
407
    /**
408
     * @param int $option
409
     *
410
     * @return self
411
     */
412 5
    protected function clearOption(int $option): self
413
    {
414 5
        $this->setOptions($this->getOptions() & ~$option);
415
416 5
        return $this;
417
    }
418
419
    /**
420
     * @param string $data
421
     * @param int    $ivLength
422
     *
423
     * @return string
424
     */
425 5 View Code Duplication
    protected function readIV(string $data, int $ivLength): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
426
    {
427 5
        $vector = substr($data, 0, $ivLength);
428 5
        $isOk   = $vector !== false && strlen($vector) === $ivLength;
429
430 5
        $isOk === true ?: $this->throwException(new CryptException($this->getReadVectorErrorMessage()));
431
432 4
        return $vector;
433
    }
434
435
    /**
436
     * @param string $data
437
     * @param int    $tagLength
438
     *
439
     * @return string
440
     */
441 2 View Code Duplication
    protected function readTag(string $data, int $tagLength): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
442
    {
443 2
        $tag  = substr($data, 0, $tagLength);
444 2
        $isOk = $tag !== false && strlen($tag) === $tagLength;
445
446 2
        $isOk === true ?: $this->throwException(new CryptException($this->getReadTagErrorMessage()));
447
448 1
        return $tag;
449
    }
450
451
    /**
452
     * @param string $data
453
     * @param int    $ivLength
454
     *
455
     * @return string
456
     */
457 4 View Code Duplication
    protected function extractData(string $data, int $ivLength): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
458
    {
459 4
        $result = substr($data, $ivLength);
460
461 4
        $isOk = $result !== false && empty($result) === false;
462 4
        $isOk === true ?: $this->throwException(new CryptException($this->getExtractDataErrorMessage()));
463
464 3
        return $result;
465
    }
466
467
    /**
468
     * @param string $data
469
     * @param string $method
470
     * @param string $password
471
     * @param int    $options
472
     * @param string $initializationVector
473
     *
474
     * @return string
475
     */
476 3 View Code Duplication
    protected function openSslEncrypt(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
477
        string $data,
478
        string $method,
479
        string $password,
480
        int $options,
481
        string $initializationVector
482
    ): string {
483 3
        $encrypted = $this->openSslEncryptImpl($data, $method, $password, $options, $initializationVector);
484
485 3
        $message = $this->getErrorMessage();
486 3
        $encrypted !== false ?: $this->throwException(new CryptException($message));
487
488 2
        return $encrypted;
489
    }
490
491
    /**
492
     * @param string $data
493
     * @param string $method
494
     * @param string $password
495
     * @param int    $options
496
     * @param string $initializationVector
497
     *
498
     * @return string
499
     */
500 1 View Code Duplication
    protected function openSslDecrypt(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
501
        string $data,
502
        string $method,
503
        string $password,
504
        int $options,
505
        string $initializationVector
506
    ): string {
507 1
        $decrypted = $this->openSslDecryptImpl($data, $method, $password, $options, $initializationVector);
508
509 1
        $decrypted !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
510
511 1
        return $decrypted;
512
    }
513
514
    /** @noinspection PhpTooManyParametersInspection
515
     * @param string       $data
516
     * @param string       $method
517
     * @param string       $password
518
     * @param int          $options
519
     * @param string       $initializationVector
520
     * @param string       $aad
521
     * @param string|null &$tag
522
     * @param int          $tagLength
523
     *
524
     * @return string
525
     */
526 1
    protected function openSslEncryptAuthenticated(
527
        string $data,
528
        string $method,
529
        string $password,
530
        int $options,
531
        string $initializationVector,
532
        string $aad,
533
        string &$tag = null,
534
        int $tagLength = 16
535
    ): string {
536 1
        $encrypted = $this->openSslEncryptAuthenticatedImpl(
537 1
            $data,
538 1
            $method,
539 1
            $password,
540 1
            $options,
541 1
            $initializationVector,
542 1
            $aad,
543 1
            $tag,
544 1
            $tagLength
545
        );
546
547 1
        $message = $this->getErrorMessage();
548 1
        $encrypted !== false ?: $this->throwException(new CryptException($message));
549
550 1
        return $encrypted;
551
    }
552
553
    /**
554
     * @param string $data
555
     * @param string $method
556
     * @param string $password
557
     * @param int    $options
558
     * @param string $initializationVector
559
     * @param string $aad
560
     * @param string $tag
561
     *
562
     * @return string
563
     */
564 1 View Code Duplication
    protected function openSslDecryptAuthenticated(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
565
        string $data,
566
        string $method,
567
        string $password,
568
        int $options,
569
        string $initializationVector,
570
        string $aad,
571
        string $tag
572
    ): string {
573
        $decrypted = $this
574 1
            ->openSslDecryptAuthenticatedImpl($data, $method, $password, $options, $initializationVector, $aad, $tag);
575
576 1
        $decrypted !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
577
578 1
        return $decrypted;
579
    }
580
581
    /**
582
     * @param string $method
583
     *
584
     * @return int
585
     */
586 6
    protected function openSslIvLength(string $method): int
587
    {
588 6
        $ivLength = $this->openSslIvLengthImpl($method);
589
590 6
        $ivLength !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
591
592 6
        return $ivLength;
593
    }
594
595
    /**
596
     * @return string
597
     */
598 1
    protected function getReadVectorErrorMessage(): string
599
    {
600 1
        return 'Reading Initialization Vector (IV) failed';
601
    }
602
603
    /**
604
     * @return string
605
     */
606 1
    protected function getReadTagErrorMessage(): string
607
    {
608 1
        return 'Reading Authenticated Encryption Tag failed';
609
    }
610
611
    /**
612
     * @return string
613
     */
614 1
    protected function getExtractDataErrorMessage(): string
615
    {
616 1
        return 'Extracting ciphertext from input data failed';
617
    }
618
619
    /**
620
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
621
     *
622
     * @param string $data
623
     * @param string $method
624
     * @param string $password
625
     * @param int    $options
626
     * @param string $initializationVector
627
     *
628
     * @return string|false
629
     */
630 2
    protected function openSslEncryptImpl(
631
        string $data,
632
        string $method,
633
        string $password,
634
        int $options,
635
        string $initializationVector
636
    ) {
637 2
        return openssl_encrypt($data, $method, $password, $options, $initializationVector);
638
    }
639
640
    /**
641
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
642
     *
643
     * @param string $data
644
     * @param string $method
645
     * @param string $password
646
     * @param int    $options
647
     * @param string $initializationVector
648
     *
649
     * @return string|false
650
     */
651 1
    protected function openSslDecryptImpl(
652
        string $data,
653
        string $method,
654
        string $password,
655
        int $options,
656
        string $initializationVector
657
    ) {
658 1
        return openssl_decrypt($data, $method, $password, $options, $initializationVector);
659
    }
660
661
    /** @noinspection PhpTooManyParametersInspection
662
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
663
     *
664
     * @param string       $data
665
     * @param string       $method
666
     * @param string       $password
667
     * @param int          $options
668
     * @param string       $initializationVector
669
     * @param string       $aad
670
     * @param string|null &$tag
671
     * @param int          $tagLength
672
     *
673
     * @return false|string
674
     */
675 1
    protected function openSslEncryptAuthenticatedImpl(
676
        string $data,
677
        string $method,
678
        string $password,
679
        int $options,
680
        string $initializationVector,
681
        string $aad,
682
        string &$tag = null,
683
        int $tagLength = 16
684
    ) {
685 1
        assert(PHP_VERSION_ID >= 70100);
686 1
        assert($this->isTagLengthMightBeValid($tagLength));
687
688 1
        $result = openssl_encrypt($data, $method, $password, $options, $initializationVector, $tag, $aad, $tagLength);
689
690 1
        return $result;
691
    }
692
693
    /**
694
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
695
     *
696
     * @param string $data
697
     * @param string $method
698
     * @param string $password
699
     * @param int    $options
700
     * @param string $initializationVector
701
     * @param string $aad
702
     * @param string $tag
703
     *
704
     * @return false|string
705
     */
706 1
    protected function openSslDecryptAuthenticatedImpl(
707
        string $data,
708
        string $method,
709
        string $password,
710
        int $options,
711
        string $initializationVector,
712
        string $aad,
713
        string $tag
714
    ) {
715 1
        assert(PHP_VERSION_ID >= 70100);
716
717 1
        return openssl_decrypt($data, $method, $password, $options, $initializationVector, $tag, $aad);
718
    }
719
720
    /**
721
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
722
     *
723
     * @param string $method
724
     *
725
     * @return int|false
726
     */
727 6
    protected function openSslIvLengthImpl(string $method)
728
    {
729 6
        return openssl_cipher_iv_length($method);
730
    }
731
732
    /**
733
     * @param int $length
734
     *
735
     * @return bool
736
     */
737 2
    private function isTagLengthMightBeValid(int $length): bool
738
    {
739
        // @link http://php.net/manual/en/function.openssl-encrypt.php
740
741 2
        return 4 <= $length && $length <= 16;
742
    }
743
}
744