Some checks failed
		
		
	
	Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Build Test Deploy / build-test (push) Has been cancelled
				
			Build Test Deploy / authority-container (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
			
				
	
	
		
			138 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			138 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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; }
 | |
|     }
 | |
| }
 |