Hashing y salting son los pasos más comunes que suelo aplicar a cualquier proceso de proteger contraseñas almacenado de la misma. Considerando la naturaleza de estos artículos, aquí dejaré directamente los links al Github para ver el proyecto y poder copiarlo, así como la tabla de contenidos para leer con mayor facilidad.
👉 Link a la clase Crypto.cs en Github
👉 Link al ejemplo de implementación con ASP.NET MVC 6 en Github
Si sólo necesitas el código, lo puedes encontrar al final de este artículo en la sección «Código de la clase final Crypto.cs».
Construyendo la clase Crypto.cs
Para nuestra clase vamos a utilizar 2 librerías fundamentalmente: System.Security.Crytopgraphy y Microsoft.AspNetCore.Cryptography.KeyDerivation.
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace HashingSaltingMVCExample.Services;
public class Crypto
{
// Ahora veremos la implementación
}
Hashing
Entendemos como hashing a una función unidireccional que permite encriptar una cadena de caracteres, de forma que es imposible deshacer esta encriptación y obtener la cadena original.
Hay múltiples algoritmos utilizados para realizar esta encriptación, pero todos tienen el objetivo de convertir esta cadena en otra que no tenga ningún tipo de parecido y que, pese a que alguien pudiera capturar este hash, no pudieran utilizarlo para suplantar tu identidad.
Texto original
→ H3110w0r1d!
Usando Algoritmo SHA-256:
2a3ed009562d28dac1d857ae48feae7bb2bf3b628623c19326d22a3594e01d42
Usando Algoritmo SHA-384:
4a23bb0677fe0024428e135ae69d3567178724fd23a110b0bcca1d3d955feb7f22a108086f6e6a913b9aa571bd60a50b
Usando Algoritmo SHA-512: 5684280634c5835e1fea4b0231d7e6d86326a090da06c37d5489fb4e931b37d30a1ab6d814e2b293f30a5c85a3b07a500ecfd8d2e9941e22a450e3d4bec30491
Pese a que hay muchos más algoritmos, esos 3 son los sugeridos por .NET y OWASP por su seguridad. Sin embargo, como los algoritmos SHA-384 y SHA-512 son computacionalmente más costosos, optaremos por el SHA-256.
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace HashingSaltingMVCExample.Services;
public class Crypto
{
public static string HashPassword(string password)
{
// password => H3110w0r1d!
SHA256 sha256 = SHA256.Create();
byte[] hashedPassword = sha256.ComputeHash(Convert.FromBase64String(password));
// 2a3ed009562d28dac1d857ae48feae7bb2bf3b628623c19326d22a3594e01d42
return Convert.ToBase64String(hashedPassword);
}
}
Salting
Salting es un método para ayudar combatir la debilidad previamente comentada sobre los Rainbow Tables. Para ello creamos una función que genere una cadena de caracteres aleatoria que vamos a añadir al principio o al final de la contraseña a hashear.
public static string GenerateSalt()
{
byte[] salt = new byte[128 / 8];
using (var rngCsp = new RNGCryptoServiceProvider())
{
rngCsp.GetNonZeroBytes(salt);
}
return Convert.ToBase64String(salt);
}
Y con esto parecería que ya podríamos modificar nuestro método HashPassword de la siguiente forma:
public static string GenerateSalt()
{
byte[] salt = new byte[128 / 8];
using (var rdnGen = RandomNumberGenerator.Create())
{
rdnGen.GetNonZeroBytes(salt);
}
return Convert.ToBase64String(salt);
}
public static Tuple<string, string> HashPassword(string password, string? salt = null)
{
// Generamos un valor aleatorio de salt o utilizamos el que almacenemos en la bbdd
string genSalt = salt ?? GenerateSalt();
// password = H3110w0r1d! + salt
SHA256 sha256 = SHA256.Create();
byte[] hashedPassword = sha256.ComputeHash(Convert.FromBase64String(password + salt));
// hashedPassword string => ??? Porque depende del salt generado
return Tuple.Create(Convert.ToBase64String(hashedPassword), genSalt);
}
Sin embargo tiene un problema fundamental, y es que si intentases ejecutar ese código, todo funcionaría hasta que hicieras es Convert.FromBase64String(password + salt) porque da un error indicándo que la conversión no pudo realizarse porque hay caracteres que no pertenecen a un array de 64 bytes.
Para solucionarlo, toda esta implementación puede hacer uso de una clase KeyDerivation, que es la razón por la que incluimos la librería Microsoft.AspNetCore.Cryptography.KeyDerivation:
public static byte[] GenerateSalt()
{
byte[] salt = new byte[128 / 8];
using (var rdnGen = RandomNumberGenerator.Create())
{
rdnGen.GetNonZeroBytes(salt);
}
return salt;
}
public static Tuple<string, string> HashPassword(string password, string? salt = null)
{
byte[] byteSalt = string.IsNullOrEmpty(salt) ? GenerateSalt() : Convert.FromBase64String(salt);
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: byteSalt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 100000,
numBytesRequested: 256 / 8
));
return Tuple.Create(hashed, Convert.ToBase64String(byteSalt));
}
Código de la clase final Crypto.cs
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace HashingSaltingMVCExample.Services;
public class Crypto
{
public static byte[] GenerateSalt()
{
byte[] salt = new byte[128 / 8];
using (var rdnGen = RandomNumberGenerator.Create())
{
rdnGen.GetNonZeroBytes(salt);
}
return salt;
}
public static Tuple<string, string> HashPassword(string password, string? salt = null)
{
byte[] byteSalt = string.IsNullOrEmpty(salt) ? GenerateSalt() : Convert.FromBase64String(salt);
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: byteSalt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 100000,
numBytesRequested: 256 / 8
));
return Tuple.Create(hashed, Convert.ToBase64String(byteSalt));
}
}
En el repositorio Github podréis ver un ejemplo de implementación de esta clase usando como base lo explicado en el artículo: «Cómo añadir autenticación y almacenarla en Cookies con .NET 6 MVC«. De todas maneras, al ser una clase POCO, puede servir para MVC, Razor, WebAPI o Blazor de manera indiferente, por lo que ya podéis proteger contraseñas utilizando este método.