0

I'm implementing a password reset functionality using Symfony2. I have questions about hashing and salting the confirmation code used to validate the reset request.

The $user variable contains an instance of Acme\SecurityBundle\Model\User. The algorithm for this class is bcrypt and the cost is 15.

Acme\SecurityBundle\Model\User

namespace Acme\SecurityBundle\Model;

use Acme\SecurityBundle\Model\om\BaseUser;
use Symfony\Component\Security\Core\Util\SecureRandom;

class User extends BaseUser
{
    public function getSalt()
    {
        $random = new SecureRandom();
        return base64_encode($random->nextBytes(128 / 8));
    }
}



Hashing and validating happens inside a controller as follows.


Hashing:

// Generate confirmation code
$tokenGenerator = new UriSafeTokenGenerator();
$resetConfirmationCodePlain = substr($tokenGenerator->generateToken(), 0, 20);
// Hash confirmation code
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$resetConfirmationCode = $encoder->encodePassword($resetConfirmationCodePlain, $user->getSalt());


Validating:

// Validate confirmation code
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$isValid = $encoder->isPasswordValid(
    $user->getResetConfirmationCode(), $confirmationCode, null
);


As you can see a salt is added while hashing, but not added while validating. Nonetheless the confirmation code validated successfully.

Can someone explain this? Does Symfony derive the salt from the hash or isn't the hash salted at all?

P.S. I update a user's password in the same way.

Stan
  • 493
  • 4
  • 15
  • Btw, the hash in de database looks like this: ``$2y$15$TjVaanFIV3psTlo4Y29HRexVg9nctTmXTcXeEoOZroZCgqMWroIT.`` – Stan Sep 10 '14 at 08:35
  • Or should I not pass a salt add all? [See this line in BCryptPasswordEncoder.php](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php#L57) – Stan Sep 10 '14 at 09:17

2 Answers2

2

Have a look at the hash-value itself...

$2y$10$nOUIs5kJ7naTuTFkBy1veuK0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
 |  |  |                     |
 |  |  |                     hash-value = K0kSxUFXfuaOKdOKf9xYT0KKIGSJwFa
 |  |  |
 |  |  salt = nOUIs5kJ7naTuTFkBy1veu (22 characters)
 |  |
 |  cost-factor = 10 = 2^10 iterations
 |
 hash-algorithm = 2y = BCrypt

...you can see that the salt is included in the stored hash-value. The verification function can read the used salt and other parameters from this stored hash, that's why you don't have to pass this parameters to the function.

According to Symfony's source code, the BCryptPasswordEncoder internally uses the PHP function password_hash(). This function will generate a safe salt if you omit the "salt" parameter, so i would recommend to leave out this parameter. If you pass a salt parameter, the function will take care that the salt has a valid format.

martinstoeckli
  • 23,430
  • 6
  • 56
  • 87
  • The ``getSalt`` method returns a base64 encoded string with a length of 24. How is the salt in the hash 22 characters? Are ``=`` characters stripped? – Stan Sep 10 '14 at 11:32
  • Thank you. I understand it now. I'll return `null` from ``getSalt`` to let PHP take care of this. – Stan Sep 10 '14 at 11:52
0

You're halfway there. The final hash produced using BCrypt comprises the cyphertext, the cost, and the salt used. The PHP password_hash function is general, and allows you to pass in a salt (this will be required for some algorithms), but if not provided on encode BCrypt will generate one, and on decode presumably it's ignored (since you can get it from the hash), so it's fine to pass in null.

So not only do you not need to pass in a salt when decoding, you probably shouldn't pass one in when encoding too, since BCrypt will take care of that in a cryptographically secure way.

See this question and the PHP docs for password_hash()

Community
  • 1
  • 1
frumious
  • 1,567
  • 15
  • 25
  • A [User Provider](http://symfony.com/doc/current/cookbook/security/custom_provider.html) requires the definition of the ``getSalt`` method. According to the note on the bottom of that page the value ``$password.'{'.$salt.'}';`` is created and then hashed with bcrypt if ``getSalt`` returns something, otherwise it's simply encoded using bcrypt. – Stan Sep 10 '14 at 11:36
  • And I don't store my salt, so how can the hash in AP successfully validate? Is get value ``getSalt`` returns completely ignored? – Stan Sep 10 '14 at 11:44
  • I suspect that Symfony doc page is inaccurate, at least for BCrypt. If you look at the source for the BCrypt PasswordEncoder, https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/BCryptPasswordEncoder.php, the cleartext password and salt are still separate when they're passed to `password_hash`. So the salt is still retrievable from the hash as we've discussed, and while you're not persisting the salt separately, you are persisting it indirectly when you save the whole hash. – frumious Sep 10 '14 at 12:15
  • https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php has a `mergePasswordAndSalt` function which behaves as in that doc, and is used by other Encoders, such as the MessageDigest one. – frumious Sep 10 '14 at 12:15