Helper pour créer des Hash avec Salt
Voici un code que j’utilise depuis longtemps pour créer rapidement des Hash avec Salt.
Le principe de base est simple :
- On complexifie le mot de passe de l’utilisateur en y ajoutant un Salt avant de Hasher le tout.
- Le Salt doit être propre à chaque utilisateur.
- On doit donc créer et stocker un Salt pour chaque utilisateur
Si vous ne connaitraient pas le principe, il est expliqué plus en détail ici.
Mon code est inspiré de ce qui se faisait il y a bien longtemps avec le MemeberShipProvider de .net Framework. Il a vocation à être utilisé dans des contextes où l’on souhaite maitriser sa propre base de données sans déléguer le travail à un Provider du .net Framework (MemberShipProvider, IdentityProvider,... etc..)
Objectif
L’idée ici est d’avoir une petite classe qui simplifie :
- La création d’un nouveau Hash (et donc la récupération d’un Salt créé pour l’occasion).
- La création d’un Hash avec Salt connu (pour permettre ensuite la comparaison de Hashs).
Création du Salt
Le Salt est produit à partir d’un RNGCryptoServiceProvider. Ce qui garantit un bon niveau de complexité pour celui-ci.
private const Int32 SaltLength = 32;
/// <summary>
/// Get a new salt
/// </summary>
/// <returns></returns>
private static String GetANewSalt()
{
// Set salt length
Byte[] salt = new Byte[SaltLength];
// Build the salt array
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
provider.GetBytes(salt);
// Return the salt as string
return Convert.ToBase64String(salt);
}
Création du hash
La création du Hash se fait en 4 étapes :
- Transformation des String en tableaux de Bytes (String à hasher + String du Salt)
- Concaténation des deux tableaux
- Hash
- Récupération du Hash sous forme d’un String
/// <summary>
/// Hash password with an existing salt
/// </summary>
/// <param name="password"></param>
/// <param name="salt"></param>
/// <returns></returns>
public static String Hash(String password, String salt)
{
// String to bites
Byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
Byte[] saltBytes = Encoding.Unicode.GetBytes(salt);
// Full bytes array to hash
Byte[] tohashBytes = new Byte[saltBytes.Length + passwordBytes.Length];
Buffer.BlockCopy(saltBytes, 0, tohashBytes, 0, saltBytes.Length);
Buffer.BlockCopy(passwordBytes, 0, tohashBytes, saltBytes.Length, passwordBytes.Length);
// Hash
var provider = new SHA512CryptoServiceProvider();
Byte[] hashedBytes = provider.ComputeHash(tohashBytes);
// Return the hash as String
return Convert.ToBase64String(hashedBytes);
}
Création simultanée d’un nouveau Hash et d’un Salt
Pour la dernière méthode, j’ai choisi de retourner une structure simple :
/// <summary>
/// Hash and Salt used
/// </summary>
public struct HashResult
{
/// <summary>
/// Hash
/// </summary>
public String Hash { get; set; }
/// <summary>
/// Salt used to create the hash
/// </summary>
public String Salt { get; set; }
}
Ce qui permet de coder une méthode telle que :
/// <summary>
/// Hash password with a new salt
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
public static HashResult Hash(String password)
{
// Get a new Salt
String salt = GetANewSalt();
// Hash the password
String hash = Hash(password, salt);
// Return the result
return new HashResult
{
Hash = hash,
Salt = salt
};
}
Côté usage
L’usage est plutôt simple et sommaire. Voici deux tests unitaires qui reprennent les deux scénarios cibles :
[TestMethod]
public void NewHashTest()
{
// Password to hash
String passwordToHash = "MyPassword|1234";
// Hash
HashResult result = HashHelper.Hash(passwordToHash);
// Tests
Assert.IsFalse(String.IsNullOrWhiteSpace(result.Hash));
Assert.IsFalse(String.IsNullOrWhiteSpace(result.Salt));
Assert.AreNotEqual(result.Hash, passwordToHash);
// Data
Debug.WriteLine($"Hash = {result.Hash}");
Debug.WriteLine($"Salt = {result.Salt}");
}
[TestMethod]
public void ExistingHashTest()
{
// Password to hash
String passwordToHash = "911Me78@Toto.lan";
// Hash
HashResult result = HashHelper.Hash(passwordToHash);
// Second hash with the Salt
String actual = HashHelper.Hash(passwordToHash, result.Salt);
// Tests
String expected = result.Hash;
Assert.IsFalse(String.IsNullOrWhiteSpace(result.Salt));
Assert.AreEqual(expected, actual);
// Data
Debug.WriteLine($"Hash = {result.Hash}");
Debug.WriteLine($"Salt = {result.Salt}");
}
Hash et Salt résultants
Pour avoir un ordre d’idée, voici le type de Hash et de Salt obtenus :
- Hash : O6AZxNkSEmZmf6ix5AIt16Ej0Tqw7BrpFjJKKlNFeOkx/OdjQFmD3E3VME0mTZ0WXCyueOEsKCmqY0ofYpAlsw==
- Salt : jywh80eNLXRmv81+NBvESwrln6qfFD3okjq5tvDvazM=
Le code intégral du Helper
Pour avoir une vision d’ensemble claire, voici le code dans son intégralité
using System;
using System.Security.Cryptography;
using System.Text;
namespace MyLib.Web.Security
{
/// <summary>
/// Helper to hash strings
/// </summary>
public static class HashHelper
{
private const Int32 SaltLength = 32;
/// <summary>
/// Hash password with a new salt
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
public static HashResult Hash(String password)
{
// Get a new Salt
String salt = GetANewSalt();
// Hash the password
String hash = Hash(password, salt);
// Return the result
return new HashResult
{
Hash = hash,
Salt = salt
};
}
/// <summary>
/// Hash password with an existing salt
/// </summary>
/// <param name="password"></param>
/// <param name="salt"></param>
/// <returns></returns>
public static String Hash(String password, String salt)
{
// String to bites
Byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
Byte[] saltBytes = Encoding.Unicode.GetBytes(salt);
// Full bytes array to hash
Byte[] tohashBytes = new Byte[saltBytes.Length + passwordBytes.Length];
Buffer.BlockCopy(saltBytes, 0, tohashBytes, 0, saltBytes.Length);
Buffer.BlockCopy(passwordBytes, 0, tohashBytes, saltBytes.Length, passwordBytes.Length);
// Hash
var provider = new SHA512CryptoServiceProvider();
Byte[] hashedBytes = provider.ComputeHash(tohashBytes);
// Return the hash as String
return Convert.ToBase64String(hashedBytes);
}
/// <summary>
/// Get a new salt
/// </summary>
/// <returns></returns>
private static String GetANewSalt()
{
// Set salt length
Byte[] salt = new Byte[SaltLength];
// Build the salt array
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
provider.GetBytes(salt);
// Return the salt as string
return Convert.ToBase64String(salt);
}
}
/// <summary>
/// Hash and Salt used
/// </summary>
public struct HashResult
{
/// <summary>
/// Hash
/// </summary>
public String Hash { get; set; }
/// <summary>
/// Salt used to create the hash
/// </summary>
public String Salt { get; set; }
}
}