Completed
Push — php7-travis-apcu ( 9bbcee...fd63c3 )
by Alexander
14:47
created

Security::pbkdf2()   C

Complexity

Conditions 16
Paths 57

Size

Total Lines 46
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 164.1303

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 5
cts 30
cp 0.1666
rs 5.0026
c 0
b 0
f 0
cc 16
eloc 30
nc 57
nop 5
crap 164.1303

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\base;
9
10
use Yii;
11
use yii\helpers\StringHelper;
12
13
/**
14
 * Security provides a set of methods to handle common security-related tasks.
15
 *
16
 * In particular, Security supports the following features:
17
 *
18
 * - Encryption/decryption: [[encryptByKey()]], [[decryptByKey()]], [[encryptByPassword()]] and [[decryptByPassword()]]
19
 * - Key derivation using standard algorithms: [[pbkdf2()]] and [[hkdf()]]
20
 * - Data tampering prevention: [[hashData()]] and [[validateData()]]
21
 * - Password validation: [[generatePasswordHash()]] and [[validatePassword()]]
22
 *
23
 * > Note: this class requires 'OpenSSL' PHP extension for random key/string generation on Windows and
24
 * for encryption/decryption on all platforms. For the highest security level PHP version >= 5.5.0 is recommended.
25
 *
26
 * For more details and usage information on Security, see the [guide article on security](guide:security-overview).
27
 *
28
 * @author Qiang Xue <[email protected]>
29
 * @author Tom Worster <[email protected]>
30
 * @author Klimov Paul <[email protected]>
31
 * @since 2.0
32
 */
