Password hashing implementation

This commit is contained in:
Alexandru Macocian
2021-11-15 21:52:39 +01:00
parent c0b5734b71
commit bf163251dd
5 changed files with 428 additions and 1 deletions
@@ -0,0 +1,47 @@
using System.Threading.Tasks;
namespace System.Hashing
{
public interface IPasswordHashingService
{
/// <summary>
/// Hash provided raw password and return the hashed value.
/// </summary>
/// <param name="raw">Base64 encoded password.</param>
/// <param name="salt">Base64 encoded salt to be used for hashing.</param>
/// <param name="length">Length of the resulting hash.</param>
/// <param name="iterations">Number of hashing iterations to be performed.</param>
/// <remarks>
/// The length of the hash will vary due to the base64 encoding of the resulting hash.
/// </remarks>
/// <returns>Base64 encoded hashed password.</returns>
Task<string> Hash(string raw, string salt, int length, int iterations);
/// <summary>
/// Hash provided raw password and return the hashed value.
/// </summary>
/// <param name="raw">Password to be hashed.</param>
/// <param name="salt">Salt to be used for hashing.</param>
/// <param name="length">Length of the resulting hash.</param>
/// <param name="iterations">Number of hashing iterations to be performed.</param>
/// <returns>Hashed password.</returns>
Task<byte[]> Hash(byte[] raw, byte[] salt, int length, int iterations);
/// <summary>
/// Verify provided password against a hashed password.
/// </summary>
/// <param name="hash">Base64 encoding of the previously hashed password.</param>
/// <param name="password">Base64 encoding of the password to be verified.</param>
/// <param name="salt">Salt used to hash the previous password.</param>
/// <param name="iterations">Number of iterations used to hash the previous password.</param>
/// <returns>Returns true if password matches the previously hashed password. Otherwise returns false.</returns>
Task<bool> VerifyPassword(string hash, string password, string salt, int iterations);
/// <summary>
/// Verify provided password against a hashed password.
/// </summary>
/// <param name="hash">Previously hashed password.</param>
/// <param name="password">Password to be verified.</param>
/// <param name="salt">Salt used to hash the previous password.</param>
/// <param name="iterations">Number of iterations used to hash the previous password.</param>
/// <returns>Returns true if password matches the previously hashed password. Otherwise returns false.</returns>
Task<bool> VerifyPassword(byte[] hash, byte[] password, byte[] salt, int iterations);
}
}
@@ -0,0 +1,101 @@
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace System.Hashing
{
public sealed class Rfc2898DeriveBytesPasswordHashingService : IPasswordHashingService
{
private const int MinimumIterations = 10000;
private const int MinimumHashLength = 32;
public Rfc2898DeriveBytesPasswordHashingService()
{
}
public async Task<string> Hash(string raw, string salt, int length, int iterations)
{
this.ValidateHashArguments(raw, salt, length, iterations);
var rawBytes = Convert.FromBase64String(raw);
var saltBytes = Convert.FromBase64String(salt);
var hashBytes = await this.HashInternal(rawBytes, saltBytes, length, iterations);
return Convert.ToBase64String(hashBytes);
}
public Task<byte[]> Hash(byte[] raw, byte[] salt, int length, int iterations)
{
this.ValidateHashArguments(raw, salt, length, iterations);
return this.HashInternal(raw, salt, length, iterations);
}
public Task<bool> VerifyPassword(string hash, string password, string salt, int iterations)
{
this.ValidateVerifyArguments(hash, password, salt, iterations);
var hashBytes = Convert.FromBase64String(hash);
var saltBytes = Convert.FromBase64String(salt);
var passwordBytes = Convert.FromBase64String(password);
return this.VerifyPasswordInternal(hashBytes, passwordBytes, saltBytes, iterations);
}
public Task<bool> VerifyPassword(byte[] hash, byte[] password, byte[] salt, int iterations)
{
this.ValidateVerifyArguments(hash, password, salt, iterations);
return this.VerifyPasswordInternal(hash, password, salt, iterations);
}
private void ValidateHashArguments(object raw, object salt, int length, int iterations)
{
_ = raw ?? throw new ArgumentNullException(nameof(raw));
_ = salt ?? throw new ArgumentNullException(nameof(salt));
if (iterations < MinimumIterations)
{
throw new InvalidOperationException($"Unable to perform secure hash. Iteration count must be over {MinimumIterations}");
}
if (length < MinimumHashLength)
{
throw new InvalidOperationException($"Unable to perform secure hash. Hash length must be over {MinimumHashLength}");
}
}
private void ValidateVerifyArguments(object hash, object password, object salt, int iterations)
{
_ = hash ?? throw new ArgumentNullException(nameof(hash));
_ = salt ?? throw new ArgumentNullException(nameof(salt));
_ = password ?? throw new ArgumentNullException(nameof(password));
if (iterations < MinimumIterations)
{
throw new InvalidOperationException($"Unable to verify hash. Iteration count must be over {MinimumIterations}");
}
}
private Task<byte[]> HashInternal(byte[] raw, byte[] salt, int length, int iterations)
{
using var pbkdf2 = new Rfc2898DeriveBytes(raw, salt, iterations);
var hash = pbkdf2.GetBytes(length);
return Task.FromResult(hash);
}
private Task<bool> VerifyPasswordInternal(byte[] hash, byte[] password, byte[] salt, int iterations)
{
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
var hashToVerify = pbkdf2.GetBytes(hash.Length);
var lengthToVerify = Math.Max(hash.Length, hashToVerify.Length);
var match = true;
if (lengthToVerify <= 0)
{
return Task.FromResult(false);
}
for(var i = 0; i < lengthToVerify; i++)
{
if (i < hash.Length && i < hashToVerify.Length)
{
if (hashToVerify[i].Equals(hash[i]) is false)
{
match = false;
}
}
}
return Task.FromResult(match && hash.Length.Equals(hashToVerify.Length));
}
}
}
@@ -2,9 +2,27 @@
{
public interface ICryptoRngProvider
{
/// <summary>
/// Populate provided array of bytes with cryptographically secure random bytes.
/// </summary>
/// <param name="data">Array of bytes to be populated.</param>
public void GetBytes(byte[] data);
/// <summary>
/// Return an array of cryptographically secure random bytes.
/// </summary>
/// <param name="byteCount">Length of the returned array.</param>
/// <returns>Array containing cryptographically secure random values.</returns>
public byte[] GetBytes(int byteCount);
/// <summary>
/// Populate provided array of bytes with cryptographically secure random non-zero bytes.
/// </summary>
/// <param name="data">Array of bytes to be populated.</param>
public void GetNonZeroBytes(byte[] data);
/// <summary>
/// Return an array of cryptographically secure random non-zero bytes.
/// </summary>
/// <param name="byteCount">Length of the returned array.</param>
/// <returns>Array containing cryptographically secure random non-zero values.</returns>
public byte[] GetNonZeroBytes(int byteCount);
}
}
@@ -7,7 +7,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<RootNamespace>System</RootNamespace>
<Version>1.1.1</Version>
<Version>1.2.0</Version>
<Authors>Alexandru Macocian</Authors>
<RepositoryUrl>https://github.com/AlexMacocian/SystemExtensions</RepositoryUrl>
</PropertyGroup>