Skip to content
Browse files

added secure random number generator

  • Loading branch information...
1 parent 2e40b0a commit 1595e93e1ed4dc97da7077d9cda9fec6b0acfa2b @schmittjoh committed Feb 18, 2012
View
17 CHANGELOG-1.1.md → CHANGELOG.md
@@ -1,4 +1,13 @@
-This document details all changes from JMSSecurityExtraBundle 1.0.x to 1.1:
+This document details all changes between different versions of JMSSecurityExtraBundle:
+
+1.2
+---
+
+- added a secure random number generator service
+
+
+1.1
+---
- The configuration option "secure_controllers" has been removed. This setting is
now automatically enabled, but it requires the JMSDiExtraBundle.
@@ -35,4 +44,8 @@ This document details all changes from JMSSecurityExtraBundle 1.0.x to 1.1:
- The "is_expr_granted" Twig function has been added if you want to check an
expression from a Twig template.
-
+
+1.0
+---
+
+Initial release
View
12 DependencyInjection/Configuration.php
@@ -55,6 +55,18 @@ public function getConfigTreeBuilder()
->useAttributeAsKey('pattern')
->prototype('scalar')->isRequired()->cannotBeEmpty()->end()
->end()
+ ->arrayNode('util')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->arrayNode('secure_random')
+ ->children()
+ ->scalarNode('connection')->cannotBeEmpty()->end()
+ ->scalarNode('table_name')->defaultValue('seed_table')->cannotBeEmpty()->end()
+ ->scalarNode('seed_provider')->cannotBeEmpty()->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
->end()
->end()
;
View
30 DependencyInjection/JMSSecurityExtraExtension.php
@@ -96,5 +96,35 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter('security.access.method_access_control',
$config['method_access_control']);
}
+
+ if (isset($config['util']['secure_random'])) {
+ $this->configureSecureRandom($config['util']['secure_random']);
+ }
+ }
+
+ private function configureSecureRandom(array $config, ContainerBuilder $container)
+ {
+ if (isset($config['seed_provider'])) {
+ $container
+ ->getDefinition('security.util.secure_random')
+ ->addMethodCall('setSeedProvider', array(new Reference($config['seed_provider'])))
+ ;
+ $container->setAlias('security.util.secure_random_seed_provider', $config['seed_provider']);
+ } else if (isset($config['connection'])) {
+ $container
+ ->getDefinition('security.util.secure_random')
+ ->addMethodCall('setConnection', array(new Reference($this->getDoctrineConnectionId($config['connection'])), $config['table_name']))
+ ;
+ $container->setAlias('security.util.secure_random_connection', $this->getDoctrineConnectionId($config['connection']));
+ $container->setParameter('security.util.secure_random_table', $config['table_name']);
+ $container
+ ->getDefinition('security.util.secure_random_schema_listener')
+ ->addTag('doctrine.event_listener', array('connection' => $config['connection'], 'event' => 'postGenerateSchema', 'lazy' => true))
+ ;
+ $container
+ ->getDefinition('security.util.secure_random_schema')
+ ->replaceArgument(0, $config['table_name'])
+ ;
+ }
}
}
View
15 Resources/config/services.xml
@@ -25,6 +25,9 @@
<parameter key="security.extra.driver_chain.class">Metadata\Driver\DriverChain</parameter>
<parameter key="security.extra.annotation_driver.class">JMS\SecurityExtraBundle\Metadata\Driver\AnnotationDriver</parameter>
<parameter key="security.extra.file_cache.class">Metadata\Cache\FileCache</parameter>
+
+ <parameter key="security.util.secure_random_schema.class">JMS\SecurityExtraBundle\Security\Util\SecureRandomSchema</parameter>
+ <parameter key="security.util.secure_random_schema_listener.class">JMS\SecurityExtraBundle\Security\EventListener\SecureRandomSchemaListener</parameter>
</parameters>
<services>
@@ -94,6 +97,18 @@
<argument>true</argument>
</call>
</service>
+
+ <!-- Utilities -->
+ <service id="security.util.secure_random" class="JMS\SecurityExtraBundle\Security\Util\SecureRandom">
+ <tag name="monolog.logger" channel="security" />
+ <argument type="service" id="logger" />
+ </service>
+ <service id="security.util.secure_random_schema" class="%security.util.secure_random_schema.class%">
+ <argument></argument><!-- Table Name -->
+ </service>
+ <service id="security.util.secure_random_schema_listener" class="%security.util.secure_random_schema_listener.class%" public="false">
+ <argument type="service" id="security.util.secure_random_schema" />
+ </service>
</services>
</container>
View
22 Security/Util/NullSeedProvider.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Security\Util;
+
+/**
+ * NullSeedProvider implementation.
+ *
+ * Never use this for anything but unit testing.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class NullSeedProvider implements SeedProviderInterface
+{
+ public function loadSeed()
+ {
+ return array('', new \DateTime());
+ }
+
+ public function updateSeed($seed)
+ {
+ }
+}
View
136 Security/Util/SecureRandom.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Security\Util;
+
+use Doctrine\DBAL\Types\Type;
+use Doctrine\DBAL\Connection;
+use Symfony\Component\HttpKernel\Log\LoggerInterface;
+
+/**
+ * A secure random number generator implementation.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class SecureRandom
+{
+ private $logger;
+ private $useOpenSsl;
+ private $con;
+ private $seed;
+ private $seedTableName;
+ private $seedUpdated;
+ private $seedLastUpdatedAt;
+ private $seedProvider;
+
+ public function __construct(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+
+ // determine whether to use OpenSSL
+ if (0 === stripos(PHP_OS, 'win')) {
+ $this->useOpenSsl = false;
+ } else if (!function_exists('openssl_random_pseudo_bytes')) {
+ $this->logger->notice('It is recommended that you enable the "openssl" extension for random number generation.');
+ $this->useOpenSsl = false;
+ } else {
+ $this->useOpenSsl = true;
+ }
+ }
+
+ /**
+ * Sets the Doctrine seed provider.
+ *
+ * @param Connection $con
+ * @param string $tableName
+ */
+ public function setConnection(Connection $con, $tableName)
+ {
+ $this->con = $con;
+ $this->seedTableName = $tableName;
+ }
+
+ /**
+ * Sets a custom seed provider implementation.
+ *
+ * Be aware that a guessable seed will severely compromise the PRNG
+ * algorithm that is employed.
+ *
+ * @param SeedProviderInterface $provider
+ */
+ public function setSeedProvider(SeedProviderInterface $provider)
+ {
+ $this->seedProvider = $provider;
+ }
+
+ /**
+ * Generates the specified number of secure random bytes.
+ *
+ * @param integer $nbBytes
+ * @return string
+ */
+ public function nextBytes($nbBytes)
+ {
+ // try OpenSSL
+ if ($this->useOpenSsl) {
+ $strong = false;
+ $bytes = openssl_random_pseudo_bytes($nbBytes, $strong);
+
+ if (false !== $bytes && true === $strong) {
+ return $bytes;
+ }
+
+ $this->logger->info('OpenSSL did not produce a secure random number.');
+ }
+
+ // initialize seed
+ if (null === $this->seed) {
+ if (null !== $this->seedProvider) {
+ list($this->seed, $this->seedLastUpdatedAt) = $this->seedProvider->loadSeed();
+ } else if (null !== $this->con) {
+ $this->initializeSeedFromDatabase();
+ } else {
+ throw new \RuntimeException('You need to either specify a database connection, or a custom seed provider.');
+ }
+ }
+
+ $bytes = '';
+ while (strlen($bytes) < $nbBytes) {
+ static $incr = 1;
+ $bytes .= hash('sha512', $incr++.$this->seed.uniqid(mt_rand(), true).$nbBytes, true);
+ $this->seed = base64_encode(hash('sha512', $this->seed.$bytes.$nbBytes, true));
+
+ if (!$this->seedUpdated && $this->seedLastUpdatedAt->getTimestamp() < time() - mt_rand(1, 10)) {
+ if (null !== $this->seedProvider) {
+ $this->seedProvider->updateSeed($this->seed);
+ } else if (null !== $this->con) {
+ $this->saveSeedToDatabase();
+ }
+
+ $this->seedUpdated = true;
+ }
+ }
+
+ return substr($bytes, 0, $nbBytes);
+ }
+
+ private function saveSeedToDatabase()
+ {
+ $this->con->executeQuery("UPDATE {$this->seedTableName} SET seed = :seed, updated_at = :updatedAt", array(
+ ':seed' => $this->seed,
+ ':updatedAt' => new \DateTime(),
+ ), array(
+ ':updatedAt' => Type::DATETIME,
+ ));
+ }
+
+ private function initializeSeedFromDatabase()
+ {
+ $stmt = $this->con->executeQuery("SELECT seed, updated_at FROM {$this->seedTableName}");
+
+ if (false === $this->seed = $stmt->fetchColumn(0)) {
+ throw new \RuntimeException('You need to initialize the generator by running the console command "init:secure-random".');
+ }
+
+ $this->seedLastUpdatedAt = new \DateTime($stmt->fetchColumn(1));
+ }
+}
View
35 Security/Util/SecureRandomSchema.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Security\Util;
+
+use Doctrine\DBAL\Schema\Schema;
+
+/**
+ * The DBAL schema that will be used if you choose the database-based
+ * seed provider.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class SecureRandomSchema extends Schema
+{
+ public function __construct($tableName)
+ {
+ parent::__construct();
+
+ $table = $this->createTable($tableName);
+ $table->addColumn('seed', 'string', array(
+ 'length' => 88,
+ 'not_null' => true,
+ ));
+ $table->addColumn('updated_at', 'datetime', array(
+ 'not_null' => true,
+ ));
+ }
+
+ public function addToSchema(Schema $schema)
+ {
+ foreach ($this->getTables() as $table) {
+ $schema->_addTable($table);
+ }
+ }
+}
View
28 Security/Util/SeedProviderInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Security\Util;
+
+/**
+ * Seed Provider Interface.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+interface SeedProviderInterface
+{
+ /**
+ * Loads the initial seed.
+ *
+ * Whatever is returned from this method, it should not be guessable.
+ *
+ * @return array of the format array(string, DateTime) where string is the
+ * initial seed, and DateTime is the last time it was updated
+ */
+ function loadSeed();
+
+ /**
+ * Updates the seed.
+ *
+ * @param string $seed
+ */
+ function updateSeed($seed);
+}
View
36 Security/Util/String.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Security\Util;
+
+/**
+ * String utility functions.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class String
+{
+ private final function __construct() {}
+
+ /**
+ * Whether two strings are equal.
+ *
+ * This function uses a constant-time algorithm to compare the strings.
+ *
+ * @param string $str1
+ * @param string $str2
+ * @return Boolean
+ */
+ public static function equals($str1, $str2)
+ {
+ if (strlen($str1) !== $c = strlen($str2)) {
+ return false;
+ }
+
+ $result = 0;
+ for ($i=0; $i<$c; $i++) {
+ $result |= ord($str1[$i]) ^ ord($str2[$i]);
+ }
+
+ return 0 === $result;
+ }
+}
View
6 Tests/Functional/AppKernel.php
@@ -27,10 +27,8 @@
}
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle;
-
use JMS\SecurityExtraBundle\Tests\Functional\TestBundle\TestBundle;
-
-use Symfony\Component\HttpKernel\Util\Filesystem;
+use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel;
@@ -59,7 +57,7 @@ public function registerBundles()
return array(
new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new \Symfony\Bundle\SecurityBundle\SecurityBundle(),
- new \Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
+ new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new \Symfony\Bundle\TwigBundle\TwigBundle(),
new TestBundle(),
new FormLoginBundle(),
View
2 Tests/Functional/BaseTestCase.php
@@ -2,7 +2,7 @@
namespace JMS\SecurityExtraBundle\Tests\Functional;
-use Symfony\Component\HttpKernel\Util\Filesystem;
+use Symfony\Component\Filesystem\Filesystem;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
View
6 Tests/Functional/MethodAccessControlTest.php
@@ -49,21 +49,25 @@ public function testAcl()
$this->importDatabaseSchema();
$this->login($client);
+ var_dump('ADDING POST');
$client->request('POST', '/post/add', array('title' => 'Foo'));
+ var_dump('POST ADDED');
$response = $client->getResponse();
$this->assertEquals('/post/edit/1', $response->headers->get('Location'),
substr($response, 0, 2000));
+ var_dump('EDITING POST');
$client->request('GET', '/post/edit/1');
+ var_dump('POST EDITED');
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode(), substr($response, 0, 2000));
$this->assertEquals('Foo', $response->getContent());
}
public function testRoleHierarchyIsRespected()
{
- $client = $this->createClient(array('config' => 'all_voters_disabled.yml'));
+ $client = $this->createClient(array('config' => 'all_voters_disabled.yml'));
$client->insulate();
$this->login($client);
View
5 Tests/Functional/config/security.yml
@@ -1,8 +1,9 @@
security:
providers:
in_memory:
- users:
- johannes: { password: test, roles: [ROLE_FOO] }
+ memory:
+ users:
+ johannes: { password: test, roles: [ROLE_FOO] }
role_hierarchy:
ROLE_FOO: [ROLE_BAR]
View
2 Tests/Security/Authorization/Expression/ExpressionVoterTest.php
@@ -8,7 +8,7 @@
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use JMS\SecurityExtraBundle\Security\Authorization\Expression\Expression;
-use Symfony\Component\HttpKernel\Util\Filesystem;
+use Symfony\Component\Filesystem\Filesystem;
use JMS\SecurityExtraBundle\Security\Authorization\Expression\ExpressionVoter;
class ExpressionVoterTest extends \PHPUnit_Framework_TestCase
View
208 Tests/Security/Util/SecureRandomTest.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Tests\Security\Util;
+
+use JMS\SecurityExtraBundle\Security\Util\NullSeedProvider;
+use Symfony\Component\HttpKernel\Log\NullLogger;
+use JMS\SecurityExtraBundle\Security\Util\SecureRandomSchema;
+use Doctrine\DBAL\DriverManager;
+use Doctrine\DBAL\Connection;
+use JMS\SecurityExtraBundle\Security\Util\SecureRandom;
+
+class SecureRandomTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * T1: Monobit test
+ *
+ * @dataProvider getPrngs
+ */
+ public function testMonobit($prng)
+ {
+ $nbOnBits = substr_count($this->getBitSequence($prng, 20000), '1');
+ $this->assertTrue($nbOnBits > 9654 && $nbOnBits < 10346, 'Monobit test failed, number of turned on bits: '.$nbOnBits);
+ }
+
+ /**
+ * T2: Chi-square test with 15 degrees of freedom (chi-Quadrat-Anpassungstest)
+ *
+ * @dataProvider getPrngs
+ */
+ public function testPoker($prng)
+ {
+ $b = $this->getBitSequence($prng, 20000);
+ $c = array();
+ for ($i=0;$i<=15;$i++) {
+ $c[$i] = 0;
+ }
+
+ for ($j=1; $j<=5000; $j++) {
+ $k = 4 * $j - 1;
+ $c[8 * $b[$k - 3] + 4 * $b[$k - 2] + 2 * $b[$k - 1] + $b[$k]] += 1;
+ }
+
+ $f = 0;
+ for ($i=0; $i<= 15; $i++) {
+ $f += $c[$i] * $c[$i];
+ }
+
+ $Y = 16/5000 * $f - 5000;
+
+ $this->assertTrue($Y > 1.03 && $Y < 57.4, 'Poker test failed, Y = '.$Y);
+ }
+
+ /**
+ * Run test
+ *
+ * @dataProvider getPrngs
+ */
+ public function testRun($prng)
+ {
+ $b = $this->getBitSequence($prng, 20000);
+
+ $runs = array();
+ for ($i=1; $i<=6; $i++) {
+ $runs[$i] = 0;
+ }
+
+ $addRun = function($run) use (&$runs) {
+ if ($run > 6) {
+ $run = 6;
+ }
+
+ $runs[$run] += 1;
+ };
+
+ $currentRun = 0;
+ $lastBit = null;
+ for ($i=0; $i<20000; $i++) {
+ if ($lastBit === $b[$i]) {
+ $currentRun += 1;
+ } else {
+ if ($currentRun > 0) {
+ $addRun($currentRun);
+ }
+
+ $lastBit = $b[$i];
+ $currentRun = 0;
+ }
+ }
+ if ($currentRun > 0) {
+ $addRun($currentRun);
+ }
+
+ $this->assertTrue($runs[1] > 2267 && $runs[1] < 2733, 'Runs of length 1 outside of defined interval: '.$runs[1]);
+ $this->assertTrue($runs[2] > 1079 && $runs[2] < 1421, 'Runs of length 2 outside of defined interval: '.$runs[2]);
+ $this->assertTrue($runs[3] > 502 && $runs[3] < 748, 'Runs of length 3 outside of defined interval: '.$runs[3]);
+ $this->assertTrue($runs[4] > 233 && $runs[4] < 402, 'Runs of length 4 outside of defined interval: '.$runs[4]);
+ $this->assertTrue($runs[5] > 90 && $runs[5] < 223, 'Runs of length 5 outside of defined interval: '.$runs[5]);
+ $this->assertTrue($runs[6] > 90 && $runs[6] < 233, 'Runs of length 6 outside of defined interval: '.$runs[6]);
+ }
+
+ /**
+ * Long-run test
+ *
+ * @dataProvider getPrngs
+ */
+ public function testLongRun($prng)
+ {
+ $b = $this->getBitSequence($prng, 20000);
+
+ $longestRun = 0;
+ $currentRun = $lastBit = null;
+ for ($i=0;$i<20000;$i++) {
+ if ($lastBit === $b[$i]) {
+ $currentRun += 1;
+ } else {
+ if ($currentRun > $longestRun) {
+ $longestRun = $currentRun;
+ }
+ $lastBit = $b[$i];
+ $currentRun = 0;
+ }
+ }
+ if ($currentRun > $longestRun) {
+ $longestRun = $currentRun;
+ }
+
+ $this->assertTrue($longestRun < 34, 'Failed longest run test: '.$longestRun);
+ }
+
+ /**
+ * Serial Correlation (Autokorrelationstest)
+ *
+ * @dataProvider getPrngs
+ */
+ public function testSerialCorrelation($prng)
+ {
+ $shift = rand(1, 5000);
+ $b = $this->getBitSequence($prng, 20000);
+
+ $Z = 0;
+ for ($i=0; $i<5000; $i++) {
+ $Z += $b[$i] === $b[$i+$shift] ? 1 : 0;
+ }
+
+ $this->assertTrue($Z > 2326 && $Z < 2674, 'Failed serial correlation test: '.$Z);
+ }
+
+ public function getPrngs()
+ {
+ $prngs = array();
+ $logger = new NullLogger();
+
+ // openssl with fallback
+ $prng = new SecureRandom($logger);
+ $prng->setSeedProvider(new NullSeedProvider());
+ $prngs[] = array($prng);
+
+ // no-openssl with database seed provider
+ if (class_exists('Doctrine\DBAL\DriverManager')) {
+ $prng = new SecureRandom($logger);
+ $con = DriverManager::getConnection(array(
+ 'driver' => 'pdo_sqlite',
+ 'memory' => true
+ ));
+
+ $schema = new SecureRandomSchema('seed_table');
+ foreach ($schema->toSql($con->getDatabasePlatform()) as $sql) {
+ $con->executeQuery($sql);
+ }
+ $con->executeQuery("INSERT INTO seed_table VALUES (:seed, :updatedAt)", array(
+ ':seed' => base64_encode(hash('sha512', uniqid(mt_rand(), true), true)),
+ ':updatedAt' => date('Y-m-d H:i:s'),
+ ));
+
+ $prng->setConnection($con, 'seed_table');
+ $this->disableOpenSsl($prng);
+
+ $prngs[] = array($prng);
+ }
+
+ // no-openssl with custom seed provider
+ $prng = new SecureRandom($logger);
+ $prng->setSeedProvider(new NullSeedProvider());
+ $this->disableOpenSsl($prng);
+ $prngs[] = array($prng);
+
+ return $prngs;
+ }
+
+ private function disableOpenSsl($prng)
+ {
+ $ref = new \ReflectionProperty($prng, 'useOpenSsl');
+ $ref->setAccessible(true);
+ $ref->setValue($prng, false);
+ }
+
+ private function getBitSequence($prng, $length)
+ {
+ $bitSequence = '';
+ for ($i=0;$i<$length; $i+=40) {
+ $value = unpack('H*', $prng->nextBytes(5));
+ $value = str_pad(base_convert($value[1], 16, 2), 40, '0', STR_PAD_LEFT);
+ $bitSequence .= $value;
+ }
+
+ return substr($bitSequence, 0, $length);
+ }
+}
View
14 Tests/Security/Util/StringTest.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace JMS\SecurityExtraBundle\Tests\Security\Util;
+
+use JMS\SecurityExtraBundle\Security\Util\String;
+
+class StringTest extends \PHPUnit_Framework_TestCase
+{
+ public function testEquals()
+ {
+ $this->assertTrue(String::equals('password', 'password'));
+ $this->assertFalse(String::equals('password', 'foo'));
+ }
+}

0 comments on commit 1595e93

Please sign in to comment.
Something went wrong with that request. Please try again.