Standard-Passwort-Hasher von ASP.NET Identity - Wie funktioniert es und ist es sicher?

162

Ich frage mich, ob der im UserManager , der mit MVC 5 und ASP.NET Identity Framework geliefert wird, standardmäßig implementierte Password Hasher sicher genug ist. Und wenn ja, könnten Sie mir erklären, wie es funktioniert?

Die IPasswordHasher-Schnittstelle sieht folgendermaßen aus:

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

Wie Sie sehen können, braucht es kein Salz, aber es wird in diesem Thread erwähnt: " Asp.net Identity Password Hashing ", dass es Salz hinter den Kulissen infiziert. Also frage ich mich, wie es das macht? Und woher kommt dieses Salz?

Ich mache mir Sorgen, dass das Salz statisch ist, was es ziemlich unsicher macht.

André Snede Kock
quelle
Ich glaube nicht, dass dies Ihre Frage direkt beantwortet, aber Brock Allen hat hier über einige Ihrer Bedenken geschrieben => brockallen.com/2013/10/20/… und auch eine Open-Source-Bibliothek zur Verwaltung und Authentifizierung der Benutzeridentität geschrieben, die verschiedene hat Funktionen der Kesselplatte
Shiva
@Shiva Danke, ich werde in die Bibliothek und das Video auf der Seite schauen. Aber ich würde mich lieber nicht mit einer externen Bibliothek befassen müssen. Nicht wenn ich es vermeiden kann.
André Snede Kock
2
Zu Ihrer Information: das Stackoverflow-Äquivalent für die Sicherheit. Obwohl Sie hier oft eine gute / richtige Antwort erhalten. Die Experten sind auf security.stackexchange.com, insbesondere der Kommentar "Ist es sicher?" Ich stellte eine ähnliche Art von Frage und die Tiefe und Qualität der Antwort war erstaunlich.
Phil Soady
@philsoady Danke, das macht natürlich Sinn, ich bin schon in einigen der anderen "Unterforen", wenn ich keine Antwort bekomme, die ich verwenden kann, werde ich zu wechseln securiry.stackexchange.com. Und danke für den Tipp!
André Snede Kock

Antworten:

227

So funktioniert die Standardimplementierung ( ASP.NET Framework oder ASP.NET Core ). Es verwendet eine Schlüsselableitungsfunktion mit zufälligem Salz, um den Hash zu erzeugen. Das Salz ist Teil der Ausgabe des KDF. Jedes Mal, wenn Sie dasselbe Passwort "hashen", erhalten Sie unterschiedliche Hashes. Um den Hash zu überprüfen, wird die Ausgabe auf das Salt und den Rest aufgeteilt, und das KDF wird erneut mit dem Kennwort mit dem angegebenen Salt ausgeführt. Wenn das Ergebnis mit dem Rest der ursprünglichen Ausgabe übereinstimmt, wird der Hash überprüft.

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

Überprüfen:

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
quelle
7
Wenn ich das richtig verstehe, gibt die HashPasswordFunktion beide in derselben Zeichenfolge zurück? Und wenn Sie es überprüfen, teilt es es erneut auf und hasht das eingehende Klartext-Passwort mit dem Salz aus der Aufteilung und vergleicht es mit dem ursprünglichen Hash?
André Snede Kock
9
@ AndréSnedeHansen, genau. Und ich empfehle Ihnen auch, entweder nach Sicherheit oder nach Kryptografie SE zu fragen. Der Teil "Ist es sicher?" Kann in den jeweiligen Kontexten besser angesprochen werden.
Andrew Savinykh
1
@shajeerpuzhakkal wie in der obigen Antwort beschrieben.
Andrew Savinykh
3
@ AndrewSavinykh Ich weiß, deshalb frage ich - was ist der Sinn? Damit der Code intelligenter aussieht? ;) Der Grund, warum ich Dinge mit Dezimalzahlen zähle, ist VIEL intuitiver (wir haben immerhin 10 Finger - zumindest die meisten von uns), daher scheint es eine unnötige Code-Verschleierung zu sein, eine Anzahl von Dingen mit Hexadezimalzahlen zu deklarieren.
Andrew Cyrul
1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- ist das, was Sie tun müssen. danach resultenthält wahr.
Andrew Savinykh
43

Da ASP.NET heutzutage Open Source ist, finden Sie es auf GitHub: AspNet.Identity 3.0 und AspNet.Identity 2.0 .

Aus den Kommentaren:

/* =======================
 * 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
quelle
Ja, und erwähnenswert, es gibt Ergänzungen zu dem Algorithmus, den zespri anzeigt.
André Snede Kock
1
Die Quelle auf GitHub ist Asp.Net.Identity 3.0, das sich noch in der Vorabversion befindet. Die Quelle der 2.0-Hash-Funktion ist CodePlex
David
1
Die neueste Implementierung finden Sie jetzt unter github.com/dotnet/aspnetcore/blob/master/src/Identity/… . Sie haben das andere Repository archiviert;)
FranzHuber23
32

Ich verstehe die akzeptierte Antwort und habe sie hochgestimmt, dachte aber, ich würde die Antwort meiner Laien hier ablegen ...

Einen Hash erstellen

  1. Das Salt wird zufällig mit der Funktion Rfc2898DeriveBytes generiert, die einen Hash und ein Salt generiert. Eingaben in Rfc2898DeriveBytes sind das Kennwort, die Größe des zu generierenden Salt und die Anzahl der durchzuführenden Hashing-Iterationen. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. Das Salz und der Hash werden dann zusammen püriert (Salz zuerst gefolgt von dem Hash) und als String codiert (so dass das Salz im Hash codiert wird). Dieser codierte Hash (der das Salt und den Hash enthält) wird dann (normalerweise) in der Datenbank gegen den Benutzer gespeichert.

Überprüfen eines Passworts gegen einen Hash

So überprüfen Sie ein Kennwort, das ein Benutzer eingibt.

  1. Das Salz wird aus dem gespeicherten Hash-Passwort extrahiert.
  2. Das Salt wird verwendet, um das Benutzereingabekennwort mit einer Überladung von Rfc2898DeriveBytes zu hashen, die ein Salt verwendet, anstatt eines zu generieren. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. Der gespeicherte Hash und der Test-Hash werden dann verglichen.

Der Hash

Unter dem Deckmantel wird der Hash mit der SHA1-Hash-Funktion ( https://en.wikipedia.org/wiki/SHA-1 ) generiert . Diese Funktion wird iterativ 1000 Mal aufgerufen (in der Standardimplementierung von Identity).

Warum ist das sicher?

  • Zufällige Salze bedeuten, dass ein Angreifer keine vorgenerierte Hash-Tabelle verwenden kann, um Passwörter zu knacken. Sie müssten für jedes Salz eine Hash-Tabelle erstellen. (Angenommen, der Hacker hat auch Ihr Salz kompromittiert)
  • Wenn 2 Passwörter identisch sind, haben sie unterschiedliche Hashes. (Dies bedeutet, dass Angreifer keine "allgemeinen" Passwörter ableiten können.)
  • Wenn Sie SHA1 1000 Mal iterativ aufrufen, muss der Angreifer dies ebenfalls tun. Die Idee ist, dass sie nicht genug Ressourcen haben, um das Passwort aus dem Hash zu erzwingen, es sei denn, sie haben Zeit auf einem Supercomputer. Dies würde die Zeit zum Generieren einer Hash-Tabelle für ein bestimmtes Salz massiv verlangsamen.
Nattrass
quelle
Danke für Ihre Erklärung. In der "Erstellen eines Hash 2." Sie erwähnen, dass das Salz und der Hash zusammen püriert sind. Wissen Sie, ob dies im PasswordHash in der AspNetUsers-Tabelle gespeichert ist? Ist das Salz irgendwo gespeichert, damit ich es sehen kann?
Einhorn2
1
@ unicorn2 Wenn Sie sich Andrew Savinykhs Antwort ansehen ... Im Abschnitt über das Hashing sieht es so aus, als ob das Salz in den ersten 16 Bytes des Byte-Arrays gespeichert ist, das Base64-codiert und in die Datenbank geschrieben ist. Sie können diese Base64-codierte Zeichenfolge in der PasswordHash-Tabelle sehen. Alles, was Sie über die Base64-Zeichenfolge sagen können, ist, dass ungefähr das erste Drittel davon das Salz ist. Das aussagekräftige Salt sind die ersten 16 Bytes der Base64-decodierten Version der vollständigen Zeichenfolge, die in der PasswordHash-Tabelle gespeichert ist
Nattrass
@ Nattrass, mein Verständnis von Hashes und Salzen ist eher rudimentär, aber wenn das Salz leicht aus dem Hash-Passwort extrahiert werden kann, worum geht es dann überhaupt beim Salzen? Ich dachte, das Salz sollte eine zusätzliche Eingabe für den Hashing-Algorithmus sein, die nicht leicht zu erraten ist.
NSouth
1
@NSouth Das eindeutige Salz macht den Hash für ein bestimmtes Passwort eindeutig. Zwei identische Passwörter haben also unterschiedliche Hashs. Wenn Sie Zugriff auf Ihren Hash und Salt haben, wird der Angreifer immer noch nicht an Ihr Passwort erinnert. Der Hash ist nicht umkehrbar. Sie müssten dort immer noch brutal jedes mögliche Passwort durchgehen. Das einzigartige Salz bedeutet nur, dass der Hacker keine gemeinsamen Passwörter ableiten kann, indem er eine Frequenzanalyse für bestimmte Hashs durchführt, wenn er es geschafft hat, Ihre gesamte Benutzertabelle zu erreichen.
Nattrass
8

Für diejenigen wie mich, die diesbezüglich brandneu sind, gibt es hier Code mit const und eine tatsächliche Möglichkeit, die Bytes [] zu vergleichen. Ich habe den gesamten Code von stackoverflow erhalten, aber consts definiert, damit Werte geändert werden können und auch

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

In Ihrem benutzerdefinierten ApplicationUserManager legen Sie für die PasswordHasher-Eigenschaft den Namen der Klasse fest, die den obigen Code enthält.

kfrosty
quelle
Dafür ... _passwordHashBytes = bytes.GetBytes(SaltByteSize); ich denke du hast das gemeint _passwordHashBytes = bytes.GetBytes(HashByteSize);... spielt in deinem Szenario keine Rolle, da beide gleich groß sind, aber im Allgemeinen ...
Akshatha