Completed
Push — next ( 64afee...7187a6 )
by Riikka
03:16
created

SecureRandom::getNumber()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
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 121
    public function __construct(Generator $generator = null)
54
    {
55 121
        if ($generator === null) {
56 8
            $generator = $this->getDefaultGenerator();
57 116
        } elseif (!$generator->isSupported()) {
58 4
            throw new GeneratorException('The provided secure random byte generator is not supported by the system');
59
        }
60
61 113
        $this->generator = $generator;
62 84
    }
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 8
    private function getDefaultGenerator()
70
    {
71 8
        foreach (self::$defaultGenerators as $generator) {
72 4
            $generator = new $generator();
73
74 4
            if ($generator->isSupported()) {
75 5
                return $generator;
76
            }
77 6
        }
78
79 4
        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 12
    public function getBytes($count)
89
    {
90 12
        $count = (int) $count;
91
92 12
        if ($count < 0) {
93 4
            throw new \InvalidArgumentException('Number of bytes must be 0 or more');
94 8
        } elseif ($count === 0) {
95 4
            return '';
96
        }
97
98 4
        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 49
    private function getNumber($limit)
107
    {
108 49
        if ($limit === 0) {
109 16
            return 0;
110 37
        } elseif ($this->generator instanceof NumberGenerator) {
111 5
            return $this->generator->getNumber(0, $limit);
112
        }
113
114 32
        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 32
    private function getByteNumber($limit)
123
    {
124 32
        for ($bits = 1, $mask = 1; $limit >> $bits > 0; $bits++) {
125 32
            $mask |= 1 << $bits;
126 24
        }
127
128 32
        $bytes = (int) ceil($bits / 8);
129
130
        do {
131 32
            $result = hexdec(bin2hex($this->generator->getBytes($bytes))) & $mask;
132 32
        } while ($result > $limit);
133
134 32
        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 29
    public function getInteger($min, $max)
145
    {
146 29
        $min = (int) $min;
147 29
        $max = (int) $max;
148
149 29
        if ($min < 0 || $max < $min) {
150 12
            throw new \InvalidArgumentException('Invalid minimum or maximum value');
151
        }
152
153 17
        return $min + $this->getNumber($max - $min);
154
    }
155
156
    /**
157
     * Returns a random float between 0 and 1 (inclusive).
158
     * @return float Random float between 0 and 1
159
     */
160 4
    public function getFloat()
161
    {
162 4
        return (float) ($this->getNumber(PHP_INT_MAX) / PHP_INT_MAX);
163
    }
164
165
    /**
166
     * Returns a number of randomly selected elements from the array.
167
     *
168
     * This method returns randomly selected elements from the array. The number
169
     * of elements is determined by by the second argument. The elements are
170
     * returned in random order but the keys are preserved.
171
     *
172
     * @param array $array Array of elements
173
     * @param int $count Number of elements to return from the array
174
     * @return array Randomly selected elements in random order
175
     * @throws \InvalidArgumentException If the count is invalid
176
     */
177 28
    public function getArray(array $array, $count)
178
    {
179 28
        $count = (int) $count;
180 28
        $size = count($array);
181
182 28
        if ($count < 0 || $count > $size) {
183 4
            throw new \InvalidArgumentException('Invalid number of elements');
184
        }
185
186 24
        $result = [];
187
188 24
        for ($i = 0; $i < $count; $i++) {
189 12
            $element = array_slice($array, $this->getNumber($size - $i - 1), 1, true);
190 12
            $result += $element;
191 12
            unset($array[key($element)]);
192 9
        }
193
194 24
        return $result;
195
    }
196
197
    /**
198
     * Returns one randomly selected value from the array.
199
     * @param array $array Array to choose from
200
     * @return mixed One randomly selected value from the array
201
     * @throws \InvalidArgumentException If the array is empty
202
     */
203 12
    public function choose(array $array)
204
    {
205 12
        if (count($array) < 1) {
206 4
            throw new \InvalidArgumentException('Array must have at least one value');
207
        }
208
209 8
        return current(array_slice($array, $this->getNumber(count($array) - 1), 1));
210
    }
211
212
    /**
213
     * Returns the array with the elements reordered in random order.
214
     * @param array $array Array to shuffle
215
     * @return array The provided array with elements in random order
216
     */
217 8
    public function shuffle(array $array)
218
    {
219 8
        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 24
    public function getSequence($choices, $length)
245
    {
246 24
        $length = (int) $length;
247 24
        $string = is_string($choices);
248
249 24
        if ($length < 0) {
250 4
            throw new \InvalidArgumentException('Invalid sequence length');
251
        }
252
253 20
        $result = $this->getSequenceValues(
254 20
            $string ? str_split($choices) : array_values($choices),
255
            $length
256 15
        );
257
258 16
        return $string ? implode('', $result) : $result;
259
    }
260
261
    /**
262
     * Returns the selected list of values for the sequence.
263
     * @param array $values List of possible values
264
     * @param int $length Number of values to return
265
     * @return array Selected list of values for the sequence
266
     */
267 20
    private function getSequenceValues(array $values, $length)
268
    {
269 20
        if ($length < 1) {
270 8
            return [];
271 12
        } elseif (count($values) < 1) {
272 4
            throw new \InvalidArgumentException('Cannot generate sequence from empty value set');
273
        }
274
275 8
        $size = count($values);
276 8
        $result = [];
277
278 8
        for ($i = 0; $i < $length; $i++) {
279 8
            $result[] = $values[$this->getNumber($size - 1)];
280 6
        }
281
282 8
        return $result;
283
    }
284
}
285