33
class Security extends Component
34
{
35
    /**
36
     * @var string The cipher to use for encryption and decryption.
37
     */
38
    public $cipher = 'AES-128-CBC';
39
    /**
40
     * @var array[] Look-up table of block sizes and key sizes for each supported OpenSSL cipher.
41
     *
42
     * In each element, the key is one of the ciphers supported by OpenSSL (@see openssl_get_cipher_methods()).
43
     * The value is an array of two integers, the first is the cipher's block size in bytes and the second is
44
     * the key size in bytes.
45
     *
46
     * > Warning: All OpenSSL ciphers that we recommend are in the default value, i.e. AES in CBC mode.
47
     *
48
     * > Note: Yii's encryption protocol uses the same size for cipher key, HMAC signature key and key
49
     * derivation salt.
50
     */
51
    public $allowedCiphers = [
52
        'AES-128-CBC' => [16, 16],
53
        'AES-192-CBC' => [16, 24],
54
        'AES-256-CBC' => [16, 32],
55
    ];
56
    /**
57
     * @var string Hash algorithm for key derivation. Recommend sha256, sha384 or sha512.
58
     * @see [hash_algos()](http://php.net/manual/en/function.hash-algos.php)
59
     */
60
    public $kdfHash = 'sha256';
61
    /**
62
     * @var string Hash algorithm for message authentication. Recommend sha256, sha384 or sha512.
63
     * @see [hash_algos()](http://php.net/manual/en/function.hash-algos.php)
64
     */
65
    public $macHash = 'sha256';
66
    /**
67
     * @var string HKDF info value for derivation of message authentication key.
68
     * @see hkdf()
69
     */
70
    public $authKeyInfo = 'AuthorizationKey';
71
    /**
72
     * @var int derivation iterations count.
73
     * Set as high as possible to hinder dictionary password attacks.
74
     */
75
    public $derivationIterations = 100000;
76
    /**
77
     * @var string strategy, which should be used to generate password hash.
78
     * Available strategies:
79
     * - 'password_hash' - use of PHP `password_hash()` function with PASSWORD_DEFAULT algorithm.
80
     *   This option is recommended, but it requires PHP version >= 5.5.0
81
     * - 'crypt' - use PHP `crypt()` function.
82
     * @deprecated since version 2.0.7, [[generatePasswordHash()]] ignores [[passwordHashStrategy]] and
83
     * uses `password_hash()` when available or `crypt()` when not.
84
     */
85
    public $passwordHashStrategy;
86
    /**
87
     * @var int Default cost used for password hashing.
88
     * Allowed value is between 4 and 31.
89
     * @see generatePasswordHash()
90
     * @since 2.0.6
91
     */
92
    public $passwordHashCost = 13;
93
94
95
    /**
96
     * Encrypts data using a password.
97
     * Derives keys for encryption and authentication from the password using PBKDF2 and a random salt,
98
     * which is deliberately slow to protect against dictionary attacks. Use [[encryptByKey()]] to
99
     * encrypt fast using a cryptographic key rather than a password. Key derivation time is
100
     * determined by [[$derivationIterations]], which should be set as high as possible.
101
     * The encrypted data includes a keyed message authentication code (MAC) so there is no need
102
     * to hash input or output data.
103
     * > Note: Avoid encrypting with passwords wherever possible. Nothing can protect against
104
     * poor-quality or compromised passwords.
105
     * @param string $data the data to encrypt
106
     * @param string $password the password to use for encryption
107
     * @return string the encrypted data
108
     * @see decryptByPassword()
109
     * @see encryptByKey()
110
     */
111 1
    public function encryptByPassword($data, $password)
112
    {
113 1
        return $this->encrypt($data, true, $password, null);
114
    }
115
116
    /**
117
     * Encrypts data using a cryptographic key.
118
     * Derives keys for encryption and authentication from the input key using HKDF and a random salt,
119
     * which is very fast relative to [[encryptByPassword()]]. The input key must be properly
120
     * random -- use [[generateRandomKey()]] to generate keys.
121
     * The encrypted data includes a keyed message authentication code (MAC) so there is no need
122
     * to hash input or output data.
123
     * @param string $data the data to encrypt
124
     * @param string $inputKey the input to use for encryption and authentication
125
     * @param string $info optional context and application specific information, see [[hkdf()]]
126
     * @return string the encrypted data
127
     * @see decryptByKey()
128
     * @see encryptByPassword()
129
     */
130 1
    public function encryptByKey($data, $inputKey, $info = null)
131
    {
132 1
        return $this->encrypt($data, false, $inputKey, $info);
133
    }
134
135
    /**
136
     * Verifies and decrypts data encrypted with [[encryptByPassword()]].
137
     * @param string $data the encrypted data to decrypt
138
     * @param string $password the password to use for decryption
139
     * @return bool|string the decrypted data or false on authentication failure
140
     * @see encryptByPassword()
141
     */
142 10
    public function decryptByPassword($data, $password)
143
    {
144 10
        return $this->decrypt($data, true, $password, null);
145
    }
146
147
    /**
148
     * Verifies and decrypts data encrypted with [[encryptByKey()]].
149
     * @param string $data the encrypted data to decrypt
150
     * @param string $inputKey the input to use for encryption and authentication
151
     * @param string $info optional context and application specific information, see [[hkdf()]]
152
     * @return bool|string the decrypted data or false on authentication failure
153
     * @see encryptByKey()
154
     */
155 10
    public function decryptByKey($data, $inputKey, $info = null)
156
    {
157 10
        return $this->decrypt($data, false, $inputKey, $info);
158
    }
159
160
    /**
161
     * Encrypts data.
162
     *
163
     * @param string $data data to be encrypted
164
     * @param bool $passwordBased set true to use password-based key derivation
165
     * @param string $secret the encryption password or key
166
     * @param string|null $info context/application specific information, e.g. a user ID
167
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
168
     *
169
     * @return string the encrypted data
170
     * @throws InvalidConfigException on OpenSSL not loaded
171
     * @throws Exception on OpenSSL error
172
     * @see decrypt()
173
     */
174 2
    protected function encrypt($data, $passwordBased, $secret, $info)
175
    {
176 2
        if (!extension_loaded('openssl')) {
177
            throw new InvalidConfigException('Encryption requires the OpenSSL PHP extension');
178
        }
179 2
        if (!isset($this->allowedCiphers[$this->cipher][0], $this->allowedCiphers[$this->cipher][1])) {
180
            throw new InvalidConfigException($this->cipher . ' is not an allowed cipher');
181
        }
182
183 2
        [$blockSize, $keySize] = $this->allowedCiphers[$this->cipher];
0 ignored issues
show
Bug introduced by
The variable $blockSize does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $keySize does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
184
185 2
        $keySalt = $this->generateRandomKey($keySize);
186 2
        if ($passwordBased) {
187 1
            $key = $this->pbkdf2($this->kdfHash, $secret, $keySalt, $this->derivationIterations, $keySize);
188
        } else {
189 1
            $key = $this->hkdf($this->kdfHash, $secret, $keySalt, $info, $keySize);
190
        }
191
192 2
        $iv = $this->generateRandomKey($blockSize);
193
194 2
        $encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
195 2
        if ($encrypted === false) {
196
            throw new \yii\base\Exception('OpenSSL failure on encryption: ' . openssl_error_string());
197
        }
198
199 2
        $authKey = $this->hkdf($this->kdfHash, $key, null, $this->authKeyInfo, $keySize);
200 2
        $hashed = $this->hashData($iv . $encrypted, $authKey);
201
202
        /*
203
         * Output: [keySalt][MAC][IV][ciphertext]
204
         * - keySalt is KEY_SIZE bytes long
205
         * - MAC: message authentication code, length same as the output of MAC_HASH
206
         * - IV: initialization vector, length $blockSize
207
         */
208 2
        return $keySalt . $hashed;
209
    }
210
211
    /**
212
     * Decrypts data.
213
     *
214
     * @param string $data encrypted data to be decrypted.
215
     * @param bool $passwordBased set true to use password-based key derivation
216
     * @param string $secret the decryption password or key
217
     * @param string|null $info context/application specific information, @see encrypt()
218
     *
219
     * @return bool|string the decrypted data or false on authentication failure
220
     * @throws InvalidConfigException on OpenSSL not loaded
221
     * @throws Exception on OpenSSL error
222
     * @see encrypt()
223
     */
224 20
    protected function decrypt($data, $passwordBased, $secret, $info)
225
    {
226 20
        if (!extension_loaded('openssl')) {
227
            throw new InvalidConfigException('Encryption requires the OpenSSL PHP extension');
228
        }
229 20
        if (!isset($this->allowedCiphers[$this->cipher][0], $this->allowedCiphers[$this->cipher][1])) {
230
            throw new InvalidConfigException($this->cipher . ' is not an allowed cipher');
231
        }
232
233 20
        [$blockSize, $keySize] = $this->allowedCiphers[$this->cipher];
0 ignored issues
show
Bug introduced by
The variable $blockSize does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $keySize does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
234
235 20
        $keySalt = StringHelper::byteSubstr($data, 0, $keySize);
236 20
        if ($passwordBased) {
237 10
            $key = $this->pbkdf2($this->kdfHash, $secret, $keySalt, $this->derivationIterations, $keySize);
238
        } else {
239 10
            $key = $this->hkdf($this->kdfHash, $secret, $keySalt, $info, $keySize);
240
        }
241
242 20
        $authKey = $this->hkdf($this->kdfHash, $key, null, $this->authKeyInfo, $keySize);
243 20
        $data = $this->validateData(StringHelper::byteSubstr($data, $keySize, null), $authKey);
244 20
        if ($data === false) {
245 2
            return false;
246
        }
247
248 20
        $iv = StringHelper::byteSubstr($data, 0, $blockSize);
249 20
        $encrypted = StringHelper::byteSubstr($data, $blockSize, null);
250
251 20
        $decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
252 20
        if ($decrypted === false) {
253
            throw new \yii\base\Exception('OpenSSL failure on decryption: ' . openssl_error_string());
254
        }
255
256 20
        return $decrypted;
257
    }
258
259
    /**
260
     * Derives a key from the given input key using the standard HKDF algorithm.
261
     * Implements HKDF specified in [RFC 5869](https://tools.ietf.org/html/rfc5869).
262
     * Recommend use one of the SHA-2 hash algorithms: sha224, sha256, sha384 or sha512.
263
     * @param string $algo a hash algorithm supported by `hash_hmac()`, e.g. 'SHA-256'
264
     * @param string $inputKey the source key
265
     * @param string $salt the random salt
266
     * @param string $info optional info to bind the derived key material to application-
267
     * and context-specific information, e.g. a user ID or API version, see
268
     * [RFC 5869](https://tools.ietf.org/html/rfc5869)
269
     * @param int $length length of the output key in bytes. If 0, the output key is
270
     * the length of the hash algorithm output.
271
     * @throws InvalidArgumentException when HMAC generation fails.
272
     * @return string the derived key
273
     */
274 27
    public function hkdf($algo, $inputKey, $salt = null, $info = null, $length = 0)
275
    {
276 27
        if (function_exists('hash_hkdf')) {
277 27
            $outputKey = hash_hkdf($algo, $inputKey, $length, $info, $salt);
278 27
            if ($outputKey === false) {
279
                throw new InvalidArgumentException('Invalid parameters to hash_hkdf()');
280
            }
281 27
            return $outputKey;
282
        }
283
284
        $test = @hash_hmac($algo, '', '', true);
285
        if (!$test) {
286
            throw new InvalidArgumentException('Failed to generate HMAC with hash algorithm: ' . $algo);
287
        }
288
        $hashLength = StringHelper::byteLength($test);
289
        if (is_string($length) && preg_match('{^\d{1,16}$}', $length)) {
290
            $length = (int) $length;
291
        }
292
        if (!is_int($length) || $length < 0 || $length > 255 * $hashLength) {
293
            throw new InvalidArgumentException('Invalid length');
294
        }
295
        $blocks = $length !== 0 ? ceil($length / $hashLength) : 1;
296
297
        if ($salt === null) {
298
            $salt = str_repeat("\0", $hashLength);
299
        }
300
        $prKey = hash_hmac($algo, $inputKey, $salt, true);
301
302
        $hmac = '';
303
        $outputKey = '';
304
        for ($i = 1; $i <= $blocks; $i++) {
305
            $hmac = hash_hmac($algo, $hmac . $info . chr($i), $prKey, true);
306
            $outputKey .= $hmac;
307
        }
308
309
        if ($length !== 0) {
310
            $outputKey = StringHelper::byteSubstr($outputKey, 0, $length);
311
        }
312
        return $outputKey;
313
    }
314
315
    /**
316
     * Derives a key from the given password using the standard PBKDF2 algorithm.
317
     * Implements HKDF2 specified in [RFC 2898](http://tools.ietf.org/html/rfc2898#section-5.2)
318
     * Recommend use one of the SHA-2 hash algorithms: sha224, sha256, sha384 or sha512.
319
     * @param string $algo a hash algorithm supported by `hash_hmac()`, e.g. 'SHA-256'
320
     * @param string $password the source password
321
     * @param string $salt the random salt
322
     * @param int $iterations the number of iterations of the hash algorithm. Set as high as
323
     * possible to hinder dictionary password attacks.
324
     * @param int $length length of the output key in bytes. If 0, the output key is
325
     * the length of the hash algorithm output.
326
     * @return string the derived key
327
     * @throws InvalidArgumentException when hash generation fails due to invalid params given.
328
     */
329 19
    public function pbkdf2($algo, $password, $salt, $iterations, $length = 0)
330
    {
331 19
        if (function_exists('hash_pbkdf2')) {
332 19
            $outputKey = hash_pbkdf2($algo, $password, $salt, $iterations, $length, true);
333 19
            if ($outputKey === false) {
334
                throw new InvalidArgumentException('Invalid parameters to hash_pbkdf2()');
335
            }
336 19
            return $outputKey;
337
        }
338
339
        // todo: is there a nice way to reduce the code repetition in hkdf() and pbkdf2()?
340
        $test = @hash_hmac($algo, '', '', true);
341
        if (!$test) {
342
            throw new InvalidArgumentException('Failed to generate HMAC with hash algorithm: ' . $algo);
343
        }
344
        if (is_string($iterations) && preg_match('{^\d{1,16}$}', $iterations)) {
345
            $iterations = (int) $iterations;
346
        }
347
        if (!is_int($iterations) || $iterations < 1) {
348
            throw new InvalidArgumentException('Invalid iterations');
349
        }
350
        if (is_string($length) && preg_match('{^\d{1,16}$}', $length)) {
351
            $length = (int) $length;
352
        }
353
        if (!is_int($length) || $length < 0) {
354
            throw new InvalidArgumentException('Invalid length');
355
        }
356
        $hashLength = StringHelper::byteLength($test);
357
        $blocks = $length !== 0 ? ceil($length / $hashLength) : 1;
358
359
        $outputKey = '';
360
        for ($j = 1; $j <= $blocks; $j++) {
361
            $hmac = hash_hmac($algo, $salt . pack('N', $j), $password, true);
362
            $xorsum = $hmac;
363
            for ($i = 1; $i < $iterations; $i++) {
364
                $hmac = hash_hmac($algo, $hmac, $password, true);
365
                $xorsum ^= $hmac;
366
            }
367
            $outputKey .= $xorsum;
368
        }
369
370
        if ($length !== 0) {
371
            $outputKey = StringHelper::byteSubstr($outputKey, 0, $length);
372
        }
373
        return $outputKey;
374
    }
375
376
    /**
377
     * Prefixes data with a keyed hash value so that it can later be detected if it is tampered.
378
     * There is no need to hash inputs or outputs of [[encryptByKey()]] or [[encryptByPassword()]]
379
     * as those methods perform the task.
380
     * @param string $data the data to be protected
381
     * @param string $key the secret key to be used for generating hash. Should be a secure
382
     * cryptographic key.
383
     * @param bool $rawHash whether the generated hash value is in raw binary format. If false, lowercase
384
     * hex digits will be generated.
385
     * @return string the data prefixed with the keyed hash
386
     * @throws InvalidConfigException when HMAC generation fails.
387
     * @see validateData()
388
     * @see generateRandomKey()
389
     * @see hkdf()
390
     * @see pbkdf2()
391
     */
392 3
    public function hashData($data, $key, $rawHash = false)
393
    {
394 3
        $hash = hash_hmac($this->macHash, $data, $key, $rawHash);
395 3
        if (!$hash) {
396
            throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
397
        }
398 3
        return $hash . $data;
399
    }
400
401
    /**
402
     * Validates if the given data is tampered.
403
     * @param string $data the data to be validated. The data must be previously
404
     * generated by [[hashData()]].
405
     * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]].
406
     * function to see the supported hashing algorithms on your system. This must be the same
407
     * as the value passed to [[hashData()]] when generating the hash for the data.
408
     * @param bool $rawHash this should take the same value as when you generate the data using [[hashData()]].
409
     * It indicates whether the hash value in the data is in binary format. If false, it means the hash value consists
410
     * of lowercase hex digits only.
411
     * hex digits will be generated.
412
     * @return string|false the real data with the hash stripped off. False if the data is tampered.
413
     * @throws InvalidConfigException when HMAC generation fails.
414
     * @see hashData()
415
     */
416 21
    public function validateData($data, $key, $rawHash = false)
417
    {
418 21
        $test = @hash_hmac($this->macHash, '', '', $rawHash);
419 21
        if (!$test) {
420
            throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);
421
        }
422 21
        $hashLength = StringHelper::byteLength($test);
423 21
        if (StringHelper::byteLength($data) >= $hashLength) {
424 21
            $hash = StringHelper::byteSubstr($data, 0, $hashLength);
425 21
            $pureData = StringHelper::byteSubstr($data, $hashLength, null);
426
427 21
            $calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash);
428
429 21
            if ($this->compareString($hash, $calculatedHash)) {
430 21
                return $pureData;
431
            }
432
        }
