BaseSecurity.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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. namespace yii\helpers;
  8. use Yii;
  9. use yii\base\Exception;
  10. use yii\base\InvalidConfigException;
  11. use yii\base\InvalidParamException;
  12. /**
  13. * BaseSecurity provides concrete implementation for [[Security]].
  14. *
  15. * Do not use BaseSecurity. Use [[Security]] instead.
  16. *
  17. * @author Qiang Xue <[email protected]>
  18. * @author Tom Worster <[email protected]>
  19. * @since 2.0
  20. */
  21. class BaseSecurity
  22. {
  23. /**
  24. * Uses AES, block size is 128-bit (16 bytes).
  25. */
  26. const CRYPT_BLOCK_SIZE = 16;
  27. /**
  28. * Uses AES-192, key size is 192-bit (24 bytes).
  29. */
  30. const CRYPT_KEY_SIZE = 24;
  31. /**
  32. * Uses SHA-256.
  33. */
  34. const DERIVATION_HASH = 'sha256';
  35. /**
  36. * Uses 1000 iterations.
  37. */
  38. const DERIVATION_ITERATIONS = 1000;
  39. /**
  40. * Encrypts data.
  41. * @param string $data data to be encrypted.
  42. * @param string $password the encryption password
  43. * @return string the encrypted data
  44. * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized
  45. * @see decrypt()
  46. */
  47. public static function encrypt($data, $password)
  48. {
  49. $module = static::openCryptModule();
  50. $data = static::addPadding($data);
  51. srand();
  52. $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
  53. $key = static::deriveKey($password, $iv);
  54. mcrypt_generic_init($module, $key, $iv);
  55. $encrypted = $iv . mcrypt_generic($module, $data);
  56. mcrypt_generic_deinit($module);
  57. mcrypt_module_close($module);
  58. return $encrypted;
  59. }
  60. /**
  61. * Decrypts data
  62. * @param string $data data to be decrypted.
  63. * @param string $password the decryption password
  64. * @return string the decrypted data
  65. * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized
  66. * @see encrypt()
  67. */
  68. public static function decrypt($data, $password)
  69. {
  70. if ($data === null) {
  71. return null;
  72. }
  73. $module = static::openCryptModule();
  74. $ivSize = mcrypt_enc_get_iv_size($module);
  75. $iv = StringHelper::byteSubstr($data, 0, $ivSize);
  76. $key = static::deriveKey($password, $iv);
  77. mcrypt_generic_init($module, $key, $iv);
  78. $decrypted = mdecrypt_generic($module, StringHelper::byteSubstr($data, $ivSize, StringHelper::byteLength($data)));
  79. mcrypt_generic_deinit($module);
  80. mcrypt_module_close($module);
  81. return static::stripPadding($decrypted);
  82. }
  83. /**
  84. * Adds a padding to the given data (PKCS #7).
  85. * @param string $data the data to pad
  86. * @return string the padded data
  87. */
  88. protected static function addPadding($data)
  89. {
  90. $pad = self::CRYPT_BLOCK_SIZE - (StringHelper::byteLength($data) % self::CRYPT_BLOCK_SIZE);
  91. return $data . str_repeat(chr($pad), $pad);
  92. }
  93. /**
  94. * Strips the padding from the given data.
  95. * @param string $data the data to trim
  96. * @return string the trimmed data
  97. */
  98. protected static function stripPadding($data)
  99. {
  100. $end = StringHelper::byteSubstr($data, -1, NULL);
  101. $last = ord($end);
  102. $n = StringHelper::byteLength($data) - $last;
  103. if (StringHelper::byteSubstr($data, $n, NULL) == str_repeat($end, $last)) {
  104. return StringHelper::byteSubstr($data, 0, $n);
  105. }
  106. return false;
  107. }
  108. /**
  109. * Derives a key from the given password (PBKDF2).
  110. * @param string $password the source password
  111. * @param string $salt the random salt
  112. * @return string the derived key
  113. */
  114. protected static function deriveKey($password, $salt)
  115. {
  116. if (function_exists('hash_pbkdf2')) {
  117. return hash_pbkdf2(self::DERIVATION_HASH, $password, $salt, self::DERIVATION_ITERATIONS, self::CRYPT_KEY_SIZE, true);
  118. }
  119. $hmac = hash_hmac(self::DERIVATION_HASH, $salt . pack('N', 1), $password, true);
  120. $xorsum = $hmac;
  121. for ($i = 1; $i < self::DERIVATION_ITERATIONS; $i++) {
  122. $hmac = hash_hmac(self::DERIVATION_HASH, $hmac, $password, true);
  123. $xorsum ^= $hmac;
  124. }
  125. return substr($xorsum, 0, self::CRYPT_KEY_SIZE);
  126. }
  127. /**
  128. * Prefixes data with a keyed hash value so that it can later be detected if it is tampered.
  129. * @param string $data the data to be protected
  130. * @param string $key the secret key to be used for generating hash
  131. * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()"
  132. * function to see the supported hashing algorithms on your system.
  133. * @return string the data prefixed with the keyed hash
  134. * @see validateData()
  135. * @see getSecretKey()
  136. */
  137. public static function hashData($data, $key, $algorithm = 'sha256')
  138. {
  139. return hash_hmac($algorithm, $data, $key) . $data;
  140. }
  141. /**
  142. * Validates if the given data is tampered.
  143. * @param string $data the data to be validated. The data must be previously
  144. * generated by [[hashData()]].
  145. * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]].
  146. * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()"
  147. * function to see the supported hashing algorithms on your system. This must be the same
  148. * as the value passed to [[hashData()]] when generating the hash for the data.
  149. * @return string the real data with the hash stripped off. False if the data is tampered.
  150. * @see hashData()
  151. */
  152. public static function validateData($data, $key, $algorithm = 'sha256')
  153. {
  154. $hashSize = StringHelper::byteLength(hash_hmac($algorithm, 'test', $key));
  155. $n = StringHelper::byteLength($data);
  156. if ($n >= $hashSize) {
  157. $hash = StringHelper::byteSubstr($data, 0, $hashSize);
  158. $data2 = StringHelper::byteSubstr($data, $hashSize, $n - $hashSize);
  159. return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false;
  160. } else {
  161. return false;
  162. }
  163. }
  164. /**
  165. * Returns a secret key associated with the specified name.
  166. * If the secret key does not exist, a random key will be generated
  167. * and saved in the file "keys.json" under the application's runtime directory
  168. * so that the same secret key can be returned in future requests.
  169. * @param string $name the name that is associated with the secret key
  170. * @param integer $length the length of the key that should be generated if not exists
  171. * @return string the secret key associated with the specified name
  172. */
  173. public static function getSecretKey($name, $length = 32)
  174. {
  175. static $keys;
  176. $keyFile = Yii::$app->getRuntimePath() . '/keys.json';
  177. if ($keys === null) {
  178. $keys = [];
  179. if (is_file($keyFile)) {
  180. $keys = json_decode(file_get_contents($keyFile), true);
  181. }
  182. }
  183. if (!isset($keys[$name])) {
  184. $keys[$name] = static::generateRandomKey($length);
  185. file_put_contents($keyFile, json_encode($keys));
  186. }
  187. return $keys[$name];
  188. }
  189. /**
  190. * Generates a random key. The key may contain uppercase and lowercase latin letters, digits, underscore, dash and dot.
  191. * @param integer $length the length of the key that should be generated
  192. * @return string the generated random key
  193. */
  194. public static function generateRandomKey($length = 32)
  195. {
  196. if (function_exists('openssl_random_pseudo_bytes')) {
  197. $key = strtr(base64_encode(openssl_random_pseudo_bytes($length, $strong)), '+/=', '_-.');
  198. if ($strong) {
  199. return substr($key, 0, $length);
  200. }
  201. }
  202. $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.';
  203. return substr(str_shuffle(str_repeat($chars, 5)), 0, $length);
  204. }
  205. /**
  206. * Opens the mcrypt module.
  207. * @return resource the mcrypt module handle.
  208. * @throws InvalidConfigException if mcrypt extension is not installed
  209. * @throws Exception if mcrypt initialization fails
  210. */
  211. protected static function openCryptModule()
  212. {
  213. if (!extension_loaded('mcrypt')) {
  214. throw new InvalidConfigException('The mcrypt PHP extension is not installed.');
  215. }
  216. // AES uses a 128-bit block size
  217. $module = @mcrypt_module_open('rijndael-128', '', 'cbc', '');
  218. if ($module === false) {
  219. throw new Exception('Failed to initialize the mcrypt module.');
  220. }
  221. return $module;
  222. }
  223. /**
  224. * Generates a secure hash from a password and a random salt.
  225. *
  226. * The generated hash can be stored in database (e.g. `CHAR(64) CHARACTER SET latin1` on MySQL).
  227. * Later when a password needs to be validated, the hash can be fetched and passed
  228. * to [[validatePassword()]]. For example,
  229. *
  230. * ~~~
  231. * // generates the hash (usually done during user registration or when the password is changed)
  232. * $hash = Security::generatePasswordHash($password);
  233. * // ...save $hash in database...
  234. *
  235. * // during login, validate if the password entered is correct using $hash fetched from database
  236. * if (Security::validatePassword($password, $hash) {
  237. * // password is good
  238. * } else {
  239. * // password is bad
  240. * }
  241. * ~~~
  242. *
  243. * @param string $password The password to be hashed.
  244. * @param integer $cost Cost parameter used by the Blowfish hash algorithm.
  245. * The higher the value of cost,
  246. * the longer it takes to generate the hash and to verify a password against it. Higher cost
  247. * therefore slows down a brute-force attack. For best protection against brute for attacks,
  248. * set it to the highest value that is tolerable on production servers. The time taken to
  249. * compute the hash doubles for every increment by one of $cost. So, for example, if the
  250. * hash takes 1 second to compute when $cost is 14 then then the compute time varies as
  251. * 2^($cost - 14) seconds.
  252. * @throws Exception on bad password parameter or cost parameter
  253. * @return string The password hash string, ASCII and not longer than 64 characters.
  254. * @see validatePassword()
  255. */
  256. public static function generatePasswordHash($password, $cost = 13)
  257. {
  258. $salt = static::generateSalt($cost);
  259. $hash = crypt($password, $salt);
  260. if (!is_string($hash) || strlen($hash) < 32) {
  261. throw new Exception('Unknown error occurred while generating hash.');
  262. }
  263. return $hash;
  264. }
  265. /**
  266. * Verifies a password against a hash.
  267. * @param string $password The password to verify.
  268. * @param string $hash The hash to verify the password against.
  269. * @return boolean whether the password is correct.
  270. * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available.
  271. * @see generatePasswordHash()
  272. */
  273. public static function validatePassword($password, $hash)
  274. {
  275. if (!is_string($password) || $password === '') {
  276. throw new InvalidParamException('Password must be a string and cannot be empty.');
  277. }
  278. if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) {
  279. throw new InvalidParamException('Hash is invalid.');
  280. }
  281. $test = crypt($password, $hash);
  282. $n = strlen($test);
  283. if ($n < 32 || $n !== strlen($hash)) {
  284. return false;
  285. }
  286. // Use a for-loop to compare two strings to prevent timing attacks. See:
  287. // http://codereview.stackexchange.com/questions/13512
  288. $check = 0;
  289. for ($i = 0; $i < $n; ++$i) {
  290. $check |= (ord($test[$i]) ^ ord($hash[$i]));
  291. }
  292. return $check === 0;
  293. }
  294. /**
  295. * Generates a salt that can be used to generate a password hash.
  296. *
  297. * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function
  298. * requires, for the Blowfish hash algorithm, a salt string in a specific format:
  299. * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters
  300. * from the alphabet "./0-9A-Za-z".
  301. *
  302. * @param integer $cost the cost parameter
  303. * @return string the random salt value.
  304. * @throws InvalidParamException if the cost parameter is not between 4 and 31
  305. */
  306. protected static function generateSalt($cost = 13)
  307. {
  308. $cost = (int)$cost;
  309. if ($cost < 4 || $cost > 31) {
  310. throw new InvalidParamException('Cost must be between 4 and 31.');
  311. }
  312. // Get 20 * 8bits of pseudo-random entropy from mt_rand().
  313. $rand = '';
  314. for ($i = 0; $i < 20; ++$i) {
  315. $rand .= chr(mt_rand(0, 255));
  316. }
  317. // Add the microtime for a little more entropy.
  318. $rand .= microtime();
  319. // Mix the bits cryptographically into a 20-byte binary string.
  320. $rand = sha1($rand, true);
  321. // Form the prefix that specifies Blowfish algorithm and cost parameter.
  322. $salt = sprintf("$2y$%02d$", $cost);
  323. // Append the random salt data in the required base64 format.
  324. $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));
  325. return $salt;
  326. }
  327. }