Contraseñas hash y salt en C#

Resuelto ACP asked hace 14 años • 17 respuestas

Estaba leyendo uno de los artículos de DavidHayden sobre Hashing User Passwords .

Realmente no puedo entender lo que está tratando de lograr.

Aquí está su código:

private static string CreateSalt(int size)
{
    //Generate a cryptographic random number.
    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
    byte[] buff = new byte[size];
    rng.GetBytes(buff);

    // Return a Base64 string representation of the random number.
    return Convert.ToBase64String(buff);
}

private static string CreatePasswordHash(string pwd, string salt)
{
    string saltAndPwd = String.Concat(pwd, salt);
    string hashedPwd =
        FormsAuthentication.HashPasswordForStoringInConfigFile(
        saltAndPwd, "sha1");
    return hashedPwd;
}

¿Existe algún otro método de C# para codificar contraseñas y agregarles sal?

ACP avatar Jan 26 '10 16:01 ACP
Aceptado

Actualización: si está utilizando .NET 6 o superior, utilice esta solución .

En realidad, esto es un poco extraño, con las conversiones de cadenas, que hace el proveedor de membresía para colocarlas en archivos de configuración. Los hashes y las sales son blobs binarios, no es necesario convertirlos en cadenas a menos que desee colocarlos en archivos de texto.

En mi libro, Beginning ASP.NET Security (oh, finalmente, una excusa para mejorar el libro), hago lo siguiente

static byte[] GenerateSaltedHash(byte[] plainText, byte[] salt)
{
  HashAlgorithm algorithm = new SHA256Managed();

  byte[] plainTextWithSaltBytes = 
    new byte[plainText.Length + salt.Length];

  for (int i = 0; i < plainText.Length; i++)
  {
    plainTextWithSaltBytes[i] = plainText[i];
  }
  for (int i = 0; i < salt.Length; i++)
  {
    plainTextWithSaltBytes[plainText.Length + i] = salt[i];
  }

  return algorithm.ComputeHash(plainTextWithSaltBytes);            
}

La generación de la sal es como el ejemplo de la pregunta. Puede convertir texto en matrices de bytes usando Encoding.UTF8.GetBytes(string). Si debe convertir un hash a su representación de cadena, puede usar Convert.ToBase64Stringy Convert.FromBase64Stringvolver a convertirlo.

Debe tener en cuenta que no puede usar el operador de igualdad en matrices de bytes, verifica las referencias y, por lo tanto, simplemente debe recorrer ambas matrices verificando cada byte de esta manera.

public static bool CompareByteArrays(byte[] array1, byte[] array2)
{
  if (array1.Length != array2.Length)
  {
    return false;
  }

  for (int i = 0; i < array1.Length; i++)
  {
    if (array1[i] != array2[i])
    {
      return false;
    }
  }

  return true;
}

Utilice siempre un nuevo salt por contraseña. Las sales no tienen que mantenerse en secreto y pueden almacenarse junto con el hachís.

blowdart avatar Jan 26 '2010 09:01 blowdart

Lo que dijo Blowdart, pero con un poco menos de código. Utilice Linq o CopyTopara concatenar matrices.

public static byte[] Hash(string value, byte[] salt)
{
    return Hash(Encoding.UTF8.GetBytes(value), salt);
}

public static byte[] Hash(byte[] value, byte[] salt)
{
    byte[] saltedValue = value.Concat(salt).ToArray();
    // Alternatively use CopyTo.
    //var saltedValue = new byte[value.Length + salt.Length];
    //value.CopyTo(saltedValue, 0);
    //salt.CopyTo(saltedValue, value.Length);

    return new SHA256Managed().ComputeHash(saltedValue);
}

Linq también tiene una manera fácil de comparar sus matrices de bytes.

public bool ConfirmPassword(string password)
{
    byte[] passwordHash = Hash(password, _passwordSalt);

    return _passwordHash.SequenceEqual(passwordHash);
}

Sin embargo, antes de implementar algo de esto, consulte esta publicación . Para el hash de contraseñas, es posible que desee un algoritmo hash lento, no uno rápido.

Con ese fin, existe la Rfc2898DeriveBytesclase que es lenta (y puede hacerse más lenta) y puede responder la segunda parte de la pregunta original en el sentido de que puede tomar una contraseña y salt y devolver un hash. Consulte esta pregunta para obtener más información. Tenga en cuenta que Stack Exchange se utilizaRfc2898DeriveBytes para el hash de contraseñas (código fuente aquí ).

