summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFabien Potencier <fabien.potencier@gmail.com>2012-07-05 12:19:25 +0200
committerFabien Potencier <fabien.potencier@gmail.com>2012-10-28 08:03:00 +0100
commit255196983ec0c1dc944057816fbba25b9ff8276c (patch)
tree9abc7b351b5a5dc0adcbde72f6ad645a652e04f1
parente3d359180c41a80803e06a5d277b3b319952c8ee (diff)
downloadsymfony-security-255196983ec0c1dc944057816fbba25b9ff8276c.zip
symfony-security-255196983ec0c1dc944057816fbba25b9ff8276c.tar.gz
symfony-security-255196983ec0c1dc944057816fbba25b9ff8276c.tar.bz2
moved the secure random class from JMSSecurityExtraBundle to Symfony (closes #3595)
-rw-r--r--CHANGELOG.md1
-rw-r--r--Core/Encoder/BasePasswordEncoder.php13
-rw-r--r--Core/Util/Prng.php104
-rw-r--r--Core/Util/SeedProviderInterface.php37
-rw-r--r--Core/Util/String.php48
-rw-r--r--Http/RememberMe/PersistentTokenBasedRememberMeServices.php35
-rwxr-xr-xTests/Core/Util/PrngTest.php179
-rwxr-xr-xTests/Core/Util/StringTest.php14
-rw-r--r--Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php6
9 files changed, 401 insertions, 36 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1c8192..251666a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ CHANGELOG
2.1.0
-----
+ * added secure random number generator
* [BC BREAK] The signature of ExceptionListener has changed
* changed the HttpUtils constructor signature to take a UrlGenerator and a UrlMatcher instead of a Router
* EncoderFactoryInterface::getEncoder() can now also take a class name as an argument
diff --git a/Core/Encoder/BasePasswordEncoder.php b/Core/Encoder/BasePasswordEncoder.php
index ae1c7d4..e73bbbd 100644
--- a/Core/Encoder/BasePasswordEncoder.php
+++ b/Core/Encoder/BasePasswordEncoder.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Core\Encoder;
+use Symfony\Component\Security\Core\Util\String;
+
/**
* BasePasswordEncoder is the base class for all password encoders.
*
@@ -77,15 +79,6 @@ abstract class BasePasswordEncoder implements PasswordEncoderInterface
*/
protected function comparePasswords($password1, $password2)
{
- if (strlen($password1) !== strlen($password2)) {
- return false;
- }
-
- $result = 0;
- for ($i = 0; $i < strlen($password1); $i++) {
- $result |= ord($password1[$i]) ^ ord($password2[$i]);
- }
-
- return 0 === $result;
+ return String::equals($password1, $password2);
}
}
diff --git a/Core/Util/Prng.php b/Core/Util/Prng.php
new file mode 100644
index 0000000..ab8baa7
--- /dev/null
+++ b/Core/Util/Prng.php
@@ -0,0 +1,104 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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 Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class Prng
+{
+ private $logger;
+ private $useOpenSsl;
+ private $seed;
+ private $seedUpdated;
+ private $seedLastUpdatedAt;
+ private $seedProvider;
+
+ /**
+ * Constructor.
+ *
+ * Be aware that a guessable seed will severely compromise the PRNG
+ * algorithm that is employed.
+ *
+ * @param SeedProviderInterface $provider
+ * @param LoggerInterface $logger
+ */
+ public function __construct(SeedProviderInterface $provider = null, LoggerInterface $logger = null)
+ {
+ $this->seedProvider = $provider;
+ $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;
+ }
+ }
+
+ /**
+ * Generates the specified number of secure random bytes.
+ *
+ * @param integer $nbBytes
+ * @return string
+ */
+ 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->seedProvider) {
+ throw new \RuntimeException('You need to specify a custom seed provider.');
+ }
+
+ list($this->seed, $this->seedLastUpdatedAt) = $this->seedProvider->loadSeed();
+ }
+
+ $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);
+ }
+
+ $this->seedUpdated = true;
+ }
+ }
+
+ return substr($bytes, 0, $nbBytes);
+ }
+}
diff --git a/Core/Util/SeedProviderInterface.php b/Core/Util/SeedProviderInterface.php
new file mode 100644
index 0000000..dd960b9
--- /dev/null
+++ b/Core/Util/SeedProviderInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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;
+
+/**
+ * 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);
+}
diff --git a/Core/Util/String.php b/Core/Util/String.php
new file mode 100644
index 0000000..096878b
--- /dev/null
+++ b/Core/Util/String.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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 <fabien@symfony.com>
+ */
+final class String
+{
+ private final 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/Http/RememberMe/PersistentTokenBasedRememberMeServices.php b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
index 8944672..d36eb01 100644
--- a/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
+++ b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php
@@ -19,6 +19,7 @@ use Symfony\Component\Security\Core\Exception\AuthenticationException;
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\Prng;
/**
* Concrete implementation of the RememberMeServicesInterface which needs
@@ -30,6 +31,12 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
{
private $tokenProvider;
+ private $prng;
+
+ public function setPrng(Prng $prng)
+ {
+ $this->prng = $prng;
+ }
/**
* Sets the token provider
@@ -79,7 +86,7 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
}
$series = $persistentToken->getSeries();
- $tokenValue = $this->generateRandomValue();
+ $tokenValue = $this->prng->nextBytes(64);
$this->tokenProvider->updateToken($series, $tokenValue, new \DateTime());
$request->attributes->set(self::COOKIE_ATTR_NAME,
new Cookie(
@@ -101,8 +108,8 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
*/
protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token)
{
- $series = $this->generateRandomValue();
- $tokenValue = $this->generateRandomValue();
+ $series = $this->prng->nextBytes(64);
+ $tokenValue = $this->prng->nextBytes(64);
$this->tokenProvider->createNewToken(
new PersistentToken(
@@ -126,26 +133,4 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices
)
);
}
-
- /**
- * 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/Tests/Core/Util/PrngTest.php b/Tests/Core/Util/PrngTest.php
new file mode 100755
index 0000000..7c9b2e2
--- /dev/null
+++ b/Tests/Core/Util/PrngTest.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Symfony\Component\Security\Tests\Core\Util;
+
+use Symfony\Component\Security\Core\Util\NullSeedProvider;
+use Symfony\Component\Security\Core\Util\PrngSchema;
+use Symfony\Component\Security\Core\Util\Prng;
+
+class PrngTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * T1: Monobit test
+ *
+ * @dataProvider getPrngs
+ */
+ public function testMonobit($prng)
+ {
+ $nbOnBits = substr_count($this->getBitSequence($prng, 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 getPrngs
+ */
+ public function testPoker($prng)
+ {
+ $b = $this->getBitSequence($prng, 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 getPrngs
+ */
+ public function testRun($prng)
+ {
+ $b = $this->getBitSequence($prng, 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 getPrngs
+ */
+ public function testLongRun($prng)
+ {
+ $b = $this->getBitSequence($prng, 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 getPrngs
+ */
+ public function testSerialCorrelation($prng)
+ {
+ $shift = rand(1, 5000);
+ $b = $this->getBitSequence($prng, 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 getPrngs()
+ {
+ $prngs = array();
+
+ // openssl with fallback
+ $prng = new Prng(new NullSeedProvider());
+ $prngs[] = array($prng);
+
+ // no-openssl with custom seed provider
+ $prng = new Prng(new NullSeedProvider());
+ $this->disableOpenSsl($prng);
+ $prngs[] = array($prng);
+
+ return $prngs;
+ }
+
+ protected function disableOpenSsl($prng)
+ {
+ $ref = new \ReflectionProperty($prng, 'useOpenSsl');
+ $ref->setAccessible(true);
+ $ref->setValue($prng, false);
+ }
+
+ private function getBitSequence($prng, $length)
+ {
+ $bitSequence = '';
+ for ($i=0;$i<$length; $i+=40) {
+ $value = unpack('H*', $prng->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/Tests/Core/Util/StringTest.php b/Tests/Core/Util/StringTest.php
new file mode 100755
index 0000000..fe4eae4
--- /dev/null
+++ b/Tests/Core/Util/StringTest.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Symfony\Component\Security\Tests\Core\Util;
+
+use Symfony\Component\Security\Core\Util\String;
+
+class StringTest extends \PHPUnit_Framework_TestCase
+{
+ public function testEquals()
+ {
+ $this->assertTrue(String::equals('password', 'password'));
+ $this->assertFalse(String::equals('password', 'foo'));
+ }
+}
diff --git a/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php b/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php
index 3b3691d..846ee9b 100644
--- a/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php
+++ b/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php
@@ -22,6 +22,7 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag;
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\Prng;
class PersistentTokenBasedRememberMeServicesTest extends \PHPUnit_Framework_TestCase
{
@@ -318,7 +319,10 @@ class PersistentTokenBasedRememberMeServicesTest extends \PHPUnit_Framework_Test
$userProvider = $this->getProvider();
}
- return new PersistentTokenBasedRememberMeServices(array($userProvider), 'fookey', 'fookey', $options, $logger);
+ $r = new PersistentTokenBasedRememberMeServices(array($userProvider), 'fookey', 'fookey', $options, $logger);
+ $r->setPrng(new Prng());
+
+ return $r;
}
protected function getProvider()