merged branch TerjeBr/bcrypt-password-encoder-2 (PR #6808)
This PR was submitted for the master branch but it was merged into the 2.2 branch instead (closes #6808). Commits ------- 0cb74a2 Added BCrypt password encoder. Discussion ---------- Bcrypt password encoder Bug fix: no Feature addition: yes Backwards compatibility break: no Symfony2 tests pass: yes Fixes the following tickets: #5932 License of the code: MIT Documentation PR: symfony/symfony-docs#1936 --------------------------------------------------------------------------- by TerjeBr at 2013-01-19T18:53:21Z Finishing PR #5974 --------------------------------------------------------------------------- by jalliot at 2013-01-20T14:07:54Z This looks very good! :) But be careful to CS or this will not be merged. --------------------------------------------------------------------------- by TerjeBr at 2013-01-20T14:17:35Z I do not understand. What do you mean by "CS"? --------------------------------------------------------------------------- by jalliot at 2013-01-20T14:20:33Z [Coding standards](http://symfony.com/doc/current/contributing/code/standards.html). You should run [PHP-CS-Fixer](https://github.com/fabpot/PHP-CS-Fixer) on your code to fix it. --------------------------------------------------------------------------- by TerjeBr at 2013-01-20T14:47:23Z The only thing php-cs-fixer.phar did was to realign some of the doc-block comments. But thank you any way for pointing out the script to me. --------------------------------------------------------------------------- by TerjeBr at 2013-01-20T15:52:07Z Why does it look like @elnur added the commits? It was me. --------------------------------------------------------------------------- by stof at 2013-01-20T16:32:12Z @TerjeBr check your git configuration to be sure it uses your email address when committing --------------------------------------------------------------------------- by TerjeBr at 2013-01-20T17:30:58Z Now the commit is in my name. But see what happens if I squash the commit wit git rebase .... --------------------------------------------------------------------------- by TerjeBr at 2013-01-20T17:33:08Z Now it looks like elnur added the commit. --------------------------------------------------------------------------- by stof at 2013-01-26T15:57:59Z @fabpot is there a chance to have this in 2.2 ?
This commit is contained in:
commit
aad2b63b0a
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* Added PBKDF2 Password encoder
|
||||
* Added BCrypt password encoder
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
@ -383,6 +383,11 @@ class MainConfiguration implements ConfigurationInterface
|
||||
->booleanNode('ignore_case')->defaultFalse()->end()
|
||||
->booleanNode('encode_as_base64')->defaultTrue()->end()
|
||||
->scalarNode('iterations')->defaultValue(5000)->end()
|
||||
->integerNode('cost')
|
||||
->min(4)
|
||||
->max(31)
|
||||
->defaultValue(13)
|
||||
->end()
|
||||
->scalarNode('id')->end()
|
||||
->end()
|
||||
->end()
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\DefinitionDecorator;
|
||||
use Symfony\Component\DependencyInjection\Alias;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
@ -464,6 +465,19 @@ class SecurityExtension extends Extension
|
||||
);
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
// message digest encoder
|
||||
$arguments = array(
|
||||
$config['algorithm'],
|
||||
|
@ -13,6 +13,7 @@
|
||||
<parameter key="security.encoder.digest.class">Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder</parameter>
|
||||
<parameter key="security.encoder.plain.class">Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder</parameter>
|
||||
<parameter key="security.encoder.pbkdf2.class">Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder</parameter>
|
||||
<parameter key="security.encoder.bcrypt.class">Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder</parameter>
|
||||
|
||||
<parameter key="security.user.provider.in_memory.class">Symfony\Component\Security\Core\User\InMemoryUserProvider</parameter>
|
||||
<parameter key="security.user.provider.in_memory.user.class">Symfony\Component\Security\Core\User\User</parameter>
|
||||
|
@ -22,6 +22,10 @@ $container->loadFromExtension('security', array(
|
||||
'iterations' => 5,
|
||||
'key_length' => 30,
|
||||
),
|
||||
'JMS\FooBundle\Entity\User6' => array(
|
||||
'algorithm' => 'bcrypt',
|
||||
'cost' => 15,
|
||||
),
|
||||
),
|
||||
'providers' => array(
|
||||
'default' => array(
|
||||
|
@ -18,6 +18,8 @@
|
||||
|
||||
<encoder class="JMS\FooBundle\Entity\User5" algorithm="pbkdf2" hash-algorithm="sha1" encode-as-base64="false" iterations="5" key-length="30" />
|
||||
|
||||
<encoder class="JMS\FooBundle\Entity\User6" algorithm="bcrypt" cost="15" />
|
||||
|
||||
<provider name="default">
|
||||
<memory>
|
||||
<user name="foo" password="foo" roles="ROLE_USER" />
|
||||
|
@ -16,6 +16,9 @@ security:
|
||||
encode_as_base64: false
|
||||
iterations: 5
|
||||
key_length: 30
|
||||
JMS\FooBundle\Entity\User6:
|
||||
algorithm: bcrypt
|
||||
cost: 15
|
||||
|
||||
providers:
|
||||
default:
|
||||
|
@ -158,6 +158,13 @@ abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase
|
||||
'class' => new Parameter('security.encoder.pbkdf2.class'),
|
||||
'arguments' => array('sha1', false, 5, 30),
|
||||
),
|
||||
'JMS\FooBundle\Entity\User6' => array(
|
||||
'class' => new Parameter('security.encoder.bcrypt.class'),
|
||||
'arguments' => array(
|
||||
new Reference('security.secure_random'),
|
||||
15,
|
||||
)
|
||||
),
|
||||
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ CHANGELOG
|
||||
implements EventSubscriberInterface
|
||||
* added secure random number generator
|
||||
* added PBKDF2 Password encoder
|
||||
* added BCrypt password encoder
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
@ -0,0 +1,146 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
|
||||
use Symfony\Component\Security\Core\Util\SecureRandomInterface;
|
||||
|
||||
/**
|
||||
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
|
||||
* @author Terje Bråten <terje@braten.be>
|
||||
*/
|
||||
class BCryptPasswordEncoder extends BasePasswordEncoder
|
||||
{
|
||||
/**
|
||||
* @var SecureRandomInterface
|
||||
*/
|
||||
private $secureRandom;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $cost;
|
||||
|
||||
private static $prefix = null;
|
||||
|
||||
/**
|
||||
* @param SecureRandomInterface $secureRandom
|
||||
* @param int $cost
|
||||
*
|
||||
* @throws \InvalidArgumentException if cost is out of range
|
||||
*/
|
||||
public function __construct(SecureRandomInterface $secureRandom, $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').'$';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encodePassword($raw, $salt = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPasswordValid($encoded, $raw, $salt = null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly encode 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
<?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\BCryptPasswordEncoder;
|
||||
|
||||
/**
|
||||
* @author Elnur Abdurrakhimov <elnur@elnur.pro>
|
||||
*/
|
||||
class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
const PASSWORD = 'password';
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \InvalidArgumentException
|
||||
*/
|
||||
public function testCostAboveRange()
|
||||
{
|
||||
new BCryptPasswordEncoder($this->secureRandom, 32);
|
||||
}
|
||||
|
||||
public function testCostInRange()
|
||||
{
|
||||
for ($cost = 4; $cost <= 31; $cost++) {
|
||||
new BCryptPasswordEncoder($this->secureRandom, $cost);
|
||||
}
|
||||
}
|
||||
|
||||
public function testResultLength()
|
||||
{
|
||||
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
|
||||
$result = $encoder->encodePassword(self::PASSWORD);
|
||||
$this->assertEquals(60, strlen($result));
|
||||
}
|
||||
|
||||
public function testValidation()
|
||||
{
|
||||
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
|
||||
$result = $encoder->encodePassword(self::PASSWORD);
|
||||
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD));
|
||||
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword'));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
|
||||
? '2y' : '2a').'$';
|
||||
$salt = 'MDEyMzQ1Njc4OWFiY2RlZe';
|
||||
$expected = crypt(self::PASSWORD, $prefix . self::VALID_COST . '$' . $salt);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user