using Konscious.Security.Cryptography; using System; using System.Text; namespace Mirea.Api.Security.Services; public class PasswordHashService { public int SaltSize { private get; init; } public int HashSize { private get; init; } public int Iterations { private get; init; } public int Memory { private get; init; } public int Parallelism { private get; init; } public string? Secret { private get; init; } private ReadOnlySpan HashPassword(string password, ReadOnlySpan salt) { var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) { Iterations = Iterations, MemorySize = Memory, DegreeOfParallelism = Parallelism, Salt = salt.ToArray() }; if (!string.IsNullOrEmpty(Secret)) argon2.KnownSecret = Convert.FromBase64String(Secret); return argon2.GetBytes(HashSize); } private static bool ConstantTimeComparison(ReadOnlySpan a, ReadOnlySpan b) { if (a.Length != b.Length) return false; int result = 0; for (int i = 0; i < a.Length; i++) result |= a[i] ^ b[i]; return result == 0; } public (string Salt, string Hash) HashPassword(string password) { var salt = GeneratorKey.GenerateBytes(SaltSize); var hash = HashPassword(password, salt); return (Convert.ToBase64String(salt), Convert.ToBase64String(hash)); } public bool VerifyPassword(string password, ReadOnlySpan salt, ReadOnlySpan hash) => ConstantTimeComparison(HashPassword(password, salt), hash); public bool VerifyPassword(string password, string saltBase64, string hashBase64) => VerifyPassword(password, Convert.FromBase64String(saltBase64), Convert.FromBase64String(hashBase64)); }