Cómo codificar una contraseña
Me gustaría almacenar el hash de una contraseña en el teléfono, pero no estoy seguro de cómo hacerlo. Parece que solo puedo encontrar métodos de cifrado. ¿Cómo se debe codificar correctamente la contraseña?
La mayoría de las otras respuestas aquí están algo desactualizadas considerando las mejores prácticas actuales (año 2012).
El algoritmo de hash de contraseñas más sólido disponible de forma nativa en .NET es PBKDF2, representado por la Rfc2898DeriveBytes
clase. El siguiente código se encuentra en una clase independiente en esta publicación: Otro ejemplo de cómo almacenar un hash de contraseña salado . Los conceptos básicos son realmente fáciles, así que aquí se desglosan:
PASO 1 Cree el valor salt con un PRNG criptográfico:
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);
PASO 2 Cree el Rfc2898DeriveBytes y obtenga el valor hash:
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
PASO 3 Combine los bytes salt y contraseña para su uso posterior:
byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);
PASO 4 Convierte la combinación de sal y hachís en una cuerda para guardarla.
string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });
PASO 5 Verifique la contraseña ingresada por el usuario con una contraseña almacenada
/* Fetch the stored value */
string savedPasswordHash = DBContext.GetUser(u => u.UserName == user).Password;
/* Extract the bytes */
byte[] hashBytes = Convert.FromBase64String(savedPasswordHash);
/* Get the salt */
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */
for (int i=0; i < 20; i++)
if (hashBytes[i+16] != hash[i])
throw new UnauthorizedAccessException();
Nota: Dependiendo de los requisitos de rendimiento de su aplicación específica, el valor 100000
se puede reducir. Un valor mínimo debería ser alrededor de 10000
.
Basado en la gran respuesta de csharptest.net , escribí una clase para esto:
public static class SecurePasswordHasher
{
/// <summary>
/// Size of salt.
/// </summary>
private const int SaltSize = 16;
/// <summary>
/// Size of hash.
/// </summary>
private const int HashSize = 20;
/// <summary>
/// Creates a hash from a password.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="iterations">Number of iterations.</param>
/// <returns>The hash.</returns>
public static string Hash(string password, int iterations)
{
// Create salt
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);
// Create hash
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
var hash = pbkdf2.GetBytes(HashSize);
// Combine salt and hash
var hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
// Convert to base64
var base64Hash = Convert.ToBase64String(hashBytes);
// Format hash with extra information
return string.Format("$MYHASH$V1${0}${1}", iterations, base64Hash);
}
/// <summary>
/// Creates a hash from a password with 10000 iterations
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The hash.</returns>
public static string Hash(string password)
{
return Hash(password, 10000);
}
/// <summary>
/// Checks if hash is supported.
/// </summary>
/// <param name="hashString">The hash.</param>
/// <returns>Is supported?</returns>
public static bool IsHashSupported(string hashString)
{
return hashString.Contains("$MYHASH$V1$");
}
/// <summary>
/// Verifies a password against a hash.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="hashedPassword">The hash.</param>
/// <returns>Could be verified?</returns>
public static bool Verify(string password, string hashedPassword)
{
// Check hash
if (!IsHashSupported(hashedPassword))
{
throw new NotSupportedException("The hashtype is not supported");
}
// Extract iteration and Base64 string
var splittedHashString = hashedPassword.Replace("$MYHASH$V1$", "").Split('$');
var iterations = int.Parse(splittedHashString[0]);
var base64Hash = splittedHashString[1];
// Get hash bytes
var hashBytes = Convert.FromBase64String(base64Hash);
// Get salt
var salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);
// Create hash with given salt
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
byte[] hash = pbkdf2.GetBytes(HashSize);
// Get result
for (var i = 0; i < HashSize; i++)
{
if (hashBytes[i + SaltSize] != hash[i])
{
return false;
}
}
return true;
}
}
Uso:
// Hash
var hash = SecurePasswordHasher.Hash("mypassword");
// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);
Un hash de muestra podría ser este:
$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn
Como puede ver, también he incluido las iteraciones en el hash para facilitar su uso y la posibilidad de actualizarlo, si es necesario.
Si está interesado en .net core, también tengo una versión de .net core en Code Review .
ACTUALIZACIÓN : ESTA RESPUESTA ESTÁ MUY DESACTUALIZADA . Utilice las recomendaciones de https://stackoverflow.com/a/10402129 o https://stackoverflow.com/a/73125177 en su lugar.
Puedes usar
var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);
o
var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);
Para obtener data
una matriz de bytes puedes usar
var data = Encoding.ASCII.GetBytes(password);
y para recuperar la cuerda de md5data
osha1data
var hashedPassword = ASCIIEncoding.GetString(md5data);
Solución 2022 (.NET 6+):
La mayoría de las otras respuestas aquí se escribieron hace años y, por lo tanto, no aprovechan muchas de las características más recientes introducidas en las versiones más recientes de .NET. Ahora se puede lograr lo mismo de forma mucho más sencilla y con mucha menos repetición y ruido. La solución que propongo también proporciona solidez adicional al permitirle modificar la configuración (por ejemplo, recuento de iteraciones, etc.) en el futuro sin romper los hash antiguos (que es lo que sucedería si usara la solución propuesta de la respuesta aceptada , por ejemplo). ).
Ventajas:
Utiliza el nuevo
Rfc2898DeriveBytes.Pbkdf2()
método estático introducido en .NET 6, eliminando la necesidad de crear instancias y también eliminar el objeto cada vez.Utiliza la nueva
RandomNumberGenerator
clase y suGetBytes
método estático, introducido en .NET 6, para generar el salt. LaRNGCryptoServiceProvider
clase utilizada en la respuesta aceptada está obsoleta .Utiliza el
CryptographicOperations.FixedTimeEquals
método (introducido en .NET Core 2.1) para comparar los bytes clave en elVerify
método, en lugar de hacer la comparación a mano, como lo hace la respuesta aceptada. Esto, además de eliminar muchos textos repetitivos ruidosos, también anula los ataques de sincronización .Utiliza SHA-256 en lugar del SHA-1 predeterminado como algoritmo subyacente, solo para estar seguro, ya que este último es un algoritmo más robusto y confiable.
La cadena devuelta por el
Hash
método tiene la siguiente estructura:[key]:[salt]:[iterations]:[algorithm]
Esta es la ventaja más importante de esta solución, significa que básicamente incluimos metadatos sobre las configuraciones utilizadas para crear el hash en la cadena final. Esto nos permite cambiar la configuración (como el número de iteraciones, el tamaño de sal/clave, etc.) en nuestra clase hasher en el futuro sin romper los hashes anteriores creados con la configuración anterior. Esto es algo que la respuesta aceptada, por ejemplo, no tiene en cuenta, ya que depende de los valores de configuración "actuales" para verificar los hashes.
Otros puntos:
- Estoy usando la representación hexadecimal de la clave y la sal en la cadena hash devuelta. En su lugar, puedes usar base64 si lo prefieres, simplemente cambiando cada aparición de
Convert.ToHexString
yConvert.FromHexString
aConvert.ToBase64
yConvert.FromBase64
respectivamente. El resto de la lógica sigue siendo exactamente la misma. - El tamaño de sal que a menudo se recomienda es de 64 bits o más. Lo configuré a 128 bits.
- El tamaño de la clave normalmente debería ser el mismo que el tamaño de salida natural del algoritmo elegido; consulte este comentario . En nuestro caso, como mencioné anteriormente, el algoritmo subyacente es SHA-256, cuyo tamaño de salida es de 256 bits, que es precisamente en lo que estamos configurando nuestro tamaño de clave.
- Si planea utilizar esto para almacenar contraseñas de usuario, normalmente se recomienda utilizar al menos 10.000 iteraciones o más. Establecí el valor predeterminado en 50.000, que, por supuesto, puedes cambiar como mejor te parezca.
El código:
public static class SecretHasher
{
private const int _saltSize = 16; // 128 bits
private const int _keySize = 32; // 256 bits
private const int _iterations = 50000;
private static readonly HashAlgorithmName _algorithm = HashAlgorithmName.SHA256;
private const char segmentDelimiter = ':';
public static string Hash(string input)
{
byte[] salt = RandomNumberGenerator.GetBytes(_saltSize);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
input,
salt,
_iterations,
_algorithm,
_keySize
);
return string.Join(
segmentDelimiter,
Convert.ToHexString(hash),
Convert.ToHexString(salt),
_iterations,
_algorithm
);
}
public static bool Verify(string input, string hashString)
{
string[] segments = hashString.Split(segmentDelimiter);
byte[] hash = Convert.FromHexString(segments[0]);
byte[] salt = Convert.FromHexString(segments[1]);
int iterations = int.Parse(segments[2]);
HashAlgorithmName algorithm = new HashAlgorithmName(segments[3]);
byte[] inputHash = Rfc2898DeriveBytes.Pbkdf2(
input,
salt,
iterations,
algorithm,
hash.Length
);
return CryptographicOperations.FixedTimeEquals(inputHash, hash);
}
}
Uso:
// Hash:
string password = "...";
string hashed = SecretHasher.Hash(password);
// Verify:
string enteredPassword = "...";
bool isPasswordCorrect = SecretHasher.Verify(enteredPassword, hashed);
Las respuestas de @csharptest.net y Christian Gollhardt son geniales, muchas gracias. Pero después de ejecutar este código en producción con millones de registros, descubrí que hay una pérdida de memoria. Las clases RNGCryptoServiceProvider y Rfc2898DeriveBytes se derivan de IDisposable pero no las eliminamos. Escribiré mi solución como respuesta si alguien necesita una versión desechada.
public static class SecurePasswordHasher
{
/// <summary>
/// Size of salt.
/// </summary>
private const int SaltSize = 16;
/// <summary>
/// Size of hash.
/// </summary>
private const int HashSize = 20;
/// <summary>
/// Creates a hash from a password.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="iterations">Number of iterations.</param>
/// <returns>The hash.</returns>
public static string Hash(string password, int iterations)
{
// Create salt
using (var rng = new RNGCryptoServiceProvider())
{
byte[] salt;
rng.GetBytes(salt = new byte[SaltSize]);
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
{
var hash = pbkdf2.GetBytes(HashSize);
// Combine salt and hash
var hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
// Convert to base64
var base64Hash = Convert.ToBase64String(hashBytes);
// Format hash with extra information
return $"$HASH|V1${iterations}${base64Hash}";
}
}
}
/// <summary>
/// Creates a hash from a password with 10000 iterations
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The hash.</returns>
public static string Hash(string password)
{
return Hash(password, 10000);
}
/// <summary>
/// Checks if hash is supported.
/// </summary>
/// <param name="hashString">The hash.</param>
/// <returns>Is supported?</returns>
public static bool IsHashSupported(string hashString)
{
return hashString.Contains("HASH|V1$");
}
/// <summary>
/// Verifies a password against a hash.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="hashedPassword">The hash.</param>
/// <returns>Could be verified?</returns>
public static bool Verify(string password, string hashedPassword)
{
// Check hash
if (!IsHashSupported(hashedPassword))
{
throw new NotSupportedException("The hashtype is not supported");
}
// Extract iteration and Base64 string
var splittedHashString = hashedPassword.Replace("$HASH|V1$", "").Split('$');
var iterations = int.Parse(splittedHashString[0]);
var base64Hash = splittedHashString[1];
// Get hash bytes
var hashBytes = Convert.FromBase64String(base64Hash);
// Get salt
var salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);
// Create hash with given salt
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
{
byte[] hash = pbkdf2.GetBytes(HashSize);
// Get result
for (var i = 0; i < HashSize; i++)
{
if (hashBytes[i + SaltSize] != hash[i])
{
return false;
}
}
return true;
}
}
}
Uso:
// Hash
var hash = SecurePasswordHasher.Hash("mypassword");
// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);