Security::encrypt()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         0.10.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Utility;
16
17
use Cake\Utility\Crypto\Mcrypt;
18
use Cake\Utility\Crypto\OpenSsl;
19
use InvalidArgumentException;
20
use RuntimeException;
21
22
/**
23
 * Security Library contains utility methods related to security
24
 */
25
class Security
26
{
27
    /**
28
     * Default hash method. If `$type` param for `Security::hash()` is not specified
29
     * this value is used. Defaults to 'sha1'.
30
     *
31
     * @var string
32
     */
33
    public static $hashType = 'sha1';
34
35
    /**
36
     * The HMAC salt to use for encryption and decryption routines
37
     *
38
     * @var string
39
     */
40
    protected static $_salt;
41
42
    /**
43
     * The crypto implementation to use.
44
     *
45
     * @var object
46
     */
47
    protected static $_instance;
48
49
    /**
50
     * Create a hash from string using given method.
51
     *
52
     * @param string $string String to hash
53
     * @param string|null $algorithm Hashing algo to use (i.e. sha1, sha256 etc.).
54
     *   Can be any valid algo included in list returned by hash_algos().
55
     *   If no value is passed the type specified by `Security::$hashType` is used.
56
     * @param mixed $salt If true, automatically prepends the application's salt
57
     *   value to $string (Security.salt).
58
     * @return string Hash
59
     * @throws \RuntimeException
60
     * @link https://book.cakephp.org/3/en/core-libraries/security.html#hashing-data
61
     */
62
    public static function hash($string, $algorithm = null, $salt = false)
63
    {
64
        if (empty($algorithm)) {
65
            $algorithm = static::$hashType;
66
        }
67
        $algorithm = strtolower($algorithm);
68
69
        $availableAlgorithms = hash_algos();
70
        if (!in_array($algorithm, $availableAlgorithms, true)) {
71
            throw new RuntimeException(sprintf(
72
                'The hash type `%s` was not found. Available algorithms are: %s',
73
                $algorithm,
74
                implode(', ', $availableAlgorithms)
75
            ));
76
        }
77
78
        if ($salt) {
79
            if (!is_string($salt)) {
80
                $salt = static::$_salt;
81
            }
82
            $string = $salt . $string;
83
        }
84
85
        return hash($algorithm, $string);
86
    }
87
88
    /**
89
     * Sets the default hash method for the Security object. This affects all objects
90
     * using Security::hash().
91
     *
92
     * @param string $hash Method to use (sha1/sha256/md5 etc.)
93
     * @return void
94
     * @see \Cake\Utility\Security::hash()
95
     */
96
    public static function setHash($hash)
97
    {
98
        static::$hashType = $hash;
99
    }
100
101
    /**
102
     * Get random bytes from a secure source.
103
     *
104
     * This method will fall back to an insecure source an trigger a warning
105
     * if it cannot find a secure source of random data.
106
     *
107
     * @param int $length The number of bytes you want.
108
     * @return string Random bytes in binary.
109
     */
110
    public static function randomBytes($length)
111
    {
112
        if (function_exists('random_bytes')) {
113
            return random_bytes($length);
114
        }
115
        if (!function_exists('openssl_random_pseudo_bytes')) {
116
            throw new RuntimeException(
117
                'You do not have a safe source of random data available. ' .
118
                'Install either the openssl extension, or paragonie/random_compat. ' .
119
                'Or use Security::insecureRandomBytes() alternatively.'
120
            );
121
        }
122
123
        $bytes = openssl_random_pseudo_bytes($length, $strongSource);
124
        if (!$strongSource) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $strongSource of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
125
            trigger_error(
126
                'openssl was unable to use a strong source of entropy. ' .
127
                'Consider updating your system libraries, or ensuring ' .
128
                'you have more available entropy.',
129
                E_USER_WARNING
130
            );
131
        }
132
133
        return $bytes;
134
    }
135
136
    /**
137
     * Creates a secure random string.
138
     *
139
     * @param int $length String length. Default 64.
140
     * @return string
141
     * @since 3.6.0
142
     */
143
    public static function randomString($length = 64)
144
    {
145
        return substr(
146
            bin2hex(Security::randomBytes(ceil($length / 2))),
147
            0,
148
            $length
149
        );
150
    }
151
152
    /**
153
     * Like randomBytes() above, but not cryptographically secure.
154
     *
155
     * @param int $length The number of bytes you want.
156
     * @return string Random bytes in binary.
157
     * @see \Cake\Utility\Security::randomBytes()
158
     */
