up
This commit is contained in:
137
src/StellaOps.Cryptography/Pbkdf2PasswordHasher.cs
Normal file
137
src/StellaOps.Cryptography/Pbkdf2PasswordHasher.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// PBKDF2-SHA256 password hasher for legacy credentials.
|
||||
/// </summary>
|
||||
public sealed class Pbkdf2PasswordHasher : IPasswordHasher
|
||||
{
|
||||
private const int SaltLengthBytes = 16;
|
||||
private const int HashLengthBytes = 32;
|
||||
private const string Prefix = "PBKDF2";
|
||||
|
||||
public string Hash(string password, PasswordHashOptions options)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(password);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
if (options.Algorithm != PasswordHashAlgorithm.Pbkdf2)
|
||||
{
|
||||
throw new InvalidOperationException("Pbkdf2PasswordHasher only supports the PBKDF2 algorithm.");
|
||||
}
|
||||
|
||||
if (options.Iterations <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("PBKDF2 requires a positive iteration count.");
|
||||
}
|
||||
|
||||
Span<byte> salt = stackalloc byte[SaltLengthBytes];
|
||||
RandomNumberGenerator.Fill(salt);
|
||||
|
||||
var hash = Derive(password, salt, options.Iterations);
|
||||
|
||||
var payload = new byte[1 + SaltLengthBytes + HashLengthBytes];
|
||||
payload[0] = 0x01;
|
||||
salt.CopyTo(payload.AsSpan(1));
|
||||
hash.CopyTo(payload.AsSpan(1 + SaltLengthBytes));
|
||||
|
||||
return $"{Prefix}.{options.Iterations}.{Convert.ToBase64String(payload)}";
|
||||
}
|
||||
|
||||
public bool Verify(string password, string encodedHash)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(password);
|
||||
ArgumentException.ThrowIfNullOrEmpty(encodedHash);
|
||||
|
||||
if (!TryParse(encodedHash, out var parsed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var computed = Derive(password, parsed.Salt, parsed.Iterations);
|
||||
return CryptographicOperations.FixedTimeEquals(parsed.Hash, computed);
|
||||
}
|
||||
|
||||
public bool NeedsRehash(string encodedHash, PasswordHashOptions desired)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(desired);
|
||||
|
||||
if (!TryParse(encodedHash, out var parsed))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (desired.Algorithm != PasswordHashAlgorithm.Pbkdf2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return parsed.Iterations != desired.Iterations;
|
||||
}
|
||||
|
||||
private static byte[] Derive(string password, ReadOnlySpan<byte> salt, int iterations)
|
||||
{
|
||||
return Rfc2898DeriveBytes.Pbkdf2(
|
||||
Encoding.UTF8.GetBytes(password),
|
||||
salt.ToArray(),
|
||||
iterations,
|
||||
HashAlgorithmName.SHA256,
|
||||
HashLengthBytes);
|
||||
}
|
||||
|
||||
private static bool TryParse(string encodedHash, out Pbkdf2Parameters parsed)
|
||||
{
|
||||
parsed = default;
|
||||
|
||||
var parts = encodedHash.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length != 3 || !string.Equals(parts[0], Prefix, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(parts[1], out var iterations) || iterations <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] payload;
|
||||
try
|
||||
{
|
||||
payload = Convert.FromBase64String(parts[2]);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.Length != 1 + SaltLengthBytes + HashLengthBytes || payload[0] != 0x01)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var salt = new byte[SaltLengthBytes];
|
||||
var hash = new byte[HashLengthBytes];
|
||||
Array.Copy(payload, 1, salt, 0, SaltLengthBytes);
|
||||
Array.Copy(payload, 1 + SaltLengthBytes, hash, 0, HashLengthBytes);
|
||||
|
||||
parsed = new Pbkdf2Parameters(iterations, salt, hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly struct Pbkdf2Parameters
|
||||
{
|
||||
public Pbkdf2Parameters(int iterations, byte[] salt, byte[] hash)
|
||||
{
|
||||
Iterations = iterations;
|
||||
Salt = salt;
|
||||
Hash = hash;
|
||||
}
|
||||
|
||||
public int Iterations { get; }
|
||||
public byte[] Salt { get; }
|
||||
public byte[] Hash { get; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user