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

Skip to content

Commit 9d089ef

Browse files
elnurfabpot
authored andcommitted
Added BCrypt password encoder.
1 parent f40c137 commit 9d089ef

File tree

11 files changed

+296
-0
lines changed

11 files changed

+296
-0
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* Added PBKDF2 Password encoder
8+
* Added BCrypt password encoder
89

910
2.1.0
1011
-----

src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,11 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode)
383383
->booleanNode('ignore_case')->defaultFalse()->end()
384384
->booleanNode('encode_as_base64')->defaultTrue()->end()
385385
->scalarNode('iterations')->defaultValue(5000)->end()
386+
->integerNode('cost')
387+
->min(4)
388+
->max(31)
389+
->defaultValue(13)
390+
->end()
386391
->scalarNode('id')->end()
387392
->end()
388393
->end()

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
16+
use Symfony\Component\DependencyInjection\Definition;
1617
use Symfony\Component\DependencyInjection\DefinitionDecorator;
1718
use Symfony\Component\DependencyInjection\Alias;
1819
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -464,6 +465,19 @@ private function createEncoder($config, ContainerBuilder $container)
464465
);
465466
}
466467

468+
// bcrypt encoder
469+
if ('bcrypt' === $config['algorithm']) {
470+
$arguments = array(
471+
new Reference('security.secure_random'),
472+
$config['cost'],
473+
);
474+
475+
return array(
476+
'class' => new Parameter('security.encoder.bcrypt.class'),
477+
'arguments' => $arguments,
478+
);
479+
}
480+
467481
// message digest encoder
468482
$arguments = array(
469483
$config['algorithm'],

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<parameter key="security.encoder.digest.class">Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder</parameter>
1414
<parameter key="security.encoder.plain.class">Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder</parameter>
1515
<parameter key="security.encoder.pbkdf2.class">Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder</parameter>
16+
<parameter key="security.encoder.bcrypt.class">Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder</parameter>
1617

1718
<parameter key="security.user.provider.in_memory.class">Symfony\Component\Security\Core\User\InMemoryUserProvider</parameter>
1819
<parameter key="security.user.provider.in_memory.user.class">Symfony\Component\Security\Core\User\User</parameter>

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
'iterations' => 5,
2323
'key_length' => 30,
2424
),
25+
'JMS\FooBundle\Entity\User6' => array(
26+
'algorithm' => 'bcrypt',
27+
'cost' => 15,
28+
),
2529
),
2630
'providers' => array(
2731
'default' => array(

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
<encoder class="JMS\FooBundle\Entity\User5" algorithm="pbkdf2" hash-algorithm="sha1" encode-as-base64="false" iterations="5" key-length="30" />
2020

21+
<encoder class="JMS\FooBundle\Entity\User6" algorithm="bcrypt" cost="15" />
22+
2123
<provider name="default">
2224
<memory>
2325
<user name="foo" password="foo" roles="ROLE_USER" />

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ security:
1616
encode_as_base64: false
1717
iterations: 5
1818
key_length: 30
19+
JMS\FooBundle\Entity\User6:
20+
algorithm: bcrypt
21+
cost: 15
1922

2023
providers:
2124
default:

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ public function testEncoders()
158158
'class' => new Parameter('security.encoder.pbkdf2.class'),
159159
'arguments' => array('sha1', false, 5, 30),
160160
),
161+
'JMS\FooBundle\Entity\User6' => array(
162+
'class' => new Parameter('security.encoder.bcrypt.class'),
163+
'arguments' => array(
164+
new Reference('security.secure_random'),
165+
15,
166+
)
167+
),
161168
)), $container->getDefinition('security.encoder_factory.generic')->getArguments());
162169
}
163170

src/Symfony/Component/Security/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
implements EventSubscriberInterface
1010
* added secure random number generator
1111
* added PBKDF2 Password encoder
12+
* added BCrypt password encoder
1213