433 3
        return false;
434
    }
435
436
    private $_useLibreSSL;
437
    private $_randomFile;
438
439
    /**
440
     * Generates specified number of random bytes.
441
     * Note that output may not be ASCII.
442
     * @see generateRandomString() if you need a string.
443
     *
444
     * @param int $length the number of bytes to generate
445
     * @return string the generated random bytes
446
     * @throws InvalidArgumentException if wrong length is specified
447
     * @throws Exception on failure.
448
     */
449 68
    public function generateRandomKey($length = 32)
450
    {
451 68
        if (!is_int($length)) {
452 3
            throw new InvalidArgumentException('First parameter ($length) must be an integer');
453
        }
454
455 65
        if ($length < 1) {
456 3
            throw new InvalidArgumentException('First parameter ($length) must be greater than 0');
457
        }
458
459
        // always use random_bytes() if it is available
460 62
        if (function_exists('random_bytes')) {
461 58
            return random_bytes($length);
462
        }
463
464
        // The recent LibreSSL RNGs are faster and likely better than /dev/urandom.
465
        // Parse OPENSSL_VERSION_TEXT because OPENSSL_VERSION_NUMBER is no use for LibreSSL.
466
        // https://bugs.php.net/bug.php?id=71143
467 4
        if ($this->_useLibreSSL === null) {
468 4
            $this->_useLibreSSL = defined('OPENSSL_VERSION_TEXT')
469 4
                && preg_match('{^LibreSSL (\d\d?)\.(\d\d?)\.(\d\d?)$}', OPENSSL_VERSION_TEXT, $matches)
470
                && (10000 * $matches[1]) + (100 * $matches[2]) + $matches[3] >= 20105;
471
        }
472
473
        // Since 5.4.0, openssl_random_pseudo_bytes() reads from CryptGenRandom on Windows instead
474
        // of using OpenSSL library. LibreSSL is OK everywhere but don't use OpenSSL on non-Windows.
475 4
        if ($this->_useLibreSSL
476
            || (
477 4
                DIRECTORY_SEPARATOR !== '/'
478 4
                && substr_compare(PHP_OS, 'win', 0, 3, true) === 0
479 4
                && function_exists('openssl_random_pseudo_bytes')
480
            )
481
        ) {
482
            $key = openssl_random_pseudo_bytes($length, $cryptoStrong);
483
            if ($cryptoStrong === false) {
484
                throw new Exception(
485
                    'openssl_random_pseudo_bytes() set $crypto_strong false. Your PHP setup is insecure.'
486
                );
487
            }
488
            if ($key !== false && StringHelper::byteLength($key) === $length) {
489
                return $key;
490
            }
491
        }
492
493
        // mcrypt_create_iv() does not use libmcrypt. Since PHP 5.3.7 it directly reads
494
        // CryptGenRandom on Windows. Elsewhere it directly reads /dev/urandom.
495 4
        if (function_exists('mcrypt_create_iv')) {
496
            $key = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
497
            if (StringHelper::byteLength($key) === $length) {
498
                return $key;
499
            }
500
        }
501
502
        // If not on Windows, try to open a random device.
503 4
        if ($this->_randomFile === null && DIRECTORY_SEPARATOR === '/') {
504
            // urandom is a symlink to random on FreeBSD.
505 4
            $device = PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom';
506
            // Check random device for special character device protection mode. Use lstat()
507
            // instead of stat() in case an attacker arranges a symlink to a fake device.
508 4
            $lstat = @lstat($device);
509 4
            if ($lstat !== false && ($lstat['mode'] & 0170000) === 020000) {
510 4
                $this->_randomFile = fopen($device, 'rb') ?: null;
511
512 4
                if (is_resource($this->_randomFile)) {
513
                    // Reduce PHP stream buffer from default 8192 bytes to optimize data
514
                    // transfer from the random device for smaller values of $length.
515
                    // This also helps to keep future randoms out of user memory space.
516 3
                    $bufferSize = 8;
517
518 3
                    if (function_exists('stream_set_read_buffer')) {
519 3
                        stream_set_read_buffer($this->_randomFile, $bufferSize);
520
                    }
521
                    // stream_set_read_buffer() isn't implemented on HHVM
522 3
                    if (function_exists('stream_set_chunk_size')) {
523 3
                        stream_set_chunk_size($this->_randomFile, $bufferSize);
524
                    }
525
                }
526
            }
527
        }
528
529 4
        if (is_resource($this->_randomFile)) {
530 3
            $buffer = '';
531 3
            $stillNeed = $length;
532 3
            while ($stillNeed > 0) {
533 3
                $someBytes = fread($this->_randomFile, $stillNeed);
534 3
                if ($someBytes === false) {
535 1
                    break;
536
                }
537 2
                $buffer .= $someBytes;
538 2
                $stillNeed -= StringHelper::byteLength($someBytes);
539 2
                if ($stillNeed === 0) {
540
                    // Leaving file pointer open in order to make next generation faster by reusing it.
541 2
                    return $buffer;
542
                }
543
            }
544 1
            fclose($this->_randomFile);
545 1
            $this->_randomFile = null;
546
        }
547
548 2
        throw new Exception('Unable to generate a random key');
549
    }
