123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- <?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;
- } elseif (!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();
- } elseif (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);
- } elseif (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:jms-secure-random".');
- }
-
- $this->seedLastUpdatedAt = new \DateTime($stmt->fetchColumn(1));
- }
- }
|