Generator.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <?php
  2. /*
  3. * The RandomLib library for securely generating random numbers and strings in PHP
  4. *
  5. * @author Anthony Ferrara <ircmaxell@ircmaxell.com>
  6. * @copyright 2011 The Authors
  7. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  8. * @version Build @@version@@
  9. */
  10. /**
  11. * The Random Number Generator Class
  12. *
  13. * Use this factory to generate cryptographic quality random numbers (strings)
  14. *
  15. * PHP version 5.3
  16. *
  17. * @category PHPPasswordLib
  18. * @package Random
  19. *
  20. * @author Anthony Ferrara <ircmaxell@ircmaxell.com>
  21. * @author Paragon Initiative Enterprises <security@paragonie.com>
  22. * @author Timo Hamina
  23. * @copyright 2011 The Authors
  24. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  25. *
  26. * @version Build @@version@@
  27. */
  28. namespace RandomLib;
  29. use SecurityLib\Util;
  30. /**
  31. * The Random Number Generator Class
  32. *
  33. * Use this factory to generate cryptographic quality random numbers (strings)
  34. *
  35. * @category PHPPasswordLib
  36. * @package Random
  37. *
  38. * @author Anthony Ferrara <ircmaxell@ircmaxell.com>
  39. * @author Paragon Initiative Enterprises <security@paragonie.com>
  40. * @author Timo Hamina
  41. */
  42. class Generator
  43. {
  44. /**
  45. * @const Flag for uppercase letters
  46. */
  47. const CHAR_UPPER = 1;
  48. /**
  49. * @const Flag for lowercase letters
  50. */
  51. const CHAR_LOWER = 2;
  52. /**
  53. * @const Flag for alpha characters (combines UPPER + LOWER)
  54. */
  55. const CHAR_ALPHA = 3; // CHAR_UPPER | CHAR_LOWER
  56. /**
  57. * @const Flag for digits
  58. */
  59. const CHAR_DIGITS = 4;
  60. /**
  61. * @const Flag for alpha numeric characters
  62. */
  63. const CHAR_ALNUM = 7; // CHAR_ALPHA | CHAR_DIGITS
  64. /**
  65. * @const Flag for uppercase hexadecimal symbols
  66. */
  67. const CHAR_UPPER_HEX = 12; // 8 | CHAR_DIGITS
  68. /**
  69. * @const Flag for lowercase hexidecimal symbols
  70. */
  71. const CHAR_LOWER_HEX = 20; // 16 | CHAR_DIGITS
  72. /**
  73. * @const Flag for base64 symbols
  74. */
  75. const CHAR_BASE64 = 39; // 32 | CHAR_ALNUM
  76. /**
  77. * @const Flag for additional symbols accessible via the keyboard
  78. */
  79. const CHAR_SYMBOLS = 64;
  80. /**
  81. * @const Flag for brackets
  82. */
  83. const CHAR_BRACKETS = 128;
  84. /**
  85. * @const Flag for punctuation marks
  86. */
  87. const CHAR_PUNCT = 256;
  88. /**
  89. * @const Flag for upper/lower-case and digits but without "B8G6I1l|0OQDS5Z2"
  90. */
  91. const EASY_TO_READ = 512;
  92. /**
  93. * @var Mixer The mixing strategy to use for this generator instance
  94. */
  95. protected $mixer = null;
  96. /**
  97. * @var array<int, Source> An array of random number sources to use for this generator
  98. */
  99. protected $sources = array();
  100. /**
  101. * @var array<int, string> The different characters, by Flag
  102. */
  103. protected $charArrays = array(
  104. self::CHAR_UPPER => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
  105. self::CHAR_LOWER => 'abcdefghijklmnopqrstuvwxyz',
  106. self::CHAR_DIGITS => '0123456789',
  107. self::CHAR_UPPER_HEX => 'ABCDEF',
  108. self::CHAR_LOWER_HEX => 'abcdef',
  109. self::CHAR_BASE64 => '+/',
  110. self::CHAR_SYMBOLS => '!"#$%&\'()* +,-./:;<=>?@[\]^_`{|}~',
  111. self::CHAR_BRACKETS => '()[]{}<>',
  112. self::CHAR_PUNCT => ',.;:',
  113. );
  114. /**
  115. * @internal
  116. * @private
  117. * @const string Ambiguous characters for "Easy To Read" sets
  118. */
  119. const AMBIGUOUS_CHARS = 'B8G6I1l|0OQDS5Z2()[]{}:;,.';
  120. /**
  121. * Build a new instance of the generator
  122. *
  123. * @param array<int, Source> $sources An array of random data sources to use
  124. * @param Mixer $mixer The mixing strategy to use for this generator
  125. */
  126. public function __construct(array $sources, Mixer $mixer)
  127. {
  128. foreach ($sources as $source) {
  129. $this->addSource($source);
  130. }
  131. $this->mixer = $mixer;
  132. }
  133. /**
  134. * Add a random number source to the generator
  135. *
  136. * @param Source $source The random number source to add
  137. *
  138. * @return Generator $this The current generator instance
  139. */
  140. public function addSource(Source $source)
  141. {
  142. $this->sources[] = $source;
  143. return $this;
  144. }
  145. /**
  146. * Generate a random number (string) of the requested size
  147. *
  148. * @param int $size The size of the requested random number
  149. *
  150. * @return string The generated random number (string)
  151. */
  152. public function generate($size)
  153. {
  154. $seeds = array();
  155. foreach ($this->sources as $source) {
  156. if ($source instanceof Source) {
  157. $seeds[] = $source->generate($size);
  158. }
  159. }
  160. return $this->mixer->mix($seeds);
  161. }
  162. /**
  163. * Generate a random integer with the given range
  164. *
  165. * @param int $min The lower bound of the range to generate
  166. * @param int $max The upper bound of the range to generate
  167. *
  168. * @return int The generated random number within the range
  169. */
  170. public function generateInt($min = 0, $max = PHP_INT_MAX)
  171. {
  172. $tmp = (int) max($max, $min);
  173. $min = (int) min($max, $min);
  174. $max = $tmp;
  175. $range = $max - $min;
  176. if ($range == 0) {
  177. return $max;
  178. } elseif ($range > PHP_INT_MAX || is_float($range) || $range < 0) {
  179. /**
  180. * This works, because PHP will auto-convert it to a float at this point,
  181. * But on 64 bit systems, the float won't have enough precision to
  182. * actually store the difference, so we need to check if it's a float
  183. * and hence auto-converted...
  184. */
  185. throw new \RangeException(
  186. 'The supplied range is too great to generate'
  187. );
  188. }
  189. $bits = $this->countBits($range) + 1;
  190. $bytes = (int) \max(\ceil($bits / 8), 1);
  191. if ($bits == 63) {
  192. /**
  193. * Fixes issue #22
  194. *
  195. * @see https://github.com/ircmaxell/RandomLib/issues/22
  196. */
  197. $mask = 0x7fffffffffffffff;
  198. } else {
  199. $mask = (int) ((1 << $bits) - 1);
  200. }
  201. /**
  202. * The mask is a better way of dropping unused bits. Basically what it does
  203. * is to set all the bits in the mask to 1 that we may need. Since the max
  204. * range is PHP_INT_MAX, we will never need negative numbers (which would
  205. * have the MSB set on the max int possible to generate). Therefore we
  206. * can just mask that away. Since pow returns a float, we need to cast
  207. * it back to an int so the mask will work.
  208. *
  209. * On a 64 bit platform, that means that PHP_INT_MAX is 2^63 - 1. Which
  210. * is also the mask if 63 bits are needed (by the log(range, 2) call).
  211. * So if the computed result is negative (meaning the 64th bit is set), the
  212. * mask will correct that.
  213. *
  214. * This turns out to be slightly better than the shift as we don't need to
  215. * worry about "fixing" negative values.
  216. */
  217. do {
  218. $test = $this->generate($bytes);
  219. /** @var int $result */
  220. $result = \hexdec(\bin2hex($test)) & $mask;
  221. } while ($result > $range);
  222. return $result + $min;
  223. }
  224. /**
  225. * Generate a random string of specified length.
  226. *
  227. * This uses the supplied character list for generating the new result
  228. * string.
  229. *
  230. * @param int $length The length of the generated string
  231. * @param int|string $characters String: An optional list of characters to use
  232. * Integer: Character flags
  233. *
  234. * @return string The generated random string
  235. */
  236. public function generateString($length, $characters = '')
  237. {
  238. if (is_int($characters)) {
  239. // Combine character sets
  240. $characters = $this->expandCharacterSets($characters);
  241. }
  242. if ($length == 0 || strlen($characters) == 1) {
  243. return '';
  244. } elseif (empty($characters)) {
  245. // Default to base 64
  246. $characters = $this->expandCharacterSets(self::CHAR_BASE64);
  247. }
  248. /**
  249. * @var string $characters
  250. */
  251. // determine how many bytes to generate
  252. // This is basically doing floor(log(strlen($characters)))
  253. // But it's fixed to work properly for all numbers
  254. $len = strlen($characters);
  255. // The max call here fixes an issue where we under-generate in cases
  256. // where less than 8 bits are needed to represent $len
  257. /** @var int $bytes */
  258. $bytes = (int) ($length * ceil(($this->countBits($len)) / 8));
  259. // determine mask for valid characters
  260. $mask = 256 - (256 % $len);
  261. $result = '';
  262. do {
  263. $rand = $this->generate($bytes);
  264. for ($i = 0; $i < $bytes; $i++) {
  265. if (\ord($rand[$i]) >= $mask) {
  266. continue;
  267. }
  268. /** @var int $idx */
  269. $idx = (int) ((int) \ord($rand[$i]) % (int) ($len));
  270. $result .= (string) ($characters[$idx]);
  271. }
  272. } while (Util::safeStrlen($result) < $length);
  273. // We may over-generate, since we always use the entire buffer
  274. return Util::safeSubstr($result, 0, $length);
  275. }
  276. /**
  277. * Get the Mixer used for this instance
  278. *
  279. * @return Mixer the current mixer
  280. */
  281. public function getMixer()
  282. {
  283. return $this->mixer;
  284. }
  285. /**
  286. * Get the Sources used for this instance
  287. *
  288. * @return array<int, Source> the current mixer
  289. */
  290. public function getSources()
  291. {
  292. return $this->sources;
  293. }
  294. /**
  295. * Count the minimum number of bits to represent the provided number
  296. *
  297. * This is basically floor(log($number, 2))
  298. * But avoids float precision issues
  299. *
  300. * @param int $number The number to count
  301. *
  302. * @return int The number of bits
  303. */
  304. protected function countBits($number)
  305. {
  306. $log2 = 0;
  307. while ($number >>= 1) {
  308. $log2++;
  309. }
  310. return $log2;
  311. }
  312. /**
  313. * Expand a character set bitwise spec into a string character set
  314. *
  315. * This will also replace EASY_TO_READ characters if the flag is set
  316. *
  317. * @param int $spec The spec to expand (bitwise combination of flags)
  318. *
  319. * @return string The expanded string
  320. */
  321. protected function expandCharacterSets($spec)
  322. {
  323. /** @var string $combined */
  324. $combined = '';
  325. if ($spec == self::EASY_TO_READ) {
  326. $spec |= self::CHAR_ALNUM;
  327. }
  328. foreach ($this->charArrays as $flag => $chars) {
  329. if ($flag == self::EASY_TO_READ) {
  330. // handle this later
  331. continue;
  332. }
  333. if (($spec & $flag) === $flag) {
  334. $combined .= $chars;
  335. }
  336. }
  337. if ($spec & self::EASY_TO_READ) {
  338. // remove ambiguous characters
  339. $combined = \str_replace(
  340. \str_split(self::AMBIGUOUS_CHARS),
  341. '',
  342. $combined
  343. );
  344. }
  345. return (string) \count_chars($combined, 3);
  346. }
  347. }