SecureRandom.php 4.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <?php
  2. namespace JMS\SecurityExtraBundle\Security\Util;
  3. use Doctrine\DBAL\Types\Type;
  4. use Doctrine\DBAL\Connection;
  5. use Symfony\Component\HttpKernel\Log\LoggerInterface;
  6. /**
  7. * A secure random number generator implementation.
  8. *
  9. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  10. */
  11. final class SecureRandom
  12. {
  13. private $logger;
  14. private $useOpenSsl;
  15. private $con;
  16. private $seed;
  17. private $seedTableName;
  18. private $seedUpdated;
  19. private $seedLastUpdatedAt;
  20. private $seedProvider;
  21. public function __construct(LoggerInterface $logger)
  22. {
  23. $this->logger = $logger;
  24. // determine whether to use OpenSSL
  25. if (0 === stripos(PHP_OS, 'win')) {
  26. $this->useOpenSsl = false;
  27. } elseif (!function_exists('openssl_random_pseudo_bytes')) {
  28. $this->logger->notice('It is recommended that you enable the "openssl" extension for random number generation.');
  29. $this->useOpenSsl = false;
  30. } else {
  31. $this->useOpenSsl = true;
  32. }
  33. }
  34. /**
  35. * Sets the Doctrine seed provider.
  36. *
  37. * @param Connection $con
  38. * @param string $tableName
  39. */
  40. public function setConnection(Connection $con, $tableName)
  41. {
  42. $this->con = $con;
  43. $this->seedTableName = $tableName;
  44. }
  45. /**
  46. * Sets a custom seed provider implementation.
  47. *
  48. * Be aware that a guessable seed will severely compromise the PRNG
  49. * algorithm that is employed.
  50. *
  51. * @param SeedProviderInterface $provider
  52. */
  53. public function setSeedProvider(SeedProviderInterface $provider)
  54. {
  55. $this->seedProvider = $provider;
  56. }
  57. /**
  58. * Generates the specified number of secure random bytes.
  59. *
  60. * @param integer $nbBytes
  61. * @return string
  62. */
  63. public function nextBytes($nbBytes)
  64. {
  65. // try OpenSSL
  66. if ($this->useOpenSsl) {
  67. $strong = false;
  68. $bytes = openssl_random_pseudo_bytes($nbBytes, $strong);
  69. if (false !== $bytes && true === $strong) {
  70. return $bytes;
  71. }
  72. $this->logger->info('OpenSSL did not produce a secure random number.');
  73. }
  74. // initialize seed
  75. if (null === $this->seed) {
  76. if (null !== $this->seedProvider) {
  77. list($this->seed, $this->seedLastUpdatedAt) = $this->seedProvider->loadSeed();
  78. } elseif (null !== $this->con) {
  79. $this->initializeSeedFromDatabase();
  80. } else {
  81. throw new \RuntimeException('You need to either specify a database connection, or a custom seed provider.');
  82. }
  83. }
  84. $bytes = '';
  85. while (strlen($bytes) < $nbBytes) {
  86. static $incr = 1;
  87. $bytes .= hash('sha512', $incr++.$this->seed.uniqid(mt_rand(), true).$nbBytes, true);
  88. $this->seed = base64_encode(hash('sha512', $this->seed.$bytes.$nbBytes, true));
  89. if (!$this->seedUpdated && $this->seedLastUpdatedAt->getTimestamp() < time() - mt_rand(1, 10)) {
  90. if (null !== $this->seedProvider) {
  91. $this->seedProvider->updateSeed($this->seed);
  92. } elseif (null !== $this->con) {
  93. $this->saveSeedToDatabase();
  94. }
  95. $this->seedUpdated = true;
  96. }
  97. }
  98. return substr($bytes, 0, $nbBytes);
  99. }
  100. private function saveSeedToDatabase()
  101. {
  102. $this->con->executeQuery("UPDATE {$this->seedTableName} SET seed = :seed, updated_at = :updatedAt", array(
  103. ':seed' => $this->seed,
  104. ':updatedAt' => new \DateTime(),
  105. ), array(
  106. ':updatedAt' => Type::DATETIME,
  107. ));
  108. }
  109. private function initializeSeedFromDatabase()
  110. {
  111. $stmt = $this->con->executeQuery("SELECT seed, updated_at FROM {$this->seedTableName}");
  112. if (false === $this->seed = $stmt->fetchColumn(0)) {
  113. throw new \RuntimeException('You need to initialize the generator by running the console command "init:jms-secure-random".');
  114. }
  115. $this->seedLastUpdatedAt = new \DateTime($stmt->fetchColumn(1));
  116. }
  117. }