2023-02-18 08:52:17 +01:00
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
2023-02-19 00:43:43 +01:00
|
|
|
|
namespace PrivaPub.StaticServices
|
2023-02-18 08:52:17 +01:00
|
|
|
|
{
|
|
|
|
|
public sealed class PasswordHasher : IPasswordHasher
|
|
|
|
|
{
|
|
|
|
|
private const int SaltSize = 16;
|
|
|
|
|
private const int KeySize = 32;
|
|
|
|
|
private HashingOptions Options { get; }
|
|
|
|
|
|
|
|
|
|
public PasswordHasher(IOptionsMonitor<HashingOptions> options)
|
|
|
|
|
{
|
|
|
|
|
Options = options.CurrentValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public (bool verified, bool needsUpgrade) Check(string hash, string password)
|
|
|
|
|
{
|
|
|
|
|
var parts = hash.Split('~', 3);
|
|
|
|
|
|
|
|
|
|
if (parts.Length != 3)
|
|
|
|
|
throw new FormatException("Unexpected hash format.");
|
|
|
|
|
|
|
|
|
|
var iterations = Convert.ToInt32(parts[2]);
|
|
|
|
|
var salt = Convert.FromBase64String(parts[1]);
|
|
|
|
|
var key = Convert.FromBase64String(parts[0]);
|
|
|
|
|
|
|
|
|
|
var needsUpgrade = iterations != Options.Iterations;
|
|
|
|
|
|
|
|
|
|
using (var algorithm = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA512))
|
|
|
|
|
{
|
|
|
|
|
var keyToCheck = algorithm.GetBytes(KeySize);
|
|
|
|
|
var verified = keyToCheck.SequenceEqual(key);
|
|
|
|
|
return (verified, needsUpgrade);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Hash(string password)
|
|
|
|
|
{
|
|
|
|
|
using (var algorithm = new Rfc2898DeriveBytes(password, SaltSize, Options.Iterations, HashAlgorithmName.SHA512))
|
|
|
|
|
{
|
|
|
|
|
var key = Convert.ToBase64String(algorithm.GetBytes(KeySize));
|
|
|
|
|
var salt = Convert.ToBase64String(algorithm.Salt);
|
|
|
|
|
return $"{key}~{salt}~{Options.Iterations}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class HashingOptions
|
|
|
|
|
{
|
|
|
|
|
public int Iterations { get; set; } = 9789;
|
|
|
|
|
}
|
|
|
|
|
}
|