Cómo hash una contraseña

117

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?

Skoder
fuente

Respuestas:

62

ACTUALIZACIÓN : ESTA RESPUESTA ES MUY ANTIGUA . En su lugar, utilice las recomendaciones de https://stackoverflow.com/a/10402129/251311 .

Puedes usar

var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);

o

var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);

Para obtener una datamatriz de bytes, puede usar

var data = Encoding.ASCII.GetBytes(password);

y recuperar cuerda de md5dataosha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);
zerkms
fuente
11
REALMENTE recomendaría usar SHA1. MD5 es un no-no a menos que mantenga la compatibilidad con versiones anteriores de un sistema existente. Además, asegúrese de incluirlo en una usingdeclaración o invocarlo Clear()cuando haya terminado de usar la implementación.
vcsjones
3
@vcsjones: No quiero hacer una guerra santa aquí, pero md5es lo suficientemente bueno para casi todo tipo de tareas. Sus vulnerabilidades también se refieren a situaciones muy específicas y casi requieren que el atacante sepa mucho sobre criptografía.
zerkms
4
Se tomó el punto @zerkms, pero si no hay razón para la compatibilidad con versiones anteriores, no hay razón para usar MD5. "Más vale prevenir que lamentar".
vcsjones
4
No hay razón para usar MD5 en este momento. Dado que el tiempo de cálculo es insignificante, no hay razón para usar MD5 excepto como compatibilidad con los sistemas existentes. Incluso si MD5 es "suficientemente bueno", no hay ningún costo para el usuario con el SHA mucho más seguro. Estoy seguro de que los zerkms saben que el comentario es más para el interrogador.
Gerald Davis
11
Tres grandes errores: 1) ASCII degrada silenciosamente las contraseñas con caracteres inusuales 2) MD5 / SHA-1 / SHA-2 simple es rápido. 3) Necesitas sal. | Utilice PBKDF2, bcrypt o scrypt en su lugar. PBKDF2 es más fácil en la clase Rfc2898DeriveBytes (no estoy seguro si está presente en WP7)
CodesInChaos
298

La mayoría de las otras respuestas aquí están algo desactualizadas con las mejores prácticas actuales. Como tal, aquí está la aplicación de usar PBKDF2 / Rfc2898DeriveBytespara almacenar y verificar contraseñas. El siguiente código está en una clase independiente en esta publicación: Otro ejemplo de cómo almacenar un hash de contraseña con sal . Los conceptos básicos son realmente fáciles, así que aquí se desglosa:

PASO 1 Cree el valor de sal 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 de sal 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 Convierta la sal y el hachís combinados en una cuerda para almacenar

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 100000se puede reducir. Debe haber un valor mínimo 10000.

csharptest.net
fuente
8
@Daniel básicamente la publicación trata sobre el uso de algo más seguro que un hash solo. Si simplemente aplica hash a una contraseña, incluso con salt, las contraseñas de sus usuarios se verán comprometidas (y probablemente se venderán / publicarán) antes de que tenga la oportunidad de decirles que la cambien. Use el código anterior para que sea difícil para el atacante, no fácil para el desarrollador.
csharptest.net
2
@DatVM No, sal nueva por cada vez que almacene un hash. es por eso que se combina con el hash de almacenamiento para que pueda verificar una contraseña.
csharptest.net
9
@CiprianJijie, el punto es que se supone que no puedes.
csharptest.net
9
En caso de que alguien esté haciendo un método VerifyPassword, si desea usar Linq y una llamada más corta para un booleano, esto sería suficiente: return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo
2
@ csharptest.net ¿Qué tipo de tamaños de matriz me recomiendan? ¿El tamaño de la matriz afecta mucho la seguridad de todos modos? No sé mucho sobre hash / criptografía
lennyy
71

Basado en la gran respuesta de csharptest.net , he escrito 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 actualizarlo.


Si está interesado en .net core, también tengo una versión .net core en Code Review .

Christian Gollhardt
fuente
1
Solo para verificar, si actualiza el motor de hash, ¿aumentaría la sección V1 de su hash y la clave fuera de eso?
Mike Cole
1
Sí, ese es el plan. A continuación, decidir sobre la base de V1, y V2el método que necesita verificación.
Christian Gollhardt
Gracias por la respuesta y la clase. Lo estoy implementando mientras hablamos.
Mike Cole
2
Sí @NelsonSilva. Eso es por la sal .
Christian Gollhardt
1
Con toda la copia / pegado de este código (incluyéndome a mí), espero que alguien hable y la publicación sea revisada si se encuentra un problema con ella. :)
pettys
14

Utilizo un hash y un salt para el cifrado de mi contraseña (es el mismo hash que usa Asp.Net Membership):

private string PasswordSalt
{
   get
   {
      var rng = new RNGCryptoServiceProvider();
      var buff = new byte[32];
      rng.GetBytes(buff);
      return Convert.ToBase64String(buff);
   }
}

