ASP.NET Identity's Default Password Hasher - Bagaimana cara kerjanya dan apakah itu aman?

162

Saya bertanya-tanya apakah Password Hasher yang default diimplementasikan dalam UserManager yang dilengkapi dengan MVC 5 dan ASP.NET Identity Framework, apakah cukup aman? Dan jika demikian, apakah Anda bisa menjelaskan kepada saya cara kerjanya?

Antarmuka IPasswordHasher terlihat seperti ini:

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

Seperti yang Anda lihat, ini tidak memerlukan banyak garam, tetapi disebutkan di utas ini: " Asp.net Identity password hashing " yang benar-benar membuat garam di belakang layar. Jadi saya bertanya-tanya bagaimana cara melakukannya? Dan dari mana asalnya garam ini?

Kekhawatiran saya adalah garam itu statis, membuatnya tidak aman.

André Snede Kock
sumber
Saya tidak berpikir ini langsung menjawab pertanyaan Anda, tetapi Brock Allen telah menulis tentang beberapa masalah Anda di sini => brockallen.com/2013/10/20/… dan juga menulis perpustakaan manajemen identitas pengguna open source dan perpustakaan otentikasi yang memiliki berbagai fitur boiler-plate seperti pengaturan
Shiva
@ Shiva Terima kasih, saya akan melihat ke perpustakaan dan video di halaman. Tapi saya lebih suka tidak harus berurusan dengan perpustakaan eksternal. Tidak kalau aku bisa menghindarinya.
André Snede Kock
2
FYI: setara dengan stackoverflow untuk keamanan. Jadi walaupun Anda akan sering mendapatkan jawaban yang baik / benar di sini. Para ahli ada di security.stackexchange.com terutama komentar "apakah aman" Saya mengajukan pertanyaan serupa dan kedalaman serta kualitas jawaban sangat mengagumkan.
phil soady
@ philsoady Terima kasih, itu tentu saja masuk akal, saya sudah berada di beberapa "sub-forum" lainnya, jika saya tidak mendapatkan jawaban, saya dapat menggunakan, saya akan pindah ke securiry.stackexchange.com. Dan terima kasih atas tipnya!
André Snede Kock

Jawaban:

227

Berikut adalah cara implementasi default ( ASP.NET Framework atau ASP.NET Core ) bekerja. Ini menggunakan Fungsi Derivasi Kunci dengan garam acak untuk menghasilkan hash. Garam dimasukkan sebagai bagian dari output KDF. Jadi, setiap kali Anda "hash" kata sandi yang sama Anda akan mendapatkan hash yang berbeda. Untuk memverifikasi hash, output dibagi kembali ke garam dan sisanya, dan KDF dijalankan lagi pada kata sandi dengan garam yang ditentukan. Jika hasilnya cocok dengan sisa output awal hash diverifikasi.

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

Memverifikasi:

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
sumber
7
Jadi jika saya mengerti ini dengan benar, HashPasswordfungsinya, mengembalikan keduanya dalam string yang sama? Dan ketika Anda memverifikasinya, ia membaginya lagi dan mem-hash password cleartext yang masuk, dengan garam dari split, dan membandingkannya dengan hash asli?
André Snede Kock
9
@ AndréSnedeHansen, tepatnya. Dan saya juga merekomendasikan Anda untuk bertanya tentang keamanan atau tentang kriptografi SE. Bagian "apakah itu aman" dapat ditangani dengan lebih baik dalam konteks masing-masing.
Andrew Savinykh
1
@shajeerpuzhakkal seperti yang dijelaskan dalam jawaban di atas.
Andrew Savinykh
3
@AndrewSavinykh Saya tahu, itu sebabnya saya bertanya - apa gunanya? Untuk membuat kode terlihat lebih pintar? ;) Sebab bagi saya menghitung barang menggunakan angka desimal adalah BANYAK lebih intuitif (kita memiliki 10 jari setelah semua - setidaknya sebagian besar dari kita), jadi menyatakan sejumlah sesuatu menggunakan hexadecimal tampaknya seperti kode yang tidak perlu dikaburkan.
Andrew Cyrul
1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- adalah apa yang perlu Anda lakukan. setelah itu resultberisi true.
Andrew Savinykh
43

Karena hari ini ASP.NET adalah open source, Anda dapat menemukannya di GitHub: AspNet.Identity 3.0 dan AspNet.Identity 2.0 .

Dari komentar:

