diff options
26 files changed, 655 insertions, 99 deletions
@@ -1,2 +1,4 @@ vendor/ composer.lock +phpunit.xml + diff --git a/CHANGELOG.md b/CHANGELOG.md index c570ac3..1305b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +2.2.0 +----- + + * `Symfony\Component\Security\Http\Firewall` and + `Symfony\Component\Security\Http\RememberMe\ResponseListener` now + implements EventSubscriberInterface + * added secure random number generator + * added PBKDF2 Password encoder + 2.1.0 ----- diff --git a/Core/Encoder/BasePasswordEncoder.php b/Core/Encoder/BasePasswordEncoder.php index ae1c7d4..1ef134b 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\StringUtils; + /** * 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 StringUtils::equals($password1, $password2); } } diff --git a/Core/Encoder/Pbkdf2PasswordEncoder.php b/Core/Encoder/Pbkdf2PasswordEncoder.php new file mode 100644 index 0000000..656545f --- /dev/null +++ b/Core/Encoder/Pbkdf2PasswordEncoder.php @@ -0,0 +1,97 @@ +<?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\Encoder; + +/** + * Pbkdf2PasswordEncoder uses the PBKDF2 (Password-Based Key Derivation Function 2). + * + * Providing a high level of Cryptographic security, + * PBKDF2 is recommended by the National Institute of Standards and Technology (NIST). + * + * But also warrants a warning, using PBKDF2 (with a high number of iterations) slows down the process. + * PBKDF2 should be used with caution and care. + * + * @author Sebastiaan Stok <s.stok@rollerscapes.net> + * @author Andrew Johnson + * @author Fabien Potencier <fabien@symfony.com> + */ +class Pbkdf2PasswordEncoder extends BasePasswordEncoder +{ + private $algorithm; + private $encodeHashAsBase64; + private $iterations; + private $length; + + /** + * Constructor. + * + * @param string $algorithm The digest algorithm to use + * @param Boolean $encodeHashAsBase64 Whether to base64 encode the password hash + * @param integer $iterations The number of iterations to use to stretch the password hash + * @param integer $length Length of derived key to create + */ + public function __construct($algorithm = 'sha512', $encodeHashAsBase64 = true, $iterations = 1000, $length = 40) + { + $this->algorithm = $algorithm; + $this->encodeHashAsBase64 = $encodeHashAsBase64; + $this->iterations = $iterations; + $this->length = $length; + } + + /** + * {@inheritdoc} + * + * @throws \LogicException when the algorithm is not supported + */ + public function encodePassword($raw, $salt) + { + if (!in_array($this->algorithm, hash_algos(), true)) { + throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm)); + } + + if (function_exists('hash_pbkdf2')) { + $digest = hash_pbkdf2($this->algorithm, $raw, $salt, $this->iterations, $this->length, true); + } else { + $digest = $this->hashPbkdf2($this->algorithm, $raw, $salt, $this->iterations, $this->length); + } + + return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + return $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + } + + private function hashPbkdf2($algorithm, $password, $salt, $iterations, $length = 0) + { + // Number of blocks needed to create the derived key + $blocks = ceil($length / strlen(hash($algorithm, null, true))); + $digest = ''; + + for ($i = 1; $i <= $blocks; $i++) { + $ib = $block = hash_hmac($algorithm, $salt . pack('N', $i), $password, true); + + // Iterations + for ($j = 1; $j < $iterations; $j++) { + $ib ^= ($block = hash_hmac($algorithm, $block, $password, true)); + } + + $digest .= $ib; + } + + return substr($digest, 0, $this->length); + } +} diff --git a/Core/Role/RoleInterface.php b/Core/Role/RoleInterface.php index a3cb266..3d4cbea 100644 --- a/Core/Role/RoleInterface.php +++ b/Core/Role/RoleInterface.php @@ -15,7 +15,7 @@ namespace Symfony\Component\Security\Core\Role; * RoleInterface represents a role granted to a user. * * A role must either have a string representation or it needs to be explicitly - * supported by an at least one AccessDecisionManager. + * supported by at least one AccessDecisionManager. * * @author Fabien Potencier <fabien@symfony.com> */ diff --git a/Core/Util/ClassUtils.php b/Core/Util/ClassUtils.php index 7b583a3..26bf1a1 100644 --- a/Core/Util/ClassUtils.php +++ b/Core/Util/ClassUtils.php @@ -37,6 +37,11 @@ class ClassUtils const MARKER_LENGTH = 6; /** + * This class should not be instantiated + */ + private function __construct() {} + + /** * Gets the real class name of a class name that could be a proxy. * * @param string|object diff --git a/Core/Util/SecureRandom.php b/Core/Util/SecureRandom.php new file mode 100644 index 0000000..77f1d8c --- /dev/null +++ b/Core/Util/SecureRandom.php @@ -0,0 +1,114 @@ +<?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 Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +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/Core/Util/SecureRandomInterface.php b/Core/Util/SecureRandomInterface.php new file mode 100644 index 0000000..bb70a7e --- /dev/null +++ b/Core/Util/SecureRandomInterface.php @@ -0,0 +1,30 @@ +<?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; + + +/** + * Interface that needs to be implemented by all secure random number generators. + * + * @author Fabien Potencier <fabien@symfony.com> + */ +interface SecureRandomInterface +{ + /** + * Generates the specified number of secure random bytes. + * + * @param integer $nbBytes + * + * @return string + */ + public function nextBytes($nbBytes); +} diff --git a/Core/Util/StringUtils.php b/Core/Util/StringUtils.php new file mode 100644 index 0000000..d21efd3 --- /dev/null +++ b/Core/Util/StringUtils.php @@ -0,0 +1,49 @@ +<?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> + */ +class StringUtils +{ + /** + * This class should not be instantiated + */ + 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/Http/EntryPoint/BasicAuthenticationEntryPoint.php b/Http/EntryPoint/BasicAuthenticationEntryPoint.php index 6ba3872..44ece5e 100644 --- a/Http/EntryPoint/BasicAuthenticationEntryPoint.php +++ b/Http/EntryPoint/BasicAuthenticationEntryPoint.php @@ -34,7 +34,7 @@ class BasicAuthenticationEntryPoint implements AuthenticationEntryPointInterface { $response = new Response(); $response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', $this->realmName)); - $response->setStatusCode(401, $authException ? $authException->getMessage() : null); + $response->setStatusCode(401); return $response; } diff --git a/Http/EntryPoint/DigestAuthenticationEntryPoint.php b/Http/EntryPoint/DigestAuthenticationEntryPoint.php index ec92419..37fba85 100644 --- a/Http/EntryPoint/DigestAuthenticationEntryPoint.php +++ b/Http/EntryPoint/DigestAuthenticationEntryPoint.php @@ -57,7 +57,7 @@ class DigestAuthenticationEntryPoint implements AuthenticationEntryPointInterfac $response = new Response(); $response->headers->set('WWW-Authenticate', $authenticateHeader); - $response->setStatusCode(401, $authException ? $authException->getMessage() : null); + $response->setStatusCode(401); return $response; } diff --git a/Http/Firewall.php b/Http/Firewall.php index a590fd9..e083fdb 100644 --- a/Http/Firewall.php +++ b/Http/Firewall.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Security\Http; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Firewall uses a FirewallMap to register security listeners for the given @@ -25,7 +27,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; * * @author Fabien Potencier <fabien@symfony.com> */ -class Firewall +class Firewall implements EventSubscriberInterface { private $map; private $dispatcher; @@ -68,4 +70,9 @@ class Firewall } } } + + public static function getSubscribedEvents() + { + return array(KernelEvents::REQUEST => array('onKernelRequest', 8)); + } } diff --git a/Http/Firewall/UsernamePasswordFormAuthenticationListener.php b/Http/Firewall/UsernamePasswordFormAuthenticationListener.php index 22330a8..388c014 100644 --- a/Http/Firewall/UsernamePasswordFormAuthenticationListener.php +++ b/Http/Firewall/UsernamePasswordFormAuthenticationListener.php @@ -55,7 +55,7 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL */ protected function requiresAuthentication(Request $request) { - if ($this->options['post_only'] && !$request->isMethod('post')) { + if ($this->options['post_only'] && !$request->isMethod('POST')) { return false; } @@ -67,14 +67,6 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL */ protected function attemptAuthentication(Request $request) { - if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) { - if (null !== $this->logger) { - $this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod())); - } - - return null; - } - if (null !== $this->csrfProvider) { $csrfToken = $request->get($this->options['csrf_parameter'], null, true); @@ -83,8 +75,13 @@ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationL } } - $username = trim($request->get($this->options['username_parameter'], null, true)); - $password = $request->get($this->options['password_parameter'], null, true); + if ($this->options['post_only']) { + $username = trim($request->request->get($this->options['username_parameter'], null, true)); + $password = $request->request->get($this->options['password_parameter'], null, true); + } else { + $username = trim($request->get($this->options['username_parameter'], null, true)); + $password = $request->get($this->options['password_parameter'], null, true); + } $request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username); diff --git a/Http/RememberMe/PersistentTokenBasedRememberMeServices.php b/Http/RememberMe/PersistentTokenBasedRememberMeServices.php index 8944672..dbb6429 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\SecureRandomInterface; /** * Concrete implementation of the RememberMeServicesInterface which needs @@ -30,6 +31,24 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 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 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices } $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 @@ class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices */ 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 @@ 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/Http/RememberMe/ResponseListener.php b/Http/RememberMe/ResponseListener.php index 6cbdcb3..03c71c7 100644 --- a/Http/RememberMe/ResponseListener.php +++ b/Http/RememberMe/ResponseListener.php @@ -12,13 +12,15 @@ namespace Symfony\Component\Security\Http\RememberMe; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Adds remember-me cookies to the Response. * * @author Johannes M. Schmitt <schmittjoh@gmail.com> */ -class ResponseListener +class ResponseListener implements EventSubscriberInterface { public function onKernelResponse(FilterResponseEvent $event) { @@ -29,4 +31,9 @@ class ResponseListener $response->headers->setCookie($request->attributes->get(RememberMeServicesInterface::COOKIE_ATTR_NAME)); } } + + public static function getSubscribedEvents() + { + return array(KernelEvents::RESPONSE => 'onKernelResponse'); + } } @@ -18,9 +18,6 @@ Resources You can run the unit tests with the following command: - phpunit - -If you also want to run the unit tests that depend on other Symfony -Components, install dev dependencies before running PHPUnit: - - php composer.phar install --dev + $ cd path/to/Symfony/Component/Security/ + $ composer.phar install --dev + $ phpunit diff --git a/Tests/Core/Encoder/Pbkdf2PasswordEncoderTest.php b/Tests/Core/Encoder/Pbkdf2PasswordEncoderTest.php new file mode 100644 index 0000000..2c98543 --- /dev/null +++ b/Tests/Core/Encoder/Pbkdf2PasswordEncoderTest.php @@ -0,0 +1,45 @@ +<?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\Tests\Core\Encoder; + +use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; + +class Pbkdf2PasswordEncoderTest extends \PHPUnit_Framework_TestCase +{ + public function testIsPasswordValid() + { + $encoder = new Pbkdf2PasswordEncoder('sha256', false, 1, 40); + + $this->assertTrue($encoder->isPasswordValid('c1232f10f62715fda06ae7c0a2037ca19b33cf103b727ba56d870c11f290a2ab106974c75607c8a3', 'password', '')); + } + + public function testEncodePassword() + { + $encoder = new Pbkdf2PasswordEncoder('sha256', false, 1, 40); + $this->assertSame('c1232f10f62715fda06ae7c0a2037ca19b33cf103b727ba56d870c11f290a2ab106974c75607c8a3', $encoder->encodePassword('password', '')); + + $encoder = new Pbkdf2PasswordEncoder('sha256', true, 1, 40); + $this->assertSame('wSMvEPYnFf2gaufAogN8oZszzxA7cnulbYcMEfKQoqsQaXTHVgfIow==', $encoder->encodePassword('password', '')); + + $encoder = new Pbkdf2PasswordEncoder('sha256', false, 2, 40); + $this->assertSame('8bc2f9167a81cdcfad1235cd9047f1136271c1f978fcfcb35e22dbeafa4634f6fd2214218ed63ebb', $encoder->encodePassword('password', '')); + } + + /** + * @expectedException LogicException + */ + public function testEncodePasswordAlgorithmDoesNotExist() + { + $encoder = new Pbkdf2PasswordEncoder('foobar'); + $encoder->encodePassword('password', ''); + } +} diff --git a/Tests/Core/Util/ClassUtilsTest.php b/Tests/Core/Util/ClassUtilsTest.php index 16378a6..edfd779 100644 --- a/Tests/Core/Util/ClassUtilsTest.php +++ b/Tests/Core/Util/ClassUtilsTest.php @@ -1,5 +1,14 @@ <?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\Tests\Core\Util { use Symfony\Component\Security\Core\Util\ClassUtils; diff --git a/Tests/Core/Util/SecureRandomTest.php b/Tests/Core/Util/SecureRandomTest.php new file mode 100755 index 0000000..c7ed016 --- /dev/null +++ b/Tests/Core/Util/SecureRandomTest.php @@ -0,0 +1,201 @@ +<?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\Tests\Core\Util; + +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(); + + // only add if openssl is indeed present + $secureRandom = new SecureRandom(); + if ($this->hasOpenSsl($secureRandom)) { + $secureRandoms[] = array($secureRandom); + } + + // 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/Tests/Core/Util/StringUtilsTest.php b/Tests/Core/Util/StringUtilsTest.php new file mode 100755 index 0000000..aac4139 --- /dev/null +++ b/Tests/Core/Util/StringUtilsTest.php @@ -0,0 +1,23 @@ +<?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\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/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php b/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php index b442309..b9e289d 100644 --- a/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php +++ b/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php @@ -34,7 +34,6 @@ class BasicAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase $this->assertEquals('Basic realm="TheRealmName"', $response->headers->get('WWW-Authenticate')); $this->assertEquals(401, $response->getStatusCode()); - $this->assertAttributeEquals('The exception message', 'statusText', $response); } public function testStartWithoutAuthException() @@ -47,6 +46,5 @@ class BasicAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase $this->assertEquals('Basic realm="TheRealmName"', $response->headers->get('WWW-Authenticate')); $this->assertEquals(401, $response->getStatusCode()); - $this->assertAttributeEquals('Unauthorized', 'statusText', $response); } } diff --git a/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php b/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php index ae0e3cc..8dfd618 100644 --- a/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php +++ b/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php @@ -34,7 +34,6 @@ class DigestAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase $response = $entryPoint->start($request, $authenticationException); $this->assertEquals(401, $response->getStatusCode()); - $this->assertAttributeEquals('TheAuthenticationExceptionMessage', 'statusText', $response); $this->assertRegExp('/^Digest realm="TheRealmName", qop="auth", nonce="[a-zA-Z0-9\/+]+={0,2}"$/', $response->headers->get('WWW-Authenticate')); } @@ -46,7 +45,6 @@ class DigestAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase $response = $entryPoint->start($request); $this->assertEquals(401, $response->getStatusCode()); - $this->assertAttributeEquals('Unauthorized', 'statusText', $response); $this->assertRegExp('/^Digest realm="TheRealmName", qop="auth", nonce="[a-zA-Z0-9\/+]+={0,2}"$/', $response->headers->get('WWW-Authenticate')); } @@ -60,7 +58,6 @@ class DigestAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase $response = $entryPoint->start($request, $nonceExpiredException); $this->assertEquals(401, $response->getStatusCode()); - $this->assertAttributeEquals('TheNonceExpiredExceptionMessage', 'statusText', $response); $this->assertRegExp('/^Digest realm="TheRealmName", qop="auth", nonce="[a-zA-Z0-9\/+]+={0,2}", stale="true"$/', $response->headers->get('WWW-Authenticate')); } } diff --git a/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php b/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php index 3b3691d..7fc3021 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\SecureRandom; class PersistentTokenBasedRememberMeServicesTest extends \PHPUnit_Framework_TestCase { @@ -318,7 +319,7 @@ class PersistentTokenBasedRememberMeServicesTest extends \PHPUnit_Framework_Test $userProvider = $this->getProvider(); } - return new PersistentTokenBasedRememberMeServices(array($userProvider), 'fookey', 'fookey', $options, $logger); + return new PersistentTokenBasedRememberMeServices(array($userProvider), 'fookey', 'fookey', $options, $logger, new SecureRandom(sys_get_temp_dir().'/_sf2.seed')); } protected function getProvider() diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php deleted file mode 100644 index 84ae3a6..0000000 --- a/Tests/bootstrap.php +++ /dev/null @@ -1,22 +0,0 @@ -<?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. - */ - -spl_autoload_register(function ($class) { - if (0 === strpos(ltrim($class, '/'), 'Symfony\Component\Security')) { - if (file_exists($file = __DIR__.'/../'.substr(str_replace('\\', '/', $class), strlen('Symfony\Component\Security')).'.php')) { - require_once $file; - } - } -}); - -if (file_exists($loader = __DIR__.'/../vendor/autoload.php')) { - require_once $loader; -} diff --git a/composer.json b/composer.json index 0cf0a30..73e07b5 100644 --- a/composer.json +++ b/composer.json @@ -17,33 +17,33 @@ ], "require": { "php": ">=5.3.3", - "symfony/event-dispatcher": "2.1.*", - "symfony/http-foundation": "2.1.*", - "symfony/http-kernel": "2.1.*" + "symfony/event-dispatcher": "2.2.*", + "symfony/http-foundation": "2.2.*", + "symfony/http-kernel": "2.2.*" }, "require-dev": { - "symfony/form": "2.1.*", - "symfony/routing": "2.1.*", - "symfony/validator": "2.1.*", + "symfony/form": "2.2.*", + "symfony/routing": "2.2.*", + "symfony/validator": "2.2.*", "doctrine/common": ">=2.2,<2.4-dev", "doctrine/dbal": ">=2.2,<2.4-dev" }, "suggest": { - "symfony/class-loader": "2.1.*", - "symfony/finder": "2.1.*", - "symfony/form": "2.1.*", - "symfony/validator": "2.1.*", - "symfony/routing": "2.1.*", + "symfony/class-loader": "2.2.*", + "symfony/finder": "2.2.*", + "symfony/form": "2.2.*", + "symfony/validator": "2.2.*", + "symfony/routing": "2.2.*", "doctrine/dbal": "to use the built-in ACL implementation" }, "autoload": { - "psr-0": { "Symfony\\Component\\Security": "" } + "psr-0": { "Symfony\\Component\\Security\\": "" } }, "target-dir": "Symfony/Component/Security", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0560cf5..f45a44e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,7 +9,7 @@ processIsolation="false" stopOnFailure="false" syntaxCheck="false" - bootstrap="Tests/bootstrap.php" + bootstrap="vendor/autoload.php" > <testsuites> <testsuite name="Symfony Security Component Test Suite"> |