123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- <?php
- /*
- * The RandomLib library for securely generating random numbers and strings in PHP
- *
- * @author Anthony Ferrara <ircmaxell@ircmaxell.com>
- * @copyright 2011 The Authors
- * @license http://www.opensource.org/licenses/mit-license.html MIT License
- * @version Build @@version@@
- */
- /**
- * The Random Number Generator Class
- *
- * Use this factory to generate cryptographic quality random numbers (strings)
- *
- * PHP version 5.3
- *
- * @category PHPPasswordLib
- * @package Random
- *
- * @author Anthony Ferrara <ircmaxell@ircmaxell.com>
- * @author Paragon Initiative Enterprises <security@paragonie.com>
- * @author Timo Hamina
- * @copyright 2011 The Authors
- * @license http://www.opensource.org/licenses/mit-license.html MIT License
- *
- * @version Build @@version@@
- */
- namespace RandomLib;
- use SecurityLib\Util;
- /**
- * The Random Number Generator Class
- *
- * Use this factory to generate cryptographic quality random numbers (strings)
- *
- * @category PHPPasswordLib
- * @package Random
- *
- * @author Anthony Ferrara <ircmaxell@ircmaxell.com>
- * @author Paragon Initiative Enterprises <security@paragonie.com>
- * @author Timo Hamina
- */
- class Generator
- {
- /**
- * @const Flag for uppercase letters
- */
- const CHAR_UPPER = 1;
- /**
- * @const Flag for lowercase letters
- */
- const CHAR_LOWER = 2;
- /**
- * @const Flag for alpha characters (combines UPPER + LOWER)
- */
- const CHAR_ALPHA = 3; // CHAR_UPPER | CHAR_LOWER
- /**
- * @const Flag for digits
- */
- const CHAR_DIGITS = 4;
- /**
- * @const Flag for alpha numeric characters
- */
- const CHAR_ALNUM = 7; // CHAR_ALPHA | CHAR_DIGITS
- /**
- * @const Flag for uppercase hexadecimal symbols
- */
- const CHAR_UPPER_HEX = 12; // 8 | CHAR_DIGITS
- /**
- * @const Flag for lowercase hexidecimal symbols
- */
- const CHAR_LOWER_HEX = 20; // 16 | CHAR_DIGITS
- /**
- * @const Flag for base64 symbols
- */
- const CHAR_BASE64 = 39; // 32 | CHAR_ALNUM
- /**
- * @const Flag for additional symbols accessible via the keyboard
- */
- const CHAR_SYMBOLS = 64;
- /**
- * @const Flag for brackets
- */
- const CHAR_BRACKETS = 128;
- /**
- * @const Flag for punctuation marks
- */
- const CHAR_PUNCT = 256;
- /**
- * @const Flag for upper/lower-case and digits but without "B8G6I1l|0OQDS5Z2"
- */
- const EASY_TO_READ = 512;
- /**
- * @var Mixer The mixing strategy to use for this generator instance
- */
- protected $mixer = null;
- /**
- * @var array<int, Source> An array of random number sources to use for this generator
- */
- protected $sources = array();
- /**
- * @var array<int, string> The different characters, by Flag
- */
- protected $charArrays = array(
- self::CHAR_UPPER => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
- self::CHAR_LOWER => 'abcdefghijklmnopqrstuvwxyz',
- self::CHAR_DIGITS => '0123456789',
- self::CHAR_UPPER_HEX => 'ABCDEF',
- self::CHAR_LOWER_HEX => 'abcdef',
- self::CHAR_BASE64 => '+/',
- self::CHAR_SYMBOLS => '!"#$%&\'()* +,-./:;<=>?@[\]^_`{|}~',
- self::CHAR_BRACKETS => '()[]{}<>',
- self::CHAR_PUNCT => ',.;:',
- );
- /**
- * @internal
- * @private
- * @const string Ambiguous characters for "Easy To Read" sets
- */
- const AMBIGUOUS_CHARS = 'B8G6I1l|0OQDS5Z2()[]{}:;,.';
- /**
- * Build a new instance of the generator
- *
- * @param array<int, Source> $sources An array of random data sources to use
- * @param Mixer $mixer The mixing strategy to use for this generator
- */
- public function __construct(array $sources, Mixer $mixer)
- {
- foreach ($sources as $source) {
- $this->addSource($source);
- }
- $this->mixer = $mixer;
- }
- /**
- * Add a random number source to the generator
- *
- * @param Source $source The random number source to add
- *
- * @return Generator $this The current generator instance
- */
- public function addSource(Source $source)
- {
- $this->sources[] = $source;
- return $this;
- }
- /**
- * Generate a random number (string) of the requested size
- *
- * @param int $size The size of the requested random number
- *
- * @return string The generated random number (string)
- */
- public function generate($size)
- {
- $seeds = array();
- foreach ($this->sources as $source) {
- if ($source instanceof Source) {
- $seeds[] = $source->generate($size);
- }
- }
- return $this->mixer->mix($seeds);
- }
- /**
- * Generate a random integer with the given range
- *
- * @param int $min The lower bound of the range to generate
- * @param int $max The upper bound of the range to generate
- *
- * @return int The generated random number within the range
- */
- public function generateInt($min = 0, $max = PHP_INT_MAX)
- {
- $tmp = (int) max($max, $min);
- $min = (int) min($max, $min);
- $max = $tmp;
- $range = $max - $min;
- if ($range == 0) {
- return $max;
- } elseif ($range > PHP_INT_MAX || is_float($range) || $range < 0) {
- /**
- * This works, because PHP will auto-convert it to a float at this point,
- * But on 64 bit systems, the float won't have enough precision to
- * actually store the difference, so we need to check if it's a float
- * and hence auto-converted...
- */
- throw new \RangeException(
- 'The supplied range is too great to generate'
- );
- }
- $bits = $this->countBits($range) + 1;
- $bytes = (int) \max(\ceil($bits / 8), 1);
- if ($bits == 63) {
- /**
- * Fixes issue #22
- *
- * @see https://github.com/ircmaxell/RandomLib/issues/22
- */
- $mask = 0x7fffffffffffffff;
- } else {
- $mask = (int) ((1 << $bits) - 1);
- }
- /**
- * The mask is a better way of dropping unused bits. Basically what it does
- * is to set all the bits in the mask to 1 that we may need. Since the max
- * range is PHP_INT_MAX, we will never need negative numbers (which would
- * have the MSB set on the max int possible to generate). Therefore we
- * can just mask that away. Since pow returns a float, we need to cast
- * it back to an int so the mask will work.
- *
- * On a 64 bit platform, that means that PHP_INT_MAX is 2^63 - 1. Which
- * is also the mask if 63 bits are needed (by the log(range, 2) call).
- * So if the computed result is negative (meaning the 64th bit is set), the
- * mask will correct that.
- *
- * This turns out to be slightly better than the shift as we don't need to
- * worry about "fixing" negative values.
- */
- do {
- $test = $this->generate($bytes);
- /** @var int $result */
- $result = \hexdec(\bin2hex($test)) & $mask;
- } while ($result > $range);
- return $result + $min;
- }
- /**
- * Generate a random string of specified length.
- *
- * This uses the supplied character list for generating the new result
- * string.
- *
- * @param int $length The length of the generated string
- * @param int|string $characters String: An optional list of characters to use
- * Integer: Character flags
- *
- * @return string The generated random string
- */
- public function generateString($length, $characters = '')
- {
- if (is_int($characters)) {
- // Combine character sets
- $characters = $this->expandCharacterSets($characters);
- }
- if ($length == 0 || strlen($characters) == 1) {
- return '';
- } elseif (empty($characters)) {
- // Default to base 64
- $characters = $this->expandCharacterSets(self::CHAR_BASE64);
- }
- /**
- * @var string $characters
- */
- // determine how many bytes to generate
- // This is basically doing floor(log(strlen($characters)))
- // But it's fixed to work properly for all numbers
- $len = strlen($characters);
- // The max call here fixes an issue where we under-generate in cases
- // where less than 8 bits are needed to represent $len
- /** @var int $bytes */
- $bytes = (int) ($length * ceil(($this->countBits($len)) / 8));
- // determine mask for valid characters
- $mask = 256 - (256 % $len);
- $result = '';
- do {
- $rand = $this->generate($bytes);
- for ($i = 0; $i < $bytes; $i++) {
- if (\ord($rand[$i]) >= $mask) {
- continue;
- }
- /** @var int $idx */
- $idx = (int) ((int) \ord($rand[$i]) % (int) ($len));
- $result .= (string) ($characters[$idx]);
- }
- } while (Util::safeStrlen($result) < $length);
- // We may over-generate, since we always use the entire buffer
- return Util::safeSubstr($result, 0, $length);
- }
- /**
- * Get the Mixer used for this instance
- *
- * @return Mixer the current mixer
- */
- public function getMixer()
- {
- return $this->mixer;
- }
- /**
- * Get the Sources used for this instance
- *
- * @return array<int, Source> the current mixer
- */
- public function getSources()
- {
- return $this->sources;
- }
- /**
- * Count the minimum number of bits to represent the provided number
- *
- * This is basically floor(log($number, 2))
- * But avoids float precision issues
- *
- * @param int $number The number to count
- *
- * @return int The number of bits
- */
- protected function countBits($number)
- {
- $log2 = 0;
- while ($number >>= 1) {
- $log2++;
- }
- return $log2;
- }
- /**
- * Expand a character set bitwise spec into a string character set
- *
- * This will also replace EASY_TO_READ characters if the flag is set
- *
- * @param int $spec The spec to expand (bitwise combination of flags)
- *
- * @return string The expanded string
- */
- protected function expandCharacterSets($spec)
- {
- /** @var string $combined */
- $combined = '';
- if ($spec == self::EASY_TO_READ) {
- $spec |= self::CHAR_ALNUM;
- }
- foreach ($this->charArrays as $flag => $chars) {
- if ($flag == self::EASY_TO_READ) {
- // handle this later
- continue;
- }
- if (($spec & $flag) === $flag) {
- $combined .= $chars;
- }
- }
- if ($spec & self::EASY_TO_READ) {
- // remove ambiguous characters
- $combined = \str_replace(
- \str_split(self::AMBIGUOUS_CHARS),
- '',
- $combined
- );
- }
- return (string) \count_chars($combined, 3);
- }
- }
|