11namespace Test ;
22
33using BCrypt . Net ;
4+ using HowToSecurePasswordsWithBCryptNET ;
45
56public class BCryptTests
67{
@@ -57,4 +58,153 @@ public void WhenHashingPasswordWithHigherWorkFactor_ThenReturnsNotNullString()
5758
5859 Assert . NotNull ( passwordHash ) ;
5960 }
60- }
61+
62+ [ Fact ]
63+ public void WhenWorkFactorBelowMinimum_ThenPasswordNeedsRehashReturnsTrue ( )
64+ {
65+ var passwordHash = BCrypt . HashPassword ( "Password123!" , workFactor : 6 ) ;
66+
67+ var result = BCrypt . PasswordNeedsRehash ( passwordHash , 11 ) ;
68+
69+ Assert . True ( result ) ;
70+ }
71+
72+ [ Fact ]
73+ public void WhenWorkFactorAtMinimum_ThenPasswordNeedsRehashReturnsFalse ( )
74+ {
75+ var passwordHash = BCrypt . HashPassword ( "Password123!" , workFactor : 11 ) ;
76+
77+ var result = BCrypt . PasswordNeedsRehash ( passwordHash , 11 ) ;
78+
79+ Assert . False ( result ) ;
80+ }
81+
82+ [ Fact ]
83+ public void WhenInterrogatingHash_ThenReturnsVersionAndWorkFactor ( )
84+ {
85+ var passwordHash = BCrypt . HashPassword ( "Password123!" ) ;
86+
87+ var info = BCrypt . InterrogateHash ( passwordHash ) ;
88+
89+ Assert . Equal ( "2a" , info . Version ) ;
90+ Assert . Equal ( "11" , info . WorkFactor ) ;
91+ Assert . Equal ( "$2a$11" , info . Settings ) ;
92+ Assert . Equal ( 60 , passwordHash . Length ) ;
93+ }
94+
95+ [ Fact ]
96+ public void WhenVerifyingPhpGenerated2yHash_ThenVerificationSucceeds ( )
97+ {
98+ // The password_verify() example from the PHP manual.
99+ const string phpHash = "$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a" ;
100+
101+ Assert . True ( BCrypt . Verify ( "rasmuslerdorf" , phpHash ) ) ;
102+ Assert . False ( BCrypt . Verify ( "Wr0ngPassword!" , phpHash ) ) ;
103+ }
104+
105+ [ Theory ]
106+ [ InlineData ( 'a' ) ]
107+ [ InlineData ( 'b' ) ]
108+ [ InlineData ( 'x' ) ]
109+ [ InlineData ( 'y' ) ]
110+ public void WhenUsingSupportedRevision_ThenHashVerifies ( char revision )
111+ {
112+ var salt = BCrypt . GenerateSalt ( 6 , revision ) ;
113+ var passwordHash = BCrypt . HashPassword ( "Password123!" , salt ) ;
114+
115+ Assert . StartsWith ( $ "$2{ revision } $", passwordHash ) ;
116+ Assert . True ( BCrypt . Verify ( "Password123!" , passwordHash ) ) ;
117+ }
118+
119+ [ Fact ]
120+ public void WhenVerifyingUnsupportedRevision_ThenThrowsSaltParseException ( )
121+ {
122+ const string hash = "$2c$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a" ;
123+
124+ Assert . Throws < SaltParseException > ( ( ) => BCrypt . Verify ( "rasmuslerdorf" , hash ) ) ;
125+ }
126+
127+ [ Fact ]
128+ public void WhenVerifyingEnhancedHashWithPlainVerify_ThenVerificationFails ( )
129+ {
130+ var passwordHash = BCrypt . EnhancedHashPassword ( "Password123!" ) ;
131+
132+ Assert . StartsWith ( "$2a$11$" , passwordHash ) ;
133+ Assert . False ( BCrypt . Verify ( "Password123!" , passwordHash ) ) ;
134+ Assert . True ( BCrypt . EnhancedVerify ( "Password123!" , passwordHash ) ) ;
135+ Assert . False ( BCrypt . EnhancedVerify ( "Password123!" , passwordHash , HashType . SHA512 ) ) ;
136+ }
137+
138+ [ Fact ]
139+ public void WhenPasswordsShareFirst72Bytes_ThenEitherVerifiesAgainstEitherHash ( )
140+ {
141+ var prefix = new string ( 'p' , 72 ) ;
142+ var firstPassword = prefix + "ONE" ;
143+ var secondPassword = prefix + "TWO" ;
144+
145+ var firstHash = BCrypt . HashPassword ( firstPassword , workFactor : 6 ) ;
146+ var secondHash = BCrypt . HashPassword ( secondPassword , workFactor : 6 ) ;
147+
148+ Assert . True ( BCrypt . Verify ( secondPassword , firstHash ) ) ;
149+ Assert . True ( BCrypt . Verify ( firstPassword , secondHash ) ) ;
150+ }
151+
152+ [ Fact ]
153+ public void WhenPasswordsShareFirst72Bytes_ThenEnhancedVerifyTellsThemApart ( )
154+ {
155+ var prefix = new string ( 'p' , 72 ) ;
156+ var firstHash = BCrypt . EnhancedHashPassword ( prefix + "ONE" , workFactor : 6 ) ;
157+
158+ Assert . False ( BCrypt . EnhancedVerify ( prefix + "TWO" , firstHash ) ) ;
159+ }
160+
161+ [ Theory ]
162+ [ InlineData ( 3 ) ]
163+ [ InlineData ( 32 ) ]
164+ public void WhenWorkFactorOutOfRange_ThenThrowsArgumentOutOfRangeException ( int workFactor )
165+ {
166+ var exception = Assert . Throws < ArgumentOutOfRangeException > (
167+ ( ) => BCrypt . HashPassword ( "Password123!" , workFactor ) ) ;
168+
169+ Assert . Equal ( "workFactor" , exception . ParamName ) ;
170+ }
171+
172+ [ Fact ]
173+ public void WhenWorkFactorAtBounds_ThenSaltIsGenerated ( )
174+ {
175+ // Never hash at work factor 31: that is 2^31 rounds. The salt alone proves the bound.
176+ Assert . StartsWith ( "$2a$04$" , BCrypt . GenerateSalt ( 4 ) ) ;
177+ Assert . StartsWith ( "$2a$31$" , BCrypt . GenerateSalt ( 31 ) ) ;
178+ }
179+
180+ [ Fact ]
181+ public void WhenStoredHashIsUnderCost_ThenLoginReturnsUpgradedHash ( )
182+ {
183+ var service = new PasswordLoginService ( ) ;
184+ var storedHash = BCrypt . HashPassword ( "Password123!" , workFactor : 6 ) ;
185+
186+ var newHash = service . LoginAndUpgrade ( "Password123!" , storedHash ) ;
187+
188+ Assert . NotNull ( newHash ) ;
189+ Assert . StartsWith ( "$2a$11$" , newHash ) ;
190+ Assert . True ( BCrypt . Verify ( "Password123!" , newHash ) ) ;
191+ }
192+
193+ [ Fact ]
194+ public void WhenStoredHashIsCurrent_ThenLoginReturnsNull ( )
195+ {
196+ var service = new PasswordLoginService ( ) ;
197+ var storedHash = BCrypt . HashPassword ( "Password123!" , workFactor : 11 ) ;
198+
199+ Assert . Null ( service . LoginAndUpgrade ( "Password123!" , storedHash ) ) ;
200+ }
201+
202+ [ Fact ]
203+ public void WhenPasswordIsWrong_ThenLoginReturnsNull ( )
204+ {
205+ var service = new PasswordLoginService ( ) ;
206+ var storedHash = BCrypt . HashPassword ( "Password123!" , workFactor : 6 ) ;
207+
208+ Assert . Null ( service . LoginAndUpgrade ( "Wr0ngPassword!" , storedHash ) ) ;
209+ }
210+ }
0 commit comments