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