I am wondering wether the Password Hasher that is default implemented in the UserManager that comes with MVC 5 and ASP.NET Identity Framework, is secure enough? And if so, if you could explain to me how it works?

IPasswordHasher interface looks like this:

public interface IPasswordHasher
{
    string HashPassword(string password);
    PasswordVerificationResult VerifyHashedPassword(string hashedPassword, 
                                                       string providedPassword);
}

As you can see, it doesn't take a salt, but it is mentioned in this thread: "Asp.net Identity password hashing" that it does infact salt it behind the scenes. So I am wondering how does it do this? And where does this salt come from?

My concern is that the salt is static, rendering it quite insecure.

Solution 1

Here is how the default implementation (ASP.NET Framework or ASP.NET Core) works. It uses a Key Derivation Function with random salt to produce the hash. The salt is included as part of the output of the KDF. Thus, each time you "hash" the same password you will get different hashes. To verify the hash the output is split back to the salt and the rest, and the KDF is run again on the password with the specified salt. If the result matches to the rest of the initial output the hash is verified.

Hashing:

public static string HashPassword(string password)
{
    byte[] salt;
    byte[] buffer2;
    if (password == null)
    {
        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);
}

Verifying:

public static bool VerifyHashedPassword(string hashedPassword, string password)
{
    byte[] buffer4;
    if (hashedPassword == null)
    {
        return false;
    }
    if (password == null)
    {
        throw new ArgumentNullException("password");
    }
    byte[] src = Convert.FromBase64String(hashedPassword);
    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(password, dst, 0x3e8))
    {
        buffer4 = bytes.GetBytes(0x20);
    }
    return ByteArraysEqual(buffer3, buffer4);
}

Solution 2

Because these days ASP.NET is open source, you can find it on GitHub: AspNet.Identity 3.0 and AspNet.Identity 2.0.

From the comments:

/* =======================
 * HASHED PASSWORD FORMATS
 * =======================
 * 
 * Version 2:
 * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
 * (See also: SDL crypto guidelines v5.1, Part III)
 * Format: { 0x00, salt, subkey }
 *
 * Version 3:
 * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
 * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
 * (All UInt32s are stored big-endian.)
 */

Solution 3

I understand the accepted answer, and have up-voted it but thought I'd dump my laymen's answer here...

Creating a hash

  1. The salt is randomly generated using the function Rfc2898DeriveBytes which generates a hash and a salt. Inputs to Rfc2898DeriveBytes are the password, the size of the salt to generate and the number of hashing iterations to perform. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. The salt and the hash are then mashed together(salt first followed by the hash) and encoded as a string (so the salt is encoded in the hash). This encoded hash (which contains the salt and hash) is then stored (typically) in the database against the user.

Checking a password against a hash

To check a password that a user inputs.

  1. The salt is extracted from the stored hashed password.
  2. The salt is used to hash the users input password using an overload of Rfc2898DeriveBytes which takes a salt instead of generating one. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. The stored hash and the test hash are then compared.

The Hash

Under the covers the hash is generated using the SHA1 hash function (https://en.wikipedia.org/wiki/SHA-1). This function is iteratively called 1000 times (In the default Identity implementation)

Why is this secure

  • Random salts means that an attacker cant use a pre-generated table of hashs to try and break passwords. They would need to generate a hash table for every salt. (Assuming here that the hacker has also compromised your salt)
  • If 2 passwords are identical they will have different hashes. (meaning attackers cant infer common passwords)
  • Iteratively calling SHA1 1000 times means that the attacker also needs to do this. The idea being that unless they have time on a supercomputer they wont have enough resource to brute force the password from the hash. It would massively slow down the time to generate a hash table for a given salt.

Solution 4

For those like me who are brand new to this, here is code with const and an actual way to compare the byte[]'s. I got all of this code from stackoverflow but defined consts so values could be changed and also

// 24 = 192 bits
    private const int SaltByteSize = 24;
    private const int HashByteSize = 24;
    private const int HasingIterationsCount = 10101;


    public static string HashPassword(string password)
    {
        // http://stackoverflow.com/questions/19957176/asp-net-identity-password-hashing

        byte[] salt;
        byte[] buffer2;
        if (password == null)
        {
            throw new ArgumentNullException("password");
        }
        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, SaltByteSize, HasingIterationsCount))
        {
            salt = bytes.Salt;
            buffer2 = bytes.GetBytes(HashByteSize);
        }
        byte[] dst = new byte[(SaltByteSize + HashByteSize) + 1];
        Buffer.BlockCopy(salt, 0, dst, 1, SaltByteSize);
        Buffer.BlockCopy(buffer2, 0, dst, SaltByteSize + 1, HashByteSize);
        return Convert.ToBase64String(dst);
    }

    public static bool VerifyHashedPassword(string hashedPassword, string password)
    {
        byte[] _passwordHashBytes;

        int _arrayLen = (SaltByteSize + HashByteSize) + 1;

        if (hashedPassword == null)
        {
            return false;
        }

        if (password == null)
        {
            throw new ArgumentNullException("password");
        }

        byte[] src = Convert.FromBase64String(hashedPassword);

        if ((src.Length != _arrayLen) || (src[0] != 0))
        {
            return false;
        }

        byte[] _currentSaltBytes = new byte[SaltByteSize];
        Buffer.BlockCopy(src, 1, _currentSaltBytes, 0, SaltByteSize);

        byte[] _currentHashBytes = new byte[HashByteSize];
        Buffer.BlockCopy(src, SaltByteSize + 1, _currentHashBytes, 0, HashByteSize);

        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, _currentSaltBytes, HasingIterationsCount))
        {
            _passwordHashBytes = bytes.GetBytes(SaltByteSize);
        }

        return AreHashesEqual(_currentHashBytes, _passwordHashBytes);

    }

    private static bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
    {
        int _minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
        var xor = firstHash.Length ^ secondHash.Length;
        for (int i = 0; i < _minHashLength; i++)
            xor |= firstHash[i] ^ secondHash[i];
        return 0 == xor;
    }