private string EncodePassword(string password, string salt)
{
   byte[] bytes = Encoding.Unicode.GetBytes(password);
   byte[] src = Encoding.Unicode.GetBytes(salt);
   byte[] dst = new byte[src.Length + bytes.Length];
   Buffer.BlockCopy(src, 0, dst, 0, src.Length);
   Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
   HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
   byte[] inarray = algorithm.ComputeHash(dst);
   return Convert.ToBase64String(inarray);
}
Martín
fuente
16
-1 por usar SHA-1 simple, que es rápido. Utilice una función de derivación de clave lenta, como PBKDF2, bcrypt o scrypt.
CodesInChaos
1
  1. Crea una sal,
  2. Crea una contraseña hash con sal
  3. Ahorre hachís y sal
  4. descifrar con contraseña y sal ... para que los desarrolladores no puedan descifrar la contraseña
public class CryptographyProcessor
{
    public string CreateSalt(int size)
    {
        //Generate a cryptographic random number.
          RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
         byte[] buff = new byte[size];
         rng.GetBytes(buff);
         return Convert.ToBase64String(buff);
    }


      public string GenerateHash(string input, string salt)
      { 
         byte[] bytes = Encoding.UTF8.GetBytes(input + salt);
         SHA256Managed sHA256ManagedString = new SHA256Managed();
         byte[] hash = sHA256ManagedString.ComputeHash(bytes);
         return Convert.ToBase64String(hash);
      }

      public bool AreEqual(string plainTextInput, string hashedInput, string salt)
      {
           string newHashedPin = GenerateHash(plainTextInput, salt);
           return newHashedPin.Equals(hashedInput); 
      }
 }
Bamidele Alegbe
fuente
1

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 desechamos. Escribiré mi solución como respuesta si alguien necesita una versión eliminada.

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);
Ibrahimozgon
fuente
0

Creo que usar KeyDerivation.Pbkdf2 es mejor que Rfc2898DeriveBytes.

Ejemplo y explicación: hash de contraseñas en ASP.NET Core

using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
 
public class Program
{
    public static void Main(string[] args)
    {
        Console.Write("Enter a password: ");
        string password = Console.ReadLine();
 
        // generate a 128-bit salt using a secure PRNG
        byte[] salt = new byte[128 / 8];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(salt);
        }
        Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");
 
        // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations)
        string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
            password: password,
            salt: salt,
            prf: KeyDerivationPrf.HMACSHA1,
            iterationCount: 10000,
            numBytesRequested: 256 / 8));
        Console.WriteLine($"Hashed: {hashed}");
    }
}
 
/*
 * SAMPLE OUTPUT
 *
 * Enter a password: Xtw9NMgx
 * Salt: NZsP6NnmfBuYeJrrAKNuVQ==
 * Hashed: /OOoOer10+tGwTRDTrQSoeCxVTFr6dtYly7d0cPxIak=
 */

Este es un código de muestra del artículo. Y es un nivel mínimo de seguridad. Para aumentarlo, usaría en lugar del parámetro KeyDerivationPrf.HMACSHA1

KeyDerivationPrf.HMACSHA256 o KeyDerivationPrf.HMACSHA512.

No comprometa el hash de contraseñas. Hay muchos métodos matemáticamente sólidos para optimizar la piratería de hash de contraseñas. Las consecuencias pueden ser desastrosas. Una vez que un malhechor pueda tener en sus manos la tabla hash de contraseñas de sus usuarios, sería relativamente fácil para él descifrar las contraseñas dado que el algoritmo es débil o la implementación es incorrecta. Tiene mucho tiempo (tiempo x potencia de la computadora) para descifrar contraseñas. El hash de la contraseña debe ser criptográficamente fuerte para convertir "mucho tiempo" en "una cantidad de tiempo irrazonable ".

Un punto más para agregar

La verificación de hash lleva tiempo (y es buena). Cuando el usuario ingresa un nombre de usuario incorrecto, no se necesita tiempo para verificar que el nombre de usuario sea incorrecto. Cuando el nombre de usuario es correcto, comenzamos la verificación de la contraseña; es un proceso relativamente largo.

Para un hacker, sería muy fácil de entender si el usuario existe o no.

Asegúrese de no devolver una respuesta inmediata cuando el nombre de usuario sea incorrecto.

No hace falta decirlo: nunca dé una respuesta sobre lo que está mal. Solo general "Las credenciales son incorrectas".

Albert Lyubarsky
fuente
1
Por cierto, la respuesta anterior stackoverflow.com/a/57508528/11603057 no es correcta y dañina. Ese es un ejemplo de hash, no de contraseña. Deben ser iteraciones de la función pseudoaleatoria durante el proceso de derivación de claves. No hay. No puedo comentarlo ni votar en contra (mi baja reputación). ¡No se pierda las respuestas incorrectas!
Albert Lyubarsky