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: /**
29: * Default hash method. If `$type` param for `Security::hash()` is not specified
30: * this value is used. Defaults to 'sha1'.
31: *
32: * @var string
33: */
34: public static $hashType = 'sha1';
35:
36: /**
37: * The HMAC salt to use for encryption and decryption routines
38: *
39: * @var string
40: */
41: protected static $_salt;
42:
43: /**
44: * The crypto implementation to use.
45: *
46: * @var object
47: */
48: protected static $_instance;
49:
50: /**
51: * Create a hash from string using given method.
52: *
53: * @param string $string String to hash
54: * @param string|null $algorithm Hashing algo to use (i.e. sha1, sha256 etc.).
55: * Can be any valid algo included in list returned by hash_algos().
56: * If no value is passed the type specified by `Security::$hashType` is used.
57: * @param mixed $salt If true, automatically prepends the application's salt
58: * value to $string (Security.salt).
59: * @return string Hash
60: * @link https://book.cakephp.org/3.0/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)) {
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: