From d6b73e2f526a0fee3fd3683e04c1fe8e79eee33b Mon Sep 17 00:00:00 2001 From: Elnur Abdurrakhimov Date: Sat, 10 Nov 2012 23:11:24 +0400 Subject: [PATCH 1/6] Added BCrypt password encoder. --- src/Symfony/Component/Security/CHANGELOG.md | 1 + .../Core/Encoder/BCryptPasswordEncoder.php | 66 +++++++++++++++++++ .../Encoder/BCryptPasswordEncoderTest.php | 60 +++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php create mode 100644 src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 1305b28335316..6e6e09e47cc35 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG implements EventSubscriberInterface * added secure random number generator * added PBKDF2 Password encoder + * added BCrypt password encoder 2.1.0 ----- diff --git a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php new file mode 100644 index 0000000000000..9efb9e8ffc7ac --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php @@ -0,0 +1,66 @@ + + * + * 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; + +/** + * @author Elnur Abdurrakhimov + */ +class BCryptPasswordEncoder extends BasePasswordEncoder +{ + /** + * @var string + */ + private $cost; + + /** + * @param int $cost + * @throws \InvalidArgumentException + */ + public function __construct($cost) + { + $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) + { + $salt = $this->generateSalt(); + + return crypt($raw, '$2a$' . $this->cost . '$' . $salt . '$'); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt = null) + { + return $this->comparePasswords($encoded, crypt($raw, $encoded)); + } + + /** + * @return string + */ + private function generateSalt() + { + return substr(base_convert(sha1(uniqid(mt_rand(), true)), 16, 36), 0, 22); + } +} diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php b/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php new file mode 100644 index 0000000000000..ca9053a1e5e22 --- /dev/null +++ b/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php @@ -0,0 +1,60 @@ + + * + * 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 + */ +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')); + } +} From 2acf6b1a891c626535626cc686f5e46c996ad2e0 Mon Sep 17 00:00:00 2001 From: Elnur Abdurrakhimov Date: Sun, 11 Nov 2012 01:01:25 +0400 Subject: [PATCH 2/6] Added support for the BCrypt password encoder configuration. --- src/Symfony/Bundle/SecurityBundle/CHANGELOG.md | 1 + .../DependencyInjection/MainConfiguration.php | 5 +++++ .../DependencyInjection/SecurityExtension.php | 12 ++++++++++++ .../SecurityBundle/Resources/config/security.xml | 1 + .../DependencyInjection/Fixtures/php/container1.php | 4 ++++ .../DependencyInjection/Fixtures/xml/container1.xml | 2 ++ .../DependencyInjection/Fixtures/yml/container1.yml | 3 +++ .../DependencyInjection/SecurityExtensionTest.php | 4 ++++ 8 files changed, 32 insertions(+) diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index b726f27f95c62..6ab7b5b5f44aa 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Added PBKDF2 Password encoder +* Added BCrypt password encoder 2.1.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 267e604828460..692d2329d22a2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -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() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index fd8fe2355a3f6..52082ac4ba303 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -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'], diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index d0fbfbcac490c..6c63031d61aa0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -13,6 +13,7 @@ Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder + Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder Symfony\Component\Security\Core\User\InMemoryUserProvider Symfony\Component\Security\Core\User\User diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php index 826b621bb4e08..6633c27ae9430 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php @@ -22,6 +22,10 @@ 'iterations' => 5, 'key_length' => 30, ), + 'JMS\FooBundle\Entity\User6' => array( + 'algorithm' => 'bcrypt', + 'cost' => 15, + ), ), 'providers' => array( 'default' => array( diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml index 7180503f3ce41..cf645eac6e774 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -18,6 +18,8 @@ + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml index bf05e049f7c84..6b4dc2d898d3a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml @@ -16,6 +16,9 @@ security: encode_as_base64: false iterations: 5 key_length: 30 + JMS\FooBundle\Entity\User6: + algorithm: bcrypt + cost: 15 providers: default: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 1d486d60204a0..5d69d9a96fb5c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -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()); } From 70ac90ba61b0e6d71510d94e272f59153ee4865c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Terje=20Br=C3=A5ten?= Date: Thu, 15 Nov 2012 20:01:06 +0100 Subject: [PATCH 3/6] Improved BCryptPasswordEncoder salt generation with correct encoding --- .../Core/Encoder/BCryptPasswordEncoder.php | 91 ++++++++++++++++++- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php index 9efb9e8ffc7ac..3922888c9119a 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php @@ -12,12 +12,20 @@ namespace Symfony\Component\Security\Core\Encoder; use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; +use Symfony\Component\Security\Core\Util\SecureRandomInterface; /** * @author Elnur Abdurrakhimov + * @author Terje BrĂ¥ten */ class BCryptPasswordEncoder extends BasePasswordEncoder { + /** + * A secure random generator + * @var SecureRandomInterface + */ + private $secure_random; + /** * @var string */ @@ -29,6 +37,10 @@ class BCryptPasswordEncoder extends BasePasswordEncoder */ 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; + $cost = (int)$cost; if ($cost < 4 || $cost > 31) { @@ -43,9 +55,14 @@ public function __construct($cost) */ public function encodePassword($raw, $salt = null) { - $salt = $this->generateSalt(); + if ($this->secure_random) { + $random = $this->secure_random->nextBytes(16); + } else { + $random = $this->get_random_bytes(16); + } + $salt = $this->encodeSalt($random) - return crypt($raw, '$2a$' . $this->cost . '$' . $salt . '$'); + return crypt($raw, '$2y$' . $this->cost . '$' . $salt); } /** @@ -57,10 +74,74 @@ public function isPasswordValid($encoded, $raw, $salt = null) } /** - * @return string + * The blowfish/bcrypt used by PHP crypt uses and 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 + */ + protected function encodeSalt($random) + { + $len = strlen($random); + if ($len<16) { + throw new \InvalidArguementException('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 = str_replace(array('A','Q','g','w'), + array('.','O','e','u'), + $lastchar); + $base64_128bit .= $lastchar; + + return $base64_128bit; + } + + /** + * Get random bytes + * + * @param integer $count Number if random bytes needed + * @return string String of random bytes that is $count bytes long */ - private function generateSalt() + protected function get_random_bytes($count) { - return substr(base_convert(sha1(uniqid(mt_rand(), true)), 16, 36), 0, 22); + $random = ''; + if (@is_readable('/dev/urandom')) { + $fh = @fopen('/dev/urandom', 'rb'); + if ($fh) { + stream_set_read_buffer($fh, 0); + stream_set_chunk_size($fh, 16); + $random=fread($fh, $count); + fclose($fh); + } + } + + if (strlen($random)<$count) { + if(function_exists('openssl_random_pseudo_bytes') && + (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) {//OpenSSL slow on Win + $random = openssl_random_pseudo_bytes($count); + } + + $len = strlen($random); + if ($len<$count) { + for ($i=$len;$i<$count;++$i) { + $random .= chr(mt_rand(0,255)); + } + } + } + + return $random; } } From e4ecdf7b56e9c601dea6363783ad49776fab1e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Terje=20Br=C3=A5ten?= Date: Thu, 15 Nov 2012 21:02:42 +0100 Subject: [PATCH 4/6] Changed tab intendation on code in BCryptPasswordEncoder --- .../Core/Encoder/BCryptPasswordEncoder.php | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php index 3922888c9119a..6c40c11cbb63d 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php @@ -90,23 +90,24 @@ public function isPasswordValid($encoded, $raw, $salt = null) */ protected function encodeSalt($random) { - $len = strlen($random); - if ($len<16) { - throw new \InvalidArguementException('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 = str_replace(array('A','Q','g','w'), - array('.','O','e','u'), - $lastchar); - $base64_128bit .= $lastchar; - - return $base64_128bit; + $len = strlen($random); + if ($len<16) { + throw new \InvalidArguementException( + '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 = str_replace(array('A','Q','g','w'), + array('.','O','e','u'), + $lastchar); + $base64_128bit .= $lastchar; + + return $base64_128bit; } /** @@ -117,31 +118,31 @@ protected function encodeSalt($random) */ protected function get_random_bytes($count) { - $random = ''; - if (@is_readable('/dev/urandom')) { - $fh = @fopen('/dev/urandom', 'rb'); - if ($fh) { - stream_set_read_buffer($fh, 0); - stream_set_chunk_size($fh, 16); - $random=fread($fh, $count); - fclose($fh); - } - } - - if (strlen($random)<$count) { - if(function_exists('openssl_random_pseudo_bytes') && - (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) {//OpenSSL slow on Win - $random = openssl_random_pseudo_bytes($count); + $random = ''; + if (@is_readable('/dev/urandom')) { + $fh = @fopen('/dev/urandom', 'rb'); + if ($fh) { + stream_set_read_buffer($fh, 0); + stream_set_chunk_size($fh, 16); + $random=fread($fh, $count); + fclose($fh); + } } - $len = strlen($random); - if ($len<$count) { - for ($i=$len;$i<$count;++$i) { - $random .= chr(mt_rand(0,255)); - } + if (strlen($random)<$count) { + if(function_exists('openssl_random_pseudo_bytes') && + (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { + $random = openssl_random_pseudo_bytes($count); + } + + $len = strlen($random); + if ($len<$count) { + for ($i=$len;$i<$count;++$i) { + $random .= chr(mt_rand(0,255)); + } + } } - } - return $random; + return $random; } } From 3a00532220803b7c2820924e7961b82a0651933e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Terje=20Br=C3=A5ten?= Date: Thu, 15 Nov 2012 22:01:19 +0100 Subject: [PATCH 5/6] fixed bug with strtr arguments and used strtr instead of substr in encodeSalt --- .../Security/Core/Encoder/BCryptPasswordEncoder.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php index 6c40c11cbb63d..f0de2057a098e 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php @@ -99,12 +99,10 @@ protected function encodeSalt($random) $random = substr($random, 0, 16); } - $base64_raw = strtr('+', '.', base64_encode($random)); + $base64_raw = strtr(base64_encode($random), '+', '.'); $base64_128bit = substr($base64_raw, 0, 21); $lastchar = substr($base64_raw, 21, 1); - $lastchar = str_replace(array('A','Q','g','w'), - array('.','O','e','u'), - $lastchar); + $lastchar = strtr($lastchar, 'AQgw','.Oeu'); $base64_128bit .= $lastchar; return $base64_128bit; From adcd75063a3cb9451bd928f9e6c7536f3265a232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Terje=20Br=C3=A5ten?= Date: Fri, 16 Nov 2012 11:17:21 +0100 Subject: [PATCH 6/6] Changed order of methods to get randomness, and added some comments --- .../Core/Encoder/BCryptPasswordEncoder.php | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php index f0de2057a098e..bc9f3c824f9c7 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php @@ -117,22 +117,27 @@ protected function encodeSalt($random) protected function get_random_bytes($count) { $random = ''; - if (@is_readable('/dev/urandom')) { - $fh = @fopen('/dev/urandom', 'rb'); - if ($fh) { - stream_set_read_buffer($fh, 0); - stream_set_chunk_size($fh, 16); - $random=fread($fh, $count); - fclose($fh); - } + + 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) { - if(function_exists('openssl_random_pseudo_bytes') && - (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { - $random = openssl_random_pseudo_bytes($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) {