In in your custom ApplicationUserManager, you set the PasswordHasher property the name of the class which contains the above code.

Solution 5

I write my class PasswordHasher based on .net6 PasswordHasher docs latest version (V3) https://github.com/dotnet/aspnetcore/blob/b56bb17db3ae73ce5a8664a2023a9b9af89499dd/src/Identity/Extensions.Core/src/PasswordHasher.cs

namespace Utilities;

public class PasswordHasher
{
    public const int Pbkdf2Iterations = 1000;


    public static string HashPasswordV3(string password)
    {
        return Convert.ToBase64String(HashPasswordV3(password, RandomNumberGenerator.Create()
            , prf: KeyDerivationPrf.HMACSHA512, iterCount: Pbkdf2Iterations, saltSize: 128 / 8
            , numBytesRequested: 256 / 8));
    }


    public static bool VerifyHashedPasswordV3(string hashedPasswordStr, string password)
    {
        byte[] hashedPassword = Convert.FromBase64String(hashedPasswordStr);
        var iterCount = default(int);
        var prf = default(KeyDerivationPrf);

        try
        {
            // Read header information
            prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
            iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
            int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);

            // Read the salt: must be >= 128 bits
            if (saltLength < 128 / 8)
            {
                return false;
            }
            byte[] salt = new byte[saltLength];
            Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);

            // Read the subkey (the rest of the payload): must be >= 128 bits
            int subkeyLength = hashedPassword.Length - 13 - salt.Length;
            if (subkeyLength < 128 / 8)
            {
                return false;
            }
            byte[] expectedSubkey = new byte[subkeyLength];
            Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);

            // Hash the incoming password and verify it
            byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
#if NETSTANDARD2_0 || NETFRAMEWORK
            return ByteArraysEqual(actualSubkey, expectedSubkey);
#elif NETCOREAPP
            return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
#else
#error Update target frameworks
#endif
        }
        catch
        {
            // This should never occur except in the case of a malformed payload, where
            // we might go off the end of the array. Regardless, a malformed payload
            // implies verification failed.
            return false;
        }
    }


    // privates
    private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
    {
        byte[] salt = new byte[saltSize];
        rng.GetBytes(salt);
        byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
        var outputBytes = new byte[13 + salt.Length + subkey.Length];
        outputBytes[0] = 0x01; // format marker
        WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
        WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
        WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
        Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
        Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
        return outputBytes;
    }

    private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
    {
        buffer[offset + 0] = (byte)(value >> 24);
        buffer[offset + 1] = (byte)(value >> 16);
        buffer[offset + 2] = (byte)(value >> 8);
        buffer[offset + 3] = (byte)(value >> 0);
    }

    private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
    {
        return ((uint)(buffer[offset + 0]) << 24)
            | ((uint)(buffer[offset + 1]) << 16)
            | ((uint)(buffer[offset + 2]) << 8)
            | ((uint)(buffer[offset + 3]));
    }

}

Use in UserController :

namespace WebApi.Controllers.UserController;

[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
    private readonly IUserService _userService;
    public UserController(IUserService userService)
    {
        _userService = userService;
    }


[HttpPost]
public async Task<IActionResult> Register(VmRegister model)
{
    var user = new User
    {
        UserName = model.UserName,
        PasswordHash = PasswordHasher.HashPasswordV3(model.Password),
        FirstName = model.FirstName,
        LastName = model.LastName,
        Mobile = model.Mobile,
        Email = model.Email,
    };
    await _userService.Add(user);
    return StatusCode(201, user.Id);
}


[HttpPost]
public async Task<IActionResult> Login(VmLogin model)
{
    var user = await _userService.GetByUserName(model.UserName);

    if (user is null || !PasswordHasher.VerifyHashedPasswordV3(user.PasswordHash, model.Password))
        throw new Exception("The UserName or Password is wrong.");
    // generate token
    return Ok();
}

}

https://github.com/mammadkoma/WebApi/tree/master/WebApi