Completed
Push — master ( 99ba41...afbd03 )
by Riikka
01:42
created

SecureRandom::getArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 14
cts 14
cp 1
rs 9.3142
c 0
b 0
f 0
cc 3
eloc 13
nc 3
nop 2
crap 3
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 98
    public function __construct(Generator $generator = null)
54
    {
55 98
        if ($generator === null) {
56 6
            $generator = $this->getDefaultGenerator();
57 93
        } elseif (!$generator->isSupported()) {
58 3
            throw new GeneratorException('The provided secure random byte generator is not supported by the system');
59
        }
60
61 92
        $this->generator = $generator;
62 92
    }
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 6
    private function getDefaultGenerator()
70
    {
71 6
        foreach (self::$defaultGenerators as $generator) {
72 3
            $generator = new $generator();
73
74 3
            if ($generator->isSupported()) {
75 3
                return $generator;
76
            }
77 2
        }
78
79 3
        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 9
    public function getBytes($count)
89
    {
90 9
        $count = (int) $count;
91
92 9
        if ($count < 0) {
93 3
            throw new \InvalidArgumentException('Number of bytes must be 0 or more');
94 6
        } elseif ($count === 0) {
95 3
            return '';
96
        }
97
98 3
        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 38
    private function getNumber($limit)
107
    {
108 38
        if ($limit === 0) {
109 12
            return 0;
110 29
        } elseif ($this->generator instanceof NumberGenerator) {
111 5
            return $this->generator->getNumber(0, $limit);
112
        }
113
114 24
        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 24
    private function getByteNumber($limit)
123
    {
124 24
        for ($bits = 1, $mask = 1; $limit >> $bits > 0; $bits++) {
125 24
            $mask |= 1 << $bits;
126 8
        }
127
128 24
        $bytes = (int) ceil($bits / 8);
129
130
        do {
131 24
            $result = hexdec(bin2hex($this->generator->getBytes($bytes))) & $mask;
132 24
        } while ($result > $limit);
133
134 24
        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 23
    public function getInteger($min, $max)
145
    {
146 23
        $min = (int) $min;
147 23
        $max = (int) $max;
148
149 23
        if ($this->isOutOfBounds($min, 0, $max)) {
150 9
            throw new \InvalidArgumentException('Invalid minimum or maximum value');
151
        }
152
153 14
        return $min + $this->getNumber($max - $min);
154
    }
155
156
    /**
157
     * Tells if the given number is not within given limits (inclusive).
158
     * @param int $number The number to test
159
     * @param int $min The minimum allowed limit
160
     * @param int $max The maximum allowed limit
161
     * @return bool True if the number is out of bounds, false if within bounds
162
     */
163 44
    private function isOutOfBounds($number, $min, $max)
164
    {
165 44
        return $number < $min || $max < $number;
166
    }
167
168
    /**
169
     * Returns a random float between 0 and 1 (excluding the number 1).
170
     * @return float Random float between 0 and 1 (excluding 1)
171
     */
172 3
    public function getRandom()
173
    {
174 3
        $bytes = $this->generator->getBytes(7);
175 3
        $result = 0.0;
176
177 3
        for ($i = 0; $i < 6; $i++) {
178 3
            $result = (ord($bytes[$i]) + $result) / 256;
179 1
        }
180
181 3
        $result = ((ord($bytes[6]) & 0b00011111) + $result) / 32;
182
183 3
        return $result;
184
    }
185
186
    /**
187
     * Returns a random float between 0 and 1 (inclusive).
188
     * @return float Random float between 0 and 1 (inclusive)
189
     */
190 3
    public function getFloat()
191
    {
192 3
        return (float) ($this->getNumber(PHP_INT_MAX) / PHP_INT_MAX);
193
    }
194
195
    /**
196
     * Returns a number of randomly selected elements from the array.
197
     *
198
     * This method returns randomly selected elements from the array. The number
199
     * of elements is determined by by the second argument. The elements are
200
     * returned in random order but the keys are preserved.
201
     *
202
     * @param array $array Array of elements
203
     * @param int $count Number of elements to return from the array
204
     * @return array Randomly selected elements in random order
205
     * @throws \InvalidArgumentException If the count is invalid
206
     */
207 21
    public function getArray(array $array, $count)
208
    {
209 21
        $count = (int) $count;
210 21
        $size = count($array);
211
212 21
        if ($this->isOutOfBounds($count, 0, $size)) {
213 3
            throw new \InvalidArgumentException('Invalid number of elements');
214
        }
215
216 18
        $result = [];
217 18
        $keys = array_keys($array);
218
219 18
        for ($i = 0; $i < $count; $i++) {
220 9
            $last = $size - $i - 1;
221 9
            $index = $this->getNumber($last);
222 9
            $result[$keys[$index]] = $array[$keys[$index]];
223 9
            $keys[$index] = $keys[$last];
224 3
        }
225
226 18
        return $result;
227
    }
228
229
    /**
230
     * Returns one randomly selected value from the array.
231
     * @param array $array The array to choose from
232
     * @return mixed One randomly selected value from the array
233
     * @throws \InvalidArgumentException If the array is empty
234
     */
235 9
    public function choose(array $array)
236
    {
237 9
        if (count($array) < 1) {
238 3
            throw new \InvalidArgumentException('Array must have at least one value');
239
        }
240
241 6
        $result = array_slice($array, $this->getNumber(count($array) - 1), 1);
242
243 6
        return current($result);
244
    }
245
246
    /**
247
     * Returns the array with the elements reordered in a random order.
248
     * @param array $array The array to shuffle
249
     * @return array The provided array with elements in a random order
250
     */
251 6
    public function shuffle(array $array)
252
    {
253 6
        return $this->getArray($array, count($array));
254
    }
255
256
    /**
257
     * Returns a random sequence of values.
258
     *
259
     * If a string is provided as the first argument, the method returns a
260
     * string with characters selected from the provided string. The length of
261
     * the returned string is determined by the second argument.
262
     *
263
     * If an array is provided as the first argument, the method returns an
264
     * array with elements selected from the provided array. The size of the
265
     * returned array is determined by the second argument.
266
     *
267
     * The functionality is similar to getArray(), except for the fact that the
268
     * returned value can contain the same character or element multiple times.
269
     * If the same character or element appears multiple times in the provided
270
     * argument, it will increase the relative chance of it appearing in the
271
     * returned value.
272
     *
273
     * @param string|array $choices Values to choose from
274
     * @param int $length Length of the sequence
275
     * @return array|string The generated random sequence
276
     * @throws \InvalidArgumentException If the choices or length is invalid
277
     */
278 18
    public function getSequence($choices, $length)
279
    {
280 18
        $length = (int) $length;
281
282 18
        if ($length < 0) {
283 3
            throw new \InvalidArgumentException('Invalid sequence length');
284
        }
285
286 15
        if (is_array($choices)) {
287 12
            return $this->getSequenceValues(array_values($choices), $length);
288
        }
289
290 12
        return implode($this->getSequenceValues(str_split((string) $choices), $length));
291
    }
292
293
    /**
294
     * Returns the selected list of values for the sequence.
295
     * @param array $values List of possible values
296
     * @param int $length Number of values to return
297
     * @return array Selected list of values for the sequence
298
     * @throws \InvalidArgumentException If the value set is empty
299
     */
300 15
    private function getSequenceValues(array $values, $length)
301
    {
302 15
        if ($length < 1) {
303 6
            return [];
304
        }
305
306 9
        if (count($values) < 1) {
307 3
            throw new \InvalidArgumentException('Cannot generate sequence from empty value set');
308
        }
309
310 6
        $size = count($values);
311 6
        $result = [];
312
313 6
        for ($i = 0; $i < $length; $i++) {
314 6
            $result[] = $values[$this->getNumber($size - 1)];
315 2
        }
316
317 6
        return $result;
318
    }
319
320
    /**
321
     * Returns a random UUID version 4 identifier.
322
     * @return string A random UUID identifier
323
     */
324
    public function getUuid()
325
    {
326 3
        $integers = array_map(function ($bytes) {
327 3
            return hexdec(bin2hex($bytes));
328 3
        }, str_split($this->generator->getBytes(16), 2));
329
330 3
        $integers[3] &= 0x0FFF;
331 3
        $integers[4] = $integers[4] & 0x3FFF | 0x8000;
332
333 3
        return vsprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x', $integers);
334
    }
335
}
336