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; }
    }
}


Jérémy Jeanson

Comments

You have to be logged in to comment this post.