using System.Security.Cryptography; using Microsoft.Extensions.Options; namespace lxc_songlist.Services; public sealed class AdminPasswordService(IOptions options) { private const int SaltBytes = 16; private const int HashBytes = 32; private const int Iterations = 210_000; public bool Verify(string password) { var configuredHash = options.Value.PasswordHash; if (!string.IsNullOrWhiteSpace(configuredHash)) { return VerifyHash(password, configuredHash); } var configuredPassword = options.Value.Password; return !string.IsNullOrEmpty(configuredPassword) && CryptographicOperations.FixedTimeEquals( System.Text.Encoding.UTF8.GetBytes(password), System.Text.Encoding.UTF8.GetBytes(configuredPassword)); } public static string CreateHash(string password) { var salt = RandomNumberGenerator.GetBytes(SaltBytes); var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, HashBytes); return $"pbkdf2${Iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}"; } private static bool VerifyHash(string password, string storedHash) { var parts = storedHash.Split('$'); if (parts.Length != 4 || parts[0] != "pbkdf2" || !int.TryParse(parts[1], out var iterations)) { return false; } try { var salt = Convert.FromBase64String(parts[2]); var expectedHash = Convert.FromBase64String(parts[3]); var actualHash = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expectedHash.Length); return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash); } catch (FormatException) { return false; } } }