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