Completed
Push — master ( 596750...db47a2 )
by Neomerx
03:17
created

SymmetricCrypt::getAdditionalAuthenticationData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
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 3
54
    /**
55 3
     * Use Authenticated Encryption with Associated Data (since PHP 7.1)
56 3
     *
57
     * @var bool
58
     */
59
    private $useAuthentication = false;
60
61 1
    /**
62
     * Additional authentication data.
63 1
     *
64
     * @var string
65 1
     */
66
    private $aad = '';
67 1
68
    /**
69 1
     * The length of the authentication tag. Its value can be between 4 and 16 for GCM (Galois/Counter Mode) mode.
70 1
     *
71 1
     * @var int
72 1
     */
73
    private $tagLength = 16;
74
75 1
    /**
76
     * @param string $method
77 1
     * @param string $password
78
     */
79
    public function __construct(string $method, string $password)
80
    {
81
        $this->setMethod($method)->setPassword($password)->asRaw();
82
    }
83 3
84
    /**
85 3
     * @inheritdoc
86
     *
87 3
     * @SuppressWarnings(PHPMD.ElseExpression)
88
     */
89 3
    public function decrypt(string $data): string
90
    {
91 3
        $this->clearErrors();
92 3
93 3
        $vector = $this->getIV();
94 3
        if (empty($vector) === true) {
95
            $ivLength = $this->openSslIvLength($this->getMethod());
96
            $vector   = $this->readIV($data, $ivLength);
97 3
            $data     = $this->extractData($data, $ivLength);
98
        }
99 2
100
        if ($this->isUseAuthentication() === true) {
101
            $tagLength = $this->getTagLength();
102
            $tag       = $this->readTag($data, $tagLength);
103
            $data      = $this->extractData($data, $tagLength);
104
105 3
            $decrypted = $this->openSslDecryptAuthenticated(
106
                $data,
107 3
                $this->getMethod(),
108
                $this->getPassword(),
109
                $this->getOptions(),
110
                $vector,
111
                $this->getAdditionalAuthenticationData(),
112
                $tag
113 3
            );
114
        } else {
115 3
            $decrypted = $this->openSslDecrypt(
116
                $data,
117
                $this->getMethod(),
118
                $this->getPassword(),
119
                $this->getOptions(),
120
                $vector
121
            );
122
        }
123 3
124
        return $decrypted;
125 3
    }
126
127 3
    /**
128
     * @inheritdoc
129 3
     *
130
     * @SuppressWarnings(PHPMD.ElseExpression)
131
     */
132
    public function encrypt(string $data): string
133
    {
134
        $this->clearErrors();
135
136
        $isAddIvToOutput = false;
137 3
        $vector          = $this->getIV();
138
        if (empty($vector) === true) {
139 3
            $vector          = $this->generateIV();
140
            $isAddIvToOutput = true;
141 3
        }
142
143 3
        if ($this->isUseAuthentication() === true) {
144
            $encrypted = $this->openSslEncryptAuthenticated(
145
                $data,
146
                $this->getMethod(),
147
                $this->getPassword(),
148
                $this->getOptions(),
149
                $vector,
150
                $this->getAdditionalAuthenticationData(),
151 1
                $tag,
152
                $this->getTagLength()
153 1
            );
154
155 1
            // 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 1
            // 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
            $encrypted = $tag . $encrypted;
161
        } else {
162
            $encrypted = $this->openSslEncrypt(
163 3
                $data,
164
                $this->getMethod(),
165 3
                $this->getPassword(),
166 2
                $this->getOptions(),
167
                $vector
168
            );
169 3
        }
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 3
        // Appendix C: Generation of Initialization Vectors
176
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
177 3
        // ...
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
        $result = $isAddIvToOutput === false ? $encrypted : $vector . $encrypted;
182
183 1
        return $result;
184
    }
185 1
186
    /**
187
     * @return string
188
     */
189
    public function getPassword(): string
190
    {
191 1
        return $this->password;
192
    }
193 1
194
    /**
195
     * @return string
196
     */
197
    public function getMethod(): string
198
    {
199 3
        return $this->method;
200
    }
201 3
202
    /**
203
     * @param string $method
204
     *
205
     * @return self
206
     */
207 3
    public function setMethod(string $method): self
