Skip to content

Commit

Permalink
Extract number generation from bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
Riimu committed Jul 11, 2017
1 parent afbd03a commit df7fccc
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 70 deletions.
9 changes: 8 additions & 1 deletion src/Generator/AbstractGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ abstract class AbstractGenerator implements Generator
public function getBytes($count)
{
$count = (int) $count;

if ($count === 0) {
return '';
}

$bytes = $this->readBytes($count);

if (!is_string($bytes)) {
throw new GeneratorException('The random byte generator did not return a string');
} elseif (strlen($bytes) !== $count) {
}

if (strlen($bytes) !== $count) {
throw new GeneratorException('The random byte generator returned an invalid number of bytes');
}

Expand Down
96 changes: 96 additions & 0 deletions src/Generator/ByteNumberGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Riimu\Kit\SecureRandom\Generator;

use Riimu\Kit\SecureRandom\GeneratorException;

/**
* A random number generator that wraps the given byte generator for generating integers.
*
* @author Riikka Kalliomäki <riikka.kalliomaki@gmail.com>
* @copyright Copyright (c) 2017 Riikka Kalliomäki
* @license http://opensource.org/licenses/mit-license.php MIT License
*/
class ByteNumberGenerator implements NumberGenerator
{
/** @var Generator The underlying byte generator */
private $byteGenerator;

/**
* NumberByteGenerator constructor.
* @param Generator $generator The underlying byte generator used to generate random bytes
*/
public function __construct(Generator $generator)
{
$this->byteGenerator = $generator;
}

/**
* Tells if the underlying byte generator is supported by the system.
* @return bool True if the generator is supported, false if not
*/
public function isSupported()
{
return $this->byteGenerator->isSupported();
}

/**
* Returns bytes read from the provided byte generator.
* @param int $count The number of bytes to read
* @return string A string of bytes
* @throws GeneratorException If there was an error generating the bytes
*/
public function getBytes($count)
{
return $this->byteGenerator->getBytes($count);
}

/**
* Returns a random integer between given minimum and maximum.
* @param int $min The minimum possible value to return
* @param int $max The maximum possible value to return
* @return int A random number between the lower and upper limit (inclusive)
* @throws \InvalidArgumentException If the provided values are invalid
* @throws GeneratorException If an error occurs generating the number
*/
public function getNumber($min, $max)
{
$min = (int) $min;
$max = (int) $max;

if ($min > $max) {
throw new \InvalidArgumentException('Invalid minimum and maximum value');
}

if ($min === $max) {
return $min;
}

return $min + $this->getByteNumber($max - $min);
}

/**
* Returns a random number generated using the random byte generator.
* @param int $limit Maximum value for the random number
* @return int The generated random number between 0 and the limit
* @throws GeneratorException If error occurs generating the random number
*/
private function getByteNumber($limit)
{
$bits = 1;
$mask = 1;

while ($limit >> $bits > 0) {
$mask |= 1 << $bits;
$bits++;
}

$bytes = (int) ceil($bits / 8);

do {
$result = hexdec(bin2hex($this->byteGenerator->getBytes($bytes))) & $mask;
} while ($result > $limit);

return $result;
}
}
74 changes: 20 additions & 54 deletions src/SecureRandom.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Riimu\Kit\SecureRandom;

use Riimu\Kit\SecureRandom\Generator\Generator;
use Riimu\Kit\SecureRandom\Generator\ByteNumberGenerator;
use Riimu\Kit\SecureRandom\Generator\NumberGenerator;

/**
Expand All @@ -18,15 +18,15 @@
*/
class SecureRandom
{
/** @var Generator The secure random byte generator used to generate bytes */
/** @var NumberGenerator The secure random generator used to generate bytes and numbers */
private $generator;

/** @var string[] List of default generators */
private static $defaultGenerators = [
'\Riimu\Kit\SecureRandom\Generator\Internal',
'\Riimu\Kit\SecureRandom\Generator\RandomReader',
'\Riimu\Kit\SecureRandom\Generator\Mcrypt',
'\Riimu\Kit\SecureRandom\Generator\OpenSSL',
Generator\Internal::class,
Generator\RandomReader::class,
Generator\Mcrypt::class,
Generator\OpenSSL::class,
];

/**
Expand All @@ -47,28 +47,33 @@ class SecureRandom
* think this provides enough security, create the desired random generator
* using /dev/random as the source.
*
* @param Generator|null $generator Random byte generator or null for default
* @param Generator\Generator|null $generator Random byte generator or null for default
* @throws GeneratorException If the provided or default generators are not supported
*/
public function __construct(Generator $generator = null)
public function __construct(Generator\Generator $generator = null)
{
if ($generator === null) {
$generator = $this->getDefaultGenerator();
} elseif (!$generator->isSupported()) {
throw new GeneratorException('The provided secure random byte generator is not supported by the system');
}

if (!$generator instanceof NumberGenerator) {
$generator = new ByteNumberGenerator($generator);
}

$this->generator = $generator;
}

/**
* Returns the first supported default secure random byte generator.
* @return Generator Supported secure random byte generator
* @return Generator\Generator Supported secure random byte generator
* @throws GeneratorException If none of the default generators are supported
*/
private function getDefaultGenerator()
{
foreach (self::$defaultGenerators as $generator) {
/** @var Generator\Generator $generator */
$generator = new $generator();

if ($generator->isSupported()) {
Expand All @@ -91,49 +96,11 @@ public function getBytes($count)

if ($count < 0) {
throw new \InvalidArgumentException('Number of bytes must be 0 or more');
} elseif ($count === 0) {
return '';
}

return $this->generator->getBytes($count);
}

/**
* Returns a random number between 0 and the limit.
* @param int $limit Maximum random number
* @return int Random number between 0 and the limit
*/
private function getNumber($limit)
{
if ($limit === 0) {
return 0;
} elseif ($this->generator instanceof NumberGenerator) {
return $this->generator->getNumber(0, $limit);
}

return $this->getByteNumber($limit);
}

/**
* Returns a random number generated using the random byte generator.
* @param int $limit Maximum value for the random number
* @return int The generated random number between 0 and the limit
*/
private function getByteNumber($limit)
{
for ($bits = 1, $mask = 1; $limit >> $bits > 0; $bits++) {
$mask |= 1 << $bits;
}

$bytes = (int) ceil($bits / 8);

do {
$result = hexdec(bin2hex($this->generator->getBytes($bytes))) & $mask;
} while ($result > $limit);

return $result;
}

/**
* Returns a random integer between two positive integers (inclusive).
* @param int $min Minimum limit
Expand All @@ -150,7 +117,7 @@ public function getInteger($min, $max)
throw new \InvalidArgumentException('Invalid minimum or maximum value');
}

return $min + $this->getNumber($max - $min);
return $this->generator->getNumber($min, $max);
}

/**
Expand Down Expand Up @@ -189,7 +156,7 @@ public function getRandom()
*/
public function getFloat()
{
return (float) ($this->getNumber(PHP_INT_MAX) / PHP_INT_MAX);
return (float) ($this->generator->getNumber(0, PHP_INT_MAX) / PHP_INT_MAX);
}

/**
Expand Down Expand Up @@ -217,10 +184,9 @@ public function getArray(array $array, $count)
$keys = array_keys($array);

for ($i = 0; $i < $count; $i++) {
$last = $size - $i - 1;
$index = $this->getNumber($last);
$index = $this->generator->getNumber($i, $size - 1);
$result[$keys[$index]] = $array[$keys[$index]];
$keys[$index] = $keys[$last];
$keys[$index] = $keys[$i];
}

return $result;
Expand All @@ -238,7 +204,7 @@ public function choose(array $array)
throw new \InvalidArgumentException('Array must have at least one value');
}

$result = array_slice($array, $this->getNumber(count($array) - 1), 1);
$result = array_slice($array, $this->generator->getNumber(0, count($array) - 1), 1);

return current($result);
}
Expand Down Expand Up @@ -311,7 +277,7 @@ private function getSequenceValues(array $values, $length)
$result = [];

for ($i = 0; $i < $length; $i++) {
$result[] = $values[$this->getNumber($size - 1)];
$result[] = $values[$this->generator->getNumber(0, $size - 1)];
}

return $result;
Expand Down
47 changes: 36 additions & 11 deletions tests/tests/GeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

use PHPUnit\Framework\TestCase;
use Riimu\Kit\SecureRandom\Generator\AbstractGenerator;
use Riimu\Kit\SecureRandom\Generator\ByteNumberGenerator;
use Riimu\Kit\SecureRandom\Generator\Generator;
use Riimu\Kit\SecureRandom\Generator\Internal;
use Riimu\Kit\SecureRandom\Generator\Mcrypt;
use Riimu\Kit\SecureRandom\Generator\OpenSSL;
use Riimu\Kit\SecureRandom\Generator\RandomReader;

/**
Expand Down Expand Up @@ -40,7 +44,7 @@ public function testInvalidNumberOfBytes()

public function testRandomReader()
{
$this->assertGeneratorWorks(new Generator\RandomReader(true));
$this->assertGeneratorWorks(new RandomReader(true));
}

public function testRandomReaderShutdown()
Expand All @@ -63,44 +67,47 @@ public function testRandomReaderShutdown()

public function testBlockingRandomReader()
{
$this->assertGeneratorWorks(new Generator\RandomReader(false));
$this->assertGeneratorWorks(new RandomReader(false));
}

public function testMcrypt()
{
$this->assertGeneratorWorks(new Generator\Mcrypt(true));
$this->assertGeneratorWorks(new Mcrypt(true));
}

public function testBlockingMcrypt()
{
$this->assertGeneratorWorks(new Generator\Mcrypt(false));
$this->assertGeneratorWorks(new Mcrypt(false));
}

public function testOpenSSL()
{
$this->assertGeneratorWorks(new Generator\OpenSSL());
$this->assertGeneratorWorks(new OpenSSL());
}

public function testOpenSSLFail()
{
$generator = new Generator\OpenSSL();
$generator = new OpenSSL();

if (!$generator->isSupported()) {
$this->markTestSkipped('Support for ' . get_class($generator) . ' is missing');
}

$method = new \ReflectionMethod($generator, 'readBytes');
$method->setAccessible(true);

$this->expectException(GeneratorException::class);
$generator->getBytes(0);
$method->invoke($generator, 0);
}

public function testInternal()
{
$this->assertGeneratorWorks(new Generator\Internal());
$this->assertGeneratorWorks(new Internal());
}

public function testInternalNumberGenerator()
{
$generator = new Generator\Internal();
$generator = new Internal();

if (!$generator->isSupported()) {
$this->markTestSkipped('Support for ' . get_class($generator) . ' is missing');
Expand All @@ -112,7 +119,7 @@ public function testInternalNumberGenerator()

public function testInternalFail()
{
$generator = new Generator\Internal();
$generator = new Internal();

if (!$generator->isSupported()) {
$this->markTestSkipped('Support for ' . get_class($generator) . ' is missing');
Expand All @@ -122,7 +129,25 @@ public function testInternalFail()
$generator->getNumber(10, 0);
}

public function assertGeneratorWorks(Generator\Generator $generator)
public function testByteNumberGeneratorInvalidLimits()
{
$generator = new ByteNumberGenerator($this->createMock(Generator::class));

$this->expectException(\InvalidArgumentException::class);
$generator->getNumber(1, 0);
}

public function testByteNumberGeneratorSupportPass()
{
$generator = $this->createMock(Generator::class);
$generator->expects($this->once())->method('isSupported')->willReturn(true);

$byteNumberGenerator = new ByteNumberGenerator($generator);

$this->assertTrue($byteNumberGenerator->isSupported());
}

public function assertGeneratorWorks(Generator $generator)
{
if (!$generator->isSupported()) {
$this->markTestSkipped('Support for ' . get_class($generator) . ' is missing');
Expand Down

0 comments on commit df7fccc

Please sign in to comment.