Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace HowToSecurePasswordsWithBCryptNET;

public class PasswordLoginService
{
public const int CurrentWorkFactor = 11;

public string? LoginAndUpgrade(string password, string storedHash)
{
if (!BCrypt.Net.BCrypt.Verify(password, storedHash))
return null;

return BCrypt.Net.BCrypt.PasswordNeedsRehash(storedHash, CurrentWorkFactor)
? BCrypt.Net.BCrypt.HashPassword(password, CurrentWorkFactor)
: null;
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Please, check test project for examples.");
using HowToSecurePasswordsWithBCryptNET;

var passwordHash = BCrypt.Net.BCrypt.HashPassword("Password123!");
Console.WriteLine($"Hash: {passwordHash}");
Console.WriteLine($"Verified: {BCrypt.Net.BCrypt.Verify("Password123!", passwordHash)}");

var oldHash = BCrypt.Net.BCrypt.HashPassword("Password123!", workFactor: 6);
var upgradedHash = new PasswordLoginService().LoginAndUpgrade("Password123!", oldHash);
Console.WriteLine($"Upgraded hash: {upgradedHash}");
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Test;

using BCrypt.Net;
using HowToSecurePasswordsWithBCryptNET;

public class BCryptTests
{
Expand Down Expand Up @@ -57,4 +58,153 @@ public void WhenHashingPasswordWithHigherWorkFactor_ThenReturnsNotNullString()

Assert.NotNull(passwordHash);
}
}

[Fact]
public void WhenWorkFactorBelowMinimum_ThenPasswordNeedsRehashReturnsTrue()
{
var passwordHash = BCrypt.HashPassword("Password123!", workFactor: 6);

var result = BCrypt.PasswordNeedsRehash(passwordHash, 11);

Assert.True(result);
}

[Fact]
public void WhenWorkFactorAtMinimum_ThenPasswordNeedsRehashReturnsFalse()
{
var passwordHash = BCrypt.HashPassword("Password123!", workFactor: 11);

var result = BCrypt.PasswordNeedsRehash(passwordHash, 11);

Assert.False(result);
}

[Fact]
public void WhenInterrogatingHash_ThenReturnsVersionAndWorkFactor()
{
var passwordHash = BCrypt.HashPassword("Password123!");

var info = BCrypt.InterrogateHash(passwordHash);

Assert.Equal("2a", info.Version);
Assert.Equal("11", info.WorkFactor);
Assert.Equal("$2a$11", info.Settings);
Assert.Equal(60, passwordHash.Length);
}

[Fact]
public void WhenVerifyingPhpGenerated2yHash_ThenVerificationSucceeds()
{
// The password_verify() example from the PHP manual.
const string phpHash = "$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a";

Assert.True(BCrypt.Verify("rasmuslerdorf", phpHash));
Assert.False(BCrypt.Verify("Wr0ngPassword!", phpHash));
}

[Theory]
[InlineData('a')]
[InlineData('b')]
[InlineData('x')]
[InlineData('y')]
public void WhenUsingSupportedRevision_ThenHashVerifies(char revision)
{
var salt = BCrypt.GenerateSalt(6, revision);
var passwordHash = BCrypt.HashPassword("Password123!", salt);

Assert.StartsWith($"$2{revision}$", passwordHash);
Assert.True(BCrypt.Verify("Password123!", passwordHash));
}

[Fact]
public void WhenVerifyingUnsupportedRevision_ThenThrowsSaltParseException()
{
const string hash = "$2c$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a";

Assert.Throws<SaltParseException>(() => BCrypt.Verify("rasmuslerdorf", hash));
}

[Fact]
public void WhenVerifyingEnhancedHashWithPlainVerify_ThenVerificationFails()
{
var passwordHash = BCrypt.EnhancedHashPassword("Password123!");

Assert.StartsWith("$2a$11$", passwordHash);
Assert.False(BCrypt.Verify("Password123!", passwordHash));
Assert.True(BCrypt.EnhancedVerify("Password123!", passwordHash));
Assert.False(BCrypt.EnhancedVerify("Password123!", passwordHash, HashType.SHA512));
}

[Fact]
public void WhenPasswordsShareFirst72Bytes_ThenEitherVerifiesAgainstEitherHash()
{
var prefix = new string('p', 72);
var firstPassword = prefix + "ONE";
var secondPassword = prefix + "TWO";

var firstHash = BCrypt.HashPassword(firstPassword, workFactor: 6);
var secondHash = BCrypt.HashPassword(secondPassword, workFactor: 6);

Assert.True(BCrypt.Verify(secondPassword, firstHash));
Assert.True(BCrypt.Verify(firstPassword, secondHash));
}

[Fact]
public void WhenPasswordsShareFirst72Bytes_ThenEnhancedVerifyTellsThemApart()
{
var prefix = new string('p', 72);
var firstHash = BCrypt.EnhancedHashPassword(prefix + "ONE", workFactor: 6);

Assert.False(BCrypt.EnhancedVerify(prefix + "TWO", firstHash));
}

[Theory]
[InlineData(3)]
[InlineData(32)]
public void WhenWorkFactorOutOfRange_ThenThrowsArgumentOutOfRangeException(int workFactor)
{
var exception = Assert.Throws<ArgumentOutOfRangeException>(
() => BCrypt.HashPassword("Password123!", workFactor));

Assert.Equal("workFactor", exception.ParamName);
}

[Fact]
public void WhenWorkFactorAtBounds_ThenSaltIsGenerated()
{
// Never hash at work factor 31: that is 2^31 rounds. The salt alone proves the bound.
Assert.StartsWith("$2a$04$", BCrypt.GenerateSalt(4));
Assert.StartsWith("$2a$31$", BCrypt.GenerateSalt(31));
}

[Fact]
public void WhenStoredHashIsUnderCost_ThenLoginReturnsUpgradedHash()
{
var service = new PasswordLoginService();
var storedHash = BCrypt.HashPassword("Password123!", workFactor: 6);

var newHash = service.LoginAndUpgrade("Password123!", storedHash);

Assert.NotNull(newHash);
Assert.StartsWith("$2a$11$", newHash);
Assert.True(BCrypt.Verify("Password123!", newHash));
}

[Fact]
public void WhenStoredHashIsCurrent_ThenLoginReturnsNull()
{
var service = new PasswordLoginService();
var storedHash = BCrypt.HashPassword("Password123!", workFactor: 11);

Assert.Null(service.LoginAndUpgrade("Password123!", storedHash));
}

[Fact]
public void WhenPasswordIsWrong_ThenLoginReturnsNull()
{
var service = new PasswordLoginService();
var storedHash = BCrypt.HashPassword("Password123!", workFactor: 6);

Assert.Null(service.LoginAndUpgrade("Wr0ngPassword!", storedHash));
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PackageReference Include="coverlet.collector" Version="10.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HowToSecurePasswordsWithBCryptNET\HowToSecurePasswordsWithBCryptNET.csproj" />
</ItemGroup>

</Project>
Loading