From 431dcf6b2f38dd7bc9b2ac747964d5c2327c5755 Mon Sep 17 00:00:00 2001 From: Yannick Date: Fri, 10 May 2024 11:54:20 +0200 Subject: [PATCH 1/4] feat : refactor PasswordStrengthValidator with a PasswordStrengthEstimatorInterface to be reused outside --- .../Constraints/PasswordStrengthValidator.php | 49 ++----------------- .../Password/PasswordStrengthEstimator.php | 41 ++++++++++++++++ .../PasswordStrengthEstimatorInterface.php | 17 +++++++ .../PasswordStrengthValidatorTest.php | 3 +- .../PasswordStrengthEstimatorTest.php | 28 +++++++++++ 5 files changed, 92 insertions(+), 46 deletions(-) create mode 100644 src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php create mode 100644 src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php create mode 100644 src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php diff --git a/src/Symfony/Component/Validator/Constraints/PasswordStrengthValidator.php b/src/Symfony/Component/Validator/Constraints/PasswordStrengthValidator.php index 96a4a74f7287a..09f2004287693 100644 --- a/src/Symfony/Component/Validator/Constraints/PasswordStrengthValidator.php +++ b/src/Symfony/Component/Validator/Constraints/PasswordStrengthValidator.php @@ -15,14 +15,12 @@ use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Password\PasswordStrengthEstimatorInterface; final class PasswordStrengthValidator extends ConstraintValidator { - /** - * @param (\Closure(string):PasswordStrength::STRENGTH_*)|null $passwordStrengthEstimator - */ public function __construct( - private readonly ?\Closure $passwordStrengthEstimator = null, + private readonly PasswordStrengthEstimatorInterface $passwordStrengthEstimator ) { } @@ -39,8 +37,8 @@ public function validate(#[\SensitiveParameter] mixed $value, Constraint $constr if (!\is_string($value) && !$value instanceof \Stringable) { throw new UnexpectedValueException($value, 'string'); } - $passwordStrengthEstimator = $this->passwordStrengthEstimator ?? self::estimateStrength(...); - $strength = $passwordStrengthEstimator((string) $value); + + $strength = $this->passwordStrengthEstimator->estimateStrength((string) $value); if ($strength < $constraint->minScore) { $this->context->buildViolation($constraint->message) @@ -49,43 +47,4 @@ public function validate(#[\SensitiveParameter] mixed $value, Constraint $constr ->addViolation(); } } - - /** - * Returns the estimated strength of a password. - * - * The higher the value, the stronger the password. - * - * @return PasswordStrength::STRENGTH_* - */ - private static function estimateStrength(#[\SensitiveParameter] string $password): int - { - if (!$length = \strlen($password)) { - return PasswordStrength::STRENGTH_VERY_WEAK; - } - $password = count_chars($password, 1); - $chars = \count($password); - - $control = $digit = $upper = $lower = $symbol = $other = 0; - foreach ($password as $chr => $count) { - match (true) { - $chr < 32 || 127 === $chr => $control = 33, - 48 <= $chr && $chr <= 57 => $digit = 10, - 65 <= $chr && $chr <= 90 => $upper = 26, - 97 <= $chr && $chr <= 122 => $lower = 26, - 128 <= $chr => $other = 128, - default => $symbol = 33, - }; - } - - $pool = $lower + $upper + $digit + $symbol + $control + $other; - $entropy = $chars * log($pool, 2) + ($length - $chars) * log($chars, 2); - - return match (true) { - $entropy >= 120 => PasswordStrength::STRENGTH_VERY_STRONG, - $entropy >= 100 => PasswordStrength::STRENGTH_STRONG, - $entropy >= 80 => PasswordStrength::STRENGTH_MEDIUM, - $entropy >= 60 => PasswordStrength::STRENGTH_WEAK, - default => PasswordStrength::STRENGTH_VERY_WEAK, - }; - } } diff --git a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php new file mode 100644 index 0000000000000..19a5c1373a6ef --- /dev/null +++ b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php @@ -0,0 +1,41 @@ + $count) { + match (true) { + $chr < 32 || 127 === $chr => $control = 33, + 48 <= $chr && $chr <= 57 => $digit = 10, + 65 <= $chr && $chr <= 90 => $upper = 26, + 97 <= $chr && $chr <= 122 => $lower = 26, + 128 <= $chr => $other = 128, + default => $symbol = 33, + }; + } + + $pool = $lower + $upper + $digit + $symbol + $control + $other; + $entropy = $chars * log($pool, 2) + ($length - $chars) * log($chars, 2); + + return match (true) { + $entropy >= 120 => PasswordStrength::STRENGTH_VERY_STRONG, + $entropy >= 100 => PasswordStrength::STRENGTH_STRONG, + $entropy >= 80 => PasswordStrength::STRENGTH_MEDIUM, + $entropy >= 60 => PasswordStrength::STRENGTH_WEAK, + default => PasswordStrength::STRENGTH_VERY_WEAK, + }; + } +} diff --git a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php new file mode 100644 index 0000000000000..083df7f45b598 --- /dev/null +++ b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php @@ -0,0 +1,17 @@ +> */ + public function getPasswords(): iterable + { + yield ['How-is-this', PasswordStrength::STRENGTH_WEAK]; + yield ['Reasonable-pwd', PasswordStrength::STRENGTH_MEDIUM]; + yield ['This 1s a very g00d Pa55word! ;-)', PasswordStrength::STRENGTH_VERY_STRONG]; + yield ['pudding-smack-👌🏼-fox-😎', PasswordStrength::STRENGTH_VERY_STRONG]; + yield [new StringableValue('How-is-this'), PasswordStrength::STRENGTH_WEAK]; + } +} From 3df4e3bc1607a8e04d73a2392653fd22711a1214 Mon Sep 17 00:00:00 2001 From: Yannick Date: Fri, 10 May 2024 15:22:24 +0200 Subject: [PATCH 2/4] fix : remove static from interface --- .../Component/Validator/Password/PasswordStrengthEstimator.php | 2 +- .../Validator/Password/PasswordStrengthEstimatorInterface.php | 2 +- .../Validator/Tests/Password/PasswordStrengthEstimatorTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php index 19a5c1373a6ef..04de6e3fddb67 100644 --- a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php +++ b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimator.php @@ -7,7 +7,7 @@ class PasswordStrengthEstimator implements PasswordStrengthEstimatorInterface { - public static function estimateStrength(#[\SensitiveParameter] string|Stringable $password): int + public function estimateStrength(#[\SensitiveParameter] string|Stringable $password): int { if (!$length = \strlen($password)) { return PasswordStrength::STRENGTH_VERY_WEAK; diff --git a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php index 083df7f45b598..4a867544044ee 100644 --- a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php +++ b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php @@ -13,5 +13,5 @@ interface PasswordStrengthEstimatorInterface * * @return PasswordStrength::STRENGTH_* */ - public static function estimateStrength(#[\SensitiveParameter] string|Stringable $password): int; + public function estimateStrength(#[\SensitiveParameter] string|Stringable $password): int; } diff --git a/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php b/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php index 2ffcc2944ebf6..161c0e5d5d072 100644 --- a/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php @@ -13,7 +13,7 @@ class PasswordStrengthEstimatorTest extends TestCase /** @dataProvider getPasswords */ public function testEstimateStrength(string|Stringable $password, int $expectedStrength): void { - self::assertEquals($expectedStrength, PasswordStrengthEstimator::estimateStrength($password)); + self::assertEquals($expectedStrength, (new PasswordStrengthEstimator())->estimateStrength($password)); } /** @return array> */ From c841c436d3d63f8a0f3e98f327fb9914470725db Mon Sep 17 00:00:00 2001 From: Yannick Date: Fri, 10 May 2024 21:34:13 +0200 Subject: [PATCH 3/4] fix : update docblock information for interface --- .../Validator/Password/PasswordStrengthEstimatorInterface.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php index 4a867544044ee..22b59c77166bf 100644 --- a/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php +++ b/src/Symfony/Component/Validator/Password/PasswordStrengthEstimatorInterface.php @@ -3,6 +3,7 @@ namespace Symfony\Component\Validator\Password; use Stringable; +use Symfony\Component\Validator\Constraints\PasswordStrength; interface PasswordStrengthEstimatorInterface { From 86098c56754554ab3b11fb782a688d1fc2d7d787 Mon Sep 17 00:00:00 2001 From: Yannick Date: Sat, 11 May 2024 18:26:44 +0200 Subject: [PATCH 4/4] test : update test from assertEquals to assertSame --- .../Validator/Tests/Password/PasswordStrengthEstimatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php b/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php index 161c0e5d5d072..8e37386064a99 100644 --- a/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Password/PasswordStrengthEstimatorTest.php @@ -13,7 +13,7 @@ class PasswordStrengthEstimatorTest extends TestCase /** @dataProvider getPasswords */ public function testEstimateStrength(string|Stringable $password, int $expectedStrength): void { - self::assertEquals($expectedStrength, (new PasswordStrengthEstimator())->estimateStrength($password)); + self::assertSame($expectedStrength, (new PasswordStrengthEstimator())->estimateStrength($password)); } /** @return array> */