using Konscious.Security.Cryptography; using System; using System.Buffers.Text; 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 static ReadOnlySpan GenerateRandomKeyBytes(int size) { var key = new byte[size]; using var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); rng.GetNonZeroBytes(key); return key; } public static string GenerateRandomKeyStringBase64(int size) => Convert.ToBase64String(GenerateRandomKeyBytes(size)); public static string GenerateRandomKeyString(int size) { var randomBytes = GenerateRandomKeyBytes(size); Span utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)]; Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _); return Encoding.UTF8.GetString(utf8Bytes); } public (string Salt, string Hash) HashPassword(string password) { var salt = GenerateRandomKeyBytes(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)); }