Completed
Push — master ( db47a2...0cf60d )
by Neomerx
03:19
created

SymmetricCrypt::disableAuthentication()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
crap 1
1
<?php namespace Limoncello\Crypt;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Limoncello\Crypt\Contracts\DecryptInterface;
20
use Limoncello\Crypt\Contracts\EncryptInterface;
21
use Limoncello\Crypt\Exceptions\CryptException;
22
23
/**
24
 * @package Limoncello\Crypt
25
 *
26
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
27
 */
28
class SymmetricCrypt extends BaseCrypt implements EncryptInterface, DecryptInterface
29
{
30
    /**
31
     * @var string
32
     */
33
    private $method;
34
35
    /**
36
     * @var string
37
     */
38
    private $password;
39
40
    /**
41
     * Such as OPENSSL_RAW_DATA, OPENSSL_ZERO_PADDING.
42
     *
43
     * @var int
44
     */
45
    private $options = 0;
46
47
    /**
48
     * @var string
49
     */
50
    private $initializationVector = '';
51
52
    // Authenticated Encryption with Associated Data options (since PHP 7.1)
53
54
    /**
55
     * Use Authenticated Encryption with Associated Data (since PHP 7.1)
56
     *
57
     * @var bool
58
     */
59
    private $useAuthentication = false;
60
61
    /**
62
     * Additional authentication data.
63
     *
64
     * @var string
65
     */
66
    private $aad = '';
67
68
    /**
69
     * The length of the authentication tag. Its value can be between 4 and 16 for GCM (Galois/Counter Mode) mode.
70
     *
71
     * @var int
72
     */
73
    private $tagLength = 16;
74
75
    /**
76
     * @param string $method
77
     * @param string $password
78
     */
79 8
    public function __construct(string $method, string $password)
80
    {
81 8
        $this->setMethod($method)->setPassword($password)->asRaw();
82
    }
83
84
    /**
85
     * @inheritdoc
86
     *
87
     * @SuppressWarnings(PHPMD.ElseExpression)
88
     */
89 5
    public function decrypt(string $data): string
90
    {
91 5
        $this->clearErrors();
92
93 5
        $vector = $this->getIV();
94 5
        if (empty($vector) === true) {
95 5
            $ivLength = $this->openSslIvLength($this->getMethod());
96 5
            $vector   = $this->readIV($data, $ivLength);
97 4
            $data     = $this->extractData($data, $ivLength);
98
        }
99
100 3
        if ($this->isUseAuthentication() === true) {
101 2
            $tagLength = $this->getTagLength();
102 2
            $tag       = $this->readTag($data, $tagLength);
103 1
            $data      = $this->extractData($data, $tagLength);
104
105 1
            $decrypted = $this->openSslDecryptAuthenticated(
106 1
                $data,
107 1
                $this->getMethod(),
108 1
                $this->getPassword(),
109 1
                $this->getOptions(),
110 1
                $vector,
111 1
                $this->getAdditionalAuthenticationData(),
112 1
                $tag
113
            );
114
        } else {
115 1
            $decrypted = $this->openSslDecrypt(
116 1
                $data,
117 1
                $this->getMethod(),
118 1
                $this->getPassword(),
119 1
                $this->getOptions(),
120 1
                $vector
121
            );
122
        }
123
124 2
        return $decrypted;
125
    }
126
127
    /**
128
     * @inheritdoc
129
     *
130
     * @SuppressWarnings(PHPMD.ElseExpression)
131
     */
132 4
    public function encrypt(string $data): string
133
    {
134 4
        $this->clearErrors();
135
136 4
        $isAddIvToOutput = false;
137 4
        $vector          = $this->getIV();
138 4
        if (empty($vector) === true) {
139 3
            $vector          = $this->generateIV();
140 3
            $isAddIvToOutput = true;
141
        }
142
143 4
        if ($this->isUseAuthentication() === true) {
144 1
            $encrypted = $this->openSslEncryptAuthenticated(
145 1
                $data,
146 1
                $this->getMethod(),
147 1
                $this->getPassword(),
148 1
                $this->getOptions(),
149 1
                $vector,
150 1
                $this->getAdditionalAuthenticationData(),
151 1
                $tag,
152 1
                $this->getTagLength()
153
            );
154
155
            // Tag/Message authentication code should be sent with the encrypted message
156
            // otherwise it won't be possible to validate and encrypt the message.
157
            // Though https://tools.ietf.org/html/rfc5084 do not directly says it should
158
            // be passed along with the encrypted message adding it is one of the possible
159
            // solutions.
160 1
            $encrypted = $tag . $encrypted;
161
        } else {
162 3
            $encrypted = $this->openSslEncrypt(
163 3
                $data,
164 3
                $this->getMethod(),
165 3
                $this->getPassword(),
166 3
                $this->getOptions(),
167 3
                $vector
168
            );
169
        }
170
171
        // Add initialization vector (IV) if it was generated otherwise it won't be possible to encrypt the message.
172
        //
173
        // Also @see http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf
174
        //
175
        // Appendix C: Generation of Initialization Vectors
176
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
177
        // ...
178
        // The IV need not be secret, so the IV, or information sufficient to determine the IV, may be
179
        // transmitted with the ciphertext.
180
        // ...
181 3
        $result = $isAddIvToOutput === false ? $encrypted : $vector . $encrypted;
182
183 3
        return $result;
184
    }
185
186
    /**
187
     * @return string
188
     */
189 4
    public function getPassword(): string
190
    {
191 4
        return $this->password;
192
    }
193
194
    /**
195
     * @return string
196
     */
197 7
    public function getMethod(): string
198
    {
199 7
        return $this->method;
200
    }
201
202
    /**
203
     * @param string $method
204
     *
205
     * @return self
206
     */
207 8
    public function setMethod(string $method): self
208
    {
209 8
        assert(
210 8
            ($availableMethods = openssl_get_cipher_methods(true)) !== false &&
211 8
            in_array($method, $availableMethods) === true
212
        );
213
214 8
        $this->method = $method;
215
216 8
        return $this;
217
    }
218
219
    /**
220
     * @param string $password
221
     *
222
     * @return self
223
     */
224 8
    public function setPassword(string $password): self
225
    {
226 8
        assert(empty($password) === false);
227
228 8
        $this->password = $password;
229
230 8
        return $this;
231
    }
232
233
    /**
234
     * @param string $value
235
     *
236
     * @return self
237
     */
238 1
    public function setIV(string $value): self
239
    {
240 1
        $this->initializationVector = $value;
241
242 1
        return $this;
243
    }
244
245
    /**
246
     * @return string
247
     */
248 7
    public function getIV(): string
249
    {
250 7
        return $this->initializationVector;
251
    }
252
253
    /**
254
     * @return self
255
     */
256 1
    public function withZeroPadding(): self
257
    {
258 1
        return $this->setOption(OPENSSL_ZERO_PADDING);
259
    }
260
261
    /**
262
     * @return self
263
     */
264 5
    public function withoutZeroPadding(): self
265
    {
266 5
        return $this->clearOption(OPENSSL_ZERO_PADDING);
267
    }
268
269
    /**
270
     * @return bool
271
     */
272 5
    public function isUseAuthentication(): bool
273
    {
274 5
        return $this->useAuthentication;
275
    }
276
277
    /**
278
     * Authenticated Encryption with Associated Data available for certain methods since PHP 7.1.
279
     *
280
     * @return self
281
     */
282 3
    public function enableAuthentication(): self
283
    {
284 3
        $this->useAuthentication = true;
285
286 3
        return $this;
287
    }
288
289
    /**
290
     * Authenticated Encryption with Associated Data available for certain methods since PHP 7.1.
291
     *
292
     * @return self
293
     */
294 1
    public function disableAuthentication(): self
295
    {
296 1
        $this->useAuthentication = false;
297
298 1
        return $this;
299
    }
300
301
    /**
302
     * @return string
303
     */
304 1
    public function getAdditionalAuthenticationData(): string
305
    {
306 1
        return $this->aad;
307
    }
308
309
    /**
310
     * @param string $data
311
     *
312
     * @return self
313
     */
314 1
    public function setAdditionalAuthenticationData(string $data): self
315
    {
316 1
        $this->aad = $data;
317
318 1
        return $this;
319
    }
320
321
    /**
322
     * @return int
323
     */
324 2
    public function getTagLength(): int
325
    {
326 2
        return $this->tagLength;
327
    }
328
329
    /**
330
     * @param int $length
331
     *
332
     * @return self
333
     */
334 2
    public function setTagLength(int $length): self
335
    {
336 2
        assert($this->isTagLengthMightBeValid($length));
337
338 2
        $this->tagLength = $length;
339
340 2
        return $this;
341
    }
342
343
    /**
344
     * @return self
345
     */
346 8
    protected function asRaw(): self
347
    {
348 8
        return $this->setOption(OPENSSL_RAW_DATA);
349
    }
350
351
    /**
352
     * @return int
353
     */
354 8
    protected function getOptions(): int
355
    {
356 8
        return $this->options;
357
    }
358
359
    /**
360
     * @param int $options
361
     *
362
     * @return self
363
     */
364 8
    protected function setOptions(int $options): self
365
    {
366 8
        $this->options = $options;
367
368 8
        return $this;
369
    }
370
371
    /**
372
     * @return string
373
     */
374 3
    protected function generateIV(): string
375
    {
376 3
        $ivLength = $this->openSslIvLength($this->getMethod());
377 3
        $ivLength !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
378
379 3
        $vector = openssl_random_pseudo_bytes($ivLength);
380
381 3
        return $vector;
382
    }
383
384
    /**
385
     * @param int $option
386
     *
387
     * @return self
388
     */
389 8
    protected function setOption(int $option): self
390
    {
391 8
        $this->setOptions($this->getOptions() | $option);
392
393 8
        return $this;
394
    }
395
396
    /**
397
     * @param int $option
398
     *
399
     * @return self
400
     */
401 5
    protected function clearOption(int $option): self
402
    {
403 5
        $this->setOptions($this->getOptions() & ~$option);
404
405 5
        return $this;
406
    }
407
408
    /**
409
     * @param string $data
410
     * @param int    $ivLength
411
     *
412
     * @return string
413
     */
414 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...
415
    {
416 5
        $vector = substr($data, 0, $ivLength);
417 5
        $isOk   = $vector !== false && strlen($vector) === $ivLength;
418
419 5
        $isOk === true ?: $this->throwException(new CryptException($this->getReadVectorErrorMessage()));
420
421 4
        return $vector;
422
    }
423
424
    /**
425
     * @param string $data
426
     * @param int    $tagLength
427
     *
428
     * @return string
429
     */
430 2
    protected function readTag(string $data, int $tagLength): string
431
    {
432 2
        $tag  = substr($data, 0, $tagLength);
433 2
        $isOk = $tag !== false && strlen($tag) === $tagLength;
434
435 2
        $isOk === true ?: $this->throwException(new CryptException($this->getReadTagErrorMessage()));
436
437 1
        return $tag;
438
    }
439
440
    /**
441
     * @param string $data
442
     * @param int    $ivLength
443
     *
444
     * @return string
445
     */
446 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...
447
    {
448 4
        $result = substr($data, $ivLength);
449
450 4
        $isOk = $result !== false && empty($result) === false;
451 4
        $isOk === true ?: $this->throwException(new CryptException($this->getExtractDataErrorMessage()));
452
453 3
        return $result;
454
    }
455
456
    /**
457
     * @param string $data
458
     * @param string $method
459
     * @param string $password
460
     * @param int    $options
461
     * @param string $initializationVector
462
     *
463
     * @return string
464
     */
465 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...
466
        string $data,
467
        string $method,
468
        string $password,
469
        int $options,
470
        string $initializationVector
471
    ): string {
472 3
        $encrypted = $this->openSslEncryptImpl($data, $method, $password, $options, $initializationVector);
473
474 3
        $message = $this->getErrorMessage();
475 3
        $encrypted !== false ?: $this->throwException(new CryptException($message));
476
477 2
        return $encrypted;
478
    }
