Cách băm mật khẩu


117

Tôi muốn lưu trữ mã băm của mật khẩu trên điện thoại, nhưng tôi không chắc chắn về cách thực hiện. Tôi dường như chỉ có thể tìm thấy các phương pháp mã hóa. Mật khẩu nên được băm đúng cách như thế nào?

Câu trả lời:


62

CẬP NHẬT : CÂU TRẢ LỜI NÀY RẤT NGHIÊM TÚC . Thay vào đó, hãy sử dụng các đề xuất từ https://stackoverflow.com/a/10402129/251311 .

Bạn có thể sử dụng

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

hoặc là

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

Để có được datadưới dạng mảng byte, bạn có thể sử dụng

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

và lấy lại chuỗi từ md5datahoặcsha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
Tôi THỰC SỰ khuyên bạn nên sử dụng SHA1. MD5 là không được trừ khi bạn đang duy trì khả năng tương thích ngược với hệ thống hiện có. Ngoài ra, hãy đảm bảo rằng bạn đặt nó trong một usingcâu lệnh hoặc gọi Clear()nó khi bạn sử dụng xong việc triển khai.
vcsjones

3
@vcsjones: Tôi không muốn thánh chiến ở đây, nhưng md5đủ tốt cho hầu hết mọi loại nhiệm vụ. Các lỗ hổng của nó cũng đề cập đến các tình huống rất cụ thể và gần như đòi hỏi kẻ tấn công phải biết nhiều về mật mã.
zerkms

4
Đã lấy điểm @zerkms, nhưng nếu không có lý do tương thích ngược thì không có lý do gì để sử dụng MD5. "Cẩn tắc vô ưu".
vcsjones

4
Không có lý do gì để sử dụng MD5 vào thời điểm này. Cho rằng thời gian tính toán là không đáng kể, không có lý do gì để sử dụng MD5 ngoại trừ khả năng tương thích với các hệ thống hiện có. Ngay cả khi MD5 là "đủ tốt", người dùng sẽ không phải trả phí SHA an toàn hơn nhiều. Tôi chắc chắn zerkms biết điều này, bình luận dành cho người hỏi nhiều hơn.
Gerald Davis

11
Ba sai lầm lớn: 1) ASCII âm thầm làm giảm mật khẩu với các ký tự bất thường 2) MD5 / SHA-1 / SHA-2 đơn giản là nhanh. 3) Bạn cần một loại muối. | Thay vào đó, hãy sử dụng PBKDF2, bcrypt hoặc scrypt. PBKDF2 dễ dàng nhất trong lớp Rfc2898DeriveBytes (không chắc liệu có trên WP7 hay không)
CodesInChaos

298

Hầu hết các câu trả lời khác ở đây có phần lỗi thời với các phương pháp hay nhất hiện nay. Như vậy ở đây là ứng dụng sử dụng PBKDF2 / Rfc2898DeriveBytesđể lưu trữ và xác minh mật khẩu. Đoạn mã sau thuộc một lớp độc lập trong bài đăng này: Một ví dụ khác về cách lưu trữ băm mật khẩu mặn . Những điều cơ bản thực sự dễ dàng, vì vậy ở đây nó được chia nhỏ:

BƯỚC 1 Tạo giá trị muối bằng PRNG mật mã:

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

BƯỚC 2 Tạo Rfc2898DeriveBytes và nhận giá trị băm:

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

BƯỚC 3 Kết hợp các byte muối và mật khẩu để sử dụng sau này:

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

BƯỚC 4 Chuyển muối + băm kết hợp thành một chuỗi để lưu trữ

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

BƯỚC 5 Xác minh mật khẩu do người dùng nhập so với mật khẩu được lưu trữ

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

Lưu ý: Tùy thuộc vào yêu cầu hiệu suất của ứng dụng cụ thể của bạn, giá trị 100000có thể được giảm xuống. Giá trị tối thiểu phải nằm trong khoảng 10000.


8
@Daniel về cơ bản, bài đăng nói về việc sử dụng một thứ gì đó an toàn hơn một hàm băm. Nếu bạn chỉ băm mật khẩu, ngay cả với muối, mật khẩu người dùng của bạn sẽ bị xâm phạm (và có khả năng được bán / xuất bản) trước khi bạn có cơ hội yêu cầu họ thay đổi mật khẩu đó. Sử dụng đoạn mã trên để gây khó khăn cho kẻ tấn công, không dễ cho nhà phát triển.
csharptest.net

2
@DatVM Không, muối mới cho mỗi lần bạn lưu trữ một hàm băm. đó là lý do tại sao nó được kết hợp với hàm băm để lưu trữ để bạn có thể xác minh mật khẩu.
csharptest.net

9
@CiprianJijie toàn bộ vấn đề là bạn không được cho là có thể.
csharptest.net

9
Trong trường hợp bất kỳ ai đang thực hiện phương thức VerifyPassword, nếu bạn muốn sử dụng Linq và một lệnh gọi ngắn hơn cho boolean, điều này sẽ thực hiện: return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo

2
@ csharptest.net Bạn đề xuất loại kích thước mảng nào? kích thước của mảng có ảnh hưởng nhiều đến bảo mật không? Tôi không biết nhiều về băm / mật mã
lennyy

71

Dựa trên câu trả lời tuyệt vời của csharptest.net , tôi đã viết một Lớp học cho điều này:

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

Sử dụng:

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

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

Một băm mẫu có thể là:

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

Như bạn có thể thấy, tôi cũng đã bao gồm các lần lặp lại trong hàm băm để dễ sử dụng và khả năng nâng cấp điều này, nếu chúng ta cần nâng cấp.


Nếu bạn quan tâm đến lõi .net, tôi cũng có phiên bản lõi .net trên Code Review .


1
Chỉ để xác minh, nếu bạn nâng cấp công cụ băm, bạn sẽ tăng phần V1 của hàm băm và khóa của nó?
Mike Cole

1
Vâng, đó là kế hoạch. Sau đó, bạn sẽ quyết định dựa trên V1V2phương pháp xác minh nào bạn cần.
Christian Gollhardt

Cảm ơn vì đã trả lời, và cả lớp. Tôi đang triển khai nó khi chúng ta nói chuyện.
Mike Cole

2
Có @NelsonSilva. Đó là do muối .
Christian Gollhardt

1
Với tất cả các bản sao / dán mã này (bao gồm cả tôi), tôi hy vọng ai đó lên tiếng và bài đăng sẽ được sửa đổi nếu phát hiện có vấn đề với nó! :)
pettys

14

Tôi sử dụng một hàm băm và một muối để mã hóa mật khẩu của mình (nó giống như hàm băm mà Hội viên Asp.Net sử dụng):

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 để sử dụng SHA-1 đơn giản, nhanh chóng. Sử dụng chức năng dẫn xuất khóa chậm, chẳng hạn như PBKDF2, bcrypt hoặc scrypt.
CodesInChaos

1
  1. Tạo muối,
  2. Tạo mật khẩu băm bằng muối
  3. Lưu cả băm và muối
  4. giải mã bằng mật khẩu và muối ... vì vậy các nhà phát triển không thể giải mã mật khẩu
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

Câu trả lời của @ csharptest.netChristian Gollhardt rất hay, cảm ơn bạn rất nhiều. Nhưng sau khi chạy mã này trên sản xuất với hàng triệu bản ghi, tôi phát hiện ra có một lỗ hổng bộ nhớ. Các lớp RNGCryptoServiceProviderRfc2898DeriveBytes có nguồn gốc từ IDisposable nhưng chúng tôi không loại bỏ chúng. Tôi sẽ viết giải pháp của mình như một câu trả lời nếu ai đó cần với phiên bản đã được xử lý.

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

    }
}

Sử dụng:

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

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

0

Tôi nghĩ rằng sử dụng KeyDerivation.Pbkdf2 tốt hơn Rfc2898DeriveBytes.

Ví dụ và giải thích: Băm mật khẩu trong 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=
 */

Đây là một mã mẫu từ bài báo. Và đó là mức bảo mật tối thiểu. Để tăng nó, tôi sẽ sử dụng thay vì tham số KeyDerivationPrf.HMACSHA1

KeyDerivationPrf.HMACSHA256 hoặc KeyDerivationPrf.HMACSHA512.

Đừng thỏa hiệp với việc băm mật khẩu. Có nhiều phương pháp toán học hợp lý để tối ưu hóa việc hack mật khẩu. Hậu quả có thể là thảm khốc. Một khi một nhân tố nam có thể nắm được bảng băm mật khẩu của người dùng của bạn, sẽ tương đối dễ dàng để anh ta bẻ khóa mật khẩu do thuật toán yếu hoặc triển khai không chính xác. Anh ta có rất nhiều thời gian (thời gian x sức mạnh máy tính) để bẻ khóa mật khẩu. Băm mật khẩu nên được mã hóa mạnh mẽ để biến "nhiều thời gian" thành " lượng thời gian không hợp lý ".

Thêm một điểm nữa

Xác minh băm cần thời gian (và nó tốt). Khi người dùng nhập sai tên người dùng, không mất thời gian để kiểm tra xem tên người dùng có chính xác hay không. Khi tên người dùng chính xác, chúng tôi bắt đầu xác minh mật khẩu - quá trình này tương đối dài.

Đối với một hacker, sẽ rất dễ hiểu nếu người dùng tồn tại hay không.

Đảm bảo không trả lại câu trả lời ngay lập tức khi tên người dùng bị sai.

Không cần phải nói: không bao giờ đưa ra câu trả lời là sai. Chỉ chung chung "Thông tin đăng nhập là sai".


1
BTW, câu trả lời trước stackoverflow.com/a/57508528/11603057 không đúng và có hại. Đó là một ví dụ về băm, không phải băm mật khẩu. Phải là các lần lặp lại của hàm giả ngẫu nhiên trong quá trình lấy khóa. Không có. Tôi không thể nhận xét nó hoặc phản đối (danh tiếng thấp của tôi). Xin đừng bỏ lỡ câu trả lời không chính xác!
Albert Lyubarsky
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.