diff --git a/composer.json b/composer.json index 6c04c49de5..504b48d92c 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,8 @@ "doctrine/dbal": "~2.2", "doctrine/orm": "~2.2,>=2.2.3", "monolog/monolog": "~1.3", - "propel/propel1": "1.6.*" + "propel/propel1": "1.6.*", + "ircmaxell/password-compat": "1.0.*" }, "autoload": { "psr-0": { "Symfony\\": "src/" }, diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index d5de24433e..936552c4e9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -452,42 +452,33 @@ class SecurityExtension extends Extension // pbkdf2 encoder if ('pbkdf2' === $config['algorithm']) { - $arguments = array( - $config['hash_algorithm'], - $config['encode_as_base64'], - $config['iterations'], - $config['key_length'], - ); - return array( - 'class' => new Parameter('security.encoder.pbkdf2.class'), - 'arguments' => $arguments, + 'class' => new Parameter('security.encoder.pbkdf2.class'), + 'arguments' => array( + $config['hash_algorithm'], + $config['encode_as_base64'], + $config['iterations'], + $config['key_length'], + ), ); } // bcrypt encoder if ('bcrypt' === $config['algorithm']) { - $arguments = array( - new Reference('security.secure_random'), - $config['cost'], - ); - return array( - 'class' => new Parameter('security.encoder.bcrypt.class'), - 'arguments' => $arguments, + 'class' => new Parameter('security.encoder.bcrypt.class'), + 'arguments' => array($config['cost']), ); } // message digest encoder - $arguments = array( - $config['algorithm'], - $config['encode_as_base64'], - $config['iterations'], - ); - return array( - 'class' => new Parameter('security.encoder.digest.class'), - 'arguments' => $arguments, + 'class' => new Parameter('security.encoder.digest.class'), + 'arguments' => array( + $config['algorithm'], + $config['encode_as_base64'], + $config['iterations'], + ), ); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 948e272205..b85e850c23 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -160,10 +160,7 @@ abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase ), 'JMS\FooBundle\Entity\User6' => array( 'class' => new Parameter('security.encoder.bcrypt.class'), - 'arguments' => array( - new Reference('security.secure_random'), - 15, - ) + 'arguments' => array(15), ), )), $container->getDefinition('security.encoder_factory.generic')->getArguments()); } diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index f0c616d080..05701440e1 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -4,11 +4,13 @@ CHANGELOG 2.3.0 ----- + * [BC BREAK] the BCrypt encoder constructor signature has changed (the first argument was removed) * added RequestContext::getQueryString() 2.2.0 ----- + * Added BCrypt password encoder * [DEPRECATION] Several route settings have been renamed (the old ones will be removed in 3.0): * The `pattern` setting for a route has been deprecated in favor of `path` diff --git a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php index 1b7572d49c..6a65fa521e 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Security\Core\Encoder; use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; -use Symfony\Component\Security\Core\Util\SecureRandomInterface; /** * @author Elnur Abdurrakhimov @@ -20,39 +19,26 @@ use Symfony\Component\Security\Core\Util\SecureRandomInterface; */ class BCryptPasswordEncoder extends BasePasswordEncoder { - /** - * @var SecureRandomInterface - */ - private $secureRandom; - /** * @var string */ private $cost; - private static $prefix = null; - /** * Constructor. * - * @param SecureRandomInterface $secureRandom A SecureRandomInterface instance - * @param integer $cost The algorithmic cost that should be used + * @param integer $cost The algorithmic cost that should be used * * @throws \InvalidArgumentException if cost is out of range */ - public function __construct(SecureRandomInterface $secureRandom, $cost) + public function __construct($cost) { - $this->secureRandom = $secureRandom; - $cost = (int) $cost; if ($cost < 4 || $cost > 31) { throw new \InvalidArgumentException('Cost must be in the range of 4-31.'); } - $this->cost = sprintf('%02d', $cost); - if (!self::$prefix) { - self::$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=') ? '2y' : '2a').'$'; - } + $this->cost = sprintf('%02d', $cost); } /** @@ -60,17 +46,7 @@ class BCryptPasswordEncoder extends BasePasswordEncoder */ public function encodePassword($raw, $salt) { - if (function_exists('password_hash')) { - return password_hash($raw, PASSWORD_BCRYPT, array('cost' => $this->cost)); - } - - $salt = self::$prefix.$this->cost.'$'.$this->encodeSalt($this->getRawSalt()); - $encoded = crypt($raw, $salt); - if (!is_string($encoded) || strlen($encoded) <= 13) { - return false; - } - - return $encoded; + return password_hash($raw, PASSWORD_BCRYPT, array('cost' => $this->cost)); } /** @@ -78,71 +54,6 @@ class BCryptPasswordEncoder extends BasePasswordEncoder */ public function isPasswordValid($encoded, $raw, $salt) { - if (function_exists('password_verify')) { - return password_verify($raw, $encoded); - } - - $crypted = crypt($raw, $encoded); - if (strlen($crypted) <= 13) { - return false; - } - - return $this->comparePasswords($encoded, $crypted); - } - - /** - * Encodes the salt to be used by Bcrypt. - * - * The blowfish/bcrypt algorithm used by PHP crypt expects a different - * set and order of characters than the usual base64_encode function. - * Regular b64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ - * Bcrypt b64: ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 - * We care because the last character in our encoded string will - * only represent 2 bits. While two known implementations of - * bcrypt will happily accept and correct a salt string which - * has the 4 unused bits set to non-zero, we do not want to take - * chances and we also do not want to waste an additional byte - * of entropy. - * - * @param bytes $random a string of 16 random bytes - * - * @return string Properly encoded salt to use with php crypt function - * - * @throws \InvalidArgumentException if string of random bytes is too short - */ - protected function encodeSalt($random) - { - $len = strlen($random); - if ($len < 16) { - throw new \InvalidArgumentException('The bcrypt salt needs 16 random bytes.'); - } - if ($len > 16) { - $random = substr($random, 0, 16); - } - - $base64raw = str_replace('+', '.', base64_encode($random)); - $salt128bit = substr($base64raw, 0, 21); - $lastchar = substr($base64raw, 21, 1); - $lastchar = strtr($lastchar, 'AQgw', '.Oeu'); - $salt128bit .= $lastchar; - - return $salt128bit; - } - - /** - * @return bytes 16 random bytes to be used in the salt - */ - protected function getRawSalt() - { - $rawSalt = false; - $numBytes = 16; - if (function_exists('mcrypt_create_iv')) { - $rawSalt = mcrypt_create_iv($numBytes, MCRYPT_DEV_URANDOM); - } - if (!$rawSalt) { - $rawSalt = $this->secureRandom->nextBytes($numBytes); - } - - return $rawSalt; + return password_verify($raw, $encoded); } } diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php b/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php index 45c8f7430f..6378433aa1 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php @@ -22,30 +22,12 @@ class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase const BYTES = '0123456789abcdef'; const VALID_COST = '04'; - const SECURE_RANDOM_INTERFACE = 'Symfony\Component\Security\Core\Util\SecureRandomInterface'; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $secureRandom; - - protected function setUp() - { - $this->secureRandom = $this->getMock(self::SECURE_RANDOM_INTERFACE); - - $this->secureRandom - ->expects($this->any()) - ->method('nextBytes') - ->will($this->returnValue(self::BYTES)) - ; - } - /** * @expectedException \InvalidArgumentException */ public function testCostBelowRange() { - new BCryptPasswordEncoder($this->secureRandom, 3); + new BCryptPasswordEncoder(3); } /** @@ -53,60 +35,28 @@ class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase */ public function testCostAboveRange() { - new BCryptPasswordEncoder($this->secureRandom, 32); + new BCryptPasswordEncoder(32); } public function testCostInRange() { for ($cost = 4; $cost <= 31; $cost++) { - new BCryptPasswordEncoder($this->secureRandom, $cost); + new BCryptPasswordEncoder($cost); } } public function testResultLength() { - $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST); + $encoder = new BCryptPasswordEncoder(self::VALID_COST); $result = $encoder->encodePassword(self::PASSWORD, null); $this->assertEquals(60, strlen($result)); } public function testValidation() { - $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST); + $encoder = new BCryptPasswordEncoder(self::VALID_COST); $result = $encoder->encodePassword(self::PASSWORD, null); $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null)); $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null)); } - - public function testValidationKnownPassword() - { - $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST); - $prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=') - ? '2y' : '2a').'$'; - - $encrypted = $prefix.'04$ABCDEFGHIJKLMNOPQRSTU.uTmwd4KMSHxbUsG7bng8x7YdA0PM1iq'; - $this->assertTrue($encoder->isPasswordValid($encrypted, self::PASSWORD, null)); - } - - public function testSecureRandomIsUsed() - { - if (function_exists('mcrypt_create_iv')) { - return; - } - - $this->secureRandom - ->expects($this->atLeastOnce()) - ->method('nextBytes') - ; - - $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST); - $result = $encoder->encodePassword(self::PASSWORD, null); - - $prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=') - ? '2y' : '2a').'$'; - $salt = 'MDEyMzQ1Njc4OWFiY2RlZe'; - $expected = crypt(self::PASSWORD, $prefix.self::VALID_COST.'$'.$salt); - - $this->assertEquals($expected, $result); - } } diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index dd4eecfb91..5ea71f38b8 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -27,7 +27,8 @@ "symfony/validator": ">=2.2,<2.4-dev", "doctrine/common": "~2.2", "doctrine/dbal": "~2.2", - "psr/log": "~1.0" + "psr/log": "~1.0", + "ircmaxell/password-compat": "1.0.*" }, "suggest": { "symfony/class-loader": "2.2.*", @@ -35,7 +36,8 @@ "symfony/form": "2.2.*", "symfony/validator": "2.2.*", "symfony/routing": "2.2.*", - "doctrine/dbal": "to use the built-in ACL implementation" + "doctrine/dbal": "to use the built-in ACL implementation", + "ircmaxell/password-compat": "1.0.*" }, "autoload": { "psr-0": { "Symfony\\Component\\Security\\": "" }