479
480
    /**
481
     * @param string $data
482
     * @param string $method
483
     * @param string $password
484
     * @param int    $options
485
     * @param string $initializationVector
486
     *
487
     * @return string
488
     */
489 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...
490
        string $data,
491
        string $method,
492
        string $password,
493
        int $options,
494
        string $initializationVector
495
    ): string {
496 1
        $decrypted = $this->openSslDecryptImpl($data, $method, $password, $options, $initializationVector);
497
498 1
        $decrypted !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
499
500 1
        return $decrypted;
501
    }
502
503
    /** @noinspection PhpTooManyParametersInspection
504
     * @param string       $data
505
     * @param string       $method
506
     * @param string       $password
507
     * @param int          $options
508
     * @param string       $initializationVector
509
     * @param string       $aad
510
     * @param string|null &$tag
511
     * @param int          $tagLength
512
     *
513
     * @return string
514
     */
515 1
    protected function openSslEncryptAuthenticated(
516
        string $data,
517
        string $method,
518
        string $password,
519
        int $options,
520
        string $initializationVector,
521
        string $aad,
522
        string &$tag = null,
523
        int $tagLength = 16
524
    ): string {
525 1
        $encrypted = $this->openSslEncryptAuthenticatedImpl(
526 1
            $data,
527 1
            $method,
528 1
            $password,
529 1
            $options,
530 1
            $initializationVector,
531 1
            $aad,
532 1
            $tag,
533 1
            $tagLength
534
        );
535
536 1
        $message = $this->getErrorMessage();
537 1
        $encrypted !== false ?: $this->throwException(new CryptException($message));
538
539 1
        return $encrypted;
540
    }
