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) { |
|
|
|
|
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(); |
|
|
|
|
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
|
|
|
|
If an expression can have both
false
, andnull
as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.