Skip to content

Commit

Permalink
added secure random number generator
Browse files Browse the repository at this point in the history
  • Loading branch information
schmittjoh committed Feb 18, 2012
1 parent 2e40b0a commit 1595e93
Show file tree
Hide file tree
Showing 16 changed files with 563 additions and 11 deletions.
17 changes: 15 additions & 2 deletions 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.
Expand Down Expand Up @@ -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
12 changes: 12 additions & 0 deletions DependencyInjection/Configuration.php
Expand Up @@ -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()
;
Expand Down
30 changes: 30 additions & 0 deletions DependencyInjection/JMSSecurityExtraExtension.php
Expand Up @@ -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'])
;
}
}
}
15 changes: 15 additions & 0 deletions Resources/config/services.xml
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
22 changes: 22 additions & 0 deletions 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)
{
}
}
136 changes: 136 additions & 0 deletions 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));
}
}
35 changes: 35 additions & 0 deletions 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);
}
}
}
28 changes: 28 additions & 0 deletions 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);
}
36 changes: 36 additions & 0 deletions 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;
}
}
6 changes: 2 additions & 4 deletions Tests/Functional/AppKernel.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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(),
Expand Down

0 comments on commit 1595e93

Please sign in to comment.