Skip to content

Commit c7ff0d2

Browse files
BCrypt.Net-Next: retarget to .NET 10, bump to 4.2.0, add rehash and compatibility tests
Both projects move from net7.0 to net10.0. BCrypt.Net-Next goes from 4.0.3 to 4.2.0 and the test stack is bumped. New tests cover PasswordNeedsRehash, InterrogateHash, a PHP-generated $2y$ hash, the accepted revisions, enhanced entropy against plain Verify, the 72-byte truncation, the work factor range, and the LoginAndUpgrade method that now lives in PasswordLoginService. Program.cs hashes, verifies and upgrades a password instead of printing a placeholder.
1 parent 09f1dfa commit c7ff0d2

5 files changed

Lines changed: 188 additions & 11 deletions

File tree

‎authorization-dotnet/HowToSecurePasswordsWithBCryptNET/HowToSecurePasswordsWithBCryptNET/HowToSecurePasswordsWithBCryptNET.csproj‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net7.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
11+
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
1212
</ItemGroup>
1313

1414
</Project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace HowToSecurePasswordsWithBCryptNET;
2+
3+
public class PasswordLoginService
4+
{
5+
public const int CurrentWorkFactor = 11;
6+
7+
public string? LoginAndUpgrade(string password, string storedHash)
8+
{
9+
if (!BCrypt.Net.BCrypt.Verify(password, storedHash))
10+
return null;
11+
12+
return BCrypt.Net.BCrypt.PasswordNeedsRehash(storedHash, CurrentWorkFactor)
13+
? BCrypt.Net.BCrypt.HashPassword(password, CurrentWorkFactor)
14+
: null;
15+
}
16+
}
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1-
// See https://aka.ms/new-console-template for more information
2-
Console.WriteLine("Please, check test project for examples.");
1+
using HowToSecurePasswordsWithBCryptNET;
2+
3+
var passwordHash = BCrypt.Net.BCrypt.HashPassword("Password123!");
4+
Console.WriteLine($"Hash: {passwordHash}");
5+
Console.WriteLine($"Verified: {BCrypt.Net.BCrypt.Verify("Password123!", passwordHash)}");
6+
7+
var oldHash = BCrypt.Net.BCrypt.HashPassword("Password123!", workFactor: 6);
8+
var upgradedHash = new PasswordLoginService().LoginAndUpgrade("Password123!", oldHash);
9+
Console.WriteLine($"Upgraded hash: {upgradedHash}");

‎authorization-dotnet/HowToSecurePasswordsWithBCryptNET/Test/BCryptTests.cs‎

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Test;
22

33
using BCrypt.Net;
4+
using HowToSecurePasswordsWithBCryptNET;
45

56
public 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+
}
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net7.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

88
<IsPackable>false</IsPackable>
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
13-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
14-
<PackageReference Include="xunit" Version="2.4.2" />
15-
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
12+
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
1616
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1717
<PrivateAssets>all</PrivateAssets>
1818
</PackageReference>
19-
<PackageReference Include="coverlet.collector" Version="3.1.2">
19+
<PackageReference Include="coverlet.collector" Version="10.1.0">
2020
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2121
<PrivateAssets>all</PrivateAssets>
2222
</PackageReference>
2323
</ItemGroup>
2424

25+
<ItemGroup>
26+
<ProjectReference Include="..\HowToSecurePasswordsWithBCryptNET\HowToSecurePasswordsWithBCryptNET.csproj" />
27+
</ItemGroup>
28+
2529
</Project>

0 commit comments

Comments
 (0)