Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Improved Bcrypt password encoder #6023

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* Added PBKDF2 Password encoder
* Added BCrypt password encoder

2.1.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
->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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,18 @@ private function createEncoder($config, ContainerBuilder $container)
);
}

// bcrypt encoder
if ('bcrypt' === $config['algorithm']) {
$arguments = array(
$config['cost'],
);

return array(
'class' => new Parameter('security.encoder.bcrypt.class'),
'arguments' => $arguments,
);
}

// message digest encoder
$arguments = array(
$config['algorithm'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
'iterations' => 5,
'key_length' => 30,
),
'JMS\FooBundle\Entity\User6' => array(
'algorithm' => 'bcrypt',
'cost' => 15,
),
),
'providers' => array(
'default' => array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ security:
encode_as_base64: false
iterations: 5
key_length: 30
JMS\FooBundle\Entity\User6:
algorithm: bcrypt
cost: 15

providers:
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ public function testEncoders()
'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(15),
),
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
}

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
implements EventSubscriberInterface
* added secure random number generator
* added PBKDF2 Password encoder
* added BCrypt password encoder

2.1.0
-----
Expand Down
151 changes: 151 additions & 0 deletions src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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 <[email protected]>
* @author Terje Bråten <[email protected]>
*/
class BCryptPasswordEncoder extends BasePasswordEncoder
{
/**
* A secure random generator
* @var SecureRandomInterface
*/
private $secure_random;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a camelCased name


/**
* @var string
*/
private $cost;

/**
* @param int $cost
* @throws \InvalidArgumentException
*/
public function __construct($cost)
{
//TODO: add SecureRandomInterface $secure_random as an argument
// to the consructor. Service id: security.secure_random
//$this->secure_random = $secure_random;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do it...


$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);
}

/**
* {@inheritdoc}
*/
public function encodePassword($raw, $salt = null)
{
if ($this->secure_random) {
$random = $this->secure_random->nextBytes(16);
} else {
$random = $this->get_random_bytes(16);
}
$salt = $this->encodeSalt($random)

return crypt($raw, '$2y$' . $this->cost . '$' . $salt);
}

/**
* {@inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt = null)
{
return $this->comparePasswords($encoded, crypt($raw, $encoded));
}

/**
* The blowfish/bcrypt used by PHP crypt uses and expects a different
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should add a short description before the long description

* 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
*/
protected function encodeSalt($random)
{
$len = strlen($random);
if ($len<16) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing spaces around the comparison operator

throw new \InvalidArguementException(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong class name (btw, this shoould be unit tested, which would have shown you the issue with a Fatal Error)

'The bcrypt salt needs 16 random bytes');
}
if ($len>16) {
$random = substr($random, 0, 16);
}

$base64_raw = strtr(base64_encode($random), '+', '.');
$base64_128bit = substr($base64_raw, 0, 21);
$lastchar = substr($base64_raw, 21, 1);
$lastchar = strtr($lastchar, 'AQgw','.Oeu');
$base64_128bit .= $lastchar;

return $base64_128bit;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use camelCased names for all variables

}

/**
* Get random bytes
*
* @param integer $count Number if random bytes needed
* @return string String of random bytes that is $count bytes long
*/
protected function get_random_bytes($count)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be deleted. There is no reason to duplicate the code

{
$random = '';

if(function_exists('openssl_random_pseudo_bytes') &&
(strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) {
// This also calls /dev/urandom and all others sorts
// of methods to get randomness, but is too slow
// on windows because of a bug there.
$random = openssl_random_pseudo_bytes($count);
}

if (strlen($random)<$count) {
// Try /dev/urandom
if (@is_readable('/dev/urandom')) {
$fh = @fopen('/dev/urandom', 'rb');
if ($fh) {
stream_set_read_buffer($fh, 0);
$random=fread($fh, $count);
fclose($fh);
}
}

// Last resort, fallback to mt_rand
$len = strlen($random);
if ($len<$count) {
for ($i=$len;$i<$count;++$i) {
$random .= chr(mt_rand(0,255));
}
}
}

return $random;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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 <[email protected]>
*/
class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
{
const PASSWORD = 'password';

/**
* @expectedException \InvalidArgumentException
*/
public function testCostBelowRange()
{
new BCryptPasswordEncoder(3);
}

/**
* @expectedException \InvalidArgumentException
*/
public function testCostAboveRange()
{
new BCryptPasswordEncoder(32);
}

public function testCostInRange()
{
for ($cost = 4; $cost <= 31; $cost++) {
new BCryptPasswordEncoder($cost);
}
}

public function testResultLength()
{
$encoder = new BCryptPasswordEncoder(4);
$result = $encoder->encodePassword(self::PASSWORD);
$this->assertEquals(60, strlen($result));
}

public function testValidation()
{
$encoder = new BCryptPasswordEncoder(4);
$result = $encoder->encodePassword(self::PASSWORD);
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword'));
}
}