SecureRandom::getArray()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 13
cts 13
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 2
crap 3
1
<?php
2
3
namespace Riimu\Kit\SecureRandom;
4
5
use Riimu\Kit\SecureRandom\Generator\ByteNumberGenerator;
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-2017 Riikka Kalliomäki
17
 * @license http://opensource.org/licenses/mit-license.php MIT License
18
 */
19
class SecureRandom
20
{
21
    /** @var NumberGenerator The secure random generator used to generate bytes and numbers */
22
    private $generator;
23
24
    /** @var string[] List of default generators */
25
    private static $defaultGenerators = [
26
        Generator\Internal::class,
27
        Generator\RandomReader::class,
28
        Generator\Mcrypt::class,
29
        Generator\OpenSSL::class,
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\Generator|null $generator Random byte generator or null for default
51
     * @throws GeneratorException If the provided or default generators are not supported
52
     */
53 101
    public function __construct(Generator\Generator $generator = null)
54
    {
55 101
        if ($generator === null) {
56 6
            $generator = $this->getDefaultGenerator();
57 96
        } elseif (!$generator->isSupported()) {
58 3
            throw new GeneratorException('The provided secure random byte generator is not supported by the system');
59
        }
60
61 95
        if (!$generator instanceof NumberGenerator) {
62 88
            $generator = new ByteNumberGenerator($generator);
63 30
        }
64
65 95
        $this->generator = $generator;
66 95
    }
67
68
    /**
69
     * Returns the first supported default secure random byte generator.
70
     * @return Generator\Generator Supported secure random byte generator
71
     * @throws GeneratorException If none of the default generators are supported
72
     */
73 6
    private function getDefaultGenerator()
74
    {
75 6
        foreach (self::$defaultGenerators as $generator) {
76
            /** @var Generator\Generator $generator */
77 3
            $generator = new $generator();
78
79 3
            if ($generator->isSupported()) {
80 3
                return $generator;
81
            }
82 2
        }
83
84 3
        throw new GeneratorException('Default secure random byte generators are not supported by the system');
85
    }
86
87
    /**
88
     * Returns a number of random bytes.
89
     * @param int $count Number of random bytes to return
90
     * @return string Randomly generated bytes
91
     * @throws \InvalidArgumentException If the count is invalid
92
     */
93 9
    public function getBytes($count)
94
    {
95 9
        $count = (int) $count;
96
97 9
        if ($count < 0) {
98 3
            throw new \InvalidArgumentException('Number of bytes must be 0 or more');
99
        }
100
101 6
        return $this->generator->getBytes($count);
102
    }
103
104
    /**
105
     * Returns a random integer between two positive integers (inclusive).
106
     * @param int $min Minimum limit
107
     * @param int $max Maximum limit
108
     * @return int Random integer between minimum and maximum limit
109
     * @throws \InvalidArgumentException If the limits are invalid
110
     */
111 26
    public function getInteger($min, $max)
112
    {
113 26
        $min = (int) $min;
114 26
        $max = (int) $max;
115
116 26
        if ($this->isOutOfBounds($min, 0, $max)) {
117 9
            throw new \InvalidArgumentException('Invalid minimum or maximum value');
118
        }
119
120 17
        return $this->generator->getNumber($min, $max);
121
    }
122
123
    /**
124
     * Tells if the given number is not within given limits (inclusive).
125
     * @param int $number The number to test
126
     * @param int $min The minimum allowed limit
127
     * @param int $max The maximum allowed limit
128
     * @return bool True if the number is out of bounds, false if within bounds
129
     */
130 47
    private function isOutOfBounds($number, $min, $max)
131
    {
132 47
        return $number < $min || $max < $number;
133
    }
134
135
    /**
136
     * Returns a random float between 0 and 1 (excluding the number 1).
137
     * @return float Random float between 0 and 1 (excluding 1)
138
     */
139 3
    public function getRandom()
140
    {
141 3
        $bytes = unpack('C7', $this->generator->getBytes(7));
142 3
        $lastByte = array_pop($bytes) & 0b00011111;
143 3
        $result = 0.0;
144
145 3
        foreach ($bytes as $byte) {
146 3
            $result = ($byte + $result) / 256;
147 1
        }
148
149 3
        $result = ($lastByte + $result) / 32;
150
151 3
        return $result;
152
    }
153
154
    /**
155
     * Returns a random float between 0 and 1 (inclusive).
156
     * @return float Random float between 0 and 1 (inclusive)
157
     */
158 3
    public function getFloat()
159
    {
160 3
        return (float) ($this->generator->getNumber(0, PHP_INT_MAX) / PHP_INT_MAX);
161
    }
162
163
    /**
164
     * Returns a number of randomly selected elements from the array.
165
     *
166
     * This method returns randomly selected elements from the array. The number
167
     * of elements is determined by by the second argument. The elements are
168
     * returned in random order but the keys are preserved.
169
     *
170
     * @param array $array Array of elements
171
     * @param int $count Number of elements to return from the array
172
     * @return array Randomly selected elements in random order
173
     * @throws \InvalidArgumentException If the count is invalid
174
     */
175 21
    public function getArray(array $array, $count)
176
    {
177 21
        $count = (int) $count;
178 21
        $size = count($array);
179
180 21
        if ($this->isOutOfBounds($count, 0, $size)) {
181 3
            throw new \InvalidArgumentException('Invalid number of elements');
182
        }
183
184 18
        $result = [];
185 18
        $keys = array_keys($array);
186
187 18
        for ($i = 0; $i < $count; $i++) {
188 9
            $index = $this->generator->getNumber($i, $size - 1);
189 9
            $result[$keys[$index]] = $array[$keys[$index]];
190 9
            $keys[$index] = $keys[$i];
191 3
        }
192
193 18
        return $result;
194
    }
195
196
    /**
197
     * Returns one randomly selected value from the array.
198
     * @param array $array The array to choose from
199
     * @return mixed One randomly selected value from the array
200
     * @throws \InvalidArgumentException If the array is empty
201
     */
202 9
    public function choose(array $array)
203
    {
204 9
        if (count($array) < 1) {
205 3
            throw new \InvalidArgumentException('Array must have at least one value');
206
        }
207
208 6
        $result = array_slice($array, $this->generator->getNumber(0, count($array) - 1), 1);
209
210 6
        return current($result);
211
    }
212
213
    /**
214
     * Returns the array with the elements reordered in a random order.
215
     * @param array $array The array to shuffle
216
     * @return array The provided array with elements in a random order
217
     */
218 6
    public function shuffle(array $array)
219
    {
220 6
        return $this->getArray($array, count($array));
221
    }
222
223
    /**
224
     * Returns a random sequence of values.
225
     *
226
     * If a string is provided as the first argument, the method returns a
227
     * string with characters selected from the provided string. The length of
228
     * the returned string is determined by the second argument.
229
     *
230
     * If an array is provided as the first argument, the method returns an
231
     * array with elements selected from the provided array. The size of the
232
     * returned array is determined by the second argument.
233
     *
234
     * The functionality is similar to getArray(), except for the fact that the
235
     * returned value can contain the same character or element multiple times.
236
     * If the same character or element appears multiple times in the provided
237
     * argument, it will increase the relative chance of it appearing in the
238
     * returned value.
239
     *
240
     * @param string|array $choices Values to choose from
241
     * @param int $length Length of the sequence
242
     * @return array|string The generated random sequence
243
     * @throws \InvalidArgumentException If the choices or length is invalid
244
     */
245 18
    public function getSequence($choices, $length)
246
    {
247 18
        $length = (int) $length;
248
249 18
        if ($length < 0) {
250 3
            throw new \InvalidArgumentException('Invalid sequence length');
251
        }
252
253 15
        if (is_array($choices)) {
254 12
            return $this->getSequenceValues(array_values($choices), $length);
255
        }
256
257 12
        return implode($this->getSequenceValues(str_split((string) $choices), $length));
258
    }
259
260
    /**
261
     * Returns the selected list of values for the sequence.
262
     * @param array $values List of possible values
263
     * @param int $length Number of values to return
264
     * @return array Selected list of values for the sequence
265
     * @throws \InvalidArgumentException If the value set is empty
266
     */
267 15
    private function getSequenceValues(array $values, $length)
268
    {
269 15
        if ($length < 1) {
270 6
            return [];
271
        }
272
273 9
        if (count($values) < 1) {
274 3
            throw new \InvalidArgumentException('Cannot generate sequence from empty value set');
275
        }
276
277 6
        $size = count($values);
278 6
        $result = [];
279
280 6
        for ($i = 0; $i < $length; $i++) {
281 6
            $result[] = $values[$this->generator->getNumber(0, $size - 1)];
282 2
        }
283
284 6
        return $result;
285
    }
286
287
    /**
288
     * Returns a random UUID version 4 identifier.
289
     * @return string A random UUID identifier
290
     */
291 3
    public function getUuid()
292
    {
293 3
        $integers = array_values(unpack('n8', $this->generator->getBytes(16)));
294
295 3
        $integers[3] &= 0x0FFF;
296 3
        $integers[4] = $integers[4] & 0x3FFF | 0x8000;
297
298 3
        return vsprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x', $integers);
299
    }
300
}
301