Skip to content

Instantly share code, notes, and snippets.

@huysentruitw
Last active May 26, 2020 08:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huysentruitw/96af92e14f0cbe1afbbf4d2287c22c70 to your computer and use it in GitHub Desktop.
Save huysentruitw/96af92e14f0cbe1afbbf4d2287c22c70 to your computer and use it in GitHub Desktop.
PasswordHasher class for generating and verifying secure password hashes
/*
* Copyright 2016-2020 Wouter Huysentruit
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
public static class PasswordHasher
{
private const char PasswordHashingIterationCountSeparator = '.';
private const int PBKDF2SubkeyLength = 256 / 8; // 256 bits
private const int SaltSize = 128 / 8; // 128 bits
private enum PasswordHashingVersion : byte
{
Version1 = 1
}
public static string GenerateRandomPassword(int length, string allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+*/!?@#")
{
if (allowedChars == null)
{
throw new ArgumentNullException("allowedChars");
}
if (allowedChars.Length == 0)
{
throw new ArgumentException("Should not be empty", "allowedChars");
}
var randomHash = GenerateRandomHash(length);
var result = new StringBuilder();
foreach (var value in randomHash)
{
result.Append(allowedChars[value % allowedChars.Length]);
}
return result.ToString();
}
private static byte[] GenerateRandomHash(int length)
{
if (length <= 0)
{
throw new ArgumentException("Invalid length", "length");
}
var bytes = new byte[length];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(bytes);
}
return bytes;
}
public static string HashPassword(string password, int? iterationCount = 10000)
{
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("password cannot be empty or null", "password");
}
var count = iterationCount ?? GetIterationsFromYear(DateTime.Now.Year);
var result = HashPasswordInternal(password, count);
return EncodeIterations(count) + PasswordHashingIterationCountSeparator + result;
}
private static int GetIterationsFromYear(int year)
{
int startYear = 2000;
int startCount = 1000;
if (year > startYear)
{
var diff = (year - startYear) / 2;
var mul = (int)Math.Pow(2, diff);
var count = (long)startCount * (long)mul;
return (int)Math.Min(count, int.MaxValue);
}
return startCount;
}
private static string HashPasswordInternal(string password, int iterationCount)
{
byte[] salt;
byte[] subkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, iterationCount))
{
salt = deriveBytes.Salt;
subkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
var outputBytes = new byte[1 + SaltSize + PBKDF2SubkeyLength];
outputBytes[0] = (byte)PasswordHashingVersion.Version1;
Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, PBKDF2SubkeyLength);
return Convert.ToBase64String(outputBytes);
}
private static string EncodeIterations(int count)
{
return count.ToString("X");
}
public static bool VerifyHashedPassword(string hashedPassword, string password)
{
if (string.IsNullOrWhiteSpace(hashedPassword))
{
throw new ArgumentException("hashedPassword cannot be empty or null", "hashedPassword");
}
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("password cannot be empty or null", "password");
}
var parts = hashedPassword.Split(PasswordHashingIterationCountSeparator);
if (parts.Length != 2)
{
return false;
}
int count = DecodeIterations(parts[0]);
if (count <= 0)
{
return false;
}
hashedPassword = parts[1];
return VerifyHashedPassword(hashedPassword, password, count);
}
private static int DecodeIterations(string prefix)
{
int val;
return int.TryParse(prefix, NumberStyles.HexNumber, null, out val) ? val : -1;
}
private static bool VerifyHashedPassword(string hashedPassword, string password, int iterationCount)
{
var hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
var version = (PasswordHashingVersion)hashedPasswordBytes[0];
if (version != PasswordHashingVersion.Version1 && hashedPasswordBytes.Length != (1 + SaltSize + PBKDF2SubkeyLength))
{
return false;
}
var salt = new byte[SaltSize];
Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
var storedSubkey = new byte[PBKDF2SubkeyLength];
Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PBKDF2SubkeyLength);
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, iterationCount))
{
generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
return Enumerable.SequenceEqual(storedSubkey, generatedSubkey);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment