Files
Moonlight/Moonlight/Core/Helpers/HashHelper.cs

172 lines
6.3 KiB
C#

using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace Moonlight.Core.Helpers;
// Src: https://codereview.stackexchange.com/questions/176697/net-core-mvc-future-proof-hashing-of-passwords
public static class HashHelper
{
/// <summary>
/// The default number of Iterations
/// </summary>
private const int DefaultIterations = 10000;
/// <summary>
/// Provides Information about a specific Hash Version
/// </summary>
private class HashVersion
{
public short Version { get; set; }
public int SaltSize { get; set; }
public int HashSize { get; set; }
public KeyDerivationPrf KeyDerivation { get; set; }
}
/// <summary>
/// Holds all possible Hash Versions
/// </summary>
private static readonly Dictionary<short, HashVersion> _versions = new Dictionary<short, HashVersion>
{
{
1, new HashVersion
{
Version = 1,
KeyDerivation = KeyDerivationPrf.HMACSHA512,
HashSize = 256 / 8,
SaltSize = 128 / 8
}
}
};
/// <summary>
/// The default Hash Version, which should be used, if a new Hash is Created
/// </summary>
private static HashVersion DefaultVersion => _versions[1];
/// <summary>
/// Checks if a given hash uses the latest version
/// </summary>
/// <param name="data">The hash</param>
/// <returns>Is the hash of the latest version?</returns>
public static bool IsLatestHashVersion(byte[] data)
{
var version = BitConverter.ToInt16(data, 0);
return version == DefaultVersion.Version;
}
/// <summary>
/// Checks if a given hash uses the latest version
/// </summary>
/// <param name="data">The hash</param>
/// <returns>Is the hash of the latest version?</returns>
public static bool IsLatestHashVersion(string data)
{
var dataBytes = Convert.FromBase64String(data);
return IsLatestHashVersion(dataBytes);
}
/// <summary>
/// Gets a random byte array
/// </summary>
/// <param name="length">The length of the byte array</param>
/// <returns>The random byte array</returns>
public static byte[] GetRandomBytes(int length)
{
var data = new byte[length];
using (var randomNumberGenerator = RandomNumberGenerator.Create())
{
randomNumberGenerator.GetBytes(data);
}
return data;
}
/// <summary>
/// Creates a Hash of a clear text
/// </summary>
/// <param name="clearText">the clear text</param>
/// <param name="iterations">the number of iteration the hash alogrythm should run</param>
/// <returns>the Hash</returns>
public static byte[] Hash(string clearText, int iterations = DefaultIterations)
{
//get current version
var currentVersion = DefaultVersion;
//get the byte arrays of the hash and meta information
var saltBytes = GetRandomBytes(currentVersion.SaltSize);
var versionBytes = BitConverter.GetBytes(currentVersion.Version);
var iterationBytes = BitConverter.GetBytes(iterations);
var hashBytes = KeyDerivation.Pbkdf2(clearText, saltBytes, currentVersion.KeyDerivation, iterations,
currentVersion.HashSize);
//calculate the indexes for the combined hash
var indexVersion = 0;
var indexIteration = indexVersion + 2;
var indexSalt = indexIteration + 4;
var indexHash = indexSalt + currentVersion.SaltSize;
//combine all data to one result hash
var resultBytes = new byte[2 + 4 + currentVersion.SaltSize + currentVersion.HashSize];
Array.Copy(versionBytes, 0, resultBytes, indexVersion, 2);
Array.Copy(iterationBytes, 0, resultBytes, indexIteration, 4);
Array.Copy(saltBytes, 0, resultBytes, indexSalt, currentVersion.SaltSize);
Array.Copy(hashBytes, 0, resultBytes, indexHash, currentVersion.HashSize);
return resultBytes;
}
/// <summary>
/// Creates a Hash of a clear text and convert it to a Base64 String representation
/// </summary>
/// <param name="clearText">the clear text</param>
/// <param name="iterations">the number of iteration the hash alogrythm should run</param>
/// <returns>the Hash</returns>
public static string HashToString(string clearText, int iterations = DefaultIterations)
{
var data = Hash(clearText, iterations);
return Convert.ToBase64String(data);
}
/// <summary>
/// Verifies a given clear Text against a hash
/// </summary>
/// <param name="clearText">The clear text</param>
/// <param name="data">The hash</param>
/// <returns>Is the hash equal to the clear text?</returns>
public static bool Verify(string clearText, byte[] data)
{
//Get the current version and number of iterations
var currentVersion = _versions[BitConverter.ToInt16(data, 0)];
var iteration = BitConverter.ToInt32(data, 2);
//Create the byte arrays for the salt and hash
var saltBytes = new byte[currentVersion.SaltSize];
var hashBytes = new byte[currentVersion.HashSize];
//Calculate the indexes of the salt and the hash
var indexSalt = 2 + 4; // Int16 (Version) and Int32 (Iteration)
var indexHash = indexSalt + currentVersion.SaltSize;
//Fill the byte arrays with salt and hash
Array.Copy(data, indexSalt, saltBytes, 0, currentVersion.SaltSize);
Array.Copy(data, indexHash, hashBytes, 0, currentVersion.HashSize);
//Hash the current clearText with the parameters given via the data
var verificationHashBytes = KeyDerivation.Pbkdf2(clearText, saltBytes, currentVersion.KeyDerivation, iteration,
currentVersion.HashSize);
//Check if generated hashes are equal
return hashBytes.SequenceEqual(verificationHashBytes);
}
/// <summary>
/// Verifies a given clear Text against a hash
/// </summary>
/// <param name="clearText">The clear text</param>
/// <param name="data">The hash</param>
/// <returns>Is the hash equal to the clear text?</returns>
public static bool Verify(string clearText, string data)
{
var dataBytes = Convert.FromBase64String(data);
return Verify(clearText, dataBytes);
}
}