159
    public static function insecureRandomBytes($length)
160
    {
161
        $length *= 2;
162
163
        $bytes = '';
164
        $byteLength = 0;
165
        while ($byteLength < $length) {
166
            $bytes .= static::hash(Text::uuid() . uniqid(mt_rand(), true), 'sha512', true);
167
            $byteLength = strlen($bytes);
168
        }
169
        $bytes = substr($bytes, 0, $length);
170
171
        return pack('H*', $bytes);
172
    }
173
174
    /**
175
     * Get the crypto implementation based on the loaded extensions.
176
     *
177
     * You can use this method to forcibly decide between mcrypt/openssl/custom implementations.
178
     *
179
     * @param \Cake\Utility\Crypto\OpenSsl|\Cake\Utility\Crypto\Mcrypt|null $instance The crypto instance to use.
180
     * @return \Cake\Utility\Crypto\OpenSsl|\Cake\Utility\Crypto\Mcrypt Crypto instance.
181
     * @throws \InvalidArgumentException When no compatible crypto extension is available.
182
     */
183
    public static function engine($instance = null)
184
    {
185
        if ($instance === null && static::$_instance === null) {
186
            if (extension_loaded('openssl')) {
187
                $instance = new OpenSsl();
188
            } elseif (extension_loaded('mcrypt')) {
189
                $instance = new Mcrypt();
0 ignored issues
show
Deprecated Code introduced by
The class Cake\Utility\Crypto\Mcrypt has been deprecated with message: 3.3.0 It is recommended to use {@see Cake\Utility\Crypto\OpenSsl} instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
190
            }
191
        }
192
        if ($instance) {
193
            static::$_instance = $instance;
194
        }
195
        if (isset(static::$_instance)) {
196
            return static::$_instance;
197
        }
198
        throw new InvalidArgumentException(
199
            'No compatible crypto engine available. ' .
200
            'Load either the openssl or mcrypt extensions'
201
        );
202
    }
203
204
    /**
205
     * Encrypts/Decrypts a text using the given key using rijndael method.
206
     *
207
     * @param string $text Encrypted string to decrypt, normal string to encrypt
208
     * @param string $key Key to use as the encryption key for encrypted data.
209
     * @param string $operation Operation to perform, encrypt or decrypt
210
     * @throws \InvalidArgumentException When there are errors.
211
     * @return string Encrypted/Decrypted string.
212
     * @deprecated 3.6.3 This method relies on functions provided by mcrypt
213
     *   extension which has been deprecated in PHP 7.1 and removed in PHP 7.2.
214
     *   There's no 1:1 replacement for this method.
215
     *   Upgrade your code to use Security::encrypt()/Security::decrypt() with
216
     *   OpenSsl engine instead.
217
     */
218
    public static function rijndael($text, $key, $operation)
219
    {
220
        if (empty($key)) {
221
            throw new InvalidArgumentException('You cannot use an empty key for Security::rijndael()');
222
        }
223
        if (empty($operation) || !in_array($operation, ['encrypt', 'decrypt'])) {
224
            throw new InvalidArgumentException('You must specify the operation for Security::rijndael(), either encrypt or decrypt');
225
        }
226
        if (mb_strlen($key, '8bit') < 32) {
227
            throw new InvalidArgumentException('You must use a key larger than 32 bytes for Security::rijndael()');
228
        }
229
        $crypto = static::engine();
230
231
        return $crypto->rijndael($text, $key, $operation);
232
    }
233
234
    /**
235
     * Encrypt a value using AES-256.
236
     *
237
     * *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes.
238
     * Any trailing null bytes will be removed on decryption due to how PHP pads messages
239
     * with nulls prior to encryption.
240
     *
241
     * @param string $plain The value to encrypt.
242
     * @param string $key The 256 bit/32 byte key to use as a cipher key.
243
     * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt.
244
     * @return string Encrypted data.
245
     * @throws \InvalidArgumentException On invalid data or key.
246
     */
247
    public static function encrypt($plain, $key, $hmacSalt = null)
248
    {
249
        self::_checkKey($key, 'encrypt()');
250
251
        if ($hmacSalt === null) {
252
            $hmacSalt = static::$_salt;
253
        }
254
        // Generate the encryption and hmac key.
255
        $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit');
256
257
        $crypto = static::engine();
258
        $ciphertext = $crypto->encrypt($plain, $key);
259
        $hmac = hash_hmac('sha256', $ciphertext, $key);
260
261
        return $hmac . $ciphertext;
262
    }
