Domyślna funkcja mieszania haseł w programie ASP.NET Identity - jak to działa i czy jest bezpieczne?

162

Zastanawiam się, czy funkcja Password Hasher, która jest domyślnie zaimplementowana w UserManager, która jest dostarczana z MVC 5 i ASP.NET Identity Framework, jest wystarczająco bezpieczna? A jeśli tak, czy mógłbyś mi wyjaśnić, jak to działa?

Interfejs IPasswordHasher wygląda następująco:

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

Jak widać, nie wymaga soli, ale jest wspomniane w tym wątku: „ Haszowanie hasła tożsamości Asp.net ”, że faktycznie zasypia go za kulisami. Więc zastanawiam się, jak to robi? A skąd ta sól?

Martwię się, że sól jest statyczna, przez co jest dość niepewna.

André Snede Kock
źródło
Nie sądzę, aby to bezpośrednio odpowiadało na twoje pytanie, ale Brock Allen napisał o niektórych z twoich obaw tutaj => brockallen.com/2013/10/20/ ... a także napisał otwartą bibliotekę zarządzania tożsamością użytkownika i uwierzytelniania, która ma różne funkcje płyty kotłowej, takie jak resetowanie hasła, haszowanie itp. github.com/brockallen/BrockAllen.MembershipReboot
Shiva
@Shiva Dzięki, przejrzę do biblioteki i wideo na stronie. Ale wolałbym nie mieć do czynienia z zewnętrzną biblioteką. Nie, jeśli mogę tego uniknąć.
André Snede Kock
2
FYI: odpowiednik przepełnienia stosu dla bezpieczeństwa. Więc chociaż często otrzymasz tutaj dobrą / poprawną odpowiedź. Eksperci są na security.stackexchange.com, zwłaszcza komentarz „czy to bezpieczne”. Zadałem podobne pytanie, a głębia i jakość odpowiedzi była niesamowita.
phil soady,
@philsoady Dzięki, to oczywiście ma sens, jestem już na kilku innych "sub-forach", jeśli nie dostanę odpowiedzi to mogę skorzystać, przejdę do securiry.stackexchange.com. Dzięki za cynk!
André Snede Kock

Odpowiedzi:

227

Oto jak działa domyślna implementacja ( ASP.NET Framework lub ASP.NET Core ). Wykorzystuje kluczową funkcję pochodną z losową solą do wytworzenia skrótu. Sól jest częścią produkcji KDF. Dlatego za każdym razem, gdy „haszujesz” to samo hasło, otrzymasz różne skróty. Aby zweryfikować hash, dane wyjściowe są dzielone z powrotem na sól i resztę, a KDF jest ponownie uruchamiany na haśle z określoną solą. Jeśli wynik pasuje do reszty początkowego wyjścia, skrót jest weryfikowany.

Haszowanie:

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

Weryfikacja:

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
źródło
7
Więc jeśli dobrze to rozumiem, HashPasswordfunkcja zwraca oba w tym samym ciągu? A kiedy to zweryfikujesz, ponownie je dzieli i haszuje przychodzące hasło w postaci zwykłego tekstu z solą z podziału i porównuje je z oryginalnym hashem?
André Snede Kock
9
@ AndréSnedeHansen, dokładnie. I ja również radzę zapytać o bezpieczeństwo lub kryptografię SE. Część „czy to bezpieczne” może być lepiej omówiona w odpowiednich kontekstach.
Andrew Savinykh,
1
@shajeerpuzhakkal, jak opisano w odpowiedzi powyżej.
Andrew Savinykh
3
@AndrewSavinykh Wiem, dlatego pytam - o co chodzi? Aby kod wyglądał mądrzej? ;) Bo dla mnie liczenie rzeczy za pomocą liczb dziesiętnych jest DUŻO bardziej intuicyjne (w końcu mamy 10 palców - przynajmniej większość z nas), więc zadeklarowanie czegoś za pomocą liczb szesnastkowych wydaje się niepotrzebnym zaciemnianiem kodu.
Andrew Cyrul
1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- to, co musisz zrobić. po tym resultzawiera true.
Andrew Savinykh
43

Ponieważ obecnie ASP.NET jest oprogramowaniem typu open source, można go znaleźć w serwisie GitHub: AspNet.Identity 3.0 i AspNet.Identity 2.0 .

Z komentarzy:

/* =======================
 * 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
źródło
Tak, i warto zauważyć, że są dodatki do algorytmu, który pokazuje zespri.
André Snede Kock
1
Źródłem w GitHub jest Asp.Net.Identity 3.0, które jest nadal w wersji wstępnej. Źródło funkcji skrótu 2.0 znajduje się na CodePlex
David
1
Najnowszą implementację można znaleźć pod adresem github.com/dotnet/aspnetcore/blob/master/src/Identity/ ... teraz. Drugie repozytorium zarchiwizowali;)
FranzHuber 23
32

Rozumiem zaakceptowaną odpowiedź i zagłosowałem za nią, ale pomyślałem, że zostawię tutaj odpowiedź moich laików ...

Tworzenie skrótu

  1. Sól jest generowana losowo przy użyciu funkcji Rfc2898DeriveBytes, która generuje skrót i sól. Dane wejściowe do Rfc2898DeriveBytes to hasło, rozmiar soli do wygenerowania i liczba iteracji mieszania do wykonania. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. Sól i haszysz są następnie rozgniatane (najpierw sól, a następnie haszysz) i kodowane jako ciąg (więc sól jest kodowana w haszyszu). Ten zakodowany skrót (który zawiera sól i skrót) jest następnie przechowywany (zazwyczaj) w bazie danych przeciwko użytkownikowi.

Sprawdzanie hasła na podstawie skrótu

Aby sprawdzić hasło wprowadzone przez użytkownika.

  1. Sól jest pobierana z przechowywanego zaszyfrowanego hasła.
  2. Sól służy do mieszania hasła wejściowego użytkownika przy użyciu przeciążenia Rfc2898DeriveBytes, które pobiera sól zamiast ją generować. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. Następnie porównuje się przechowywany hash i testowy hash.

Hash

Pod okładkami hash jest generowany za pomocą funkcji skrótu SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Ta funkcja jest iteracyjnie nazywana 1000 razy (w domyślnej implementacji tożsamości)

Dlaczego jest to bezpieczne

  • Losowe sole oznaczają, że atakujący nie może użyć wstępnie wygenerowanej tabeli skrótów do próby złamania haseł. Musieliby wygenerować tablicę skrótów dla każdej soli. (Zakładając, że haker przejął również twoją sól)
  • Jeśli 2 hasła są identyczne, będą miały różne skróty. (co oznacza, że ​​napastnicy nie mogą wywnioskować „typowych” haseł)
  • Iteracyjne wywoływanie SHA1 1000 razy oznacza, że ​​atakujący również musi to zrobić. Chodzi o to, że jeśli nie mają czasu na superkomputerze, nie będą mieli wystarczających zasobów, aby brutalnie wymusić hasło z skrótu. Znacznie spowolniłoby to czas generowania tablicy mieszania dla danej soli.
Nattrass
źródło
Dziękuję za wyjaśnienie. W sekcji „Tworzenie skrótu 2” wspomniałeś, że sól i hash są zmiksowane razem, czy wiesz, czy jest to przechowywane w PasswordHash w tabeli AspNetUsers. Czy sól jest gdzieś przechowywana, abym mógł ją zobaczyć?
jednorożec 2
1
@ unicorn2 Jeśli spojrzysz na odpowiedź Andrew Savinykha ... W sekcji o haszowaniu wygląda na to, że sól jest przechowywana w pierwszych 16 bajtach tablicy bajtów, która jest zakodowana w Base64 i zapisana w bazie danych. Byłbyś w stanie zobaczyć ten ciąg zakodowany w Base64 w tabeli PasswordHash. Wszystko, co możesz powiedzieć o strunie Base64, to mniej więcej pierwsza trzecia jej to sól. Znacząca sól to pierwsze 16 bajtów zdekodowanej w Base64 wersji pełnego ciągu przechowywanego w tabeli PasswordHash
Nattrass
@Nattrass, Moje rozumienie haszów i soli jest raczej szczątkowe, ale jeśli sól można łatwo wydobyć z zaszyfrowanego hasła, po pierwsze, jaki jest sens solenia. Myślałem, że sól miała być dodatkowym wejściem do algorytmu haszującego, którego nie można łatwo odgadnąć.
NSouth
1
@NSouth Unikalna sól sprawia, że ​​hash jest unikalny dla danego hasła. Zatem dwa identyczne hasła będą miały różne skróty. Dostęp do twojego hasha i soli nadal nie powoduje, że osoba atakująca pamięta twoje hasło. Hash nie jest odwracalny. Nadal musieliby brutalnie przebić się przez każde możliwe hasło. Unikalna sól oznacza po prostu, że haker nie może wywnioskować wspólnych haseł, przeprowadzając analizę częstotliwości określonych skrótów, jeśli udało mu się zdobyć całą tabelę użytkowników.
Nattrass
8

Dla takich jak ja, którzy są zupełnie nowicjuszami, oto kod ze stałą i rzeczywisty sposób porównywania bajtów []. Otrzymałem cały ten kod z przepełnienia stosu, ale zdefiniowałem stałe, więc wartości można zmieniać, a także

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

W swoim niestandardowym ApplicationUserManager ustawisz właściwość PasswordHasher nazwę klasy, która zawiera powyższy kod.

kfrosty
źródło
Z tego powodu … _passwordHashBytes = bytes.GetBytes(SaltByteSize); myślę, że miałeś to na myśli _passwordHashBytes = bytes.GetBytes(HashByteSize);… Nie ma to znaczenia w twoim scenariuszu, ponieważ oba są tego samego rozmiaru, ale ogólnie…
Akshatha,