diff --git a/Security/PasswordHashService.cs b/Security/PasswordHashService.cs new file mode 100644 index 0000000..a299901 --- /dev/null +++ b/Security/PasswordHashService.cs @@ -0,0 +1,76 @@ +using System.Buffers.Text; +using System.Text; +using Konscious.Security.Cryptography; + +namespace Security; + +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)); +} \ No newline at end of file