263
264
    /**
265
     * Check the encryption key for proper length.
266
     *
267
     * @param string $key Key to check.
268
     * @param string $method The method the key is being checked for.
269
     * @return void
270
     * @throws \InvalidArgumentException When key length is not 256 bit/32 bytes
271
     */
272
    protected static function _checkKey($key, $method)
273
    {
274
        if (mb_strlen($key, '8bit') < 32) {
275
            throw new InvalidArgumentException(
276
                sprintf('Invalid key for %s, key must be at least 256 bits (32 bytes) long.', $method)
277
            );
278
        }
279
    }
280
281
    /**
282
     * Decrypt a value using AES-256.
283
     *
284
     * @param string $cipher The ciphertext to decrypt.
285
     * @param string $key The 256 bit/32 byte key to use as a cipher key.
286
     * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt.
287
     * @return string|bool Decrypted data. Any trailing null bytes will be removed.
288
     * @throws \InvalidArgumentException On invalid data or key.
289
     */
290
    public static function decrypt($cipher, $key, $hmacSalt = null)
291
    {
292
        self::_checkKey($key, 'decrypt()');
293
        if (empty($cipher)) {
294
            throw new InvalidArgumentException('The data to decrypt cannot be empty.');
295
        }
296
        if ($hmacSalt === null) {
297
            $hmacSalt = static::$_salt;
298
        }
299
300
        // Generate the encryption and hmac key.
301
        $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit');
302
303
        // Split out hmac for comparison
304
        $macSize = 64;
305
        $hmac = mb_substr($cipher, 0, $macSize, '8bit');
306
        $cipher = mb_substr($cipher, $macSize, null, '8bit');
307
308
        $compareHmac = hash_hmac('sha256', $cipher, $key);
309
        if (!static::constantEquals($hmac, $compareHmac)) {
310
            return false;
311
        }
312
313
        $crypto = static::engine();
314
315
        return $crypto->decrypt($cipher, $key);
316
    }
317
318
    /**
319
     * A timing attack resistant comparison that prefers native PHP implementations.
320
     *
321
     * @param string $original The original value.
322
     * @param string $compare The comparison value.
323
     * @return bool
324
     * @see https://github.com/resonantcore/php-future/
325
     * @since 3.6.2
326
     */
327
    public static function constantEquals($original, $compare)
328
    {
329
        if (!is_string($original) || !is_string($compare)) {
330
            return false;
331
        }
332
        if (function_exists('hash_equals')) {
333
            return hash_equals($original, $compare);
334
        }
335
        $originalLength = mb_strlen($original, '8bit');
336
        $compareLength = mb_strlen($compare, '8bit');
337
        if ($originalLength !== $compareLength) {
338
            return false;
339
        }
340
        $result = 0;
341
        for ($i = 0; $i < $originalLength; $i++) {
342
            $result |= (ord($original[$i]) ^ ord($compare[$i]));
343
        }
344
345
        return $result === 0;
346
    }
347
348
    /**
349
     * Gets the HMAC salt to be used for encryption/decryption
350
     * routines.
351
     *
352
     * @return string The currently configured salt
353
     */
354
    public static function getSalt()
355
    {
356
        return static::$_salt;
357
    }
358
359
    /**
360
     * Sets the HMAC salt to be used for encryption/decryption
361
     * routines.
362
     *
363
     * @param string $salt The salt to use for encryption routines.
364
     * @return void
365
     */
366
    public static function setSalt($salt)
367
    {
368
        static::$_salt = (string)$salt;
369
    }
370
371
    /**
372
     * Gets or sets the HMAC salt to be used for encryption/decryption
373
     * routines.
374
     *
375
     * @deprecated 3.5.0 Use getSalt()/setSalt() instead.
376
     * @param string|null $salt The salt to use for encryption routines. If null returns current salt.
377
     * @return string The currently configured salt
378
     */
379
    public static function salt($salt = null)
380
    {
381
        deprecationWarning(
382
            'Security::salt() is deprecated. ' .
383
            'Use Security::getSalt()/setSalt() instead.'
384
        );
385
        if ($salt === null) {
386
            return static::$_salt;
387
        }
388
389
        return static::$_salt = (string)$salt;
390
    }
391
}
392