XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.ServerInteractive

[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Security.Cryptography;

namespace XFEExtension.NetCore.ServerInteractive.Models.UserModels;

/// <summary>版本化 PBKDF2 密码凭据。</summary>
public sealed class PasswordCredential
{
    public string Algorithm { get; set; } = "PBKDF2-HMAC-SHA256";
    public int Version { get; set; } = 1;
    public string Salt { get; set; } = string.Empty;
    public int Iterations { get; set; } = PasswordHasher.DefaultIterations;
    public string Hash { get; set; } = string.Empty;
}

/// <summary>使用 .NET 标准密码学 API 的密码哈希器。</summary>
public static class PasswordHasher
{
    public const int DefaultIterations = 600_000;
    private const int SaltSize = 16;
    private const int HashSize = 32;
    private const int MaximumIterations = 5_000_000;

    public static PasswordCredential Hash(string password, int iterations = DefaultIterations)
    {
        ArgumentException.ThrowIfNullOrEmpty(password);
        if (iterations is < 100_000 or > MaximumIterations) throw new ArgumentOutOfRangeException(nameof(iterations));
        var salt = RandomNumberGenerator.GetBytes(SaltSize);
        var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, HashSize);
        return new() { Salt = Convert.ToBase64String(salt), Iterations = iterations, Hash = Convert.ToBase64String(hash) };
    }

    public static bool Verify(string password, PasswordCredential credential)
    {
        ArgumentNullException.ThrowIfNull(credential);
        if (credential.Algorithm != "PBKDF2-HMAC-SHA256" || credential.Version != 1 || credential.Iterations is < 100_000 or > MaximumIterations)
            return false;
        try
        {
            var salt = Convert.FromBase64String(credential.Salt);
            var expected = Convert.FromBase64String(credential.Hash);
            if (salt.Length != SaltSize || expected.Length != HashSize) return false;
            var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, credential.Iterations, HashAlgorithmName.SHA256, expected.Length);
            return CryptographicOperations.FixedTimeEquals(actual, expected);
        }
        catch (FormatException) { return false; }
    }

    internal static bool FixedTimePlainTextEquals(string supplied, string stored)
    {
        var suppliedHash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(supplied));
        var storedHash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(stored));
        return CryptographicOperations.FixedTimeEquals(suppliedHash, storedHash);
    }
}