feature #31594 [Security] add PasswordEncoderInterface::needsRehash() (nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

[Security] add PasswordEncoderInterface::needsRehash()

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Split from #31153, with tests.

Commits
-------

50590dce81 [Security] add PasswordEncoderInterface::needsRehash()
This commit is contained in:
Robin Chalas 2019-06-04 05:02:33 +02:00
commit 1768c9365c
13 changed files with 111 additions and 0 deletions

View File

@ -41,6 +41,11 @@ MonologBridge
* The `RouteProcessor` has been marked final.
Security
--------
* Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` should add a new `needsRehash()` method
TwigBridge
----------

View File

@ -313,6 +313,7 @@ Routing
Security
--------
* Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` must have a new `needsRehash()` method
* The `Role` and `SwitchUserRole` classes have been removed.
* The `getReachableRoles()` method of the `RoleHierarchy` class has been removed. It has been replaced by the new
`getReachableRoleNames()` method.

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.4.0
-----
* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
4.3.0
-----

View File

@ -20,6 +20,14 @@ abstract class BasePasswordEncoder implements PasswordEncoderInterface
{
const MAX_PASSWORD_LENGTH = 4096;
/**
* {@inheritdoc}
*/
public function needsRehash(string $encoded): bool
{
return false;
}
/**
* Demerges a merge password and salt string.
*

View File

@ -87,4 +87,12 @@ final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSalti
return \strlen($raw) <= self::MAX_PASSWORD_LENGTH && password_verify($raw, $encoded);
}
/**
* {@inheritdoc}
*/
public function needsRehash(string $encoded): bool
{
return password_needs_rehash($encoded, $this->algo, $this->options);
}
}

View File

@ -17,6 +17,8 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException;
* PasswordEncoderInterface is the interface for all encoders.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @method bool needsRehash(string $encoded)
*/
interface PasswordEncoderInterface
{

View File

@ -99,4 +99,20 @@ final class SodiumPasswordEncoder implements PasswordEncoderInterface, SelfSalti
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
}
/**
* {@inheritdoc}
*/
public function needsRehash(string $encoded): bool
{
if (\function_exists('sodium_crypto_pwhash_str_needs_rehash')) {
return \sodium_crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit);
}
if (\extension_loaded('libsodium')) {
return \Sodium\crypto_pwhash_str_needs_rehash($encoded, $this->opsLimit, $this->memLimit);
}
throw new LogicException('Libsodium is not available. You should either install the sodium extension, upgrade to PHP 7.2+ or use a different encoder.');
}
}

View File

@ -46,4 +46,14 @@ class UserPasswordEncoder implements UserPasswordEncoderInterface
return $encoder->isPasswordValid($user->getPassword(), $raw, $user->getSalt());
}
/**
* {@inheritdoc}
*/
public function needsRehash(UserInterface $user, string $encoded): bool
{
$encoder = $this->encoderFactory->getEncoder($user);
return method_exists($encoder, 'needsRehash') && $encoder->needsRehash($encoded);
}
}

View File

@ -17,6 +17,8 @@ use Symfony\Component\Security\Core\User\UserInterface;
* UserPasswordEncoderInterface is the interface for the password encoder service.
*
* @author Ariel Ferrandini <arielferrandini@gmail.com>
*
* @method bool needsRehash(UserInterface $user, string $encoded)
*/
interface UserPasswordEncoderInterface
{

View File

@ -60,6 +60,12 @@ class BasePasswordEncoderTest extends TestCase
$this->assertFalse($this->invokeIsPasswordTooLong(str_repeat('a', 10)));
}
public function testNeedsRehash()
{
$encoder = new PasswordEncoder();
$this->assertFalse($encoder->needsRehash('foo'));
}
protected function invokeDemergePasswordAndSalt($password)
{
$encoder = new PasswordEncoder();

View File

@ -67,4 +67,17 @@ class NativePasswordEncoderTest extends TestCase
$this->assertFalse($encoder->isPasswordValid($result, str_repeat('a', 73), 'salt'));
$this->assertTrue($encoder->isPasswordValid($result, str_repeat('a', 72), 'salt'));
}
public function testNeedsRehash()
{
$encoder = new NativePasswordEncoder(4, 11000, 4);
$this->assertTrue($encoder->needsRehash('dummyhash'));
$hash = $encoder->encodePassword('foo', 'salt');
$this->assertFalse($encoder->needsRehash($hash));
$encoder = new NativePasswordEncoder(5, 11000, 5);
$this->assertTrue($encoder->needsRehash($hash));
}
}

View File

@ -60,4 +60,17 @@ class SodiumPasswordEncoderTest extends TestCase
$result = $encoder->encodePassword('password', 'salt');
$this->assertTrue($encoder->isPasswordValid($result, 'password', 'anotherSalt'));
}
public function testNeedsRehash()
{
$encoder = new SodiumPasswordEncoder(4, 11000);
$this->assertTrue($encoder->needsRehash('dummyhash'));
$hash = $encoder->encodePassword('foo', 'salt');
$this->assertFalse($encoder->needsRehash($hash));
$encoder = new SodiumPasswordEncoder(5, 11000);
$this->assertTrue($encoder->needsRehash($hash));
}
}

View File

@ -12,7 +12,10 @@
namespace Symfony\Component\Security\Core\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder;
use Symfony\Component\Security\Core\User\User;
class UserPasswordEncoderTest extends TestCase
{
@ -68,4 +71,23 @@ class UserPasswordEncoderTest extends TestCase
$isValid = $passwordEncoder->isPasswordValid($userMock, 'plainPassword');
$this->assertTrue($isValid);
}
public function testNeedsRehash()
{
$user = new User('username', null);
$encoder = new NativePasswordEncoder(4, 20000, 4);
$mockEncoderFactory = $this->getMockBuilder(EncoderFactoryInterface::class)->getMock();
$mockEncoderFactory->expects($this->any())
->method('getEncoder')
->with($user)
->will($this->onConsecutiveCalls($encoder, $encoder, new NativePasswordEncoder(5, 20000, 5), $encoder));
$passwordEncoder = new UserPasswordEncoder($mockEncoderFactory);
$hash = $passwordEncoder->encodePassword($user, 'foo', 'salt');
$this->assertFalse($passwordEncoder->needsRehash($user, $hash));
$this->assertTrue($passwordEncoder->needsRehash($user, $hash));
$this->assertFalse($passwordEncoder->needsRehash($user, $hash));
}
}