El hash de contraseña predeterminado de la identidad de ASP.NET: ¿cómo funciona y es seguro?

162

Me pregunto si el Hasher de contraseñas que se implementa por defecto en el UserManager que viene con MVC 5 y ASP.NET Identity Framework, ¿es lo suficientemente seguro? Y si es así, ¿podría explicarme cómo funciona?

La interfaz IPasswordHasher se ve así:

public interface IPasswordHasher
{
    string HashPassword(string password);
    PasswordVerificationResult VerifyHashedPassword(string hashedPassword, 
                                                       string providedPassword);
}

Como puede ver, no se necesita nada, pero se menciona en este hilo: " Asp.net Identity hashing hash " que de hecho lo hace sal detrás de escena. Entonces, me pregunto cómo hace esto. ¿Y de dónde viene esta sal?

Mi preocupación es que la sal es estática, lo que la hace bastante insegura.

André Snede Kock
fuente
No creo que esto responda directamente a su pregunta, pero Brock Allen ha escrito sobre algunas de sus preocupaciones aquí => brockallen.com/2013/10/20/… y también ha escrito una biblioteca de autenticación y gestión de identidad de usuario de código abierto que tiene varias características de la placa de la caldera como restablecimiento de contraseña, hashing, etc. github.com/brockallen/BrockAllen.MembershipReboot
Shiva
@ Shiva Gracias, buscaré en la biblioteca y el video en la página. Pero preferiría no tener que lidiar con una biblioteca externa. No si puedo evitarlo.
André Snede Kock
2
FYI: el equivalente de stackoverflow para seguridad. Entonces, aunque a menudo obtendrá una respuesta buena / correcta aquí. Los expertos están en security.stackexchange.com especialmente el comentario "¿es seguro"? Hice una pregunta similar y la profundidad y calidad de la respuesta fue increíble.
Phil Soady
@philsoady Gracias, eso tiene sentido, por supuesto, ya estoy en algunos de los otros "sub-foros", si no obtengo una respuesta, puedo usar, me trasladaré a securiry.stackexchange.com. Y gracias por el consejo!
André Snede Kock

Respuestas:

227

Así es como funciona la implementación predeterminada ( ASP.NET Framework o ASP.NET Core ). Utiliza una función de derivación clave con sal aleatoria para producir el hash. La sal se incluye como parte de la salida del KDF. Por lo tanto, cada vez que "hash" la misma contraseña obtendrá hashes diferentes. Para verificar el hash, la salida se divide de nuevo en la sal y el resto, y el KDF se ejecuta nuevamente en la contraseña con la sal especificada. Si el resultado coincide con el resto de la salida inicial, se verifica el hash.

Hashing:

public static string HashPassword(string password)
{
    byte[] salt;
    byte[] buffer2;
    if (password == null)
    {
        throw new ArgumentNullException("password");
    }
    using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
    {
        salt = bytes.Salt;
        buffer2 = bytes.GetBytes(0x20);
    }
    byte[] dst = new byte[0x31];
    Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
    Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
    return Convert.ToBase64String(dst);
}

Verificando:

public static bool VerifyHashedPassword(string hashedPassword, string password)
{
    byte[] buffer4;
    if (hashedPassword == null)
    {
        return false;
    }
    if (password == null)
    {
        throw new ArgumentNullException("password");
    }
    byte[] src = Convert.FromBase64String(hashedPassword);
    if ((src.Length != 0x31) || (src[0] != 0))
    {
        return false;
    }
    byte[] dst = new byte[0x10];
    Buffer.BlockCopy(src, 1, dst, 0, 0x10);
    byte[] buffer3 = new byte[0x20];
    Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
    using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8))
    {
        buffer4 = bytes.GetBytes(0x20);
    }
    return ByteArraysEqual(buffer3, buffer4);
}
Andrew Savinykh
fuente
77
Entonces, si entiendo esto correctamente, la HashPasswordfunción, ¿devuelve ambos en la misma cadena? Y cuando lo verifica, lo divide nuevamente y vuelve a cifrar la contraseña de texto sin cifrar entrante, con la sal de la división, y la compara con el hash original.
André Snede Kock
9
@ AndréSnedeHansen, exactamente. Y también te recomiendo que preguntes sobre seguridad o sobre criptografía SE. La parte "es seguro" puede abordarse mejor en esos contextos respectivos.
Andrew Savinykh
1
@shajeerpuzhakkal como se describe en la respuesta anterior.
Andrew Savinykh
3
@ AndrewSavinykh Lo sé, por eso te pregunto: ¿qué sentido tiene? ¿Para que el código se vea más inteligente? ;) Porque para mí contar cosas usando números decimales es MUCHO más intuitivo (tenemos 10 dedos después de todo, al menos la mayoría de nosotros), por lo que declarar una cantidad de algo usando hexadecimales parece una ofuscación innecesaria del código.
Andrew Cyrul
1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);: es lo que debe hacer. después de eso resultcontiene cierto.
Andrew Savinykh
43

