Passed
Push — master ( 469d65...b71b8c )
by Alexander
01:28
created

Crypt   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 242
Duplicated Lines 0 %

Test Coverage

Coverage 76.67%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 19
eloc 61
c 1
b 0
f 0
dl 0
loc 242
rs 10
ccs 46
cts 60
cp 0.7667

10 Methods

Rating   Name   Duplication   Size   Complexity  
A encryptByKey() 0 3 1
A encrypt() 0 28 3
A withAuthKeyInfo() 0 5 1
A encryptByPassword() 0 3 1
A withDerivationInterations() 0 5 1
A decryptByPassword() 0 3 1
A withKdfAlgorithm() 0 5 1
A decryptByKey() 0 3 1
A __construct() 0 10 3
B decrypt() 0 35 6
1
<?php
2
namespace Yiisoft\Security;
3
4
use Yiisoft\Strings\StringHelper;
5
6
final class Crypt
7
{
8
    /**
9
     * @var string The cipher to use for encryption and decryption.
10
     */
11
    private $cipher;
12
13
    /**
14
     * @var array[] Look-up table of block sizes and key sizes for each supported OpenSSL cipher.
15
     *
16
     * In each element, the key is one of the ciphers supported by OpenSSL {@see openssl_get_cipher_methods()}.
17
     * The value is an array of two integers, the first is the cipher's block size in bytes and the second is
18
     * the key size in bytes.
19
     *
20
     * > Note: Yii's encryption protocol uses the same size for cipher key, HMAC signature key and key
21
     * derivation salt.
22
     */
23
    private const ALLOWED_CIPHERS = [
24
        'AES-128-CBC' => [16, 16],
25
        'AES-192-CBC' => [16, 24],
26
        'AES-256-CBC' => [16, 32],
27
    ];
28
29
    /**
30
     * @var string Hash algorithm for key derivation. Recommend sha256, sha384 or sha512.
31
     * @see http://php.net/manual/en/function.hash-algos.php
32
     */
33
    private $kdfAlgorithm = 'sha256';
34
35
    /**
36
     * @var string HKDF info value for derivation of message authentication key.
37
     */
38
    private $authKeyInfo = 'AuthorizationKey';
39
    /**
40
     * @var int derivation iterations count.
41
     * Set as high as possible to hinder dictionary password attacks.
42
     */
43
    private $derivationIterations = 100000;
44
45 42
    public function __construct(string $cipher = 'AES-128-CBC')
46
    {
47 42
        if (!extension_loaded('openssl')) {
48
            throw new \RuntimeException('Encryption requires the OpenSSL PHP extension');
49
        }
50 42
        if (!isset(self::ALLOWED_CIPHERS[$cipher][0], self::ALLOWED_CIPHERS[$cipher][1])) {
51
            throw new \RuntimeException($cipher . ' is not an allowed cipher');
52
        }
53
54 42
        $this->cipher = $cipher;
55 42
    }
56
57
    public function withKdfAlgorithm(string $algorithm): self
58
    {
59
        $new = clone $this;
60
        $new->kdfAlgorithm = $algorithm;
61
        return $new;
62
    }
63
64
    public function withAuthKeyInfo(string $info): self
65
    {
66
        $new = clone $this;
67
        $new->authKeyInfo = $info;
68
        return $new;
69
    }
70
71 42
    public function withDerivationInterations(int $interations): self
72
    {
73 42
        $new = clone $this;
74 42
        $new->derivationIterations = $interations;
75 42
        return $new;
76
    }
77
78
    /**
79
     * Encrypts data using a password.
80
     * Derives keys for encryption and authentication from the password using PBKDF2 and a random salt,
81
     * which is deliberately slow to protect against dictionary attacks. Use {@see encryptByKey()} to
82
     * encrypt fast using a cryptographic key rather than a password. Key derivation time is
83
     * determined by {@see $derivationIterations}}, which should be set as high as possible.
84
     * The encrypted data includes a keyed message authentication code (MAC) so there is no need
85
     * to hash input or output data.
86
     * > Note: Avoid encrypting with passwords wherever possible. Nothing can protect against
87
     * poor-quality or compromised passwords.
88
     * @param string $data the data to encrypt
89
     * @param string $password the password to use for encryption
90
     * @return string the encrypted data
91
     * @throws \RuntimeException on OpenSSL not loaded
92
     * @throws \Exception on OpenSSL error
93
     * @see decryptByPassword()
94
     * @see encryptByKey()
95
     */
96 2
    public function encryptByPassword(string $data, string $password): string
97
    {
98 2
        return $this->encrypt($data, true, $password, null);
99
    }
100
101
    /**
102
     * Encrypts data using a cryptographic key.
103
     * Derives keys for encryption and authentication from the input key using HKDF and a random salt,
104
     * which is very fast relative to {@see encryptByPassword()}. The input key must be properly
105
     * random — use {@see random_bytes()} to generate keys.
106
     * The encrypted data includes a keyed message authentication code (MAC) so there is no need
107
     * to hash input or output data.
108
     * @param string $data the data to encrypt
109
     * @param string $inputKey the input to use for encryption and authentication
110
     * @param string|null $info context/application specific information, e.g. a user ID
111
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
112
     * @return string the encrypted data
113
     * @throws \RuntimeException on OpenSSL not loaded
114
     * @throws \Exception on OpenSSL error
115
     * @see decryptByKey()
116
     * @see encryptByPassword()
117
     */
118 4
    public function encryptByKey(string $data, string $inputKey, string $info = null): string
119
    {
120 4
        return $this->encrypt($data, false, $inputKey, $info);
121
    }
122
123
    /**
124
     * Verifies and decrypts data encrypted with {@see encryptByPassword()}.
125
     * @param string $data the encrypted data to decrypt
126
     * @param string $password the password to use for decryption
127
     * @return string the decrypted data
128
     * @throws \RuntimeException on OpenSSL not loaded
129
     * @throws \Exception on OpenSSL errors
130
     * @throws AuthenticationFailure on authentication failure
131
     * @see encryptByPassword()
132
     */
133 20
    public function decryptByPassword(string $data, string $password): string
134
    {
135 20
        return $this->decrypt($data, true, $password, null);
136
    }
137
138
    /**
139
     * Verifies and decrypts data encrypted with {@see encryptByKey()}.
140
     * @param string $data the encrypted data to decrypt
141
     * @param string $inputKey the input to use for encryption and authentication
142
     * @param string|null $info context/application specific information, e.g. a user ID
143
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
144
     * @return string the decrypted data
145
     * @throws \RuntimeException on OpenSSL not loaded
146
     * @throws \Exception on OpenSSL errors
147
     * @throws AuthenticationFailure on authentication failure
148
     * @see encryptByKey()
149
     */
150 22
    public function decryptByKey($data, $inputKey, $info = null): string
151
    {
152 22
        return $this->decrypt($data, false, $inputKey, $info);
153
    }
154
155
    /**
156
     * Encrypts data.
157
     *
158
     * @param string $data data to be encrypted
159
     * @param bool $passwordBased set true to use password-based key derivation
160
     * @param string $secret the encryption password or key
161
     * @param string|null $info context/application specific information, e.g. a user ID
162
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
163
     *
164
     * @return string the encrypted data
165
     * @throws \RuntimeException on OpenSSL not loaded
166
     * @throws \Exception on OpenSSL error
167
     * @see decrypt()
168
     */
169 6
    private function encrypt(string $data, bool $passwordBased, string $secret, ?string $info): string
170
    {
171 6
        [$blockSize, $keySize] = self::ALLOWED_CIPHERS[$this->cipher];
172
173 6
        $keySalt = random_bytes($keySize);
174 6
        if ($passwordBased) {
175 2
            $key = hash_pbkdf2($this->kdfAlgorithm, $secret, $keySalt, $this->derivationIterations, $keySize, true);
176
        } else {
177 4
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, $info, $keySalt);
0 ignored issues
show
Bug introduced by
It seems like $info can also be of type null; however, parameter $info of hash_hkdf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

177
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, /** @scrutinizer ignore-type */ $info, $keySalt);
Loading history...
178
        }
