Files
lxc-songlist/Services/AdminPasswordService.cs
2026-06-03 20:24:57 -07:00

55 lines
1.9 KiB
C#

using System.Security.Cryptography;
using Microsoft.Extensions.Options;
namespace lxc_songlist.Services;
public sealed class AdminPasswordService(IOptions<AdminAuthOptions> 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;
}
}
}