Skip to main content

This password hasher is the same used by ASP.NET Identity.

using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using JWTAPI.Core.Security.Hashing;

namespace JWTAPI.Security.Hashing;

public interface IPasswordHasher
{
    string HashPassword(string password);
    bool PasswordMatches(string providedPassword, string passwordHash);
}

/// <summary>
/// This password hasher is the same used by ASP.NET Identity.
/// Explanation: https://stackoverflow.com/questions/20621950/asp-net-identity-default-password-hasher-how-does-it-work-and-is-it-secure
/// Full implementation: https://gist.github.com/malkafly/e873228cb9515010bdbe
/// </summary>
public class PasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        byte[] salt;
        byte[] buffer2;
        if (string.IsNullOrEmpty(password))
        {
            throw new ArgumentNullException("password");
        }
        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
        {
            salt = bytes.Salt;
            buffer2 = bytes.GetBytes(0x20);
        }
        byte[] dst = new byte[0x31];
        Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
        Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
        return Convert.ToBase64String(dst);
    }

    public bool PasswordMatches(string providedPassword, string passwordHash)
    {
        byte[] buffer4;
        if (passwordHash == null)
        {
            return false;
        }
        if (providedPassword == null)
        {
            throw new ArgumentNullException("providedPassword");
        }
        byte[] src = Convert.FromBase64String(passwordHash);
        if ((src.Length != 0x31) || (src[0] != 0))
        {
            return false;
        }
        byte[] dst = new byte[0x10];
        Buffer.BlockCopy(src, 1, dst, 0, 0x10);
        byte[] buffer3 = new byte[0x20];
        Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(providedPassword, dst, 0x3e8))
        {
            buffer4 = bytes.GetBytes(0x20);
        }
        return ByteArraysEqual(buffer3, buffer4);
    }

    [MethodImpl(MethodImplOptions.NoOptimization)]
    private bool ByteArraysEqual(byte[] a, byte[] b)
    {
        if (ReferenceEquals(a, b))
        {
            return true;
        }

        if (a == null || b == null || a.Length != b.Length)
        {
            return false;
        }

        bool areSame = true;
        for (int i = 0; i < a.Length; i++)
        {
            areSame &= (a[i] == b[i]);
        }
        return areSame;
    }
}

// --------------------------------------
// Source: https://github.com/m-jovanovic/event-reminder/blob/main/EventReminder.Infrastructure/Cryptography/PasswordHasher.cs

using System;
using System.Security.Cryptography;
using EventReminder.Application.Core.Abstractions.Cryptography;
using EventReminder.Domain.Services;
using EventReminder.Domain.ValueObjects;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;

namespace EventReminder.Infrastructure.Cryptography
{
    /// <summary>
    /// Represents the password hasher, used for hashing passwords and verifying hashed passwords.
    /// </summary>
    internal sealed class PasswordHasher : IPasswordHasher, IPasswordHashChecker, IDisposable
    {
        private const KeyDerivationPrf Prf = KeyDerivationPrf.HMACSHA256;
        private const int IterationCount = 10000;
        private const int NumberOfBytesRequested = 256 / 8;
        private const int SaltSize = 128 / 8;
        private readonly RandomNumberGenerator _rng;

        /// <summary>
        /// Initializes a new instance of the <see cref="PasswordHasher"/> class.
        /// </summary>
        public PasswordHasher() => _rng = new RNGCryptoServiceProvider();

        /// <inheritdoc />
        public string HashPassword(Password password)
        {
            if (password is null)
            {
                throw new ArgumentNullException(nameof(password));
            }

            string hashedPassword = Convert.ToBase64String(HashPasswordInternal(password));

            return hashedPassword;
        }

        /// <inheritdoc />
        public bool HashesMatch(string passwordHash, string providedPassword)
        {
            if (passwordHash is null)
            {
                throw new ArgumentNullException(nameof(passwordHash));
            }

            if (providedPassword is null)
            {
                throw new ArgumentNullException(nameof(providedPassword));
            }

            byte[] decodedHashedPassword = Convert.FromBase64String(passwordHash);

            if (decodedHashedPassword.Length == 0)
            {
                return false;
            }

            bool verified = VerifyPasswordHashInternal(decodedHashedPassword, providedPassword);

            return verified;
        }

        /// <inheritdoc />
        public void Dispose() => _rng.Dispose();

        /// <summary>
        /// Returns the bytes of the hash for the specified password.
        /// </summary>
        /// <param name="password">The password to be hashed.</param>
        /// <returns>The bytes of the hash for the specified password.</returns>
        private byte[] HashPasswordInternal(string password)
        {
            byte[] salt = GetRandomSalt();

            byte[] subKey = KeyDerivation.Pbkdf2(password, salt, Prf, IterationCount, NumberOfBytesRequested);

            byte[] outputBytes = new byte[salt.Length + subKey.Length];

            Buffer.BlockCopy(salt, 0, outputBytes, 0, salt.Length);

            Buffer.BlockCopy(subKey, 0, outputBytes, salt.Length, subKey.Length);

            return outputBytes;
        }

        /// <summary>
        /// Gets a randomly generated salt.
        /// </summary>
        /// <returns>The randomly generated salt.</returns>
        private byte[] GetRandomSalt()
        {
            byte[] salt = new byte[SaltSize];

            _rng.GetBytes(salt);

            return salt;
        }

        /// <summary>
        /// Verifies the bytes of the hashed password with the specified password.
        /// </summary>
        /// <param name="hashedPassword">The bytes of the hashed password.</param>
        /// <param name="password">The password to verify with.</param>
        /// <returns>True if the hashes match, otherwise false.</returns>
        private static bool VerifyPasswordHashInternal(byte[] hashedPassword, string password)
        {
            try
            {
                byte[] salt = new byte[SaltSize];

                Buffer.BlockCopy(hashedPassword, 0, salt, 0, salt.Length);

                int subKeyLength = hashedPassword.Length - salt.Length;

                if (subKeyLength < SaltSize)
                {
                    return false;
                }

                byte[] expectedSubKey = new byte[subKeyLength];

                Buffer.BlockCopy(hashedPassword, salt.Length, expectedSubKey, 0, expectedSubKey.Length);

                byte[] actualSubKey = KeyDerivation.Pbkdf2(password, salt, Prf, IterationCount, subKeyLength);

                return ByteArraysEqual(actualSubKey, expectedSubKey);
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Returns true if the specified byte arrays are equal, otherwise false.
        /// </summary>
        /// <param name="a">The first byte array.</param>
        /// <param name="b">The second byte array.</param>
        /// <returns>True if the arrays are equal, otherwise false.</returns>
        private static bool ByteArraysEqual(byte[] a, byte[] b)
        {
            if (a == null && b == null)
            {
                return true;
            }

            if (a == null || b == null || a.Length != b.Length)
            {
                return false;
            }

            bool areSame = true;

            for (int i = 0; i < a.Length; i++)
            {
                areSame &= a[i] == b[i];
            }

            return areSame;
        }
    }
}