209

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.

André Snede
  • 9,899
  • 7
  • 43
  • 67
  • I don't think this directly answers your question, but Brock Allen has written about some of your concerns here => http://brockallen.com/2013/10/20/the-good-the-bad-and-the-ugly-of-asp-net-identity/ and also written an open source user identity management and authentication library that has various boiler-plate features like password reset, hashing etc etc. https://github.com/brockallen/BrockAllen.MembershipReboot – Shiva Dec 16 '13 at 22:22
  • @Shiva Thanks, I will look into the library and the video on the page. But I would rather not have to deal with an external library. Not if I can avoid it. – André Snede Dec 16 '13 at 22:33
  • 2
    FYI: the stackoverflow equivalent for security. So although you will often get a good/correct answer here. The experts are on http://security.stackexchange.com/ especially the comment "is it secure" I asked a similar sort of question and the depth and quality of answer was amazing. – phil soady Dec 16 '13 at 22:50
  • @philsoady Thanks, that makes sense of course, Im already on a few of the other "sub-forums", if I do not get an answer, I can use, I will move over to `securiry.stackexchange.com`. And thanks for the tip! – André Snede Dec 16 '13 at 22:52

6 Answers6

280

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);
}
Johann
  • 4,107
  • 3
  • 40
  • 39
Andrew Savinykh
  • 25,351
  • 17
  • 103
  • 158
  • 12
    So if I understand this correctly, the `HashPassword` function, returns both in the same string? And when you verify it, it splits it again it up again, and hashes the incoming cleartext password, with the salt from the split, and compares it with the original hash? – André Snede Dec 16 '13 at 22:55
  • 12
    @AndréSnedeHansen, exactly. And I too recommend you asking either on security or on cryptography SE. The "is it secure" part may be addressed better in those respective contexts. – Andrew Savinykh Dec 16 '13 at 22:56
  • @zespri: Any idea *why* byte0 of the output is set to 0x00? Seen in `VerifyHashedPassword(..)` at `src[0] != 0` – DeepSpace101 Aug 27 '15 at 02:31
  • @DeepSpace101, no. If I had to guess, I'd say, that this is a minimal precaution against invalid input, to filter out values that are *definitely* not a valid hash. – Andrew Savinykh Aug 27 '15 at 06:19
  • Is aps.net identity using pbkdf2? – Jeeva J Oct 12 '16 at 09:59
  • @JeevaJsb 2.0 uses Rfc2898 as seen above. Identity 3.0 (rc1 went out Nov 18, 2016) uses PBKDF2 – David Nov 25 '16 at 15:53
  • I have one password in database which is hashed and saved.So if i am making password check at the time of login how I will get same hashed password?? – Shajeer Puzhakkal Feb 07 '17 at 07:22
  • @shajeerpuzhakkal you don't. – Andrew Savinykh Feb 07 '17 at 08:38
  • So how password hashing is using in password save – Shajeer Puzhakkal Feb 07 '17 at 11:17
  • 1
    @shajeerpuzhakkal as described in the answer above. – Andrew Savinykh Feb 07 '17 at 18:49
  • By the way, what's the purpose of assigning hex values to int variables (like 'count')? Are there any performance advantages? – Andrew Cyrul Apr 13 '17 at 08:06
  • @AndrewCyrul I'm sorry I do not understand the question. There is no `count` variable in the code above, and it does not matter if you use hexadecimals or not - they represent the same number no matter what the base is. – Andrew Savinykh Apr 13 '17 at 08:30
  • @AndrewSavinykh BlockCopy has 5 parameters, the last one is 'count' and it is an int. So why use '0x10' or '0x20' instead of just '16' or '32'? – Andrew Cyrul Apr 13 '17 at 09:17
  • @AndrewCyrul you can use either. Both mean the same. – Andrew Savinykh Apr 13 '17 at 09:19
  • 6
    @AndrewSavinykh I know, that's why I'm asking - what's the point? To make the code look smarter? ;) Cause for me counting stuff using decimal numbers is A LOT more intuitive (we have 10 fingers after all - at least most of us), so declaring a number of something using hexadecimals seems like an unnecessary code obfuscation. – Andrew Cyrul Apr 13 '17 at 09:27
  • @AndrewCyrul to reiterate - this is equal choice. So the point would be the same in both cases - to express that number. I'm guessing what's intuitive is highly subjective, since it did not even occur to me when you asked the question, that a software developer might have a problem reading 0x10 as 16. Now when you explained that this is what your problem is I understand your point. But that's not an issue I ever encountered with my peer. With experience people usually get quite comfortable with this stuff. – Andrew Savinykh Apr 13 '17 at 09:43
  • @AndrewCyrul another thing that might help here is to realize that this code is obtained by decompiling the methods in question, so those variable names and literals is what decompiler produced. If you look at the link that Bruno edited into my answer you will be pleased to discover, that human written source code has nice constants instead of these numbers and will be undoubtedly easier for you to read. – Andrew Savinykh Apr 13 '17 at 09:50
  • @AndrewSavinykh I'm not saying it's a problem. Hell, it wasn't even me, who found this code - I'm just asking for a coleague that doesn't have a SO account :) It's just more of a personal taste - when I think of the size of a 32-byte value, I see '32' instead of '0x20' :) And the fact, that this code has been decompiled, explains a bit more :D – Andrew Cyrul Apr 13 '17 at 09:55
  • @AndrewSavinykh what if the user wants to know his password or the admin needs to send the login info (username and password) to the user? – Shomaail Apr 25 '18 at 08:08
  • 1
    @shomaail not gonna happen. If password is lost it must be reset. Admin cannot have access to users password due to security reasons. In fact no one else but the user should know the password full stop. – Andrew Savinykh Apr 26 '18 at 02:09
  • Verify step is always returns false for me. I have a password in database hashed using `HashPassword` method. Then when I need to check one password i need to hash password then verify if they are equal(hashed password and what i have in database), right ? – Mihai Alexandru-Ionut Aug 13 '18 at 13:24
  • 2
    @MihaiAlexandru-Ionut `var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);` - is what you need to do. after that `result` contains true. – Andrew Savinykh Aug 13 '18 at 19:57
  • @DeepSpace101 I believe byte0 specifies the version of the hash used. 0x00 is V2, in the example above it uses the byte derivation constructor whithout specifying the algorithm which defaults to the now vulnerable SHA1. When byte0 is 0x01 indicates V3 which uses SHA512. There is a compatibility option to use SHA1 and keep passwords hashed with the old version working, but default options set the version to V3 and SHA512 – Miguel May 13 '23 at 05:49