/* =======================
 * 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
sumber
Ya, dan patut dicatat, ada tambahan pada algoritma yang ditunjukkan zespri.
André Snede Kock
1
Sumber di GitHub adalah Asp.Net.Identity 3.0 yang masih dalam tahap pra-rilis. Sumber fungsi hash 2.0 ada di CodePlex
David
1
Implementasi terbaru dapat ditemukan di github.com/dotnet/aspnetcore/blob/master/src/Identity/… sekarang. Mereka mengarsip repositori lainnya;)
FranzHuber23
32

Saya mengerti jawaban yang diterima, dan telah memilihnya tetapi saya pikir saya akan membuang jawaban orang awam saya di sini ...

Membuat hash

  1. Garam dihasilkan secara acak menggunakan fungsi Rfc2898DeriveBytes yang menghasilkan hash dan garam. Input ke Rfc2898DeriveBytes adalah kata sandi, ukuran garam yang akan dihasilkan, dan jumlah iterasi hashing yang harus dilakukan. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. Garam dan hash kemudian dihaluskan bersama (garam pertama diikuti oleh hash) dan dikodekan sebagai string (sehingga garam dikodekan dalam hash). Hash yang disandikan ini (yang mengandung garam dan hash) kemudian disimpan (biasanya) dalam database melawan pengguna.

Memeriksa kata sandi terhadap hash

Untuk memeriksa kata sandi yang dimasukkan pengguna.

  1. Garam diekstraksi dari kata sandi hash yang tersimpan.
  2. Garam digunakan untuk hash pengguna memasukkan kata sandi menggunakan kelebihan Rfc2898DeriveBytes yang mengambil garam alih-alih menghasilkan satu. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. Hash yang disimpan dan hash tes kemudian dibandingkan.

Hash

Di bawah penutup hash dihasilkan menggunakan fungsi hash SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Fungsi ini disebut berulang 1000 kali (Dalam implementasi Identity default)

Mengapa ini aman?

  • Garam acak berarti penyerang tidak dapat menggunakan tabel hash yang dibuat sebelumnya untuk mencoba dan menghancurkan kata sandi. Mereka perlu membuat tabel hash untuk setiap garam. (Dengan asumsi di sini bahwa peretas juga telah mengkompromikan garam Anda)
  • Jika 2 kata sandi identik, mereka akan memiliki hash yang berbeda. (artinya penyerang tidak dapat menyimpulkan kata sandi 'umum')
  • Iteratif memanggil SHA1 1000 kali berarti penyerang juga perlu melakukan ini. Idenya adalah bahwa kecuali mereka punya waktu di superkomputer mereka tidak akan memiliki cukup sumber daya untuk memaksa kata sandi dari hash. Ini akan sangat memperlambat waktu untuk menghasilkan tabel hash untuk garam yang diberikan.
Nattrass
sumber
Terima kasih atas penjelasan anda Dalam "Membuat hash 2." Anda menyebutkan bahwa garam dan hash dihaluskan bersama, apakah Anda tahu jika ini disimpan di PasswordHash di tabel AspNetUsers. Apakah garam disimpan di mana saja untuk saya lihat?
unicorn2
1
@ unicorn2 Jika Anda melihat jawaban Andrew Savinykh ... Di bagian tentang hashing sepertinya garam disimpan dalam 16 byte pertama dari byte array yang Base64 dikodekan dan ditulis ke database. Anda akan dapat melihat string yang disandikan Base64 ini di tabel PasswordHash. Yang dapat Anda katakan tentang string Base64 adalah bahwa kira-kira sepertiga pertama darinya adalah garam. Garam yang berarti adalah 16 byte pertama dari versi decoding Base64 dari string penuh yang disimpan dalam tabel PasswordHash
Nattrass
@Nattrass, Pemahaman saya tentang hash dan garam agak sederhana, tetapi jika garam mudah diekstrak dari kata sandi hash, apa gunanya pengasinan di tempat pertama. Saya pikir garam itu dimaksudkan sebagai input tambahan untuk algoritma hashing yang tidak dapat dengan mudah ditebak.
NSouth
1
@NSouth Garam unik membuat hash unik untuk kata sandi yang diberikan. Jadi dua kata sandi yang identik akan memiliki hash yang berbeda. Memiliki akses ke hash dan garam Anda masih tidak membuat penyerang mengingat kata sandi Anda. Hash tidak dapat dibalik. Mereka masih perlu melakukan kekerasan melalui setiap kata sandi yang mungkin ada. Garam unik hanya berarti bahwa peretas tidak dapat menyimpulkan kata sandi umum dengan melakukan analisis frekuensi pada hash tertentu jika mereka berhasil mendapatkan seluruh tabel pengguna Anda.
Nattrass
8

Bagi mereka seperti saya yang baru dengan ini, berikut adalah kode dengan const dan cara aktual untuk membandingkan byte []. Saya mendapatkan semua kode ini dari stackoverflow tetapi mendefinisikan konstanta sehingga nilainya dapat diubah dan juga

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

Di dalam ApplicationUserManager kustom Anda, Anda mengatur properti PasswordHasher nama kelas yang berisi kode di atas.

kfrosty
sumber
Untuk ini .. _passwordHashBytes = bytes.GetBytes(SaltByteSize); Saya kira Anda maksudkan ini _passwordHashBytes = bytes.GetBytes(HashByteSize);.. Tidak masalah dalam skenario Anda karena keduanya berukuran sama tetapi secara umum ..
Akshatha