ASP.NET Identity predeterminado Password Hasher, ¿cómo funciona y es seguro?

Me pregunto si la contraseña hasher implementada por defecto en el UserManager que viene con MVC 5 y ASP.NET Identity Framework es lo suficientemente segura. Y si es así, ¿podrías 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 una sal, pero se menciona en este hilo: ” Asignación de contraseñas de identidad de Asp.net ” que lo hace sal de hecho entre bastidores. Entonces me pregunto ¿cómo hace esto? ¿Y de dónde viene esta sal?

Mi preocupación es que la sal es estática, por lo que es bastante inseguro.

Así es como funciona la implementación predeterminada . 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ás hashes diferentes. Para verificar el hash, la salida se divide en sal y el rest, y el KDF se ejecuta nuevamente en la contraseña con el valor especificado. Si el resultado coincide con el rest de la salida inicial, se comprueba 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); } 

Debido a que en estos días ASP.NET es de código abierto, puedes 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.) */ 

Entiendo la respuesta aceptada, y la he votado positivamente, pero pensé que abandonaría la respuesta de mis profanos aquí …

Creando 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 para generar y el número de iteraciones hash para realizar. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. La sal y el hash se trituran juntos (sal primero seguido del hash) y se codifican como una cadena (por lo que la sal se codifica en el hash). Este hash codificado (que contiene sal y hash) se almacena (normalmente) en la base de datos contra el usuario.

Verificar 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 cifrar la contraseña de entrada de los usuarios mediante 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.

The Hash

Bajo las cubiertas, el hash se genera usando la función 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 hashs pregenerada para intentar descifrar las contraseñas. Tendrían que generar una tabla hash para cada sal. (Suponiendo que el hacker también haya comprometido su sal)
  • Si 2 contraseñas son idénticas, tendrán diferentes valores hash. (es decir, 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 los recursos suficientes para utilizar la contraseña del hash. Se ralentizaría masivamente el tiempo para generar una tabla hash para una sal dada.

Para aquellos como yo, que somos nuevos en esto, aquí hay un código con const y una forma real de comparar el byte []. Obtuve todo este código de stackoverflow pero conste definido para que los valores puedan ser cambiados 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, establece la propiedad PasswordHasher con el nombre de la clase que contiene el código anterior.