Debido a que actualmente ASP.NET es de código abierto, puede encontrarlo en GitHub: AspNet.Identity 3.0 y AspNet.Identity 2.0 .

De los comentarios:

/* =======================
 * HASHED PASSWORD FORMATS
 * =======================
 * 
 * Version 2:
 * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
 * (See also: SDL crypto guidelines v5.1, Part III)
 * Format: { 0x00, salt, subkey }
 *
 * Version 3:
 * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
 * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
 * (All UInt32s are stored big-endian.)
 */
Knelis
fuente
Sí, y vale la pena señalar, hay adiciones al algoritmo que muestra zespri.
André Snede Kock el
1
La fuente en GitHub es Asp.Net.Identity 3.0, que todavía está en prelanzamiento. La fuente de la función hash 2.0 está en CodePlex
David
1
La implementación más reciente se puede encontrar en github.com/dotnet/aspnetcore/blob/master/src/Identity/… ahora.
Archivaron
32

Entiendo la respuesta aceptada, y la he votado pero pensé en dejar aquí la respuesta de mis laicos ...

Crear un hash

  1. La sal se genera aleatoriamente utilizando la función Rfc2898DeriveBytes que genera un hash y una sal. Las entradas a Rfc2898DeriveBytes son la contraseña, el tamaño de la sal a generar y el número de iteraciones de hashing a realizar. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. La sal y el hash se mezclan (sal primero seguida del hash) y se codifican como una cadena (por lo que la sal se codifica en el hash). Este hash codificado (que contiene la sal y el hash) se almacena (normalmente) en la base de datos contra el usuario.

Comprobación de una contraseña contra un hash

Para verificar una contraseña que ingresa un usuario.

  1. La sal se extrae de la contraseña hash almacenada.
  2. La sal se utiliza para trocear la contraseña de entrada de los usuarios utilizando una sobrecarga de Rfc2898DeriveBytes que toma una sal en lugar de generar una. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. El hash almacenado y el hash de prueba se comparan.

El hachís

Debajo de las cubiertas, el hash se genera utilizando la función de hash SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Esta función se llama iterativamente 1000 veces (en la implementación de identidad predeterminada)

¿Por qué es esto seguro?

  • Sales aleatorias significa que un atacante no puede usar una tabla de hash pregenerada para intentar descifrar contraseñas. Tendrían que generar una tabla hash para cada sal. (Suponiendo aquí que el hacker también ha comprometido su sal)
  • Si 2 contraseñas son idénticas, tendrán hashes diferentes. (lo que significa que los atacantes no pueden inferir contraseñas "comunes")
  • Llamar iterativamente a SHA1 1000 veces significa que el atacante también necesita hacer esto. La idea es que, a menos que tengan tiempo en una supercomputadora, no tendrán suficientes recursos para forzar la contraseña del hash. Reduciría enormemente el tiempo para generar una tabla hash para una sal determinada.
