diff options
Diffstat (limited to 'Core/Encoder')
-rw-r--r-- | Core/Encoder/BCryptPasswordEncoder.php | 148 | ||||
-rw-r--r-- | Core/Encoder/BasePasswordEncoder.php | 15 | ||||
-rw-r--r-- | Core/Encoder/EncoderFactory.php | 2 | ||||
-rw-r--r-- | Core/Encoder/Pbkdf2PasswordEncoder.php | 97 |
4 files changed, 252 insertions, 10 deletions
diff --git a/Core/Encoder/BCryptPasswordEncoder.php b/Core/Encoder/BCryptPasswordEncoder.php new file mode 100644 index 0000000..1b7572d --- /dev/null +++ b/Core/Encoder/BCryptPasswordEncoder.php @@ -0,0 +1,148 @@ +<?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; + + /** + * Constructor. + * + * @param SecureRandomInterface $secureRandom A SecureRandomInterface instance + * @param integer $cost The algorithmic cost that should be used + * + * @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) + { + 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) + { + 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); + } + + /** + * 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; + } +} diff --git a/Core/Encoder/BasePasswordEncoder.php b/Core/Encoder/BasePasswordEncoder.php index ae1c7d4..c26c9ce 100644 --- a/Core/Encoder/BasePasswordEncoder.php +++ b/Core/Encoder/BasePasswordEncoder.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Core\Encoder; +use Symfony\Component\Security\Core\Util\StringUtils; + /** * BasePasswordEncoder is the base class for all password encoders. * @@ -50,6 +52,8 @@ abstract class BasePasswordEncoder implements PasswordEncoderInterface * @param string $salt the salt to be used * * @return string a merged password and salt + * + * @throws \InvalidArgumentException */ protected function mergePasswordAndSalt($password, $salt) { @@ -77,15 +81,6 @@ abstract class BasePasswordEncoder implements PasswordEncoderInterface */ protected function comparePasswords($password1, $password2) { - if (strlen($password1) !== strlen($password2)) { - return false; - } - - $result = 0; - for ($i = 0; $i < strlen($password1); $i++) { - $result |= ord($password1[$i]) ^ ord($password2[$i]); - } - - return 0 === $result; + return StringUtils::equals($password1, $password2); } } diff --git a/Core/Encoder/EncoderFactory.php b/Core/Encoder/EncoderFactory.php index 9429441..8bad61f 100644 --- a/Core/Encoder/EncoderFactory.php +++ b/Core/Encoder/EncoderFactory.php @@ -51,6 +51,8 @@ class EncoderFactory implements EncoderFactoryInterface * @param array $config * * @return PasswordEncoderInterface + * + * @throws \InvalidArgumentException */ private function createEncoder(array $config) { diff --git a/Core/Encoder/Pbkdf2PasswordEncoder.php b/Core/Encoder/Pbkdf2PasswordEncoder.php new file mode 100644 index 0000000..656545f --- /dev/null +++ b/Core/Encoder/Pbkdf2PasswordEncoder.php @@ -0,0 +1,97 @@ +<?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; + +/** + * Pbkdf2PasswordEncoder uses the PBKDF2 (Password-Based Key Derivation Function 2). + * + * Providing a high level of Cryptographic security, + * PBKDF2 is recommended by the National Institute of Standards and Technology (NIST). + * + * But also warrants a warning, using PBKDF2 (with a high number of iterations) slows down the process. + * PBKDF2 should be used with caution and care. + * + * @author Sebastiaan Stok <s.stok@rollerscapes.net> + * @author Andrew Johnson + * @author Fabien Potencier <fabien@symfony.com> + */ +class Pbkdf2PasswordEncoder extends BasePasswordEncoder +{ + private $algorithm; + private $encodeHashAsBase64; + private $iterations; + private $length; + + /** + * Constructor. + * + * @param string $algorithm The digest algorithm to use + * @param Boolean $encodeHashAsBase64 Whether to base64 encode the password hash + * @param integer $iterations The number of iterations to use to stretch the password hash + * @param integer $length Length of derived key to create + */ + public function __construct($algorithm = 'sha512', $encodeHashAsBase64 = true, $iterations = 1000, $length = 40) + { + $this->algorithm = $algorithm; + $this->encodeHashAsBase64 = $encodeHashAsBase64; + $this->iterations = $iterations; + $this->length = $length; + } + + /** + * {@inheritdoc} + * + * @throws \LogicException when the algorithm is not supported + */ + public function encodePassword($raw, $salt) + { + if (!in_array($this->algorithm, hash_algos(), true)) { + throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm)); + } + + if (function_exists('hash_pbkdf2')) { + $digest = hash_pbkdf2($this->algorithm, $raw, $salt, $this->iterations, $this->length, true); + } else { + $digest = $this->hashPbkdf2($this->algorithm, $raw, $salt, $this->iterations, $this->length); + } + + return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest); + } + + /** + * {@inheritdoc} + */ + public function isPasswordValid($encoded, $raw, $salt) + { + return $this->comparePasswords($encoded, $this->encodePassword($raw, $salt)); + } + + private function hashPbkdf2($algorithm, $password, $salt, $iterations, $length = 0) + { + // Number of blocks needed to create the derived key + $blocks = ceil($length / strlen(hash($algorithm, null, true))); + $digest = ''; + + for ($i = 1; $i <= $blocks; $i++) { + $ib = $block = hash_hmac($algorithm, $salt . pack('N', $i), $password, true); + + // Iterations + for ($j = 1; $j < $iterations; $j++) { + $ib ^= ($block = hash_hmac($algorithm, $block, $password, true)); + } + + $digest .= $ib; + } + + return substr($digest, 0, $this->length); + } +} |