ElggCrypto.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <?php
  2. /**
  3. * \ElggCrypto
  4. *
  5. * @package Elgg.Core
  6. * @subpackage Crypto
  7. *
  8. * @access private
  9. */
  10. class ElggCrypto {
  11. /**
  12. * Character set for temp passwords (no risk of embedded profanity/glyphs that look similar)
  13. */
  14. const CHARS_PASSWORD = 'bcdfghjklmnpqrstvwxyz2346789';
  15. /**
  16. * Character set for hexadecimal
  17. */
  18. const CHARS_HEX = '0123456789abcdef';
  19. /**
  20. * Generate a string of highly randomized bytes (over the full 8-bit range).
  21. *
  22. * @param int $length Number of bytes needed
  23. * @return string Random bytes
  24. *
  25. * @author George Argyros <argyros.george@gmail.com>
  26. * @copyright 2012, George Argyros. All rights reserved.
  27. * @license Modified BSD
  28. * @link https://github.com/GeorgeArgyros/Secure-random-bytes-in-PHP/blob/master/srand.php Original
  29. *
  30. * Redistribution and use in source and binary forms, with or without
  31. * modification, are permitted provided that the following conditions are met:
  32. * * Redistributions of source code must retain the above copyright
  33. * notice, this list of conditions and the following disclaimer.
  34. * * Redistributions in binary form must reproduce the above copyright
  35. * notice, this list of conditions and the following disclaimer in the
  36. * documentation and/or other materials provided with the distribution.
  37. * * Neither the name of the <organization> nor the
  38. * names of its contributors may be used to endorse or promote products
  39. * derived from this software without specific prior written permission.
  40. *
  41. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  42. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  43. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  44. * DISCLAIMED. IN NO EVENT SHALL GEORGE ARGYROS BE LIABLE FOR ANY
  45. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  46. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  47. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  48. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  49. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  50. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  51. */
  52. public function getRandomBytes($length) {
  53. $SSLstr = '4'; // http://xkcd.com/221/
  54. /**
  55. * Our primary choice for a cryptographic strong randomness function is
  56. * openssl_random_pseudo_bytes.
  57. */
  58. if (function_exists('openssl_random_pseudo_bytes') && substr(PHP_OS, 0, 3) !== 'WIN') {
  59. $SSLstr = openssl_random_pseudo_bytes($length, $strong);
  60. if ($strong) {
  61. return $SSLstr;
  62. }
  63. }
  64. /**
  65. * If mcrypt extension is available then we use it to gather entropy from
  66. * the operating system's PRNG. This is better than reading /dev/urandom
  67. * directly since it avoids reading larger blocks of data than needed.
  68. */
  69. if (function_exists('mcrypt_create_iv') && substr(PHP_OS, 0, 3) !== 'WIN') {
  70. $str = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
  71. if ($str !== false) {
  72. return $str;
  73. }
  74. }
  75. /**
  76. * No build-in crypto randomness function found. We collect any entropy
  77. * available in the PHP core PRNGs along with some filesystem info and memory
  78. * stats. To make this data cryptographically strong we add data either from
  79. * /dev/urandom or if its unavailable, we gather entropy by measuring the
  80. * time needed to compute a number of SHA-1 hashes.
  81. */
  82. $str = '';
  83. $bits_per_round = 2; // bits of entropy collected in each clock drift round
  84. $msec_per_round = 400; // expected running time of each round in microseconds
  85. $hash_len = 20; // SHA-1 Hash length
  86. $total = $length; // total bytes of entropy to collect
  87. $handle = @fopen('/dev/urandom', 'rb');
  88. if ($handle && function_exists('stream_set_read_buffer')) {
  89. @stream_set_read_buffer($handle, 0);
  90. }
  91. do {
  92. $bytes = ($total > $hash_len) ? $hash_len : $total;
  93. $total -= $bytes;
  94. //collect any entropy available from the PHP system and filesystem
  95. $entropy = rand() . uniqid(mt_rand(), true) . $SSLstr;
  96. $entropy .= implode('', @fstat(@fopen(__FILE__, 'r')));
  97. $entropy .= memory_get_usage() . getmypid();
  98. $entropy .= serialize($_ENV) . serialize($_SERVER);
  99. if (function_exists('posix_times')) {
  100. $entropy .= serialize(posix_times());
  101. }
  102. if (function_exists('zend_thread_id')) {
  103. $entropy .= zend_thread_id();
  104. }
  105. if ($handle) {
  106. $entropy .= @fread($handle, $bytes);
  107. } else {
  108. // Measure the time that the operations will take on average
  109. for ($i = 0; $i < 3; $i++) {
  110. $c1 = microtime(true);
  111. $var = sha1(mt_rand());
  112. for ($j = 0; $j < 50; $j++) {
  113. $var = sha1($var);
  114. }
  115. $c2 = microtime(true);
  116. $entropy .= $c1 . $c2;
  117. }
  118. // Based on the above measurement determine the total rounds
  119. // in order to bound the total running time.
  120. $rounds = (int) ($msec_per_round * 50 / (int) (($c2 - $c1) * 1000000));
  121. // Take the additional measurements. On average we can expect
  122. // at least $bits_per_round bits of entropy from each measurement.
  123. $iter = $bytes * (int) (ceil(8 / $bits_per_round));
  124. for ($i = 0; $i < $iter; $i++) {
  125. $c1 = microtime();
  126. $var = sha1(mt_rand());
  127. for ($j = 0; $j < $rounds; $j++) {
  128. $var = sha1($var);
  129. }
  130. $c2 = microtime();
  131. $entropy .= $c1 . $c2;
  132. }
  133. }
  134. // We assume sha1 is a deterministic extractor for the $entropy variable.
  135. $str .= sha1($entropy, true);
  136. } while ($length > strlen($str));
  137. if ($handle) {
  138. @fclose($handle);
  139. }
  140. return substr($str, 0, $length);
  141. }
  142. /**
  143. * Get an HMAC token builder/validator object
  144. *
  145. * @param mixed $data HMAC data or serializable data
  146. * @param string $algo Hash algorithm
  147. * @param string $key Optional key (default uses site secret)
  148. *
  149. * @return \Elgg\Security\Hmac
  150. */
  151. public function getHmac($data, $algo = 'sha256', $key = '') {
  152. if (!$key) {
  153. $key = _elgg_services()->siteSecret->get(true);
  154. }
  155. return new Elgg\Security\Hmac($key, [$this, 'areEqual'], $data, $algo);
  156. }
  157. /**
  158. * Generate a random string of specified length.
  159. *
  160. * Uses supplied character list for generating the new string.
  161. * If no character list provided - uses Base64 URL character set.
  162. *
  163. * @param int $length Desired length of the string
  164. * @param string|null $chars Characters to be chosen from randomly. If not given, the Base64 URL
  165. * charset will be used.
  166. *
  167. * @return string The random string
  168. *
  169. * @throws InvalidArgumentException
  170. *
  171. * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
  172. * @license http://framework.zend.com/license/new-bsd New BSD License
  173. *
  174. * @see https://github.com/zendframework/zf2/blob/master/library/Zend/Math/Rand.php#L179
  175. */
  176. public function getRandomString($length, $chars = null) {
  177. if ($length < 1) {
  178. throw new \InvalidArgumentException('Length should be >= 1');
  179. }
  180. if (empty($chars)) {
  181. $numBytes = ceil($length * 0.75);
  182. $bytes = $this->getRandomBytes($numBytes);
  183. $string = substr(rtrim(base64_encode($bytes), '='), 0, $length);
  184. // Base64 URL
  185. return strtr($string, '+/', '-_');
  186. }
  187. if ($chars == self::CHARS_HEX) {
  188. // hex is easy
  189. $bytes = $this->getRandomBytes(ceil($length / 2));
  190. return substr(bin2hex($bytes), 0, $length);
  191. }
  192. $listLen = strlen($chars);
  193. if ($listLen == 1) {
  194. return str_repeat($chars, $length);
  195. }
  196. $bytes = $this->getRandomBytes($length);
  197. $pos = 0;
  198. $result = '';
  199. for ($i = 0; $i < $length; $i++) {
  200. $pos = ($pos + ord($bytes[$i])) % $listLen;
  201. $result .= $chars[$pos];
  202. }
  203. return $result;
  204. }
  205. /**
  206. * Are two strings equal (compared in constant time)?
  207. *
  208. * @param string $str1 First string to compare
  209. * @param string $str2 Second string to compare
  210. *
  211. * @return bool
  212. *
  213. * Based on password_verify in PasswordCompat
  214. * @author Anthony Ferrara <ircmaxell@php.net>
  215. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  216. * @copyright 2012 The Authors
  217. */
  218. public function areEqual($str1, $str2) {
  219. $len1 = $this->strlen($str1);
  220. $len2 = $this->strlen($str2);
  221. if ($len1 !== $len2) {
  222. return false;
  223. }
  224. $status = 0;
  225. for ($i = 0; $i < $len1; $i++) {
  226. $status |= (ord($str1[$i]) ^ ord($str2[$i]));
  227. }
  228. return $status === 0;
  229. }
  230. /**
  231. * Count the number of bytes in a string
  232. *
  233. * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
  234. * In this case, strlen() will count the number of *characters* based on the internal encoding. A
  235. * sequence of bytes might be regarded as a single multibyte character.
  236. *
  237. * Use elgg_strlen() to count UTF-characters instead of bytes.
  238. *
  239. * @param string $binary_string The input string
  240. *
  241. * @return int The number of bytes
  242. *
  243. * From PasswordCompat\binary\_strlen
  244. * @author Anthony Ferrara <ircmaxell@php.net>
  245. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  246. * @copyright 2012 The Authors
  247. */
  248. protected function strlen($binary_string) {
  249. if (function_exists('mb_strlen')) {
  250. return mb_strlen($binary_string, '8bit');
  251. }
  252. return strlen($binary_string);
  253. }
  254. }