mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Move Infrastructure.PasswordHashing into Features.Users (#286)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using Features.Users.Identity;
|
||||
using Features.Users.Repository;
|
||||
using Features.Users.Services;
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
|
||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||
<ProjectReference Include="..\Features.PhotoUpload\Features.PhotoUpload.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Konscious.Security.Cryptography;
|
||||
|
||||
namespace Features.Users.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Hashes and verifies passwords using the Argon2id algorithm via Konscious.Security.Cryptography.
|
||||
/// </summary>
|
||||
public class Argon2Infrastructure : IPasswordInfrastructure
|
||||
{
|
||||
private const int SaltSize = 16; // 128-bit
|
||||
private const int HashSize = 32; // 256-bit
|
||||
private const int ArgonIterations = 4;
|
||||
private const int ArgonMemoryKb = 65536; // 64MB
|
||||
private const int DegreeOfParallelism = 2; // Hardcoded to prevent thread pool exhaustion
|
||||
|
||||
public string Hash(string password)
|
||||
{
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
|
||||
using Argon2id argon2 = new(Encoding.UTF8.GetBytes(password))
|
||||
{
|
||||
Salt = salt,
|
||||
DegreeOfParallelism = DegreeOfParallelism,
|
||||
MemorySize = ArgonMemoryKb,
|
||||
Iterations = ArgonIterations,
|
||||
};
|
||||
|
||||
byte[] hash = argon2.GetBytes(HashSize);
|
||||
|
||||
// The PHC string standard requires Base64 without the trailing '=' padding characters
|
||||
string b64Salt = Convert.ToBase64String(salt).TrimEnd('=');
|
||||
string b64Hash = Convert.ToBase64String(hash).TrimEnd('=');
|
||||
|
||||
// Argon2 version is currently 19 (0x13 in hex)
|
||||
return $"$argon2id$v=19$m={ArgonMemoryKb},t={ArgonIterations},p={DegreeOfParallelism}${b64Salt}${b64Hash}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Uses a fixed-time comparison to mitigate timing attacks. Supports standard PHC string format.</remarks>
|
||||
/// <returns>
|
||||
/// <c>false</c> if the password doesn't match, if <paramref name="stored" /> is malformed, or if any
|
||||
/// other error occurs during verification.
|
||||
/// </returns>
|
||||
public bool Verify(string password, string stored)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Split the PHC format: $argon2id$v=19$m=65536,t=3,p=4$salt$hash
|
||||
string[] parts = stored.Split('$', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// We expect exactly 5 parts when empty entries (the one before the first $) are removed
|
||||
if (parts.Length != 5 || parts[0] != "argon2id")
|
||||
return false;
|
||||
|
||||
int memory = 0,
|
||||
iterations = 0,
|
||||
parallelism = 0;
|
||||
string[] parameters = parts[2].Split(',');
|
||||
|
||||
foreach (string param in parameters)
|
||||
{
|
||||
if (param.StartsWith("m="))
|
||||
memory = int.Parse(param.Substring(2));
|
||||
else if (param.StartsWith("t="))
|
||||
iterations = int.Parse(param.Substring(2));
|
||||
else if (param.StartsWith("p="))
|
||||
parallelism = int.Parse(param.Substring(2));
|
||||
}
|
||||
|
||||
byte[] salt = DecodeBase64NoPadding(parts[3]);
|
||||
byte[] expectedHash = DecodeBase64NoPadding(parts[4]);
|
||||
|
||||
// Use the parameters embedded in the stored string, so previously hashed
|
||||
// passwords remain verifiable after a parameter change.
|
||||
using Argon2id argon2 = new(Encoding.UTF8.GetBytes(password))
|
||||
{
|
||||
Salt = salt,
|
||||
DegreeOfParallelism = parallelism,
|
||||
MemorySize = memory,
|
||||
Iterations = iterations,
|
||||
};
|
||||
|
||||
byte[] actualHash = argon2.GetBytes(expectedHash.Length);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// .NET's Convert.FromBase64String requires padding. The PHC format drops padding.
|
||||
/// This helper restores the padding before converting.
|
||||
/// </summary>
|
||||
private static byte[] DecodeBase64NoPadding(string base64)
|
||||
{
|
||||
string padded = base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=');
|
||||
return Convert.FromBase64String(padded);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using Infrastructure.PasswordHashing;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Features.Users.Identity;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Features.Users.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Service for hashing and verifying user passwords.
|
||||
/// </summary>
|
||||
public interface IPasswordInfrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Hashes a plaintext password, generating a new random salt.
|
||||
/// </summary>
|
||||
public string Hash(string password);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a plaintext password against a previously stored hash produced by <see cref="Hash" />.
|
||||
/// </summary>
|
||||
public bool Verify(string password, string stored);
|
||||
}
|
||||
Reference in New Issue
Block a user