550
551
    /**
552
     * Generates a random string of specified length.
553
     * The string generated matches [A-Za-z0-9_-]+ and is transparent to URL-encoding.
554
     *
555
     * @param int $length the length of the key in characters
556
     * @return string the generated random key
557
     * @throws Exception on failure.
558
     */
559 14
    public function generateRandomString($length = 32)
560
    {
561 14
        if (!is_int($length)) {
562
            throw new InvalidArgumentException('First parameter ($length) must be an integer');
563
        }
564
565 14
        if ($length < 1) {
566
            throw new InvalidArgumentException('First parameter ($length) must be greater than 0');
567
        }
568
569 14
        $bytes = $this->generateRandomKey($length);
570 14
        return substr(StringHelper::base64UrlEncode($bytes), 0, $length);
571
    }
572
573
    /**
574
     * Generates a secure hash from a password and a random salt.
575
     *
576
     * The generated hash can be stored in database.
577
     * Later when a password needs to be validated, the hash can be fetched and passed
578
     * to [[validatePassword()]]. For example,
579
     *
580
     * ```php
581
     * // generates the hash (usually done during user registration or when the password is changed)
582
     * $hash = Yii::$app->getSecurity()->generatePasswordHash($password);
583
     * // ...save $hash in database...
584
     *
585
     * // during login, validate if the password entered is correct using $hash fetched from database
586
     * if (Yii::$app->getSecurity()->validatePassword($password, $hash) {
587
     *     // password is good
588
     * } else {
589
     *     // password is bad
590
     * }
591
     * ```
592
     *
593
     * @param string $password The password to be hashed.
594
     * @param int $cost Cost parameter used by the Blowfish hash algorithm.
595
     * The higher the value of cost,
596
     * the longer it takes to generate the hash and to verify a password against it. Higher cost
597
     * therefore slows down a brute-force attack. For best protection against brute-force attacks,
598
     * set it to the highest value that is tolerable on production servers. The time taken to
599
     * compute the hash doubles for every increment by one of $cost.
600
     * @return string The password hash string. When [[passwordHashStrategy]] is set to 'crypt',
601
     * the output is always 60 ASCII characters, when set to 'password_hash' the output length
602
     * might increase in future versions of PHP (http://php.net/manual/en/function.password-hash.php)
603
     * @throws Exception on bad password parameter or cost parameter.
604
     * @see validatePassword()
605
     */
