1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* The RandomLib library for securely generating random numbers and strings in PHP |
5
|
|
|
* |
6
|
|
|
* @author Anthony Ferrara <[email protected]> |
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 <[email protected]> |
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
|
|
|
*/ |
29
|
|
|
namespace 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 <[email protected]> |
40
|
|
|
* @author Timo Hamina |
41
|
|
|
*/ |
42
|
|
|
class 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
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.