Completed
Push — master ( afbd03...df7fcc )
by Riikka
01:15
created

SecureRandom   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 283
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 30
lcom 2
cbo 4
dl 0
loc 283
ccs 84
cts 84
cp 1
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 4
A getDefaultGenerator() 0 13 3
A getBytes() 0 10 2
A getInteger() 0 11 2
A isOutOfBounds() 0 4 2
A getRandom() 0 13 2
A getFloat() 0 4 1
A getArray() 0 20 3
A choose() 0 10 2
A shuffle() 0 4 1
A getSequence() 0 14 3
A getSequenceValues() 0 19 4
A getUuid() 0 11 1
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, 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 98
    public function __construct(Generator\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
        if (!$generator instanceof NumberGenerator) {
62 85
            $generator = new ByteNumberGenerator($generator);
63 29
        }
64
65 92
        $this->generator = $generator;
66 92
    }
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 23
    public function getInteger($min, $max)
112
    {
113 23
        $min = (int) $min;
114 23
        $max = (int) $max;
115
116 23
        if ($this->isOutOfBounds($min, 0, $max)) {
117 9
            throw new \InvalidArgumentException('Invalid minimum or maximum value');
118
        }
119
120 14
        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 44
    private function isOutOfBounds($number, $min, $max)
131
    {
132 44
        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 = $this->generator->getBytes(7);
142 3
        $result = 0.0;
143
144 3
        for ($i = 0; $i < 6; $i++) {
145 3
            $result = (ord($bytes[$i]) + $result) / 256;
146 1
        }
147
148 3
        $result = ((ord($bytes[6]) & 0b00011111) + $result) / 32;
149
150 3
        return $result;
151
    }
152
153
    /**
154
     * Returns a random float between 0 and 1 (inclusive).
155
     * @return float Random float between 0 and 1 (inclusive)
156
     */
157 3
    public function getFloat()
158
    {
159 3
        return (float) ($this->generator->getNumber(0, PHP_INT_MAX) / PHP_INT_MAX);
160
    }
161
162
    /**
163
     * Returns a number of randomly selected elements from the array.
164
     *
165
     * This method returns randomly selected elements from the array. The number
166
     * of elements is determined by by the second argument. The elements are
167
     * returned in random order but the keys are preserved.
168
     *
169
     * @param array $array Array of elements
170
     * @param int $count Number of elements to return from the array
171
     * @return array Randomly selected elements in random order
172
     * @throws \InvalidArgumentException If the count is invalid
173
     */
174 21
    public function getArray(array $array, $count)
175
    {
176 21
        $count = (int) $count;
177 21
        $size = count($array);
178
179 21
        if ($this->isOutOfBounds($count, 0, $size)) {
180 3
            throw new \InvalidArgumentException('Invalid number of elements');
181
        }
182
183 18
        $result = [];
184 18
        $keys = array_keys($array);
185
186 18
        for ($i = 0; $i < $count; $i++) {
187 9
            $index = $this->generator->getNumber($i, $size - 1);
188 9
            $result[$keys[$index]] = $array[$keys[$index]];
189 9
            $keys[$index] = $keys[$i];
190 3
        }
191
192 18
        return $result;
193
    }
194
195
    /**
196
     * Returns one randomly selected value from the array.
197
     * @param array $array The array to choose from
198
     * @return mixed One randomly selected value from the array
199
     * @throws \InvalidArgumentException If the array is empty
200
     */
201 9
    public function choose(array $array)
202
    {
203 9
        if (count($array) < 1) {
204 3
            throw new \InvalidArgumentException('Array must have at least one value');
205
        }
206
207 6
        $result = array_slice($array, $this->generator->getNumber(0, count($array) - 1), 1);
208
209 6
        return current($result);
210
    }
211
212
    /**
213
     * Returns the array with the elements reordered in a random order.
214
     * @param array $array The array to shuffle
215
     * @return array The provided array with elements in a random order
216
     */
217 6
    public function shuffle(array $array)
218
    {
219 6
        return $this->getArray($array, count($array));
220
    }
221
222
    /**
223
     * Returns a random sequence of values.
224
     *
225
     * If a string is provided as the first argument, the method returns a
226
     * string with characters selected from the provided string. The length of
227
     * the returned string is determined by the second argument.
228
     *
229
     * If an array is provided as the first argument, the method returns an
230
     * array with elements selected from the provided array. The size of the
231
     * returned array is determined by the second argument.
232
     *
233
     * The functionality is similar to getArray(), except for the fact that the
234
     * returned value can contain the same character or element multiple times.
235
     * If the same character or element appears multiple times in the provided
236
     * argument, it will increase the relative chance of it appearing in the
237
     * returned value.
238
     *
239
     * @param string|array $choices Values to choose from
240
     * @param int $length Length of the sequence
241
     * @return array|string The generated random sequence
242
     * @throws \InvalidArgumentException If the choices or length is invalid
243
     */
244 18
    public function getSequence($choices, $length)
245
    {
246 18
        $length = (int) $length;
247
248 18
        if ($length < 0) {
249 3
            throw new \InvalidArgumentException('Invalid sequence length');
250
        }
251
252 15
        if (is_array($choices)) {
253 12
            return $this->getSequenceValues(array_values($choices), $length);
254
        }
255
256 12
        return implode($this->getSequenceValues(str_split((string) $choices), $length));
257
    }
258
259
    /**
260
     * Returns the selected list of values for the sequence.
261
     * @param array $values List of possible values
262
     * @param int $length Number of values to return
263
     * @return array Selected list of values for the sequence
264
     * @throws \InvalidArgumentException If the value set is empty
265
     */
266 15
    private function getSequenceValues(array $values, $length)
267
    {
268 15
        if ($length < 1) {
269 6
            return [];
270
        }
271
272 9
        if (count($values) < 1) {
273 3
            throw new \InvalidArgumentException('Cannot generate sequence from empty value set');
274
        }
275
276 6
        $size = count($values);
277 6
        $result = [];
278
279 6
        for ($i = 0; $i < $length; $i++) {
280 6
            $result[] = $values[$this->generator->getNumber(0, $size - 1)];
281 2
        }
282
283 6
        return $result;
284
    }
285
286
    /**
287
     * Returns a random UUID version 4 identifier.
288
     * @return string A random UUID identifier
289
     */
290
    public function getUuid()
291
    {
292 3
        $integers = array_map(function ($bytes) {
293 3
            return hexdec(bin2hex($bytes));
294 3
        }, str_split($this->generator->getBytes(16), 2));
295
296 3
        $integers[3] &= 0x0FFF;
297 3
        $integers[4] = $integers[4] & 0x3FFF | 0x8000;
298
299 3
        return vsprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x', $integers);
300
    }
301
}
302