*/ 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)); } }