using System; using System.Security.Cryptography; using Microsoft.AspNetCore.Cryptography.KeyDerivation; namespace MiniWiki.Web.Auth; public class PasswordService { private const int SaltSize = 16; private const int HashSize = 32; private const int Iterations = 600_000; // OWASP recommendation /// /// Hashes a password using PBKDF2 with a random salt /// /// The password to hash /// The hashed password /// Thrown if is empty public string HashPassword(string password) { ArgumentException.ThrowIfNullOrWhiteSpace(password); var salt = RandomNumberGenerator.GetBytes(SaltSize); var hash = KeyDerivation.Pbkdf2( password, salt, KeyDerivationPrf.HMACSHA256, Iterations, HashSize); var combined = new byte[SaltSize + HashSize]; Buffer.BlockCopy(salt, 0, combined, 0, SaltSize); Buffer.BlockCopy(hash, 0, combined, SaltSize, HashSize); return Convert.ToBase64String(combined); } /// /// Verifies if the given hash is equal to the /// /// The password to compare /// The hashed password to compare /// True if the passwords match public bool VerifyPassword(string password, string hashedPassword) { if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hashedPassword)) return false; try { var combined = Convert.FromBase64String(hashedPassword); if (combined.Length != SaltSize + HashSize) return false; // extract salt var salt = new byte[SaltSize]; Buffer.BlockCopy(combined, 0, salt, 0, SaltSize); // extract hash var storedHash = new byte[HashSize]; Buffer.BlockCopy(combined, SaltSize, storedHash, 0, HashSize); // hash the provided password with the extracted salt var computedHash = KeyDerivation.Pbkdf2( password, salt, KeyDerivationPrf.HMACSHA256, Iterations, HashSize); // compare hashes return CryptographicOperations.FixedTimeEquals(storedHash, computedHash); } catch { return false; } } }