From 50590dce81cae24cc0dd97918403df59bc5cdfda Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Apr 2019 21:13:54 +0200 Subject: [PATCH] [Security] add PasswordEncoderInterface::needsRehash() --- UPGRADE-4.4.md | 5 +++++ UPGRADE-5.0.md | 1 + src/Symfony/Component/Security/CHANGELOG.md | 5 +++++ .../Core/Encoder/BasePasswordEncoder.php | 8 +++++++ .../Core/Encoder/NativePasswordEncoder.php | 8 +++++++ .../Core/Encoder/PasswordEncoderInterface.php | 2 ++ .../Core/Encoder/SodiumPasswordEncoder.php | 16 ++++++++++++++ .../Core/Encoder/UserPasswordEncoder.php | 10 +++++++++ .../Encoder/UserPasswordEncoderInterface.php | 2 ++ .../Tests/Encoder/BasePasswordEncoderTest.php | 6 +++++ .../Encoder/NativePasswordEncoderTest.php | 13 +++++++++++ .../Encoder/SodiumPasswordEncoderTest.php | 13 +++++++++++ .../Tests/Encoder/UserPasswordEncoderTest.php | 22 +++++++++++++++++++ 13 files changed, 111 insertions(+) diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 73d80523a2..822164d41c 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -41,6 +41,11 @@ MonologBridge * The `RouteProcessor` has been marked final. +Security +-------- + + * Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` should add a new `needsRehash()` method + TwigBridge ---------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 1f1bd58ff5..72c7b28288 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -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. diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 24d15f7e78..78fba3fa99 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.4.0 +----- + + * Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface` + 4.3.0 ----- diff --git a/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php index 3c3ea1aa17..2609b3c7aa 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php @@ -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. * diff --git a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php index a99d064eeb..a05eb288e5 100644 --- a/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/NativePasswordEncoder.php @@ -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); + } } diff --git a/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php b/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php index e0573051eb..748b82d859 100644 --- a/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php +++ b/src/Symfony/Component/Security/Core/Encoder/PasswordEncoderInterface.php @@ -17,6 +17,8 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; * PasswordEncoderInterface is the interface for all encoders. * * @author Fabien Potencier + * + * @method bool needsRehash(string $encoded) */ interface PasswordEncoderInterface { diff --git a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php index e9bd6a63c9..82cb1e17dd 100644 --- a/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php @@ -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.'); + } } diff --git a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php index 3efc8c6d48..ad9d929deb 100644 --- a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoder.php @@ -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); + } } diff --git a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php index 7861caab20..911bbe5282 100644 --- a/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php +++ b/src/Symfony/Component/Security/Core/Encoder/UserPasswordEncoderInterface.php @@ -17,6 +17,8 @@ use Symfony\Component\Security\Core\User\UserInterface; * UserPasswordEncoderInterface is the interface for the password encoder service. * * @author Ariel Ferrandini + * + * @method bool needsRehash(UserInterface $user, string $encoded) */ interface UserPasswordEncoderInterface { diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php index 2251cfdf90..2b101c3b3b 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php @@ -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(); diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php index 681b91a1ee..55e518b491 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/NativePasswordEncoderTest.php @@ -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)); + } } diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php index 84c8b4849e..daa0da0120 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/SodiumPasswordEncoderTest.php @@ -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)); + } } diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/UserPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/UserPasswordEncoderTest.php index 41a602f976..9bd10a9642 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/UserPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/UserPasswordEncoderTest.php @@ -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)); + } }