Security.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. use Cake\Utility\Crypto\Mcrypt;
  17. use Cake\Utility\Crypto\OpenSsl;
  18. use InvalidArgumentException;
  19. use RuntimeException;
  20. /**
  21. * Security Library contains utility methods related to security
  22. */
  23. class Security
  24. {
  25. /**
  26. * Default hash method. If `$type` param for `Security::hash()` is not specified
  27. * this value is used. Defaults to 'sha1'.
  28. *
  29. * @var string
  30. */
  31. public static $hashType = 'sha1';
  32. /**
  33. * The HMAC salt to use for encryption and decryption routines
  34. *
  35. * @var string
  36. */
  37. protected static $_salt;
  38. /**
  39. * The crypto implementation to use.
  40. *
  41. * @var object
  42. */
  43. protected static $_instance;
  44. /**
  45. * Create a hash from string using given method.
  46. *
  47. * @param string $string String to hash
  48. * @param string|null $algorithm Hashing algo to use (i.e. sha1, sha256 etc.).
  49. * Can be any valid algo included in list returned by hash_algos().
  50. * If no value is passed the type specified by `Security::$hashType` is used.
  51. * @param mixed $salt If true, automatically prepends the application's salt
  52. * value to $string (Security.salt).
  53. * @return string Hash
  54. * @link https://book.cakephp.org/3.0/en/core-libraries/security.html#hashing-data
  55. */
  56. public static function hash($string, $algorithm = null, $salt = false)
  57. {
  58. if (empty($algorithm)) {
  59. $algorithm = static::$hashType;
  60. }
  61. $algorithm = strtolower($algorithm);
  62. $availableAlgorithms = hash_algos();
  63. if (!in_array($algorithm, $availableAlgorithms)) {
  64. throw new RuntimeException(sprintf(
  65. 'The hash type `%s` was not found. Available algorithms are: %s',
  66. $algorithm,
  67. implode(', ', $availableAlgorithms)
  68. ));
  69. }
  70. if ($salt) {
  71. if (!is_string($salt)) {
  72. $salt = static::$_salt;
  73. }
  74. $string = $salt . $string;
  75. }
  76. return hash($algorithm, $string);
  77. }
  78. /**
  79. * Sets the default hash method for the Security object. This affects all objects
  80. * using Security::hash().
  81. *
  82. * @param string $hash Method to use (sha1/sha256/md5 etc.)
  83. * @return void
  84. * @see \Cake\Utility\Security::hash()
  85. */
  86. public static function setHash($hash)
  87. {
  88. static::$hashType = $hash;
  89. }
  90. /**
  91. * Get random bytes from a secure source.
  92. *
  93. * This method will fall back to an insecure source an trigger a warning
  94. * if it cannot find a secure source of random data.
  95. *
  96. * @param int $length The number of bytes you want.
  97. * @return string Random bytes in binary.
  98. */
  99. public static function randomBytes($length)
  100. {
  101. if (function_exists('random_bytes')) {
  102. return random_bytes($length);
  103. }
  104. if (!function_exists('openssl_random_pseudo_bytes')) {
  105. throw new RuntimeException(
  106. 'You do not have a safe source of random data available. ' .
  107. 'Install either the openssl extension, or paragonie/random_compat. ' .
  108. 'Or use Security::insecureRandomBytes() alternatively.'
  109. );
  110. }
  111. $bytes = openssl_random_pseudo_bytes($length, $strongSource);
  112. if (!$strongSource) {
  113. trigger_error(
  114. 'openssl was unable to use a strong source of entropy. ' .
  115. 'Consider updating your system libraries, or ensuring ' .
  116. 'you have more available entropy.',
  117. E_USER_WARNING
  118. );
  119. }
  120. return $bytes;
  121. }
  122. /**
  123. * Creates a secure random string.
  124. *
  125. * @param int $length String length. Default 64.
  126. * @return string
  127. * @since 3.6.0
  128. */
  129. public static function randomString($length = 64)
  130. {
  131. return substr(
  132. bin2hex(Security::randomBytes(ceil($length / 2))),
  133. 0,
  134. $length
  135. );
  136. }
  137. /**
  138. * Like randomBytes() above, but not cryptographically secure.
  139. *
  140. * @param int $length The number of bytes you want.
  141. * @return string Random bytes in binary.
  142. * @see \Cake\Utility\Security::randomBytes()
  143. */
  144. public static function insecureRandomBytes($length)
  145. {
  146. $length *= 2;
  147. $bytes = '';
  148. $byteLength = 0;
  149. while ($byteLength < $length) {
  150. $bytes .= static::hash(Text::uuid() . uniqid(mt_rand(), true), 'sha512', true);
  151. $byteLength = strlen($bytes);
  152. }
  153. $bytes = substr($bytes, 0, $length);
  154. return pack('H*', $bytes);
  155. }
  156. /**
  157. * Get the crypto implementation based on the loaded extensions.
  158. *
  159. * You can use this method to forcibly decide between mcrypt/openssl/custom implementations.
  160. *
  161. * @param \Cake\Utility\Crypto\OpenSsl|\Cake\Utility\Crypto\Mcrypt|null $instance The crypto instance to use.
  162. * @return \Cake\Utility\Crypto\OpenSsl|\Cake\Utility\Crypto\Mcrypt Crypto instance.
  163. * @throws \InvalidArgumentException When no compatible crypto extension is available.
  164. */
  165. public static function engine($instance = null)
  166. {
  167. if ($instance === null && static::$_instance === null) {
  168. if (extension_loaded('openssl')) {
  169. $instance = new OpenSsl();
  170. } elseif (extension_loaded('mcrypt')) {
  171. $instance = new Mcrypt();
  172. }
  173. }
  174. if ($instance) {
  175. static::$_instance = $instance;
  176. }
  177. if (isset(static::$_instance)) {
  178. return static::$_instance;
  179. }
  180. throw new InvalidArgumentException(
  181. 'No compatible crypto engine available. ' .
  182. 'Load either the openssl or mcrypt extensions'
  183. );
  184. }
  185. /**
  186. * Encrypts/Decrypts a text using the given key using rijndael method.
  187. *
  188. * @param string $text Encrypted string to decrypt, normal string to encrypt
  189. * @param string $key Key to use as the encryption key for encrypted data.
  190. * @param string $operation Operation to perform, encrypt or decrypt
  191. * @throws \InvalidArgumentException When there are errors.
  192. * @return string Encrypted/Decrypted string.
  193. * @deprecated 3.6.3 This method relies on functions provided by mcrypt
  194. * extension which has been deprecated in PHP 7.1 and removed in PHP 7.2.
  195. * There's no 1:1 replacement for this method.
  196. * Upgrade your code to use Security::encrypt()/Security::decrypt() with
  197. * OpenSsl engine instead.
  198. */
  199. public static function rijndael($text, $key, $operation)
  200. {
  201. if (empty($key)) {
  202. throw new InvalidArgumentException('You cannot use an empty key for Security::rijndael()');
  203. }
  204. if (empty($operation) || !in_array($operation, ['encrypt', 'decrypt'])) {
  205. throw new InvalidArgumentException('You must specify the operation for Security::rijndael(), either encrypt or decrypt');
  206. }
  207. if (mb_strlen($key, '8bit') < 32) {
  208. throw new InvalidArgumentException('You must use a key larger than 32 bytes for Security::rijndael()');
  209. }
  210. $crypto = static::engine();
  211. return $crypto->rijndael($text, $key, $operation);
  212. }
  213. /**
  214. * Encrypt a value using AES-256.
  215. *
  216. * *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes.
  217. * Any trailing null bytes will be removed on decryption due to how PHP pads messages
  218. * with nulls prior to encryption.
  219. *
  220. * @param string $plain The value to encrypt.
  221. * @param string $key The 256 bit/32 byte key to use as a cipher key.
  222. * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt.
  223. * @return string Encrypted data.
  224. * @throws \InvalidArgumentException On invalid data or key.
  225. */
  226. public static function encrypt($plain, $key, $hmacSalt = null)
  227. {
  228. self::_checkKey($key, 'encrypt()');
  229. if ($hmacSalt === null) {
  230. $hmacSalt = static::$_salt;
  231. }
  232. // Generate the encryption and hmac key.
  233. $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit');
  234. $crypto = static::engine();
  235. $ciphertext = $crypto->encrypt($plain, $key);
  236. $hmac = hash_hmac('sha256', $ciphertext, $key);
  237. return $hmac . $ciphertext;
  238. }
  239. /**
  240. * Check the encryption key for proper length.
  241. *
  242. * @param string $key Key to check.
  243. * @param string $method The method the key is being checked for.
  244. * @return void
  245. * @throws \InvalidArgumentException When key length is not 256 bit/32 bytes
  246. */
  247. protected static function _checkKey($key, $method)
  248. {
  249. if (mb_strlen($key, '8bit') < 32) {
  250. throw new InvalidArgumentException(
  251. sprintf('Invalid key for %s, key must be at least 256 bits (32 bytes) long.', $method)
  252. );
  253. }
  254. }
  255. /**
  256. * Decrypt a value using AES-256.
  257. *
  258. * @param string $cipher The ciphertext to decrypt.
  259. * @param string $key The 256 bit/32 byte key to use as a cipher key.
  260. * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt.
  261. * @return string|bool Decrypted data. Any trailing null bytes will be removed.
  262. * @throws \InvalidArgumentException On invalid data or key.
  263. */
  264. public static function decrypt($cipher, $key, $hmacSalt = null)
  265. {
  266. self::_checkKey($key, 'decrypt()');
  267. if (empty($cipher)) {
  268. throw new InvalidArgumentException('The data to decrypt cannot be empty.');
  269. }
  270. if ($hmacSalt === null) {
  271. $hmacSalt = static::$_salt;
  272. }
  273. // Generate the encryption and hmac key.
  274. $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit');
  275. // Split out hmac for comparison
  276. $macSize = 64;
  277. $hmac = mb_substr($cipher, 0, $macSize, '8bit');
  278. $cipher = mb_substr($cipher, $macSize, null, '8bit');
  279. $compareHmac = hash_hmac('sha256', $cipher, $key);
  280. if (!static::constantEquals($hmac, $compareHmac)) {
  281. return false;
  282. }
  283. $crypto = static::engine();
  284. return $crypto->decrypt($cipher, $key);
  285. }
  286. /**
  287. * A timing attack resistant comparison that prefers native PHP implementations.
  288. *
  289. * @param string $original The original value.
  290. * @param string $compare The comparison value.
  291. * @return bool
  292. * @see https://github.com/resonantcore/php-future/
  293. * @since 3.6.2
  294. */
  295. public static function constantEquals($original, $compare)
  296. {
  297. if (!is_string($original) || !is_string($compare)) {
  298. return false;
  299. }
  300. if (function_exists('hash_equals')) {
  301. return hash_equals($original, $compare);
  302. }
  303. $originalLength = mb_strlen($original, '8bit');
  304. $compareLength = mb_strlen($compare, '8bit');
  305. if ($originalLength !== $compareLength) {
  306. return false;
  307. }
  308. $result = 0;
  309. for ($i = 0; $i < $originalLength; $i++) {
  310. $result |= (ord($original[$i]) ^ ord($compare[$i]));
  311. }
  312. return $result === 0;
  313. }
  314. /**
  315. * Gets the HMAC salt to be used for encryption/decryption
  316. * routines.
  317. *
  318. * @return string The currently configured salt
  319. */
  320. public static function getSalt()
  321. {
  322. return static::$_salt;
  323. }
  324. /**
  325. * Sets the HMAC salt to be used for encryption/decryption
  326. * routines.
  327. *
  328. * @param string $salt The salt to use for encryption routines.
  329. * @return void
  330. */
  331. public static function setSalt($salt)
  332. {
  333. static::$_salt = (string)$salt;
  334. }
  335. /**
  336. * Gets or sets the HMAC salt to be used for encryption/decryption
  337. * routines.
  338. *
  339. * @deprecated 3.5.0 Use getSalt()/setSalt() instead.
  340. * @param string|null $salt The salt to use for encryption routines. If null returns current salt.
  341. * @return string The currently configured salt
  342. */
  343. public static function salt($salt = null)
  344. {
  345. deprecationWarning(
  346. 'Security::salt() is deprecated. ' .
  347. 'Use Security::getSalt()/setSalt() instead.'
  348. );
  349. if ($salt === null) {
  350. return static::$_salt;
  351. }
  352. return static::$_salt = (string)$salt;
  353. }
  354. }