Cómo codificar una contraseña

Resuelto Skoder asked hace 13 años • 12 respuestas

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 avatar Nov 15 '10 10:11 Skoder
Aceptado

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 Rfc2898DeriveBytesclase. 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 100000se puede reducir. Un valor mínimo debería ser alrededor de 10000.

csharptest.net avatar May 01 '2012 18:05 csharptest.net

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 .

Christian Gollhardt avatar Aug 24 '2015 20:08 Christian Gollhardt

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 datauna matriz de bytes puedes usar

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

y para recuperar la cuerda de md5dataosha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);
zerkms avatar Nov 15 '2010 03:11 zerkms

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 RandomNumberGeneratorclase y su GetBytesmétodo estático, introducido en .NET 6, para generar el salt. La RNGCryptoServiceProviderclase utilizada en la respuesta aceptada está obsoleta .

  • Utiliza el CryptographicOperations.FixedTimeEqualsmétodo (introducido en .NET Core 2.1) para comparar los bytes clave en el Verifymé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 Hashmé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.ToHexStringy Convert.FromHexStringa Convert.ToBase64y Convert.FromBase64respectivamente. 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);
Arad Alvand avatar Jul 26 '2022 14:07 Arad Alvand

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);
ibrahimozgon avatar Feb 05 '2020 07:02 ibrahimozgon