diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
index 6f9257a42a46..536b40cca67b 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
@@ -138,5 +138,12 @@
+
+
+
+
+ %kernel.cache_dir%/secure_random.seed
+
+
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml
index 9beab1df0ea9..e508a743c51e 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml
@@ -45,6 +45,12 @@
class="%security.authentication.rememberme.services.persistent.class%"
parent="security.authentication.rememberme.services.abstract"
abstract="true">
+
+
+
+
+
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Util;
+
+use Symfony\Component\HttpKernel\Log\LoggerInterface;
+
+/**
+ * A secure random number generator implementation.
+ *
+ * @author Fabien Potencier
+ * @author Johannes M. Schmitt
+ */
+final class SecureRandom implements SecureRandomInterface
+{
+ private $logger;
+ private $useOpenSsl;
+ private $seed;
+ private $seedUpdated;
+ private $seedLastUpdatedAt;
+ private $seedFile;
+
+ /**
+ * Constructor.
+ *
+ * Be aware that a guessable seed will severely compromise the PRNG
+ * algorithm that is employed.
+ *
+ * @param string $seedFile
+ * @param LoggerInterface $logger
+ */
+ public function __construct($seedFile = null, LoggerInterface $logger = null)
+ {
+ $this->seedFile = $seedFile;
+ $this->logger = $logger;
+
+ // determine whether to use OpenSSL
+ if (defined('PHP_WINDOWS_VERSION_BUILD') && version_compare(PHP_VERSION, '5.3.4', '<')) {
+ $this->useOpenSsl = false;
+ } elseif (!function_exists('openssl_random_pseudo_bytes')) {
+ if (null !== $this->logger) {
+ $this->logger->notice('It is recommended that you enable the "openssl" extension for random number generation.');
+ }
+ $this->useOpenSsl = false;
+ } else {
+ $this->useOpenSsl = true;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function nextBytes($nbBytes)
+ {
+ // try OpenSSL
+ if ($this->useOpenSsl) {
+ $bytes = openssl_random_pseudo_bytes($nbBytes, $strong);
+
+ if (false !== $bytes && true === $strong) {
+ return $bytes;
+ }
+
+ if (null !== $this->logger) {
+ $this->logger->info('OpenSSL did not produce a secure random number.');
+ }
+ }
+
+ // initialize seed
+ if (null === $this->seed) {
+ if (null === $this->seedFile) {
+ throw new \RuntimeException('You need to specify a file path to store the seed.');
+ }
+
+ if (is_file($this->seedFile)) {
+ list($this->seed, $this->seedLastUpdatedAt) = $this->readSeed();
+ } else {
+ $this->seed = uniqid(mt_rand(), true);
+ $this->updateSeed();
+ }
+ }
+
+ $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));
+ $this->updateSeed();
+ }
+
+ return substr($bytes, 0, $nbBytes);
+ }
+
+ private function readSeed()
+ {
+ return json_decode(file_get_contents($this->seedFile));
+ }
+
+ private function updateSeed()
+ {
+ if (!$this->seedUpdated && $this->seedLastUpdatedAt < time() - mt_rand(1, 10)) {
+ file_put_contents($this->seedFile, json_encode(array($this->seed, microtime(true))));
+ }
+
+ $this->seedUpdated = true;
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/Util/SecureRandomInterface.php b/src/Symfony/Component/Security/Core/Util/SecureRandomInterface.php
new file mode 100644
index 000000000000..64830a9d1f2d
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Util/SecureRandomInterface.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Util;
+
+use Symfony\Component\HttpKernel\Log\LoggerInterface;
+
+/**
+ * Interface that needs to be implemented by all secure random number generators.
+ *
+ * @author Fabien Potencier
+ */
+interface SecureRandomInterface
+{
+ /**
+ * Generates the specified number of secure random bytes.
+ *
+ * @param integer $nbBytes
+ *
+ * @return string
+ */
+ public function nextBytes($nbBytes);
+}
diff --git a/src/Symfony/Component/Security/Core/Util/StringUtils.php b/src/Symfony/Component/Security/Core/Util/StringUtils.php
new file mode 100644
index 000000000000..a73b4715b3f4
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Util/StringUtils.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Util;
+
+/**
+ * String utility functions.
+ *
+ * @author Fabien Potencier
+ */
+final class StringUtils
+{
+ final private function __construct()
+ {
+ }
+
+ /**
+ * Compares two strings.
+ *
+ * This method implements a constant-time algorithm to compare strings.
+ *
+ * @param string $str1 The first string
+ * @param string $str2 The second string
+ *
+ * @return Boolean true if the two strings are the same, false otherwise
+ */
+ 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;
+ }
+}
diff --git a/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
index 8944672f29b0..dbb64299112e 100644
--- a/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
+++ b/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
@@ -19,6 +19,7 @@
use Symfony\Component\Security\Core\Exception\CookieTheftException;
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Util\SecureRandomInterface;
/**
* Concrete implementation of the RememberMeServicesInterface which needs
@@ -30,6 +31,24 @@
class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
{
private $tokenProvider;
+ private $secureRandom;
+
+ /**
+ * Constructor.
+ *
+ * @param array $userProviders
+ * @param string $key
+ * @param string $providerKey
+ * @param array $options
+ * @param LoggerInterface $logger
+ * @param SecureRandomInterface $secureRandom
+ */
+ public function __construct(array $userProviders, $key, $providerKey, array $options = array(), LoggerInterface $logger = null, SecureRandomInterface $secureRandom)
+ {
+ parent::__construct($userProviders, $key, $providerKey, $options, $logger);
+
+ $this->secureRandom = $secureRandom;
+ }
/**
* Sets the token provider
@@ -79,7 +98,7 @@ protected function processAutoLoginCookie(array $cookieParts, Request $request)
}
$series = $persistentToken->getSeries();
- $tokenValue = $this->generateRandomValue();
+ $tokenValue = $this->secureRandom->nextBytes(64);
$this->tokenProvider->updateToken($series, $tokenValue, new \DateTime());
$request->attributes->set(self::COOKIE_ATTR_NAME,
new Cookie(
@@ -101,8 +120,8 @@ protected function processAutoLoginCookie(array $cookieParts, Request $request)
*/
protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token)
{
- $series = $this->generateRandomValue();
- $tokenValue = $this->generateRandomValue();
+ $series = $this->secureRandom->nextBytes(64);
+ $tokenValue = $this->secureRandom->nextBytes(64);
$this->tokenProvider->createNewToken(
new PersistentToken(
@@ -126,26 +145,4 @@ protected function onLoginSuccess(Request $request, Response $response, TokenInt
)
);
}
-
- /**
- * Generates a cryptographically strong random value
- *
- * @return string
- */
- protected function generateRandomValue()
- {
- if (function_exists('openssl_random_pseudo_bytes')) {
- $bytes = openssl_random_pseudo_bytes(64, $strong);
-
- if (true === $strong && false !== $bytes) {
- return base64_encode($bytes);
- }
- }
-
- if (null !== $this->logger) {
- $this->logger->warn('Could not produce a cryptographically strong random value. Please install/update the OpenSSL extension.');
- }
-
- return base64_encode(hash('sha512', uniqid(mt_rand(), true), true));
- }
}
diff --git a/src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php b/src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php
index 16378a646b22..edfd779a8eac 100644
--- a/src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php
+++ b/src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php
@@ -1,5 +1,14 @@
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
namespace Symfony\Component\Security\Tests\Core\Util
{
use Symfony\Component\Security\Core\Util\ClassUtils;
diff --git a/src/Symfony/Component/Security/Tests/Core/Util/SecureRandomTest.php b/src/Symfony/Component/Security/Tests/Core/Util/SecureRandomTest.php
new file mode 100755
index 000000000000..1a4bdbd8b9f2
--- /dev/null
+++ b/src/Symfony/Component/Security/Tests/Core/Util/SecureRandomTest.php
@@ -0,0 +1,205 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Tests\Core\Util;
+
+use Symfony\Component\Security\Core\Util\NullSeedProvider;
+use Symfony\Component\Security\Core\Util\SecureRandom;
+
+class SecureRandomTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * T1: Monobit test
+ *
+ * @dataProvider getSecureRandoms
+ */
+ public function testMonobit($secureRandom)
+ {
+ $nbOnBits = substr_count($this->getBitSequence($secureRandom, 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 getSecureRandoms
+ */
+ public function testPoker($secureRandom)
+ {
+ $b = $this->getBitSequence($secureRandom, 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 getSecureRandoms
+ */
+ public function testRun($secureRandom)
+ {
+ $b = $this->getBitSequence($secureRandom, 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 getSecureRandoms
+ */
+ public function testLongRun($secureRandom)
+ {
+ $b = $this->getBitSequence($secureRandom, 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 getSecureRandoms
+ */
+ public function testSerialCorrelation($secureRandom)
+ {
+ $shift = rand(1, 5000);
+ $b = $this->getBitSequence($secureRandom, 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 getSecureRandoms()
+ {
+ $secureRandoms = array();
+
+ // openssl
+ $secureRandom = new SecureRandom();
+ // only add if openssl is indeed present
+ if ($this->hasOpenSsl($secureRandom)) {
+ $secureRandoms[] = array($secureRandom);
+ } else {
+ $this->markTestSkipped('OpenSSL is not available');
+ }
+
+ // no-openssl with custom seed provider
+ $secureRandom = new SecureRandom(sys_get_temp_dir().'/_sf2.seed');
+ $this->disableOpenSsl($secureRandom);
+ $secureRandoms[] = array($secureRandom);
+
+ return $secureRandoms;
+ }
+
+ protected function disableOpenSsl($secureRandom)
+ {
+ $ref = new \ReflectionProperty($secureRandom, 'useOpenSsl');
+ $ref->setAccessible(true);
+ $ref->setValue($secureRandom, false);
+ $ref->setAccessible(false);
+ }
+
+ protected function hasOpenSsl($secureRandom)
+ {
+ $ref = new \ReflectionProperty($secureRandom, 'useOpenSsl');
+ $ref->setAccessible(true);
+
+ $ret = $ref->getValue($secureRandom);
+
+ $ref->setAccessible(false);
+
+ return $ret;
+ }
+
+ private function getBitSequence($secureRandom, $length)
+ {
+ $bitSequence = '';
+ for ($i = 0; $i < $length; $i += 40) {
+ $value = unpack('H*', $secureRandom->nextBytes(5));
+ $value = str_pad(base_convert($value[1], 16, 2), 40, '0', STR_PAD_LEFT);
+ $bitSequence .= $value;
+ }
+
+ return substr($bitSequence, 0, $length);
+ }
+}
diff --git a/src/Symfony/Component/Security/Tests/Core/Util/StringUtilsTest.php b/src/Symfony/Component/Security/Tests/Core/Util/StringUtilsTest.php
new file mode 100755
index 000000000000..aac4139254a4
--- /dev/null
+++ b/src/Symfony/Component/Security/Tests/Core/Util/StringUtilsTest.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Tests\Core\Util;
+
+use Symfony\Component\Security\Core\Util\StringUtils;
+
+class StringUtilsTest extends \PHPUnit_Framework_TestCase
+{
+ public function testEquals()
+ {
+ $this->assertTrue(StringUtils::equals('password', 'password'));
+ $this->assertFalse(StringUtils::equals('password', 'foo'));
+ }
+}
diff --git a/src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php b/src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php
index 3b3691d3c6b0..3cd1528f5546 100644
--- a/src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php
+++ b/src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php
@@ -22,6 +22,7 @@
use Symfony\Component\Security\Http\RememberMe\PersistentTokenBasedRememberMeServices;
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
use Symfony\Component\Security\Core\Exception\CookieTheftException;
+use Symfony\Component\Security\Core\Util\SecureRandom;
class PersistentTokenBasedRememberMeServicesTest extends \PHPUnit_Framework_TestCase
{
@@ -318,7 +319,7 @@ protected function getService($userProvider = null, $options = array(), $logger
$userProvider = $this->getProvider();
}
- return new PersistentTokenBasedRememberMeServices(array($userProvider), 'fookey', 'fookey', $options, $logger);
+ return new PersistentTokenBasedRememberMeServices(array($userProvider), 'fookey', 'fookey', $options, $logger, new SecureRandom());
}
protected function getProvider()