541
542
    /**
543
     * @param string $data
544
     * @param string $method
545
     * @param string $password
546
     * @param int    $options
547
     * @param string $initializationVector
548
     * @param string $aad
549
     * @param string $tag
550
     *
551
     * @return string
552
     */
553 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...
554
        string $data,
555
        string $method,
556
        string $password,
557
        int $options,
558
        string $initializationVector,
559
        string $aad,
560
        string $tag
561
    ): string {
562
        $decrypted = $this
563 1
            ->openSslDecryptAuthenticatedImpl($data, $method, $password, $options, $initializationVector, $aad, $tag);
564
565 1
        $decrypted !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
566
567 1
        return $decrypted;
568
    }
569
570
    /**
571
     * @param string $method
572
     *
573
     * @return int
574
     */
575 6
    protected function openSslIvLength(string $method): int
576
    {
577 6
        $ivLength = $this->openSslIvLengthImpl($method);
578
579 6
        $ivLength !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
580
581 6
        return $ivLength;
582
    }
583
584
    /**
585
     * @return string
586
     */
587 1
    protected function getReadVectorErrorMessage(): string
588
    {
589 1
        return 'Reading Initialization Vector (IV) failed';
590
    }
591
592
    /**
593
     * @return string
594
     */
595 1
    protected function getReadTagErrorMessage(): string