179
180 6
        $iv = random_bytes($blockSize);
181
182 6
        $encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
183 6
        if ($encrypted === false) {
184
            throw new \RuntimeException('OpenSSL failure on encryption: ' . openssl_error_string());
185
        }
186
187 6
        $authKey = hash_hkdf($this->kdfAlgorithm, $key, $keySize, $this->authKeyInfo);
188 6
        $signed = (new Mac())->sign($iv . $encrypted, $authKey);
189
190
        /*
191
         * Output: [keySalt][MAC][IV][ciphertext]
192
         * - keySalt is KEY_SIZE bytes long
193
         * - MAC: message authentication code, length same as the output of MAC_HASH
194
         * - IV: initialization vector, length $blockSize
195
         */
196 6
        return $keySalt . $signed;
197
    }
198
199
    /**
200
     * Decrypts data.
201
     *
202
     * @param string $data encrypted data to be decrypted.
203
     * @param bool $passwordBased set true to use password-based key derivation
204
     * @param string $secret the decryption password or key
205
     * @param string|null $info context/application specific information, @see encrypt()
206
     *
207
     * @return string the decrypted data
208
     * @throws \RuntimeException on OpenSSL not loaded
209
     * @throws \Exception on OpenSSL errors
210
     * @throws AuthenticationFailure on authentication failure
211
     * @see encrypt()
212
     */
213 42
    private function decrypt(string $data, bool $passwordBased, string $secret, ?string $info): string
214
    {
215 42
        if (!extension_loaded('openssl')) {
216
            throw new \RuntimeException('Encryption requires the OpenSSL PHP extension');
217
        }
218 42
        if (!isset(self::ALLOWED_CIPHERS[$this->cipher][0], self::ALLOWED_CIPHERS[$this->cipher][1])) {
219
            throw new \RuntimeException($this->cipher . ' is not an allowed cipher');
220
        }
221
222 42
        [$blockSize, $keySize] = self::ALLOWED_CIPHERS[$this->cipher];
223
224 42
        $keySalt = StringHelper::byteSubstr($data, 0, $keySize);
225 42
        if ($passwordBased) {
226 20
            $key = hash_pbkdf2($this->kdfAlgorithm, $secret, $keySalt, $this->derivationIterations, $keySize, true);
227
        } else {
228 22
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, $info, $keySalt);
0 ignored issues
show
Bug introduced by
It seems like $info can also be of type null; however, parameter $info of hash_hkdf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

228
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, /** @scrutinizer ignore-type */ $info, $keySalt);
Loading history...
229
        }
230
231 42
        $authKey = hash_hkdf($this->kdfAlgorithm, $key, $keySize, $this->authKeyInfo);
232
233
        try {
234 42
            $data = (new Mac())->getMessage(StringHelper::byteSubstr($data, $keySize), $authKey);
235 3
        } catch (DataIsTampered $e) {
236 3
            throw new AuthenticationFailure('Failed to decrypt data');
237
        }
238
239 39
        $iv = StringHelper::byteSubstr($data, 0, $blockSize);
240 39
        $encrypted = StringHelper::byteSubstr($data, $blockSize);
241
242 39
        $decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
243 39
        if ($decrypted === false) {
244
            throw new \RuntimeException('OpenSSL failure on decryption: ' . openssl_error_string());
245
        }
246
247 39
        return $decrypted;
248
    }
249
}
250