208
    {
209 3
        assert(
210
            ($availableMethods = openssl_get_cipher_methods(true)) !== false &&
211
            in_array($method, $availableMethods) === true
212
        );
213
214
        $this->method = $method;
215
216
        return $this;
217 3
    }
218
219 3
    /**
220
     * @param string $password
221 3
     *
222
     * @return self
223 3
     */
224
    public function setPassword(string $password): self
225
    {
226
        assert(empty($password) === false);
227
228
        $this->password = $password;
229 2
230
        return $this;
231 2
    }
232
233 2
    /**
234
     * @param string $value
235
     *
236
     * @return self
237
     */
238
    public function setIV(string $value): self
239
    {
240
        $this->initializationVector = $value;
241 3
242
        return $this;
243 3
    }
244
245 3
    /**
246
     * @return string
247 3
     */
248
    public function getIV(): string
249
    {
250
        return $this->initializationVector;
251
    }
252
253
    /**
254
     * @return self
255 3
     */
256
    public function withZeroPadding(): self
257 3
    {
258
        return $this->setOption(OPENSSL_ZERO_PADDING);
259 3
    }
260
261 3
    /**
262
     * @return self
263
     */
264
    public function withoutZeroPadding(): self
265
    {
266
        return $this->clearOption(OPENSSL_ZERO_PADDING);
267
    }
268
269
    /**
270
     * @return bool
271
     */
272
    public function isUseAuthentication(): bool
273
    {
274
        return $this->useAuthentication;
275 1
    }
276
277 1
    /**
278
     * Authenticated Encryption with Associated Data available for certain methods since PHP 7.1.
279
     *
280
     * @return self
281
     */
282
    public function enableAuthentication(): self
283
    {
284
        $this->useAuthentication = true;
285
286
        return $this;
287
    }
288
289
    /**
290
     * Authenticated Encryption with Associated Data available for certain methods since PHP 7.1.
291 2
     *
292
     * @return self
293 2
     */
294
    public function disableAuthentication(): self
295
    {
296
        $this->useAuthentication = false;
297
298
        return $this;
299
    }
300
301
    /**
302
     * @return string
303
     */
304
    public function getAdditionalAuthenticationData(): string
305
    {
306
        return $this->aad;
307
    }
308
309
    /**
310
     * @param string $data
311
     *
312
     * @return self
313
     */
314
    public function setAdditionalAuthenticationData(string $data): self
315
    {
316
        $this->aad = $data;
317
318
        return $this;
319
    }
320
321
    /**
322
     * @return int
323
     */
324
    public function getTagLength(): int
325
    {
326
        return $this->tagLength;
327
    }
328
329
    /**
330
     * @param int $length
331
     *
332
     * @return self
333
     */
334
    public function setTagLength(int $length): self
335
    {
336
        assert($this->isTagLengthMightBeValid($length));
337
338
        $this->tagLength = $length;
339
340
        return $this;
341
    }
342
343
    /**
344
     * @return self
345
     */
346
    protected function asRaw(): self
347
    {
348
        return $this->setOption(OPENSSL_RAW_DATA);
349
    }
350
351
    /**
352
     * @return int
353
     */
354
    protected function getOptions(): int
355
    {
356
        return $this->options;
357
    }
358
359
    /**
360
     * @param int $options
361
     *
362
     * @return self
363
     */
364
    protected function setOptions(int $options): self
365
    {
366
        $this->options = $options;
367
368
        return $this;
369
    }
370
371
    /**
372
     * @return string
373
     */
374
    protected function generateIV(): string
375
    {
376
        $ivLength = $this->openSslIvLength($this->getMethod());
377
        $ivLength !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
378
379
        $vector = openssl_random_pseudo_bytes($ivLength);
380
381
        return $vector;
382
    }
383
384
    /**
385
     * @param int $option
386
     *
387
     * @return self
388
     */
389
    protected function setOption(int $option): self
390
    {
391
        $this->setOptions($this->getOptions() | $option);
392
393
        return $this;
394
    }
395
396
    /**
397
     * @param int $option
398
     *
399
     * @return self
400
     */
401
    protected function clearOption(int $option): self
402
    {
403
        $this->setOptions($this->getOptions() & ~$option);
404
405
        return $this;
406
    }