596
    {
597 1
        return 'Reading Authenticated Encryption Tag failed';
598
    }
599
600
    /**
601
     * @return string
602
     */
603 1
    protected function getExtractDataErrorMessage(): string
604
    {
605 1
        return 'Extracting ciphertext from input data failed';
606
    }
607
608
    /**
609
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
610
     *
611
     * @param string $data
612
     * @param string $method
613
     * @param string $password
614
     * @param int    $options
615
     * @param string $initializationVector
616
     *
617
     * @return string|false
618
     */
619 2
    protected function openSslEncryptImpl(
620
        string $data,
621
        string $method,
622
        string $password,
623
        int $options,
624
        string $initializationVector
625
    ) {
626 2
        return openssl_encrypt($data, $method, $password, $options, $initializationVector);
627
    }
628
629
    /**
630
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
631
     *
632
     * @param string $data
633
     * @param string $method
634
     * @param string $password
635
     * @param int    $options
636
     * @param string $initializationVector
637
     *
638
     * @return string|false
639
     */
640 1
    protected function openSslDecryptImpl(
641
        string $data,
642
        string $method,
643
        string $password,
644
        int $options,
645
        string $initializationVector
646
    ) {
647 1
        return openssl_decrypt($data, $method, $password, $options, $initializationVector);
648
    }
649
650
    /** @noinspection PhpTooManyParametersInspection
651
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
652
     *
653
     * @param string       $data
654
     * @param string       $method
655
     * @param string       $password
656
     * @param int          $options
657
     * @param string       $initializationVector
658
     * @param string       $aad
659
     * @param string|null &$tag
660
     * @param int          $tagLength
661
     *
662
     * @return false|string
663
     */
664 1
    protected function openSslEncryptAuthenticatedImpl(
665
        string $data,
666
        string $method,
667
        string $password,
668
        int $options,
669
        string $initializationVector,
670
        string $aad,
671
        string &$tag = null,
672
        int $tagLength = 16
673
    ) {
674 1
        assert(PHP_VERSION_ID >= 70100);
675 1
        assert($this->isTagLengthMightBeValid($tagLength));
676
677 1
        $result = openssl_encrypt($data, $method, $password, $options, $initializationVector, $tag, $aad, $tagLength);
678
679 1
        return $result;
680
    }
681
682
    /**
683
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
684
     *
685
     * @param string $data
686
     * @param string $method
687
     * @param string $password
688
     * @param int    $options
689
     * @param string $initializationVector
690
     * @param string $aad
691
     * @param string $tag
692
     *
693
     * @return false|string
694
     */
695 1
    protected function openSslDecryptAuthenticatedImpl(
696
        string $data,
697
        string $method,
698
        string $password,
699
        int $options,
700
        string $initializationVector,
701
        string $aad,
702
        string $tag
703
    ) {
704 1
        assert(PHP_VERSION_ID >= 70100);
705
706 1
        return openssl_decrypt($data, $method, $password, $options, $initializationVector, $tag, $aad);
707
    }
708
709
    /**
710
     * We need this wrapper for testing purposes so we can mock system call to Open SSL.
711
     *
712
     * @param string $method
713
     *
714
     * @return int|false
715
     */
716 6
    protected function openSslIvLengthImpl(string $method)
717
    {
718 6
        return openssl_cipher_iv_length($method);
719
    }
720
721
    /**
722
     * @param int $length
723
     *
724
     * @return bool
725
     */
726 2
    private function isTagLengthMightBeValid(int $length): bool
727
    {
728
        // @link http://php.net/manual/en/function.openssl-encrypt.php
729
730 2
        return 4 <= $length && $length <= 16;
731
    }
732
}
733