1314
2.1.0
1415
-----
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Encoder;
13+
14+
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
15+
use Symfony\Component\Security\Core\Util\SecureRandomInterface;
16+
17+
/**
18+
* @author Elnur Abdurrakhimov <[email protected]>
19+
* @author Terje Bråten <[email protected]>
20+
*/
21+
class BCryptPasswordEncoder extends BasePasswordEncoder
22+
{
23+
/**
24+
* @var SecureRandomInterface
25+
*/
26+
private $secureRandom;
27+
28+
/**
29+
* @var string
30+
*/
31+
private $cost;
32+
33+
private static $prefix = null;
34+
35+
/**
36+
* @param SecureRandomInterface $secureRandom
37+
* @param int $cost
38+
*
39+
* @throws \InvalidArgumentException if cost is out of range
40+
*/
41+
public function __construct(SecureRandomInterface $secureRandom, $cost)
42+
{
43+
$this->secureRandom = $secureRandom;
44+
45+
$cost = (int) $cost;
46+
if ($cost < 4 || $cost > 31) {
47+
throw new \InvalidArgumentException('Cost must be in the range of 4-31');
48+
}
49+
$this->cost = sprintf("%02d", $cost);
50+
51+
if (!self::$prefix) {
52+
self::$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
53+
? '2y' : '2a').'$';
54+
}
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function encodePassword($raw, $salt = null)
61+
{
62+
if (function_exists('password_hash')) {
63+
return password_hash($raw, PASSWORD_BCRYPT, array('cost' => $this->cost));
64+
}
65+
66+
$salt = self::$prefix.$this->cost.'$'.
67+
$this->encodeSalt($this->getRawSalt());
68+
$encoded = crypt($raw, $salt);
69+
if (!is_string($encoded) || strlen($encoded) <= 13) {
70+
return false;
71+
}
72+
73+
return $encoded;
74+
}
75+
76+
/**
77+
* {@inheritdoc}
78+
*/
79+
public function isPasswordValid($encoded, $raw, $salt = null)
80+
{
81+
if (function_exists('password_verify')) {
82+
return password_verify($raw, $encoded);
83+
}
84+
85+
$crypted = crypt($raw, $encoded);
86+
if (strlen($crypted) <= 13) {
87+
return false;
88+
}
89+
90+
return $this->comparePasswords($encoded, $crypted);
91+
}
92+
93+
/**
94+
* Correctly encode the salt to be used by Bcrypt.
95+
* The blowfish/bcrypt algorithm used by PHP crypt expects a different
96+
* set and order of characters than the usual base64_encode function.
97+
* Regular b64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
98+
* Bcrypt b64: ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
99+
* We care because the last character in our encoded string will
100+
* only represent 2 bits. While two known implementations of
101+
* bcrypt will happily accept and correct a salt string which
102+
* has the 4 unused bits set to non-zero, we do not want to take
103+
* chances and we also do not want to waste an additional byte
104+
* of entropy.
105+
*
106+
* @param bytes $random a string of 16 random bytes
107+
* @return string Properly encoded salt to use with php crypt function
108+
*
109+
* @throws \InvalidArgumentException if string of random bytes is too short
110+
*/
111+
protected function encodeSalt($random)
112+
{
113+
$len = strlen($random);
114+
if ($len < 16) {
115+
throw new \InvalidArgumentException('The bcrypt salt needs 16 random bytes');
116+
}
117+
if ($len > 16) {
118+
$random = substr($random, 0, 16);
119+
}
120+
121+
$base64raw = str_replace('+', '.', base64_encode($random));
122+
$salt128bit = substr($base64raw, 0, 21);
123+
$lastchar = substr($base64raw, 21, 1);
124+
$lastchar = strtr($lastchar, 'AQgw','.Oeu');
125+
$salt128bit .= $lastchar;
126+
127+
return $salt128bit;
128+
}
129+
130+
/**
131+
* @return bytes 16 random bytes to be used in the salt
132+
*/
133+
protected function getRawSalt()
134+
{
135+
$rawSalt = false;
136+
$numBytes = 16;
137+
if (function_exists('mcrypt_create_iv')) {
138+
$rawSalt = mcrypt_create_iv($numBytes, MCRYPT_DEV_URANDOM);
139+
}
140+
if (!$rawSalt) {
141+
$rawSalt = $this->secureRandom->nextBytes($numBytes);
142+
}
143+
144+
return $rawSalt;
145+
}
146+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Tests\Core\Encoder;
13+
14+
use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
15+
16+
/**
17+
* @author Elnur Abdurrakhimov <[email protected]>
18+
*/
19+
class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
20+
{
21+
const PASSWORD = 'password';
22+
const BYTES = '0123456789abcdef';
23+
const VALID_COST = '04';
24+
25+
const SECURE_RANDOM_INTERFACE = 'Symfony\Component\Security\Core\Util\SecureRandomInterface';
26+
27+
/**
28+
* @var \PHPUnit_Framework_MockObject_MockObject
29+
*/
30+
private $secureRandom;
31+
32+
protected function setUp()
33+
{
34+
$this->secureRandom = $this->getMock(self::SECURE_RANDOM_INTERFACE);
35+
36+
$this->secureRandom
37+
->expects($this->any())
38+
->method('nextBytes')
39+
->will($this->returnValue(self::BYTES))
40+
;
41+
}
42+
43+
/**
44+
* @expectedException \InvalidArgumentException
45+
*/
46+
public function testCostBelowRange()
47+
{
48+
new BCryptPasswordEncoder($this->secureRandom, 3);
49+
}
50+
51+
/**
52+
* @expectedException \InvalidArgumentException
53+
*/
54+
public function testCostAboveRange()
55+
{
56+
new BCryptPasswordEncoder($this->secureRandom, 32);
57+
}
58+
59+
public function testCostInRange()
60+
{
61+
for ($cost = 4; $cost <= 31; $cost++) {
62+
new BCryptPasswordEncoder($this->secureRandom, $cost);
63+
}
64+
}
65+
66+
public function testResultLength()
67+
{
68+
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
69+
$result = $encoder->encodePassword(self::PASSWORD);
70+
$this->assertEquals(60, strlen($result));
71+
}
72+
73+
public function testValidation()
74+
{
75+
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
76+
$result = $encoder->encodePassword(self::PASSWORD);
77+
$this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD));
78+
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword'));
79+
}
80+
81+
public function testValidationKnownPassword()
82+
{
83+
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
84+
$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
85+
? '2y' : '2a').'$';
86+
87+
$encrypted = $prefix.'04$ABCDEFGHIJKLMNOPQRSTU.uTmwd4KMSHxbUsG7bng8x7YdA0PM1iq';
88+
$this->assertTrue($encoder->isPasswordValid($encrypted, self::PASSWORD));
89+
}
90+
91+
public function testSecureRandomIsUsed()
92+
{
93+
if (function_exists('mcrypt_create_iv')) {
94+
return;
95+
}
96+
97+
$this->secureRandom
98+
->expects($this->atLeastOnce())
99+
->method('nextBytes')
100+
;
101+
102+
$encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
103+
$result = $encoder->encodePassword(self::PASSWORD);
104+
105+
$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
106+
? '2y' : '2a').'$';
107+
$salt = 'MDEyMzQ1Njc4OWFiY2RlZe';
108+
$expected = crypt(self::PASSWORD, $prefix . self::VALID_COST . '$' . $salt);
109+
110+
$this->assertEquals($expected, $result);
111+
}
112+
}

0 commit comments

Comments
 (0)