Adam Boddington avatar Jan 05 '2012 22:01 Adam Boddington

He estado leyendo que las funciones hash como SHA256 no estaban realmente diseñadas para almacenar contraseñas: https://patrickmn.com/security/storing-passwords-securely/#notpasswordhashes

En su lugar, existían funciones de derivación de claves adaptativas como PBKDF2, bcrypt o scrypt. Aquí hay uno basado en PBKDF2 que Microsoft escribió para PasswordHasher en su biblioteca Microsoft.AspNet.Identity:

/* =======================
 * HASHED PASSWORD FORMATS
 * =======================
 * 
 * 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.)
 */

public string HashPassword(string password)
{
    var prf = KeyDerivationPrf.HMACSHA256;
    var rng = RandomNumberGenerator.Create();
    const int iterCount = 10000;
    const int saltSize = 128 / 8;
    const int numBytesRequested = 256 / 8;

    // Produce a version 3 (see comment above) text hash.
    var salt = new byte[saltSize];
    rng.GetBytes(salt);
    var subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);

    var outputBytes = new byte[13 + salt.Length + subkey.Length];
    outputBytes[0] = 0x01; // format marker
    WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
    WriteNetworkByteOrder(outputBytes, 5, iterCount);
    WriteNetworkByteOrder(outputBytes, 9, saltSize);
    Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
    Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
    return Convert.ToBase64String(outputBytes);
}

public bool VerifyHashedPassword(string hashedPassword, string providedPassword)
{
    var decodedHashedPassword = Convert.FromBase64String(hashedPassword);

    // Wrong version
    if (decodedHashedPassword[0] != 0x01)
        return false;

    // Read header information
    var prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedHashedPassword, 1);
    var iterCount = (int)ReadNetworkByteOrder(decodedHashedPassword, 5);
    var saltLength = (int)ReadNetworkByteOrder(decodedHashedPassword, 9);

    // Read the salt: must be >= 128 bits
    if (saltLength < 128 / 8)
    {
        return false;
    }
    var salt = new byte[saltLength];
    Buffer.BlockCopy(decodedHashedPassword, 13, salt, 0, salt.Length);

    // Read the subkey (the rest of the payload): must be >= 128 bits
    var subkeyLength = decodedHashedPassword.Length - 13 - salt.Length;
    if (subkeyLength < 128 / 8)
    {
        return false;
    }
    var expectedSubkey = new byte[subkeyLength];
    Buffer.BlockCopy(decodedHashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);

    // Hash the incoming password and verify it
    var actualSubkey = KeyDerivation.Pbkdf2(providedPassword, salt, prf, iterCount, subkeyLength);
    return actualSubkey.SequenceEqual(expectedSubkey);
}

private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
    buffer[offset + 0] = (byte)(value >> 24);
    buffer[offset + 1] = (byte)(value >> 16);
    buffer[offset + 2] = (byte)(value >> 8);
    buffer[offset + 3] = (byte)(value >> 0);
}

private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{
    return ((uint)(buffer[offset + 0]) << 24)
        | ((uint)(buffer[offset + 1]) << 16)
        | ((uint)(buffer[offset + 2]) << 8)
        | ((uint)(buffer[offset + 3]));
}

Tenga en cuenta que esto requiere la instalación del paquete nuget Microsoft.AspNetCore.Cryptography.KeyDerivation , que requiere .NET Standard 2.0 (.NET 4.6.1 o superior). Para versiones anteriores de .NET, consulte la clase Crypto de la biblioteca System.Web.Helpers de Microsoft.

Actualización de noviembre de 2015
Respuesta actualizada para usar una implementación de una biblioteca de Microsoft diferente que usa hash PBKDF2-HMAC-SHA256 en lugar de PBKDF2-HMAC-SHA1 (tenga en cuenta que PBKDF2-HMAC-SHA1 sigue siendo seguro si iterCount es lo suficientemente alto). Puede consultar la fuente de la que se copió el código simplificado, ya que en realidad maneja la validación y actualización de los hashes implementados en la respuesta anterior, lo cual es útil si necesita aumentar iterCount en el futuro.

Michael avatar May 31 '2013 12:05 Michael