Skip to content

Commit

Permalink
feat: argon 2 password hasher
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions committed Nov 5, 2023
1 parent 521d864 commit ae23dc0
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 50 deletions.
38 changes: 37 additions & 1 deletion TheIdServer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{1FC87EDC-78E7-465E-9721-D7B481D64ED7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{5CDF0690-B16C-4A75-80C2-F1EDC7C26514}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aguacongas.TheIdServer.Identity.Argon2PasswordHasher", "src\Identity\Aguacongas.TheIdServer.Identity.Argon2PasswordHasher\Aguacongas.TheIdServer.Identity.Argon2PasswordHasher.csproj", "{90BF8847-F021-470D-A2ED-37C0B0A6E388}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aguacongas.TheIdServer.Identity.Argon2PasswordHasher.Test", "test\Identity\Aguacongas.TheIdServer.Identity.Argon2PasswordHasher.Test\Aguacongas.TheIdServer.Identity.Argon2PasswordHasher.Test.csproj", "{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1318,6 +1326,30 @@ Global
{D2B86706-5A7D-4414-8AE4-E579B4DEA462}.Release|x64.Build.0 = Release|Any CPU
{D2B86706-5A7D-4414-8AE4-E579B4DEA462}.Release|x86.ActiveCfg = Release|Any CPU
{D2B86706-5A7D-4414-8AE4-E579B4DEA462}.Release|x86.Build.0 = Release|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Debug|x64.ActiveCfg = Debug|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Debug|x64.Build.0 = Debug|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Debug|x86.ActiveCfg = Debug|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Debug|x86.Build.0 = Debug|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Release|Any CPU.Build.0 = Release|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Release|x64.ActiveCfg = Release|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Release|x64.Build.0 = Release|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Release|x86.ActiveCfg = Release|Any CPU
{90BF8847-F021-470D-A2ED-37C0B0A6E388}.Release|x86.Build.0 = Release|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Debug|x64.ActiveCfg = Debug|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Debug|x64.Build.0 = Debug|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Debug|x86.ActiveCfg = Debug|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Debug|x86.Build.0 = Debug|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Release|Any CPU.Build.0 = Release|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Release|x64.ActiveCfg = Release|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Release|x64.Build.0 = Release|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Release|x86.ActiveCfg = Release|Any CPU
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -1342,7 +1374,7 @@ Global
{D5F297EF-4649-46D4-994A-03F26CF4D5DE} = {F6FFE02C-E4E0-43C3-A2EC-34B62E7281C6}
{86767A50-87BE-4A2A-B53E-CDAD29DCD67E} = {F6FFE02C-E4E0-43C3-A2EC-34B62E7281C6}
{A36931C8-4A4E-4A92-961C-D0AA6E8CB9C7} = {98BBB159-779B-497B-927B-723592E84860}
{837ED9F5-91A3-4AEB-9A29-22881643F6C2} = {DF545B54-78FB-42D0-A842-A1490A579B37}
{837ED9F5-91A3-4AEB-9A29-22881643F6C2} = {1FC87EDC-78E7-465E-9721-D7B481D64ED7}
{9F64CDFF-6347-4F62-B74F-C6CA2743E667} = {DF545B54-78FB-42D0-A842-A1490A579B37}
{8A3B91E5-02A3-42F0-B2E7-E419B5086FFA} = {DF545B54-78FB-42D0-A842-A1490A579B37}
{C2806393-D1EB-4DDE-8A4B-AA65014B8D08} = {8A3B91E5-02A3-42F0-B2E7-E419B5086FFA}
Expand Down Expand Up @@ -1420,6 +1452,10 @@ Global
{DC84EE86-A50E-4183-B092-899499E748A9} = {5D0BA166-1FCB-4D8B-97D6-655A5E16FF74}
{389ABB64-4F0B-4DD8-8B40-9FC01E1D3C50} = {5D0BA166-1FCB-4D8B-97D6-655A5E16FF74}
{D2B86706-5A7D-4414-8AE4-E579B4DEA462} = {5D0BA166-1FCB-4D8B-97D6-655A5E16FF74}
{1FC87EDC-78E7-465E-9721-D7B481D64ED7} = {DF545B54-78FB-42D0-A842-A1490A579B37}
{5CDF0690-B16C-4A75-80C2-F1EDC7C26514} = {DE50F426-4409-4573-8502-93364ED12E0C}
{90BF8847-F021-470D-A2ED-37C0B0A6E388} = {1FC87EDC-78E7-465E-9721-D7B481D64ED7}
{4ED9CAC7-0113-4B25-A580-6B1D5DA05173} = {5CDF0690-B16C-4A75-80C2-F1EDC7C26514}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5283BE0B-F6F2-4458-B12F-64C78CFF8CBA}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>Olivier Lefebvre</Authors>
<Description>Argon2 Password Hasher for ASP.NET Core Identity.</Description>
<Copyright>Copyright (c) 2023 @Olivier Lefebvre</Copyright>
<PackageProjectUrl>https://github.com/Aguafrommars/TheIdServer/tree/master/src/Aguacongas.TheIdServer.Identity.Argon2PasswordHasher</PackageProjectUrl>
<RepositoryUrl>https://github.com/aguacongas/TheIdServer</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>aspnetcore;identity;Argon2;password;hashing;hash;security</PackageTags>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageIcon>package-icon.png</PackageIcon>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Geralt" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0-rc.2.23479.6" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.0-rc.2.23480.2" />
</ItemGroup>
<ItemGroup>
<None Include="package-icon.png" Pack="true" PackagePath="" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Geralt;
using Microsoft.Extensions.Options;

namespace Aguacongas.TheIdServer.Identity.Argon2PasswordHasher;
internal class Argon2Id : IArgon2Id
{
private readonly IOptions<Argon2PasswordHasherOptions> _options;

public Argon2Id(IOptions<Argon2PasswordHasherOptions> options)
{
ArgumentNullException.ThrowIfNull(options);
_options = options;
}
public Span<byte> ComputeHash(ReadOnlySpan<byte> password)
{
var settings = _options.Value;
var hash = new Span<byte>(new byte[Argon2id.MaxHashSize]);
Argon2id.ComputeHash(hash, password, settings.Interations, settings.Memory);
return hash;
}

public bool NeedsRehash(ReadOnlySpan<byte> hash)
{
var settings = _options.Value;
return Argon2id.NeedsRehash(hash, settings.Interations, settings.Memory);
}

public bool VerifyHash(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> password)
=> Argon2id.VerifyHash(hash, password);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Buffers;
using System.Text;

namespace Aguacongas.TheIdServer.Identity.Argon2PasswordHasher;

/// <summary>
/// Argon2 passwor hasher
/// </summary>
/// <typeparam name="TUser"></typeparam>
public class Argon2PasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : class
{
private readonly IArgon2Id _argon2Id;
private readonly IOptions<Argon2PasswordHasherOptions> _options;

/// <summary>
/// Initialize a new instance of <see cref="Argon2PasswordHasher{TUser}"/>
/// </summary>
/// <param name="argon2Id"><see cref="IArgon2Id"/> implementation</param>
/// <param name="options">password hasher options</param>
public Argon2PasswordHasher(IArgon2Id argon2Id, IOptions<Argon2PasswordHasherOptions> options)
{
ArgumentNullException.ThrowIfNull(argon2Id);
ArgumentNullException.ThrowIfNull(options);
_argon2Id = argon2Id;
_options = options;
}

/// <inheritdoc/>
public string HashPassword(TUser user, string password)
{
ArgumentException.ThrowIfNullOrWhiteSpace(password);

var passwordSpan = new ReadOnlySpan<byte>(Encoding.UTF8.GetBytes(password));
var hash = _argon2Id.ComputeHash(passwordSpan);
return Convert.ToBase64String(new byte[]
{
_options.Value.HashPrefix
}
.Concat(hash.ToArray())
.ToArray());
}

/// <inheritdoc/>
public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
{
ArgumentException.ThrowIfNullOrWhiteSpace(hashedPassword);
ArgumentException.ThrowIfNullOrWhiteSpace(providedPassword);

byte[] decodedHashedPassword;
try
{
decodedHashedPassword = Convert.FromBase64String(hashedPassword);
}
catch (FormatException)
{
return PasswordVerificationResult.Failed;
}

var hashSpan = decodedHashedPassword.AsSpan()[1..];

try
{
if (!_argon2Id.VerifyHash(hashSpan,
new ReadOnlySpan<byte>(Encoding.UTF8.GetBytes(providedPassword))))
{
return PasswordVerificationResult.Failed;
}
}
catch (ArgumentOutOfRangeException)
{
return PasswordVerificationResult.Failed;
}
catch(FormatException)
{
return PasswordVerificationResult.Failed;
}

if (_argon2Id.NeedsRehash(hashSpan))
{
return PasswordVerificationResult.SuccessRehashNeeded;
}

return PasswordVerificationResult.Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Geralt;
using System.ComponentModel.DataAnnotations;

namespace Aguacongas.TheIdServer.Identity.Argon2PasswordHasher;

/// <summary>
/// Argon2 password hasher options
/// </summary>
public class Argon2PasswordHasherOptions
{
/// <summary>
/// Number of iteration to use. 2 by default.
/// </summary>
[Range(Argon2id.MinIterations, int.MaxValue)]
public int Interations { get; set; } = 2;

/// <summary>
/// Memory to use. 67108864 by default.
/// </summary>
[Range(Argon2id.MinMemorySize, int.MaxValue)]
public int Memory { get; set; } = 67108864;

/// <summary>
/// Hash prefix to inform it was generated by this hasher. 0xA0 by default.
/// </summary>
public byte HashPrefix { get; set; } = 0xA0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Aguacongas.TheIdServer.Identity.Argon2PasswordHasher.Test")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Aguacongas.TheIdServer.Identity.Argon2PasswordHasher;
using Geralt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// <see cref="IServiceCollection"/> extensions
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Add argon2 password hasher services in DI
/// </summary>
/// <typeparam name="TUser"></typeparam>
/// <param name="services"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IServiceCollection AddArgon2PasswordHasher<TUser>(this IServiceCollection services, Action<Argon2PasswordHasherOptions>? configure = null) where TUser : class
{
services.AddOptions<Argon2PasswordHasherOptions>()
.Configure(options => configure?.Invoke(options))
.Validate(options => options.Memory > Argon2id.MinMemorySize && options.Interations > Argon2id.MinIterations);

return services.AddTransient<IArgon2Id, Argon2Id>()
.AddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
}

/// <summary>
/// Add argon2 password hasher services in DI
/// </summary>
/// <typeparam name="TUser"></typeparam>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddArgon2PasswordHasher<TUser>(this IServiceCollection services, IConfiguration configuration) where TUser : class
=> services.AddArgon2PasswordHasher<TUser>(configuration.Bind);


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Aguacongas.TheIdServer.Identity.Argon2PasswordHasher;

/// <summary>
/// Implement Argon2
/// </summary>
public interface IArgon2Id
{
/// <summary>
/// Compute the hash
/// </summary>
/// <param name="password"></param>
Span<byte> ComputeHash(ReadOnlySpan<byte> password);

/// <summary>
/// Verify the hash
/// </summary>
/// <param name="hash"></param>
/// <param name="password"></param>
/// <returns></returns>
bool VerifyHash(ReadOnlySpan<byte> hash, ReadOnlySpan<byte> password);

/// <summary>
/// Verify if the hash needs to be rehashed
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
bool NeedsRehash(ReadOnlySpan<byte> hash);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Argon2 Password Hasher for ASP.NET Core Identity

An implementation of IPasswordHasher<TUser> using [Geralt](https://www.geralt.xyz/).

## Installation

```csharp
services.AddIdentity<TUser, TRole>();
services.AddArgon2PasswordHasher<TUser>();
```

### Options

Default values:

``` json
"Argon2PasswordHasherOptions": {
"Interations": 2,
"Memory": 67108864,
"HashPrefix": 0xA0
}
```

- **Interations** can not be less than 1
- **Memory** can not be less than 8192

Read [Geralt Password hashing Notes](https://www.geralt.xyz/password-hashing#notes) for more information to configure the hasher.

`AddArgon2PasswordHasher` can take an action to configure an `Argon2PasswordHasherOptions` instance:

```cs
services.AddArgon2PasswordHasher<TUser>(options => configuration.Bind(options));
```

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit ae23dc0

Please sign in to comment.