Completed
Push — master ( b4b967...38e8ce )
by Riikka
02:15
created

SecureRandom::getUuid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Riimu\Kit\SecureRandom;
4
5
use Riimu\Kit\SecureRandom\Generator\Generator;
6
use Riimu\Kit\SecureRandom\Generator\NumberGenerator;
7
8
/**
9
 * Library for normalizing bytes returned by secure random byte generators.
10
 *
11
 * SecureRandom takes bytes generated by secure random byte generators and
12
 * normalizes (i.e. provides even distribution) for other common usages, such
13
 * as generation of integers, floats and randomizing arrays.
14
 *
15
 * @author Riikka Kalliomäki <[email protected]>
16
 * @copyright Copyright (c) 2014, Riikka Kalliomäki
17
 * @license http://opensource.org/licenses/mit-license.php MIT License
18
 */
19
class SecureRandom
20
{
21
    /** @var Generator The secure random byte generator used to generate bytes */
22
    private $generator;
23
24
    /** @var string[] List of default generators */
25
    private static $defaultGenerators = [
26
        '\Riimu\Kit\SecureRandom\Generator\Internal',
27
        '\Riimu\Kit\SecureRandom\Generator\RandomReader',
28
        '\Riimu\Kit\SecureRandom\Generator\Mcrypt',
29
        '\Riimu\Kit\SecureRandom\Generator\OpenSSL',
30
    ];
31
32
    /**
33
     * Creates a new instance of SecureRandom.
34
     *
35
     * You can either provide a generator to use for generating random bytes or
36
     * give null as the argument to use default generators. If null is provided,
37
     * the constructor will attempt to create the random byte generators in the
38
     * following order until it finds one that is supported:
39
     *
40
     * - Internal
41
     * - RandomReader
42
     * - Mcrypt
43
     * - OpenSSL
44
     *
45
     * Note that since most cases require non-blocking random generation, the
46
     * default generators use /dev/urandom as the random source. If you do not
47
     * think this provides enough security, create the desired random generator
48
     * using /dev/random as the source.
49
     *
50
     * @param Generator|null $generator Random byte generator or null for default
51
     * @throws GeneratorException If the provided or default generators are not supported
52
     */
53 31
    public function __construct(Generator $generator = null)
54
    {
55 31
        if ($generator === null) {
56 2
            $generator = $this->getDefaultGenerator();
57 29
        } elseif (!$generator->isSupported()) {
58 1
            throw new GeneratorException('The provided secure random byte generator is not supported by the system');
59
        }
60
61 29
        $this->generator = $generator;
62 29
    }
63
64
    /**
65
     * Returns the first supported default secure random byte generator.
66
     * @return Generator Supported secure random byte generator
67
     * @throws GeneratorException If none of the default generators are supported
68
     */
69 2
    private function getDefaultGenerator()
70
    {
71 2
        foreach (self::$defaultGenerators as $generator) {
72 1
            $generator = new $generator();
73
74 1
            if ($generator->isSupported()) {
75 1
                return $generator;
76
            }
77
        }
78
79 1
        throw new GeneratorException('Default secure random byte generators are not supported by the system');
80
    }
81
82
    /**
83
     * Returns a number of random bytes.
84
     * @param int $count Number of random bytes to return
85
     * @return string Randomly generated bytes
86
     * @throws \InvalidArgumentException If the count is invalid
87
     */
88 3
    public function getBytes($count)
89
    {
90 3
        $count = (int) $count;
91
92 3
        if ($count < 0) {
93 1
            throw new \InvalidArgumentException('Number of bytes must be 0 or more');
94 2
        } elseif ($count === 0) {
95 1
            return '';
96
        }
97
98 1
        return $this->generator->getBytes($count);
99
    }
100
101
    /**
102
     * Returns a random number between 0 and the limit.
103
     * @param int $limit Maximum random number
104
     * @return int Random number between 0 and the limit
105
     */
106 13
    private function getNumber($limit)
107
    {
108 13
        if ($limit === 0) {
109 4
            return 0;
110 10
        } elseif ($this->generator instanceof NumberGenerator) {
111 2
            return $this->generator->getNumber(0, $limit);
112
        }
113
114 8
        return $this->getByteNumber($limit);
115
    }
116
117
    /**
118
     * Returns a random number generated using the random byte generator.
119
     * @param int $limit Maximum value for the random number
120
     * @return int The generated random number between 0 and the limit
121
     */
122 8
    private function getByteNumber($limit)
123
    {
124 8
        for ($bits = 1, $mask = 1; $limit >> $bits > 0; $bits++) {
125 8
            $mask |= 1 << $bits;
126
        }
127
128 8
        $bytes = (int) ceil($bits / 8);
129
130
        do {
131 8
            $result = hexdec(bin2hex($this->generator->getBytes($bytes))) & $mask;
132 8
        } while ($result > $limit);
133
134 8
        return $result;
135
    }
136
137
    /**
138
     * Returns a random integer between two positive integers (inclusive).
139
     * @param int $min Minimum limit
140
     * @param int $max Maximum limit
141
     * @return int Random integer between minimum and maximum limit
142
     * @throws \InvalidArgumentException If the limits are invalid
143
     */
144 8
    public function getInteger($min, $max)
145
    {
146 8
        $min = (int) $min;
147 8
        $max = (int) $max;
148
149 8
        if ($min < 0 || $max < $min) {
150 3
            throw new \InvalidArgumentException('Invalid minimum or maximum value');
151
        }
152
153 5
        return $min + $this->getNumber($max - $min);
154
    }
155
156
    /**
157
     * Returns a random float between 0 and 1 (excluding the number 1).
158
     * @return float Random float between 0 and 1 (excluding 1)
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
159
     */
160
    public function getRandom()
161
    {
162
        $bytes = $this->generator->getBytes(7);
163
        $result = 0;
164
165
        for ($i = 0; $i < 7; $i++) {
166
            $result += hexdec(bin2hex($bytes[$i]));
167
            $result /= 256;
168
        }
169
170
        return $result;
171
    }
172
173
    /**
174
     * Returns a random float between 0 and 1 (inclusive).
175
     * @return float Random float between 0 and 1 (inclusive)
176
     */
177 1
    public function getFloat()
178
    {
179 1
        return (float) ($this->getNumber(PHP_INT_MAX) / PHP_INT_MAX);
180
    }
181
182
    /**
183
     * Returns a number of randomly selected elements from the array.
184
     *
185
     * This method returns randomly selected elements from the array. The number
186
     * of elements is determined by by the second argument. The elements are
187
     * returned in random order but the keys are preserved.
188
     *
189
     * @param array $array Array of elements
190
     * @param int $count Number of elements to return from the array
191
     * @return array Randomly selected elements in random order
192
     * @throws \InvalidArgumentException If the count is invalid
193
     */
194 7
    public function getArray(array $array, $count)
195
    {
196 7
        $count = (int) $count;
197 7
        $size = count($array);
198
199 7
        if ($count < 0 || $count > $size) {
200 1
            throw new \InvalidArgumentException('Invalid number of elements');
201
        }
202
203 6
        $result = [];
204 6
        $keys = array_keys($array);
205
206 6
        for ($last = $size - 1; $size - $last <= $count; $last--) {
207 3
            $index = $this->getNumber($last);
208 3
            $result[$keys[$index]] = $array[$keys[$index]];
209
210 3
            if ($index < $last) {
211 3
                $keys[$index] = $keys[$last];
212
            }
213
        }
214
215 6
        return $result;
216
    }
217
218
    /**
219
     * Returns one randomly selected value from the array.
220
     * @param array $array The array to choose from
221
     * @return mixed One randomly selected value from the array
222
     * @throws \InvalidArgumentException If the array is empty
223
     */
224 3
    public function choose(array $array)
225
    {
226 3
        if (count($array) < 1) {
227 1
            throw new \InvalidArgumentException('Array must have at least one value');
228
        }
229
230 2
        return $array[array_keys($array)[$this->getNumber(count($array) - 1)]];
231
    }
232
233
    /**
234
     * Returns the array with the elements reordered in a random order.
235
     * @param array $array The array to shuffle
236
     * @return array The provided array with elements in a random order
237
     */
238 2
    public function shuffle(array $array)
239
    {
240 2
        return $this->getArray($array, count($array));
241
    }
242
243
    /**
244
     * Returns a random sequence of values.
245
     *
246
     * If a string is provided as the first argument, the method returns a
247
     * string with characters selected from the provided string. The length of
248
     * the returned string is determined by the second argument.
249
     *
250
     * If an array is provided as the first argument, the method returns an
251
     * array with elements selected from the provided array. The size of the
252
     * returned array is determined by the second argument.
253
     *
254
     * The functionality is similar to getArray(), except for the fact that the
255
     * returned value can contain the same character or element multiple times.
256
     * If the same character or element appears multiple times in the provided
257
     * argument, it will increase the relative chance of it appearing in the
258
     * returned value.
259
     *
260
     * @param string|array $choices Values to choose from
261
     * @param int $length Length of the sequence
262
     * @return array|string The generated random sequence
263
     * @throws \InvalidArgumentException If the choices or length is invalid
264
     */
265 6
    public function getSequence($choices, $length)
266
    {
267 6
        $length = (int) $length;
268
269 6
        if ($length < 0) {
270 1
            throw new \InvalidArgumentException('Invalid sequence length');
271
        }
272
273 5
        if (is_array($choices)) {
274 4
            return $this->getSequenceValues(array_values($choices), $length);
275
        } else {
276 4
            return implode($this->getSequenceValues(str_split((string) $choices), $length));
277
        }
278
    }
279
280
    /**
281
     * Returns the selected list of values for the sequence.
282
     * @param array $values List of possible values
283
     * @param int $length Number of values to return
284
     * @return array Selected list of values for the sequence
285
     */
286 5
    private function getSequenceValues(array $values, $length)
287
    {
288 5
        if ($length < 1) {
289 2
            return [];
290 3
        } elseif (count($values) < 1) {
291 1
            throw new \InvalidArgumentException('Cannot generate sequence from empty value set');
292
        }
293
294 2
        $size = count($values);
295 2
        $result = [];
296
297 2
        for ($i = 0; $i < $length; $i++) {
298 2
            $result[] = $values[$this->getNumber($size - 1)];
299
        }
300
301 2
        return $result;
302
    }
303
304
    /**
305
     * Returns a random UUID version 4 identifier.
306
     * @return string A random UUID identifier
307
     */
308
    public function getUuid()
309
    {
310
        $integers = array_map(function ($bytes) {
311
            return hexdec(bin2hex($bytes));
312
        }, str_split($this->generator->getBytes(16), 2));
313
314
        $integers[3] = $integers[3] & 0x0FFF;
315
        $integers[4] = $integers[4] & 0x3FFF | 0x8000;
316
317
        return vsprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x', $integers);
318
    }
319
}
320