1+ <?php
2+
3+ namespace ProgrammatorDev \Validator \Rule ;
4+
5+ use ProgrammatorDev \Validator \Exception \PasswordStrengthException ;
6+ use ProgrammatorDev \Validator \Exception \UnexpectedOptionException ;
7+ use ProgrammatorDev \Validator \Exception \UnexpectedTypeException ;
8+
9+ class PasswordStrength extends AbstractRule implements RuleInterface
10+ {
11+ private const STRENGTH_VERY_WEAK = 'very-weak ' ;
12+ public const STRENGTH_WEAK = 'weak ' ;
13+ public const STRENGTH_MEDIUM = 'medium ' ;
14+ public const STRENGTH_STRONG = 'strong ' ;
15+ public const STRENGTH_VERY_STRONG = 'very-strong ' ;
16+
17+ private const STRENGTH_OPTIONS = [
18+ self ::STRENGTH_WEAK ,
19+ self ::STRENGTH_MEDIUM ,
20+ self ::STRENGTH_STRONG ,
21+ self ::STRENGTH_VERY_STRONG
22+ ];
23+
24+ private const STRENGTH_SCORE = [
25+ self ::STRENGTH_VERY_WEAK => 0 ,
26+ self ::STRENGTH_WEAK => 1 ,
27+ self ::STRENGTH_MEDIUM => 2 ,
28+ self ::STRENGTH_STRONG => 3 ,
29+ self ::STRENGTH_VERY_STRONG => 4
30+ ];
31+
32+ private string $ message = 'The password strength is not strong enough. ' ;
33+
34+ public function __construct (
35+ private readonly string $ minStrength = self ::STRENGTH_MEDIUM ,
36+ ?string $ message = null
37+ )
38+ {
39+ $ this ->message = $ message ?? $ this ->message ;
40+ }
41+
42+ public function assert (#[\SensitiveParameter] mixed $ value , ?string $ name = null ): void
43+ {
44+ if (!\in_array ($ this ->minStrength , self ::STRENGTH_OPTIONS )) {
45+ throw new UnexpectedOptionException ('minStrength ' , self ::STRENGTH_OPTIONS , $ this ->minStrength );
46+ }
47+
48+ if (!\is_string ($ value )) {
49+ throw new UnexpectedTypeException ('string ' , get_debug_type ($ value ));
50+ }
51+
52+ $ minScore = self ::STRENGTH_SCORE [$ this ->minStrength ];
53+ $ score = self ::STRENGTH_SCORE [$ this ->calcStrength ($ value )];
54+
55+ if ($ minScore > $ score ) {
56+ throw new PasswordStrengthException (
57+ message: $ this ->message ,
58+ parameters: [
59+ 'name ' => $ name ,
60+ 'minStrength ' => $ this ->minStrength
61+ ]
62+ );
63+ }
64+ }
65+
66+ private function calcStrength (#[\SensitiveParameter] string $ password ): string
67+ {
68+ $ length = \strlen ($ password );
69+ $ chars = \count_chars ($ password , 1 );
70+
71+ $ control = $ digit = $ upper = $ lower = $ symbol = $ other = 0 ;
72+ foreach ($ chars as $ char => $ count ) {
73+ match (true ) {
74+ ($ char < 32 || $ char === 127 ) => $ control = 33 ,
75+ ($ char >= 48 && $ char <= 57 ) => $ digit = 10 ,
76+ ($ char >= 65 && $ char <= 90 ) => $ upper = 26 ,
77+ ($ char >= 97 && $ char <= 122 ) => $ lower = 26 ,
78+ ($ char >= 128 ) => $ other = 128 ,
79+ default => $ symbol = 33 ,
80+ };
81+ }
82+
83+ $ pool = $ control + $ digit + $ upper + $ lower + $ symbol + $ other ;
84+ $ entropy = \log (\pow ($ pool , $ length ), 2 );
85+
86+ return match (true ) {
87+ $ entropy >= 128 => self ::STRENGTH_VERY_STRONG ,
88+ $ entropy >= 96 => self ::STRENGTH_STRONG ,
89+ $ entropy >= 80 => self ::STRENGTH_MEDIUM ,
90+ $ entropy >= 64 => self ::STRENGTH_WEAK ,
91+ default => self ::STRENGTH_VERY_WEAK
92+ };
93+ }
94+ }
0 commit comments