606 1
    public function generatePasswordHash($password, $cost = null)
607
    {
608 1
        if ($cost === null) {
609 1
            $cost = $this->passwordHashCost;
610
        }
611
612 1
        if (function_exists('password_hash')) {
613
            /* @noinspection PhpUndefinedConstantInspection */
614 1
            return password_hash($password, PASSWORD_DEFAULT, ['cost' => $cost]);
615
        }
616
617
        $salt = $this->generateSalt($cost);
618
        $hash = crypt($password, $salt);
619
        // strlen() is safe since crypt() returns only ascii
620
        if (!is_string($hash) || strlen($hash) !== 60) {
621
            throw new Exception('Unknown error occurred while generating hash.');
622
        }
623
624
        return $hash;
625
    }
626
627
    /**
628
     * Verifies a password against a hash.
629
     * @param string $password The password to verify.
630
     * @param string $hash The hash to verify the password against.
631
     * @return bool whether the password is correct.
632
     * @throws InvalidArgumentException on bad password/hash parameters or if crypt() with Blowfish hash is not
633
     * available.
634
     * @see generatePasswordHash()
635
     */
636 1
    public function validatePassword($password, $hash)
637
    {
638 1
        if (!is_string($password) || $password === '') {
639
            throw new InvalidArgumentException('Password must be a string and cannot be empty.');
640
        }
641
642 1
        if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches)
