Crypt   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 17
eloc 57
c 3
b 0
f 0
dl 0
loc 279
ccs 55
cts 55
cp 1
rs 10

10 Methods

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

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

233
        $signed = (new Mac())->sign($iv . /** @scrutinizer ignore-type */ $encrypted, $authKey);
Loading history...
234
235
        /*
236
         * Output: [keySalt][MAC][IV][ciphertext]
237
         * - keySalt is KEY_SIZE bytes long
238
         * - MAC: message authentication code, length same as the output of MAC_HASH
239
         * - IV: initialization vector, length $blockSize
240
         */
241 7
        return $keySalt . $signed;
242
    }
243
244
    /**
245
     * Decrypts data.
246
     *
247
     * @param string $data encrypted data to be decrypted.
248
     * @param bool $passwordBased set true to use password-based key derivation
249
     * @param string $secret the decryption password or key
250
     * @param string $info context/application specific information, @see encrypt()
251
     *
252
     * @throws \RuntimeException on OpenSSL not loaded
253
     * @throws \Exception on OpenSSL errors
254
     * @throws AuthenticationException on authentication failure
255
     *
256
     * @return string the decrypted data
257
     *
258
     * @see encrypt()
259
     */
260 43
    private function decrypt(string $data, bool $passwordBased, string $secret, string $info): string
261
    {
262 43
        [$blockSize, $keySize] = self::ALLOWED_CIPHERS[$this->cipher];
263
264 43
        $keySalt = StringHelper::byteSubstring($data, 0, $keySize);
265 43
        if ($passwordBased) {
266 21
            $key = hash_pbkdf2($this->kdfAlgorithm, $secret, $keySalt, $this->derivationIterations, $keySize, true);
267
        } else {
268 22
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, $info, $keySalt);
269
        }
270
271 43
        $authKey = hash_hkdf($this->kdfAlgorithm, $key, $keySize, $this->authorizationKeyInfo);
272
273
        try {
274 43
            $data = (new Mac())->getMessage(StringHelper::byteSubstring($data, $keySize), $authKey);
275 3
        } catch (DataIsTamperedException $e) {
276 3
            throw new AuthenticationException();
277
        }
278
279 40
        $iv = StringHelper::byteSubstring($data, 0, $blockSize);
280 40
        $encrypted = StringHelper::byteSubstring($data, $blockSize);
281
282 40
        $decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
283 40
        if ($decrypted === false) {
284 1
            throw new \RuntimeException('OpenSSL failure on decryption: ' . openssl_error_string());
285
        }
286
287 39
        return $decrypted;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $decrypted could return the type true which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
288
    }
289
}
290