407
408
    /**
409
     * @param string $data
410
     * @param int    $ivLength
411
     *
412
     * @return string
413
     */
414 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
        $vector = substr($data, 0, $ivLength);
417
        $isOk   = $vector !== false && strlen($vector) === $ivLength;
418
419
        $isOk === true ?: $this->throwException(new CryptException($this->getReadVectorErrorMessage()));
420
421
        return $vector;
422
    }
423
424
    /**
425
     * @param string $data
426
     * @param int    $tagLength
427
     *
428
     * @return string
429
     */
430
    protected function readTag(string $data, int $tagLength): string
431
    {
432
        $tag  = substr($data, 0, $tagLength);
433
        $isOk = $tag !== false && strlen($tag) === $tagLength;
434
435
        $isOk === true ?: $this->throwException(new CryptException($this->getReadTagErrorMessage()));
436
437
        return $tag;
438
    }
439
440
    /**
441
     * @param string $data
442
     * @param int    $ivLength
443
     *
444
     * @return string
445
     */
446 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
        $result = substr($data, $ivLength);
449
450
        $isOk = $result !== false && empty($result) === false;
451
        $isOk === true ?: $this->throwException(new CryptException($this->getExtractDataErrorMessage()));
452
453
        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 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
        $encrypted = $this->openSslEncryptImpl($data, $method, $password, $options, $initializationVector);
473
474
        $message = $this->getErrorMessage();
475
        $encrypted !== false ?: $this->throwException(new CryptException($message));
476
477
        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 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
        $decrypted = $this->openSslDecryptImpl($data, $method, $password, $options, $initializationVector);
497
498
        $decrypted !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
499
500
        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
    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
        $encrypted = $this->openSslEncryptAuthenticatedImpl(
526
            $data,
527
            $method,
528
            $password,
529
            $options,
530
            $initializationVector,
531
            $aad,
532
            $tag,
533
            $tagLength
534
        );
535
536
        $message = $this->getErrorMessage();
537
        $encrypted !== false ?: $this->throwException(new CryptException($message));
538
539
        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 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
            ->openSslDecryptAuthenticatedImpl($data, $method, $password, $options, $initializationVector, $aad, $tag);
564
565
        $decrypted !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
566
567
        return $decrypted;
568
    }
569
570
    /**
571
     * @param string $method
572
     *
573
     * @return int
574
     */
575
    protected function openSslIvLength(string $method): int
576
    {
577
        $ivLength = $this->openSslIvLengthImpl($method);
578
579
        $ivLength !== false ?: $this->throwException(new CryptException($this->getErrorMessage()));
580
581
        return $ivLength;
582
    }
583
584
    /**
585
     * @return string
586
     */
587
    protected function getReadVectorErrorMessage(): string
588
    {
589
        return 'Reading Initialization Vector (IV) failed';
590
    }
591
592
    /**
593
     * @return string
594
     */
595
    protected function getReadTagErrorMessage(): string
596
    {
597
        return 'Reading Authenticated Encryption Tag failed';
598
    }
599
600
    /**
601
     * @return string
602
     */
603
    protected function getExtractDataErrorMessage(): string
604
    {
605
        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
    protected function openSslEncryptImpl(
620
        string $data,
621
        string $method,
622
        string $password,
623
        int $options,
624
        string $initializationVector
625
    ) {
626
        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
    protected function openSslDecryptImpl(
641
        string $data,
642
        string $method,
643
        string $password,
644
        int $options,
645
        string $initializationVector
646
    ) {
647
        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
    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
        assert(PHP_VERSION_ID >= 70100);
675
        assert($this->isTagLengthMightBeValid($tagLength));
676
677
        $result = openssl_encrypt($data, $method, $password, $options, $initializationVector, $tag, $aad, $tagLength);
678
679
        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
    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
        assert(PHP_VERSION_ID >= 70100);
705
706
        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
    protected function openSslIvLengthImpl(string $method)
717
    {
718
        return openssl_cipher_iv_length($method);
719
    }
720
721
    /**
722
     * @param int $length
723
     *
724
     * @return bool
725
     */
726
    private function isTagLengthMightBeValid(int $length): bool
727
    {
728
        // @link http://php.net/manual/en/function.openssl-encrypt.php
729
730
        return 4 <= $length && $length <= 16;
731
    }
732
}
733