54

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.)
 */
Florian K
  • 602
  • 9
  • 30
Knelis
  • 6,782
  • 2
  • 34
  • 54
  • Yes, and worth noting, there are additions to the algorithm zespri is showing. – André Snede Jan 12 '16 at 21:36
  • 1
    The source on GitHub is Asp.Net.Identity 3.0 which is still in prerelease. The source of the 2.0 hash function is on [CodePlex](https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.Core/Crypto.cs) – David Nov 25 '16 at 15:52
  • 2
    The newest implementation can be found under https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs now. They archived the other repository ;) – FranzHuber23 Jan 31 '20 at 09:13
45

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 can’t 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 can’t 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 won’t 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.
Nattrass
  • 1,283
  • 16
  • 27
  • Thanks for your explanation. In the "Creating a hash 2." you mention that the salt and hash are mashed together, do you know if this is stored in the PasswordHash in the AspNetUsers table. Is the salt stored anywhere for me to see? – unicorn2 Mar 09 '18 at 08:12
  • 1
    @unicorn2 If you take a look at Andrew Savinykh's answer... In the section about hashing it looks like the salt is stored in the first 16 bytes of the byte array which is Base64 encoded and written to the database. You would be able to see this Base64 encoded string in the PasswordHash table. All you can say about the Base64 string is that roughly the first third of it is the salt. The meaningful salt is the first 16 bytes of the Base64 decoded version of the full string stored in the PasswordHash table – Nattrass Mar 18 '18 at 16:32
  • 1
    @Nattrass, My understanding of hashes and salts is rather rudimentary, but if the salt is easily extracted from the hashed password, what is the point of salting in the first place. I thought the salt was meant to be an extra input to the hashing algorithm that couldn't easily be guessed. – NSouth May 27 '19 at 17:42
  • 3
    @NSouth The unique salt makes the hash unique for a given password. So two identical passwords will have different hash's. Having access to your hash and salt still doesn't get the attacker your password remember. The hash isn't reversible. They would still need to brute force there way through every possible password. The unique salt just means that the hacker can't infer a common passwords by doing a frequency analysis on specific hash's if they have managed to get hold of your entire user table. – Nattrass May 28 '19 at 10:49
9

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.

kfrosty
  • 805
  • 1
  • 8
  • 14
  • For this.. `_passwordHashBytes = bytes.GetBytes(SaltByteSize);` I guess you meant this `_passwordHashBytes = bytes.GetBytes(HashByteSize);`.. Doesn't matter in your scenario since both are of the same size but in general.. – Akshatha Feb 05 '18 at 02:34
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

M Komaei
  • 7,006
  • 2
  • 28
  • 34
0

After following the answer from Andrew Savinykh I've made the following changes. I'm using Dapper with an existing DB which was configured with AspNet Identity.

Please note that PasswordHasherCompatibilityMode.IdentityV2 works great if you're using AspNet Identity. Not tested yet for AspNetCore Identity.

Here is the GitHub Gist for complete class.