Skip to content

Instantly share code, notes, and snippets.

@C0BR4cH
Created November 2, 2018 10:30
Show Gist options
  • Select an option

  • Save C0BR4cH/f154c7901c8910fb6b3a5e687f43b38c to your computer and use it in GitHub Desktop.

Select an option

Save C0BR4cH/f154c7901c8910fb6b3a5e687f43b38c to your computer and use it in GitHub Desktop.
Simple C# password hasher running from .NET 2.0 onward
using System;
using System.Security.Cryptography;
namespace SaltyCrypter
{
/// <summary>
/// Creates the salt and hash for a password
/// </summary>
public class CryptoMagic
{
private const int hashBlockSize = 5;
private const int algorithmIndex = 0;
private const int pbkdf2IterationsIndex = 1;
private const int bytesSizeIndex = 2;
private const int saltIndex = 3;
private const int hashIndex = 4;
/// <summary>
/// SHA-1 algorithm name
/// </summary>
public const string Sha1Name = "sha1";
/// <summary>
/// Amount of output bytes for SHA-1
/// </summary>
public const int Sha1Bytes = 20;
/// <summary>
/// Minimum of iterations for PBKDF2
/// </summary>
public const int MinPbkdf2Iterations = 64000;
/// <summary>
/// Size of the bytes array for salt and hash
/// </summary>
public int BytesSize { get; private set; } = Sha1Bytes;
/// <summary>
/// Generates the password hash.
/// The output is formatted as followed:
/// <code>algorithm:iterations:hashSize:salt:hash</code>
/// </summary>
/// <param name="password">Password to hash</param>
/// <param name="iterations">(Optional) Amount of iterations for PBKDF2 algorithm. Default and minimum is 64000</param>
/// <returns>Hash formatted in blocks</returns>
public string GeneratePasswordHash(string password, int iterations = -1)
{
// Get configures iterations
int pbkdf2Iterations = iterations > MinPbkdf2Iterations ? iterations : MinPbkdf2Iterations;
byte[] salt = GenerateSalt();
byte[] hash = GetPbkdf2Bytes(password, salt, pbkdf2Iterations, BytesSize);
return $"{Sha1Name}:{pbkdf2Iterations}:{BytesSize}:{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
}
/// <summary>
/// Verifies if the password matches to the hash.
/// The hash needs to be formatted in blocks like algorithm:iterations:hashSize:salt:hash
/// </summary>
/// <param name="password">Password to verify</param>
/// <param name="hash">Hash formatted in blocks</param>
/// <returns><code>true</code> if the password matches with the hash, <code>false</code> otherwise.</returns>
public bool VerifyPassowrd(string password, string hash)
{
// Parameter validation
if (string.IsNullOrEmpty(password) || password.Trim().Length == 0)
{
throw new ArgumentException("Password can't be empty / null.", nameof(password));
}
if (string.IsNullOrEmpty(hash) || hash.Trim().Length == 0)
{
throw new ArgumentException("Hash can't be empty / null.", nameof(hash));
}
// Hash validation
string[] hashBlocks = hash.Split(':');
if (hashBlocks.Length != hashBlockSize)
{
throw new InvalidHashException("Hash needs to be in format 'algorithm:pbkdf2Iterations:hashSize:salt:hash'.");
}
// Algorithm validation
if (hashBlocks[algorithmIndex] != Sha1Name)
{
throw new InvalidHashException($"Hash algorithm of {hashBlocks[algorithmIndex]} is invalid. Needs to be {Sha1Name}");
}
// Iterations validation
int iterations = -1;
try
{
iterations = int.Parse(hashBlocks[pbkdf2IterationsIndex]);
}
catch (OverflowException e)
{
throw new InvalidHashException($"Iterations of {hashBlocks[pbkdf2IterationsIndex]} are to large.", e);
}
catch (Exception e)
{
if (e is FormatException || e is ArgumentException)
{
throw new InvalidHashException($"Could not parse {hashBlocks[pbkdf2IterationsIndex]} into an int.", e);
}
throw new InvalidHashException("Invalid argument for iterations.", e);
}
// Salt validation
byte[] saltBytes = null;
try
{
saltBytes = Convert.FromBase64String(hashBlocks[saltIndex]);
}
catch (Exception e)
{
if (e is ArgumentException || e is FormatException)
{
throw new InvalidHashException($"Could not parse {hashBlocks[saltIndex]} into a byte array.", e);
}
throw new InvalidHashException("$Invalid argument for salt.", e);
}
// Hash validation
byte[] hashBytes = null;
try
{
hashBytes = Convert.FromBase64String(hashBlocks[hashIndex]);
}
catch (Exception e)
{
if (e is ArgumentException || e is FormatException)
{
throw new InvalidHashException($"Could not parse {hashBlocks[hashIndex]} into a byte array.", e);
}
throw new InvalidHashException("$Invalid argument for hash.", e);
}
// Hash size validation
int hashSize = -1;
try
{
hashSize = int.Parse(hashBlocks[bytesSizeIndex]);
}
catch (OverflowException e)
{
throw new InvalidHashException($"Hashsize of {hashBlocks[bytesSizeIndex]} are to large.", e);
}
catch (Exception e)
{
if (e is FormatException || e is ArgumentException)
{
throw new InvalidHashException($"Could not parse {hashBlocks[bytesSizeIndex]} into an int.", e);
}
throw new InvalidHashException("Invalid argument for iterations.", e);
}
// Hash size validation
if (hashSize != hashBytes.Length)
{
throw new InvalidHashException("Hash length doesn't match the stored hash size.");
}
// Hash provided password
byte[] verifyingHash = GetPbkdf2Bytes(password, saltBytes, iterations, hashSize);
// Check if both hashes are the same
return SlowEquals(hashBytes, verifyingHash);
}
private static bool SlowEquals(byte[] a, byte[] b)
{
uint diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
{
diff |= (uint)(a[i] ^ b[i]);
}
return diff == 0;
}
private byte[] GenerateSalt()
{
byte[] salt = new byte[BytesSize];
RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider();
try
{
cryptoRandom.GetBytes(salt);
}
finally
{
cryptoRandom = null;
}
return salt;
}
private byte[] GetPbkdf2Bytes(string password, byte[] salt, int iterations, int outputBytesSize)
{
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
byte[] output = null;
try
{
output = pbkdf2.GetBytes(outputBytesSize);
}
finally
{
pbkdf2.Reset();
pbkdf2 = null;
}
return output;
}
}
}
using System;
namespace SaltyCrypter
{
/// <summary>
/// Exception when the hash is invalid in any way
/// </summary>
public class InvalidHashException : Exception
{
public InvalidHashException(string message) : base(message) { }
public InvalidHashException(string message, Exception e) : base(message, e) { }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment