Wie man ein Passwort hasht


117

Ich möchte den Hash eines Passworts auf dem Telefon speichern, bin mir aber nicht sicher, wie ich das machen soll. Ich kann nur scheinbar Verschlüsselungsmethoden finden. Wie soll das Passwort richtig gehasht werden?

Antworten:


62

UPDATE : Diese Antwort ist ernsthaft veraltet . Bitte verwenden Sie stattdessen die Empfehlungen unter https://stackoverflow.com/a/10402129/251311 .

Sie können entweder verwenden

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

oder

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

Um dataals Byte-Array zu erhalten, könnten Sie verwenden

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

und um String von md5dataoder zurück zu bekommensha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
Ich würde wirklich empfehlen, SHA1 zu verwenden. MD5 ist ein Nein-Nein, es sei denn, Sie behalten die Abwärtskompatibilität mit einem vorhandenen System bei. Stellen Sie außerdem sicher, dass Sie es in eine usingAnweisung einfügen oder aufrufen Clear(), wenn Sie mit der Implementierung fertig sind.
vcsjones

3
@vcsjones: Ich möchte hier keinen heiligen Krieg führen, ist aber md5gut genug für fast alle Arten von Aufgaben. Seine Schwachstellen beziehen sich auch auf sehr spezifische Situationen und erfordern fast, dass der Angreifer viel über Kryptographie weiß.
Zerkms

4
@ zerkms Punkt genommen, aber wenn es keinen Grund für die Abwärtskompatibilität gibt, gibt es keinen Grund, MD5 zu verwenden. "Sicher ist sicher".
vcsjones

4
Derzeit besteht kein Grund, MD5 zu verwenden. Da die Rechenzeit unbedeutend ist, gibt es keinen Grund, MD5 zu verwenden, außer als Kompatibilität mit vorhandenen Systemen. Selbst wenn MD5 "gut genug" ist, entstehen dem Benutzer keine Kosten für die weitaus sicherere SHA. Ich bin sicher, Zerkms wissen, dass der Kommentar eher für den Fragesteller ist.
Gerald Davis

11
Drei große Fehler: 1) ASCII verschlechtert stillschweigend Passwörter mit ungewöhnlichen Zeichen. 2) Normales MD5 / SHA-1 / SHA-2 ist schnell. 3) Du brauchst ein Salz. | Verwenden Sie stattdessen PBKDF2, bcrypt oder scrypt. PBKDF2 ist am einfachsten in der Rfc2898DeriveBytes-Klasse (nicht sicher, ob auf WP7 vorhanden)
CodesInChaos

298

Die meisten anderen Antworten hier sind mit den heutigen Best Practices etwas veraltet. Als solches ist hier die Anwendung der Verwendung von PBKDF2 / Rfc2898DeriveByteszum Speichern und Überprüfen von Passwörtern. Der folgende Code befindet sich in diesem Beitrag in einer eigenständigen Klasse: Ein weiteres Beispiel für das Speichern eines gesalzenen Kennwort-Hash . Die Grundlagen sind wirklich einfach, daher ist es hier aufgeschlüsselt:

SCHRITT 1 Erstellen Sie den Salzwert mit einem kryptografischen PRNG:

byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);

SCHRITT 2 Erstellen Sie die Rfc2898DeriveBytes und erhalten Sie den Hashwert:

var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);

SCHRITT 3 Kombinieren Sie die Salt- und Passwortbytes für die spätere Verwendung:

byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);

SCHRITT 4 Verwandeln Sie das kombinierte Salz + Hasch in eine Zeichenfolge zur Lagerung

string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });

SCHRITT 5 Überprüfen Sie das vom Benutzer eingegebene Passwort anhand eines gespeicherten Passworts

