รหัสผ่านเริ่มต้นของ ASP.NET Identity Hasher - ทำงานอย่างไรและปลอดภัยอย่างไร?


162

ฉันสงสัยว่ารหัสผ่าน Hasher ที่นำมาใช้เป็นค่าเริ่มต้นในUserManagerที่มาพร้อมกับ MVC 5 และ ASP.NET Identity Framework นั้นมีความปลอดภัยเพียงพอหรือไม่ และถ้าเป็นเช่นนั้นถ้าคุณสามารถอธิบายให้ฉันฟังว่ามันทำงานอย่างไร

ส่วนต่อประสาน IPasswordHasher มีลักษณะเช่นนี้:

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

อย่างที่คุณเห็นมันไม่ใช้เกลือ แต่มีการกล่าวถึงในหัวข้อนี้: "การแฮ็กรหัสผ่าน Asp.net Identity " ซึ่งจะทำให้เกลือเค็มอยู่เบื้องหลัง ดังนั้นฉันสงสัยว่ามันจะทำอย่างไร และเกลือนี้มาจากไหน?

ความกังวลของฉันคือเกลือนั้นคงที่ทำให้มันไม่ปลอดภัย


ฉันไม่คิดว่านี่จะตอบคำถามของคุณโดยตรง แต่ Brock Allen เขียนเกี่ยวกับข้อกังวลของคุณที่นี่ => brockallen.com/2013/10/20/ …และยังเขียนการจัดการข้อมูลผู้ใช้โอเพ่นซอร์สและห้องสมุดการตรวจสอบความถูกต้องที่มีความหลากหลาย คุณสมบัติหม้อไอน้ำเช่นการรีเซ็ตรหัสผ่านการแฮ็ก ฯลฯ เป็นต้นgithub.com/brockallen/BrockAllen.MembershipReboot
Shiva

@ Shiva ขอบคุณฉันจะดูในไลบรารีและวิดีโอบนหน้า แต่ฉันไม่ต้องการที่จะจัดการกับห้องสมุดภายนอก ไม่ใช่ถ้าฉันสามารถหลีกเลี่ยงได้
André Snede Kock

2
FYI: stackoverflow ที่เทียบเท่าเพื่อความปลอดภัย ดังนั้นแม้ว่าคุณจะได้รับคำตอบที่ดี / ถูกต้องบ่อยครั้งที่นี่ ผู้เชี่ยวชาญอยู่ที่security.stackexchange.com โดยเฉพาะความคิดเห็นที่ "ปลอดภัย" ฉันถามคำถามแบบเดียวกันและความลึกและคุณภาพของคำตอบก็น่าทึ่ง
ฟิล soady

ขอบคุณ @philsoady ที่ทำให้ความรู้สึกของหลักสูตรอิ่มแล้วในบางส่วนของคนอื่น "ย่อยฟอรั่ม" securiry.stackexchange.comถ้าฉันไม่ได้คำตอบที่ฉันสามารถใช้ผมจะย้ายไปยัง และขอขอบคุณสำหรับเคล็ดลับ!
André Snede Kock

คำตอบ:


227

นี่คือการใช้งานเริ่มต้น ( ASP.NET FrameworkหรือASP.NET Core ) มันใช้ฟังก์ชั่น Key Derivationพร้อมเกลือแบบสุ่มในการผลิตแฮช เกลือถูกรวมเป็นส่วนหนึ่งของผลลัพธ์ของ KDF ดังนั้นทุกครั้งที่คุณ "แฮช" รหัสผ่านเดียวกันคุณจะได้รับแฮชที่แตกต่างกัน ในการตรวจสอบแฮชเอาต์พุตจะถูกแบ่งกลับเป็นเกลือและที่เหลือและ KDF จะรันอีกครั้งบนรหัสผ่านด้วยเกลือที่ระบุ หากผลลัพธ์ตรงกับส่วนที่เหลือของเอาต์พุตเริ่มต้นจะมีการตรวจสอบแฮช

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

การตรวจสอบ:

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

7
ดังนั้นหากฉันเข้าใจอย่างถูกต้องHashPasswordฟังก์ชันจะส่งคืนทั้งสองอย่างในสตริงเดียวกัน และเมื่อคุณตรวจสอบมันจะแยกมันอีกครั้งอีกครั้งและแฮชรหัสผ่าน cleartext ที่เข้ามาด้วยเกลือจากตัวแยกและเปรียบเทียบกับแฮชดั้งเดิม?
André Snede Kock

9
@ AndréSnedeHansenอย่างแน่นอน และฉันก็แนะนำให้คุณถามเกี่ยวกับความปลอดภัยหรือการเข้ารหัส SE ส่วน "มีความปลอดภัย" อาจได้รับการแก้ไขได้ดีขึ้นในบริบทที่เกี่ยวข้อง
Andrew Savinykh

1
@shajeerpuzhakkal ตามที่อธิบายไว้ในคำตอบข้างต้น
Andrew Savinykh

3
@ AndrewSavinykh ฉันรู้ว่านั่นเป็นเหตุผลที่ฉันถาม - ประเด็นคืออะไร? เพื่อให้โค้ดดูฉลาดขึ้น? ;) สาเหตุที่ฉันนับจำนวนสิ่งโดยใช้เลขทศนิยมนั้นง่ายกว่ามาก (เรามี 10 นิ้ว - อย่างน้อยพวกเราส่วนใหญ่) ดังนั้นการประกาศจำนวนสิ่งที่ใช้เลขฐานสิบหกดูเหมือนว่าเป็นรหัสที่ไม่จำเป็น
แอนดรู Cyrul

1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- เป็นสิ่งที่คุณต้องทำ หลังจากนั้นresultมีจริง
Andrew Savinykh

43

เพราะ ASP.NET วันนี้มาเปิดคุณสามารถค้นหาได้ใน GitHub: AspNet.Identity 3.0และAspNet.Identity 2.0

จากความคิดเห็นที่:

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

ใช่และน่าสังเกตว่ามีส่วนเพิ่มเติมที่อัลกอริทึม zespri แสดงอยู่
André Snede Kock

1
แหล่งที่มาบน GitHub คือ Asp.Net.Identity 3.0 ซึ่งยังอยู่ในช่วงก่อนวางจำหน่าย แหล่งที่มาของฟังก์ชันแฮช 2.0 อยู่ในCodePlex
David

1
การใช้งานใหม่ล่าสุดสามารถพบได้ในgithub.com/dotnet/aspnetcore/blob/master/src/Identity/ตอนนี้ พวกเขาเก็บที่เก็บอื่น ๆ ))
FranzHuber23

32

ฉันเข้าใจคำตอบที่ยอมรับแล้วและได้รับการโหวต แต่คิดว่าฉันจะทิ้งคำตอบของคนธรรมดาของฉันที่นี่ ...

การสร้างแฮช

  1. เกลือถูกสร้างแบบสุ่มโดยใช้ฟังก์ชัน Rfc2898DeriveBytesซึ่งสร้างแฮชและเกลือ อินพุตไปยังRfc2898DeriveBytesเป็นรหัสผ่านขนาดของเกลือที่จะสร้างและจำนวนการวนซ้ำที่จะดำเนินการ https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. จากนั้นเกลือและแฮชจะถูกบดรวมกัน (เกลือก่อนตามด้วยแฮช) และเข้ารหัสเป็นสตริง (ดังนั้นเกลือจะถูกเข้ารหัสในแฮช) แฮชที่เข้ารหัสนี้ (ซึ่งมีเกลือและแฮช) จะถูกจัดเก็บ (โดยทั่วไป) ในฐานข้อมูลกับผู้ใช้

ตรวจสอบรหัสผ่านกับแฮช

เพื่อตรวจสอบรหัสผ่านที่ผู้ใช้ป้อน

  1. เกลือถูกดึงออกมาจากรหัสผ่านที่แฮชไว้
  2. เกลือถูกใช้เพื่อแฮชรหัสผ่านของผู้ใช้โดยใช้โอเวอร์โหลดของRfc2898DeriveBytesซึ่งใช้เกลือแทนการสร้างรหัส https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. แฮชที่เก็บไว้และแฮชทดสอบจะถูกเปรียบเทียบ

The Hash

ภายใต้หน้าปกแฮชจะถูกสร้างขึ้นโดยใช้ฟังก์ชันแฮช SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ) ฟังก์ชั่นนี้เรียกว่าซ้ำ 1,000 ครั้ง (ในการใช้งานข้อมูลประจำตัวเริ่มต้น)

ทำไมความปลอดภัยนี้

  • เกลือแบบสุ่มหมายความว่าผู้โจมตีไม่สามารถใช้แฮชตารางที่สร้างไว้ล่วงหน้าเพื่อลองและทำลายรหัสผ่าน พวกเขาจำเป็นต้องสร้างตารางแฮชสำหรับเกลือทุกชนิด (สมมติว่าที่นี่แฮ็กเกอร์ได้ทำลายเกลือของคุณด้วย)
  • หากรหัสผ่าน 2 รหัสเหมือนกันจะมีแฮชต่างกัน (หมายถึงผู้โจมตีไม่สามารถอนุมานรหัสผ่าน 'ทั่วไป' ได้)
  • การเรียก SHA1 ซ้ำหลายครั้งหมายความว่าผู้โจมตีจำเป็นต้องทำเช่นนี้ ความคิดที่ว่าถ้าพวกเขามีเวลาในซูเปอร์คอมพิวเตอร์พวกเขาจะไม่มีทรัพยากรเพียงพอที่จะเดรัจฉานบังคับให้รหัสผ่านจากแฮช มันจะชะลอเวลาลงอย่างมากเพื่อสร้างตารางแฮชสำหรับเกลือที่กำหนด

ขอบคุณสำหรับคำอธิบายของคุณ ใน "การสร้างแฮช 2" คุณพูดถึงว่าเกลือและแฮชถูกบดเข้าด้วยกันคุณรู้ไหมว่าสิ่งนี้ถูกเก็บไว้ใน PasswordHash ในตาราง AspNetUsers เกลือเก็บไว้ที่ใดก็ได้เพื่อให้ฉันดูหรือไม่?
คอร์น 2

1
@ unicorn2 ถ้าคุณดูคำตอบของ Andrew Savinykh ... ในส่วนเกี่ยวกับการ hashing ดูเหมือนว่าเกลือจะถูกเก็บไว้ใน 16 ไบต์แรกของอาร์เรย์ไบต์ซึ่งเป็น Base64 ที่เข้ารหัสและเขียนลงในฐานข้อมูล คุณจะสามารถเห็นสตริงที่เข้ารหัส Base64 นี้ในตาราง PasswordHash สิ่งที่คุณสามารถพูดเกี่ยวกับสตริงเบส 64 คือประมาณหนึ่งในสามของมันคือเกลือ เกลือที่มีความหมายคือ 16 ไบต์แรกของเวอร์ชันถอดรหัส Base64 ของสตริงเต็มที่เก็บไว้ในตารางรหัสผ่าน
แฮ็ต Nattrass

@Nattrass ความเข้าใจของฉันเกี่ยวกับแฮชและเกลือนั้นค่อนข้างพื้นฐาน แต่ถ้าเกลือถูกดึงออกมาจากรหัสผ่านที่ถูกแฮชได้อย่างง่ายดายสิ่งที่เป็นจุดเริ่มต้นของการทำเกลือ ฉันคิดว่าเกลือนั้นมีไว้เพื่อเป็นข้อมูลเพิ่มเติมในอัลกอริทึมคร่ำเครียดที่ไม่สามารถเดาได้ง่าย
NSouth

1
@ เซาท์เกลือที่ไม่ซ้ำกันทำให้แฮชที่ไม่ซ้ำกันสำหรับรหัสผ่านที่กำหนด ดังนั้นรหัสผ่านที่เหมือนกันสองรายการจะมีแฮชต่างกัน การเข้าถึงแฮชและซอลท์ของคุณยังคงไม่ทำให้ผู้โจมตีจำรหัสผ่านของคุณได้ แฮชไม่สามารถย้อนกลับได้ พวกเขายังคงต้องเดรัจฉานบังคับผ่านทางที่เป็นไปได้ทุกรหัสผ่าน เกลือที่มีลักษณะเฉพาะนั้นหมายความว่าแฮ็กเกอร์ไม่สามารถอนุมานรหัสผ่านทั่วไปได้โดยทำการวิเคราะห์ความถี่ของแฮชที่เฉพาะเจาะจงหากพวกเขาสามารถจัดการตารางผู้ใช้ทั้งหมดของคุณได้
Nattrass

8

สำหรับผู้ที่ชอบฉันซึ่งเป็นแบรนด์ใหม่ของที่นี่นี่คือรหัสกับ const และวิธีที่แท้จริงในการเปรียบเทียบไบต์ [] ฉันได้รับรหัสนี้ทั้งหมดจาก stackoverflow แต่กำหนด const ดังนั้นค่าสามารถเปลี่ยนแปลงได้และ

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

ใน ApplicationUserManager แบบกำหนดเองของคุณคุณตั้งค่าคุณสมบัติ PasswordHasher เป็นชื่อของคลาสที่มีรหัสข้างต้น


สำหรับสิ่งนี้.. _passwordHashBytes = bytes.GetBytes(SaltByteSize); ฉันคิดว่าคุณหมายถึงสิ่งนี้_passwordHashBytes = bytes.GetBytes(HashByteSize);.. ไม่สำคัญกับสถานการณ์ของคุณเนื่องจากทั้งสองมีขนาดเท่ากัน แต่โดยทั่วไป ..
Akshatha
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.