Câu trả lời:
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 data
dướ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ừ md5data
hoặcsha1data
var hashedPassword = ASCIIEncoding.GetString(md5data);
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ã.
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ị 100000
có thể được giảm xuống. Giá trị tối thiểu phải nằm trong khoảng 10000
.
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 .
V1
và V2
phương pháp xác minh nào bạn cần.
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);
}
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);
}
}
Câu trả lời của @ csharptest.net và Christian 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 RNGCryptoServiceProvider và Rfc2898DeriveBytes 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);
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".
using
câu lệnh hoặc gọiClear()
nó khi bạn sử dụng xong việc triển khai.