/* 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();

Hinweis: Abhängig von den Leistungsanforderungen Ihrer spezifischen Anwendung kann der Wert 100000reduziert werden. Ein Mindestwert sollte vorhanden sein 10000.


8
@ Daniel im Grunde geht es in dem Beitrag darum, etwas Sichereres als einen Hash allein zu verwenden. Wenn Sie selbst mit Salt einfach ein Passwort hashen, werden die Passwörter Ihrer Benutzer kompromittiert (und wahrscheinlich verkauft / veröffentlicht), bevor Sie ihnen überhaupt die Möglichkeit geben, sie zu ändern. Verwenden Sie den obigen Code, um es dem Angreifer schwer und dem Entwickler nicht leicht zu machen.
csharptest.net

2
@DatVM Nein, neues Salz für jedes Mal, wenn Sie einen Hash speichern. Aus diesem Grund wird es mit dem Hash für die Speicherung kombiniert, damit Sie ein Kennwort überprüfen können.
csharptest.net

9
@CiprianJijie der springende Punkt ist, dass Sie nicht in der Lage sein sollen.
csharptest.net

9
Wenn jemand eine VerifyPassword-Methode ausführt und Linq und einen kürzeren Aufruf für einen Booleschen Wert verwenden möchte, würde dies Folgendes tun: return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo

2
@ csharptest.net Welche Arraygrößen empfehlen Sie? Beeinflusst die Größe des Arrays die Sicherheit trotzdem stark? Ich weiß nicht so viel über Hashing / Kryptographie
Lennyy

71

Basierend auf der großartigen Antwort von csharptest.net habe ich eine Klasse dafür geschrieben:

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;
    }
}

Verwendung:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

Ein Beispiel-Hash könnte sein:

$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn

Wie Sie sehen können, habe ich auch die Iterationen in den Hash aufgenommen, um die Verwendung zu vereinfachen und die Möglichkeit zu bieten, diese zu aktualisieren, falls ein Upgrade erforderlich ist.


Wenn Sie sich für .net Core interessieren, habe ich auch eine .net Core-Version für Code Review .


1
Nur um zu überprüfen, ob Sie, wenn Sie die Hashing-Engine aktualisieren, den V1-Abschnitt Ihres Hashs inkrementieren und davon abkoppeln würden?
Mike Cole

1
Ja das ist der Plan. Sie würden dann entscheiden, basierend auf V1und V2welche Überprüfungsmethode Sie benötigen.
Christian Gollhardt

Danke für die Antwort und die Klasse. Ich implementiere es, während wir sprechen.
Mike Cole

2
Ja @NelsonSilva. Das liegt am Salz .
Christian Gollhardt

1
Bei all dem Kopieren / Einfügen dieses Codes (einschließlich mir) hoffe ich, dass sich jemand meldet und der Beitrag überarbeitet wird, wenn ein Problem damit gefunden wird! :)
Pettys

14

Ich verwende einen Hash und ein Salt für meine Passwortverschlüsselung (es ist der gleiche Hash, den die Asp.Net-Mitgliedschaft verwendet):

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);
}

16
-1 für die Verwendung von einfachem SHA-1, was schnell ist. Verwenden Sie eine langsame Schlüsselableitungsfunktion wie PBKDF2, bcrypt oder scrypt.
CodesInChaos

1
  1. Erstelle ein Salz,
  2. Erstellen Sie ein Hash-Passwort mit Salt
  3. Speichern Sie sowohl Haschisch als auch Salz
  4. Mit Passwort und Salt entschlüsseln ... damit Entwickler das Passwort nicht entschlüsseln können
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); 
      }
 }

1

Die Antworten von @ csharptest.net und Christian Gollhardt sind großartig, vielen Dank. Nachdem ich diesen Code in der Produktion mit Millionen von Datensätzen ausgeführt hatte, stellte ich fest, dass ein Speicherverlust vorliegt. Die Klassen RNGCryptoServiceProvider und Rfc2898DeriveBytes werden von IDisposable abgeleitet, aber wir verfügen nicht über sie. Ich werde meine Lösung als Antwort schreiben, wenn jemand eine entsorgte Version benötigt.

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;
        }

    }
}

Verwendung:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

0

Ich denke, KeyDerivation.Pbkdf2 ist besser als Rfc2898DeriveBytes.

Beispiel und Erklärung: Hash-Passwörter in 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=
 */

Dies ist ein Beispielcode aus dem Artikel. Und es ist eine Mindestsicherheitsstufe. Um es zu erhöhen, würde ich anstelle des Parameters KeyDerivationPrf.HMACSHA1 verwenden

KeyDerivationPrf.HMACSHA256 oder KeyDerivationPrf.HMACSHA512.

Gehen Sie beim Passwort-Hashing keine Kompromisse ein. Es gibt viele mathematisch fundierte Methoden, um das Hacken von Passwort-Hashs zu optimieren. Folgen könnten katastrophal sein. Sobald ein Übeltäter die Passwort-Hash-Tabelle Ihrer Benutzer in die Hände bekommen kann, ist es für ihn relativ einfach, Passwörter zu knacken, wenn der Algorithmus schwach oder die Implementierung falsch ist. Er hat viel Zeit (Zeit x Computerleistung), um Passwörter zu knacken. Das Passwort-Hashing sollte kryptografisch stark sein, um "viel Zeit" in " unangemessen viel Zeit " umzuwandeln .

Noch ein Punkt zum Hinzufügen

Die Hash-Überprüfung braucht Zeit (und es ist gut). Wenn der Benutzer einen falschen Benutzernamen eingibt, dauert es keine Zeit, um zu überprüfen, ob der Benutzername falsch ist. Wenn der Benutzername korrekt ist, starten wir die Passwortüberprüfung - es ist ein relativ langer Prozess.

Für einen Hacker wäre es sehr leicht zu verstehen, ob ein Benutzer existiert oder nicht.

Stellen Sie sicher, dass Sie keine sofortige Antwort zurückgeben, wenn der Benutzername falsch ist.

Unnötig zu sagen: Geben Sie niemals eine Antwort, was falsch ist. Nur allgemeine "Anmeldeinformationen sind falsch".


1
Übrigens ist die vorherige Antwort stackoverflow.com/a/57508528/11603057 nicht korrekt und schädlich. Dies ist ein Beispiel für Hashing, nicht für Passwort-Hashing. Muss Iterationen der Pseudozufallsfunktion während des Schlüsselableitungsprozesses sein. Es gibt kein. Ich kann es nicht kommentieren oder ablehnen (mein geringer Ruf). Bitte verpassen Sie keine falschen Antworten!
Albert Lyubarsky
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.