Nattrass
fuente
Gracias por tu explicación. En "Crear un hash 2". Usted menciona que la sal y el hash se combinan, ¿sabe si esto se almacena en PasswordHash en la tabla AspNetUsers? ¿La sal se almacena en algún lugar para que yo vea?
unicorn2
1
@ unicorn2 Si echa un vistazo a la respuesta de Andrew Savinykh ... En la sección sobre el hash parece que la sal se almacena en los primeros 16 bytes de la matriz de bytes que está codificada en Base64 y escrita en la base de datos. Podrá ver esta cadena codificada en Base64 en la tabla PasswordHash. Todo lo que puedes decir sobre la cadena Base64 es que aproximadamente el primer tercio es la sal. La sal significativa son los primeros 16 bytes de la versión decodificada Base64 de la cadena completa almacenada en la tabla PasswordHash
Nattrass
@ Nattrass, mi comprensión de los hashes y sales es bastante rudimentaria, pero si la sal se extrae fácilmente de la contraseña hash, ¿cuál es el punto de salazón en primer lugar? Pensé que la sal debía ser una entrada adicional al algoritmo de hash que no podía adivinarse fácilmente.
NSouth
1
@NSouth La sal única hace que el hash sea único para una contraseña determinada. Entonces dos contraseñas idénticas tendrán diferentes hash. Tener acceso a su hash y salt todavía no le recuerda al atacante su contraseña. El hash no es reversible. Todavía tendrían que usar la fuerza bruta a través de cada contraseña posible. La sal única solo significa que el pirata informático no puede inferir contraseñas comunes al hacer un análisis de frecuencia en hash específicos si han logrado obtener toda su tabla de usuarios.
Nattrass
8

Para aquellos como yo que son nuevos en esto, aquí hay un código con const y una forma real de comparar los bytes [] '. Obtuve todo este código de stackoverflow pero definí consts para poder cambiar los valores y también

// 24 = 192 bits
    private const int SaltByteSize = 24;
    private const int HashByteSize = 24;
    private const int HasingIterationsCount = 10101;


    public static string HashPassword(string password)
    {
        // http://stackoverflow.com/questions/19957176/asp-net-identity-password-hashing

        byte[] salt;
        byte[] buffer2;
        if (password == null)
        {
            throw new ArgumentNullException("password");
        }
        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, SaltByteSize, HasingIterationsCount))
        {
            salt = bytes.Salt;
            buffer2 = bytes.GetBytes(HashByteSize);
        }
        byte[] dst = new byte[(SaltByteSize + HashByteSize) + 1];
        Buffer.BlockCopy(salt, 0, dst, 1, SaltByteSize);
        Buffer.BlockCopy(buffer2, 0, dst, SaltByteSize + 1, HashByteSize);
        return Convert.ToBase64String(dst);
    }

    public static bool VerifyHashedPassword(string hashedPassword, string password)
    {
        byte[] _passwordHashBytes;

        int _arrayLen = (SaltByteSize + HashByteSize) + 1;

        if (hashedPassword == null)
        {
            return false;
        }

        if (password == null)
        {
            throw new ArgumentNullException("password");
        }

        byte[] src = Convert.FromBase64String(hashedPassword);

        if ((src.Length != _arrayLen) || (src[0] != 0))
        {
            return false;
        }

        byte[] _currentSaltBytes = new byte[SaltByteSize];
        Buffer.BlockCopy(src, 1, _currentSaltBytes, 0, SaltByteSize);

        byte[] _currentHashBytes = new byte[HashByteSize];
        Buffer.BlockCopy(src, SaltByteSize + 1, _currentHashBytes, 0, HashByteSize);

        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, _currentSaltBytes, HasingIterationsCount))
        {
            _passwordHashBytes = bytes.GetBytes(SaltByteSize);
        }

        return AreHashesEqual(_currentHashBytes, _passwordHashBytes);

    }

    private static bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
    {
        int _minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
        var xor = firstHash.Length ^ secondHash.Length;
        for (int i = 0; i < _minHashLength; i++)
            xor |= firstHash[i] ^ secondHash[i];
        return 0 == xor;
    }

En su ApplicationUserManager personalizado, configura la propiedad PasswordHasher con el nombre de la clase que contiene el código anterior.

kfrosty
fuente
Para esto ... _passwordHashBytes = bytes.GetBytes(SaltByteSize); supongo que querías decir esto _passwordHashBytes = bytes.GetBytes(HashByteSize);... No importa en tu escenario ya que ambos son del mismo tamaño pero en general ...
Akshatha