643 1
            || $matches[1] < 4
644 1
            || $matches[1] > 30
645
        ) {
646
            throw new InvalidArgumentException('Hash is invalid.');
647
        }
648
649 1
        if (function_exists('password_verify')) {
650 1
            return password_verify($password, $hash);
651
        }
652
653
        $test = crypt($password, $hash);
654
        $n = strlen($test);
655
        if ($n !== 60) {
656
            return false;
657
        }
658
659
        return $this->compareString($test, $hash);
660
    }
661
662
    /**
663
     * Generates a salt that can be used to generate a password hash.
664
     *
665
     * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function
666
     * requires, for the Blowfish hash algorithm, a salt string in a specific format:
667
     * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters
668
     * from the alphabet "./0-9A-Za-z".
669
     *
670
     * @param int $cost the cost parameter
671
     * @return string the random salt value.
672
     * @throws InvalidArgumentException if the cost parameter is out of the range of 4 to 31.
673
     */
674
    protected function generateSalt($cost = 13)
675
    {
676
        $cost = (int) $cost;
677
        if ($cost < 4 || $cost > 31) {
678
            throw new InvalidArgumentException('Cost must be between 4 and 31.');
679
        }
680
681
        // Get a 20-byte random string
682
        $rand = $this->generateRandomKey(20);
683
        // Form the prefix that specifies Blowfish (bcrypt) algorithm and cost parameter.
684
        $salt = sprintf('$2y$%02d$', $cost);
685
        // Append the random salt data in the required base64 format.
686
        $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));
