Did you like how the PasswordHasher was implemented in ASP.NET Core? What is the <TUser> used for in class definition? Why only SHA1 and SHA256 are supported? Why the size of salt is a constant? Is the default ASP.NET Core implementation a good choice?
If you are not familiar with the default implementation of PasswordHasher in ASP.NET Core, I would like to recommend an excellent article which explains everything.
Some ideas for better implementation of PasswordHasher:
- Setting only the hashing algorithm name should be enough for working with the PasswordHasher
- The salt size and iterations count can be defined as parameters by user or can be counted as default values related to the hashing algorithms
- SHA1 is good for runtime performance, SHA256 is enough for security storage, SHA384 and SHA512 hashing algorithms should be also implemented
- The result should not contain any hashing parameters, which means: do no not keep salt size, iterations count or hashing algorithm name in the hash
- The result should be the
string
that can be stored in the database with predictable length - Internal
byte[]
comparator for verifing passwords should be a microservice
The default password hasher for ASP.NET Core Identity uses PBKDF2
for password hashing that is not support all hashing algorithms. The Rfc2898DeriveBytes class from the System.Security.Cryptography
namespace supports all that we need to get the result we wanted. This class can generate pseudo-randomized salt and supports all SHA hashing algorithms.
public string HashPassword(string password)
{
byte[] saltBuffer;
byte[] hashBuffer;
using (var keyDerivation = new Rfc2898DeriveBytes(password, options.SaltSize, options.Iterations, options.HashAlgorithmName))
{
saltBuffer = keyDerivation.Salt;
hashBuffer = keyDerivation.GetBytes(options.HashSize);
}
byte[] result = new byte[options.HashSize + options.SaltSize];
Buffer.BlockCopy(hashBuffer, 0, result, 0, options.HashSize);
Buffer.BlockCopy(saltBuffer, 0, result, options.HashSize, options.SaltSize);
return Convert.ToBase64String(result);
}
public bool VerifyHashedPassword(string hashedPassword, string providedPassword)
{
byte[] hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
if (hashedPasswordBytes.Length != options.HashSize + options.SaltSize)
{
return false;
}
byte[] hashBytes = new byte[options.HashSize];
Buffer.BlockCopy(hashedPasswordBytes, 0, hashBytes, 0, options.HashSize);
byte[] saltBytes = new byte[options.SaltSize];
Buffer.BlockCopy(hashedPasswordBytes, options.HashSize, saltBytes, 0, options.SaltSize);
byte[] providedHashBytes;
using (var keyDerivation = new Rfc2898DeriveBytes(providedPassword, saltBytes, options.Iterations, options.HashAlgorithmName))
{
providedHashBytes = keyDerivation.GetBytes(options.HashSize);
}
return comparer.Equals(hashBytes, providedHashBytes);
}
The parameters for PasswordHasher can be specified in Startup.cs
via Options pattern. Also, in Startup.cs
can be registered the PasswordHasher as a microservice.
public void ConfigureServices(IServiceCollection services)
{
// Configuring PasswordHasher
services.Configure<PasswordHasherOptions>(options =>
{
options.HashAlgorithm = PasswordHasherAlgorithms.SHA1;
options.SaltSize = 16;
options.Iterations = 8192;
});
// Registering PasswordHasher
services.AddPasswordHasher();
services.AddMvc();
}
public class IndexModel : PageModel
{
private readonly IPasswordHasher hasher;
public IndexModel(IPasswordHasher hasher)
{
this.hasher = hasher;
}
public void OnGet()
{
var password = "my password";
var passwordHash = hasher.HashPassword(password);
var passwordCheck = hasher.VerifyHashedPassword(passwordHash, password);
}
}
Having questions? Contact me and I will help you sort it out.
<style>.inner { min-width: 800px !important; max-width: 60% !important;}</style>