merged branch fabpot/bcrypt (PR #7853)
This PR was merged into the master branch. Discussion ---------- [Security] Outsource all the BCrypt heavy lifting to a library | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | BC breaks? | yes | Deprecations? | no | Tests pass? | yes | License | MIT The [BCrypt bundle](https://github.com/elnur/ElnurBlowfishPasswordEncoderBundle) is already using the library. This is a working implementation of #7247 Commits -------c83546d
[Security] tweaked previous commitb2e553a
Outsource all the BCrypt heavy lifting to a library
This commit is contained in:
commit
ccc52a4e4b
@ -65,7 +65,8 @@
|
|||||||
"doctrine/dbal": "~2.2",
|
"doctrine/dbal": "~2.2",
|
||||||
"doctrine/orm": "~2.2,>=2.2.3",
|
"doctrine/orm": "~2.2,>=2.2.3",
|
||||||
"monolog/monolog": "~1.3",
|
"monolog/monolog": "~1.3",
|
||||||
"propel/propel1": "1.6.*"
|
"propel/propel1": "1.6.*",
|
||||||
|
"ircmaxell/password-compat": "1.0.*"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-0": { "Symfony\\": "src/" },
|
"psr-0": { "Symfony\\": "src/" },
|
||||||
|
@ -452,42 +452,33 @@ class SecurityExtension extends Extension
|
|||||||
|
|
||||||
// pbkdf2 encoder
|
// pbkdf2 encoder
|
||||||
if ('pbkdf2' === $config['algorithm']) {
|
if ('pbkdf2' === $config['algorithm']) {
|
||||||
$arguments = array(
|
return array(
|
||||||
|
'class' => new Parameter('security.encoder.pbkdf2.class'),
|
||||||
|
'arguments' => array(
|
||||||
$config['hash_algorithm'],
|
$config['hash_algorithm'],
|
||||||
$config['encode_as_base64'],
|
$config['encode_as_base64'],
|
||||||
$config['iterations'],
|
$config['iterations'],
|
||||||
$config['key_length'],
|
$config['key_length'],
|
||||||
);
|
),
|
||||||
|
|
||||||
return array(
|
|
||||||
'class' => new Parameter('security.encoder.pbkdf2.class'),
|
|
||||||
'arguments' => $arguments,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// bcrypt encoder
|
// bcrypt encoder
|
||||||
if ('bcrypt' === $config['algorithm']) {
|
if ('bcrypt' === $config['algorithm']) {
|
||||||
$arguments = array(
|
|
||||||
new Reference('security.secure_random'),
|
|
||||||
$config['cost'],
|
|
||||||
);
|
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'class' => new Parameter('security.encoder.bcrypt.class'),
|
'class' => new Parameter('security.encoder.bcrypt.class'),
|
||||||
'arguments' => $arguments,
|
'arguments' => array($config['cost']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// message digest encoder
|
// message digest encoder
|
||||||
$arguments = array(
|
return array(
|
||||||
|
'class' => new Parameter('security.encoder.digest.class'),
|
||||||
|
'arguments' => array(
|
||||||
$config['algorithm'],
|
$config['algorithm'],
|
||||||
$config['encode_as_base64'],
|
$config['encode_as_base64'],
|
||||||
$config['iterations'],
|
$config['iterations'],
|
||||||
);
|
),
|
||||||
|
|
||||||
return array(
|
|
||||||
'class' => new Parameter('security.encoder.digest.class'),
|
|
||||||
'arguments' => $arguments,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,10 +160,7 @@ abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase
|
|||||||
),
|
),
|
||||||
'JMS\FooBundle\Entity\User6' => array(
|
'JMS\FooBundle\Entity\User6' => array(
|
||||||
'class' => new Parameter('security.encoder.bcrypt.class'),
|
'class' => new Parameter('security.encoder.bcrypt.class'),
|
||||||
'arguments' => array(
|
'arguments' => array(15),
|
||||||
new Reference('security.secure_random'),
|
|
||||||
15,
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
|
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,13 @@ CHANGELOG
|
|||||||
2.3.0
|
2.3.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
* [BC BREAK] the BCrypt encoder constructor signature has changed (the first argument was removed)
|
||||||
* added RequestContext::getQueryString()
|
* added RequestContext::getQueryString()
|
||||||
|
|
||||||
2.2.0
|
2.2.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
* Added BCrypt password encoder
|
||||||
* [DEPRECATION] Several route settings have been renamed (the old ones will be removed in 3.0):
|
* [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`
|
* The `pattern` setting for a route has been deprecated in favor of `path`
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
namespace Symfony\Component\Security\Core\Encoder;
|
namespace Symfony\Component\Security\Core\Encoder;
|
||||||
|
|
||||||
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
|
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
|
||||||
use Symfony\Component\Security\Core\Util\SecureRandomInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
|
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
|
||||||
@ -20,39 +19,26 @@ use Symfony\Component\Security\Core\Util\SecureRandomInterface;
|
|||||||
*/
|
*/
|
||||||
class BCryptPasswordEncoder extends BasePasswordEncoder
|
class BCryptPasswordEncoder extends BasePasswordEncoder
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var SecureRandomInterface
|
|
||||||
*/
|
|
||||||
private $secureRandom;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private $cost;
|
private $cost;
|
||||||
|
|
||||||
private static $prefix = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* 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
|
* @throws \InvalidArgumentException if cost is out of range
|
||||||
*/
|
*/
|
||||||
public function __construct(SecureRandomInterface $secureRandom, $cost)
|
public function __construct($cost)
|
||||||
{
|
{
|
||||||
$this->secureRandom = $secureRandom;
|
|
||||||
|
|
||||||
$cost = (int) $cost;
|
$cost = (int) $cost;
|
||||||
if ($cost < 4 || $cost > 31) {
|
if ($cost < 4 || $cost > 31) {
|
||||||
throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
|
throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
|
||||||
}
|
}
|
||||||
$this->cost = sprintf('%02d', $cost);
|
|
||||||
|
|
||||||
if (!self::$prefix) {
|
$this->cost = sprintf('%02d', $cost);
|
||||||
self::$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=') ? '2y' : '2a').'$';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,89 +46,14 @@ class BCryptPasswordEncoder extends BasePasswordEncoder
|
|||||||
*/
|
*/
|
||||||
public function encodePassword($raw, $salt)
|
public function encodePassword($raw, $salt)
|
||||||
{
|
{
|
||||||
if (function_exists('password_hash')) {
|
|
||||||
return password_hash($raw, PASSWORD_BCRYPT, array('cost' => $this->cost));
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function isPasswordValid($encoded, $raw, $salt)
|
public function isPasswordValid($encoded, $raw, $salt)
|
||||||
{
|
{
|
||||||
if (function_exists('password_verify')) {
|
|
||||||
return password_verify($raw, $encoded);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -22,30 +22,12 @@ class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
|
|||||||
const BYTES = '0123456789abcdef';
|
const BYTES = '0123456789abcdef';
|
||||||
const VALID_COST = '04';
|
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
|
* @expectedException \InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function testCostBelowRange()
|
public function testCostBelowRange()
|
||||||
{
|
{
|
||||||
new BCryptPasswordEncoder($this->secureRandom, 3);
|
new BCryptPasswordEncoder(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,60 +35,28 @@ class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testCostAboveRange()
|
public function testCostAboveRange()
|
||||||
{
|
{
|
||||||
new BCryptPasswordEncoder($this->secureRandom, 32);
|
new BCryptPasswordEncoder(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCostInRange()
|
public function testCostInRange()
|
||||||
{
|
{
|
||||||
for ($cost = 4; $cost <= 31; $cost++) {
|
for ($cost = 4; $cost <= 31; $cost++) {
|
||||||
new BCryptPasswordEncoder($this->secureRandom, $cost);
|
new BCryptPasswordEncoder($cost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testResultLength()
|
public function testResultLength()
|
||||||
{
|
{
|
||||||
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
|
$encoder = new BCryptPasswordEncoder(self::VALID_COST);
|
||||||
$result = $encoder->encodePassword(self::PASSWORD, null);
|
$result = $encoder->encodePassword(self::PASSWORD, null);
|
||||||
$this->assertEquals(60, strlen($result));
|
$this->assertEquals(60, strlen($result));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testValidation()
|
public function testValidation()
|
||||||
{
|
{
|
||||||
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
|
$encoder = new BCryptPasswordEncoder(self::VALID_COST);
|
||||||
$result = $encoder->encodePassword(self::PASSWORD, null);
|
$result = $encoder->encodePassword(self::PASSWORD, null);
|
||||||
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
|
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
|
||||||
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,8 @@
|
|||||||
"symfony/validator": ">=2.2,<2.4-dev",
|
"symfony/validator": ">=2.2,<2.4-dev",
|
||||||
"doctrine/common": "~2.2",
|
"doctrine/common": "~2.2",
|
||||||
"doctrine/dbal": "~2.2",
|
"doctrine/dbal": "~2.2",
|
||||||
"psr/log": "~1.0"
|
"psr/log": "~1.0",
|
||||||
|
"ircmaxell/password-compat": "1.0.*"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"symfony/class-loader": "2.2.*",
|
"symfony/class-loader": "2.2.*",
|
||||||
@ -35,7 +36,8 @@
|
|||||||
"symfony/form": "2.2.*",
|
"symfony/form": "2.2.*",
|
||||||
"symfony/validator": "2.2.*",
|
"symfony/validator": "2.2.*",
|
||||||
"symfony/routing": "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": {
|
"autoload": {
|
||||||
"psr-0": { "Symfony\\Component\\Security\\": "" }
|
"psr-0": { "Symfony\\Component\\Security\\": "" }
|
||||||
|
Reference in New Issue
Block a user