using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace Sunfish.Auth;
/// <summary>
/// Hashes de contrasena con PBKDF2-SHA256: "pbkdf2-sha256$iteraciones$sal$hash" (Base64).
/// La contrasena nunca se guarda en claro.
/// </summary>
public static class Passwords
{
public const int DefaultIterations = 100_000;
private const string Prefix = "pbkdf2-sha256";
private const int SaltBytes = 16;
private const int HashBytes = 32;
public static string Hash(string password, int iterations = DefaultIterations)
{
byte[] salt = RandomNumberGenerator.GetBytes(SaltBytes);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, HashBytes);
return string.Join('$', Prefix, iterations.ToString(CultureInfo.InvariantCulture),
Convert.ToBase64String(salt), Convert.ToBase64String(hash));
}
public static bool Verify(string password, string stored)
{
var parts = stored.Split('$');
if (parts.Length != 4 || parts[0] != Prefix ||
!int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out int iterations) || iterations <= 0)
return false;
byte[] salt, expected;
try
{
salt = Convert.FromBase64String(parts[2]);
expected = Convert.FromBase64String(parts[3]);
}
catch (FormatException)
{
return false;
}
byte[] actual = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}
}