687
688
        return $salt;
689
    }
690
691
    /**
692
     * Performs string comparison using timing attack resistant approach.
693
     * @see http://codereview.stackexchange.com/questions/13512
694
     * @param string $expected string to compare.
695
     * @param string $actual user-supplied string.
696
     * @return bool whether strings are equal.
697
     */
698 41
    public function compareString($expected, $actual)
699
    {
700 41
        $expected .= "\0";
701 41
        $actual .= "\0";
702 41
        $expectedLength = StringHelper::byteLength($expected);
703 41
        $actualLength = StringHelper::byteLength($actual);
704 41
        $diff = $expectedLength - $actualLength;
705 41
        for ($i = 0; $i < $actualLength; $i++) {
706 41
            $diff |= (ord($actual[$i]) ^ ord($expected[$i % $expectedLength]));
707
        }
708 41
        return $diff === 0;
709
    }
710
711
    /**
712
     * Masks a token to make it uncompressible.
713
     * Applies a random mask to the token and prepends the mask used to the result making the string always unique.
714
     * Used to mitigate BREACH attack by randomizing how token is outputted on each request.
715
     * @param string $token An unmasked token.
716
     * @return string A masked token.
717
     * @since 2.0.12
718
     */
719 39
    public function maskToken($token)
720
    {
721
        // The number of bytes in a mask is always equal to the number of bytes in a token.
722 39
        $mask = $this->generateRandomKey(StringHelper::byteLength($token));
723 38
        return StringHelper::base64UrlEncode($mask . ($mask ^ $token));
724
    }
725
726
    /**
727
     * Unmasks a token previously masked by `maskToken`.
728
     * @param string $maskedToken A masked token.
729
     * @return string An unmasked token, or an empty string in case of token format is invalid.
730
     * @since 2.0.12
731
     */
732 8
    public function unmaskToken($maskedToken)
733
    {
734 8
        $decoded = StringHelper::base64UrlDecode($maskedToken);
735 8
        $length = StringHelper::byteLength($decoded) / 2;
736
        // Check if the masked token has an even length.
737 8
        if (!is_int($length)) {
738 1
            return '';
739
        }
740 8
        return StringHelper::byteSubstr($decoded, $length, $length) ^ StringHelper::byteSubstr($decoded, 0, $length);
741
    }
742
}
743