Odpowiedzi:
AKTUALIZACJA : TA ODPOWIEDŹ JEST POWAŻNIE PRZESTAŁA . Zamiast tego skorzystaj z zaleceń z https://stackoverflow.com/a/10402129/251311 .
Możesz użyć
var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);
lub
var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);
Aby uzyskać data
tablicę bajtów, możesz użyć
var data = Encoding.ASCII.GetBytes(password);
i odzyskać ciąg z md5data
lubsha1data
var hashedPassword = ASCIIEncoding.GetString(md5data);
md5
jest wystarczająco dobry do prawie wszystkich rodzajów zadań. Jego luki odnoszą się również do bardzo specyficznych sytuacji i prawie wymagają od atakującego dużej wiedzy na temat kryptografii.
Większość pozostałych odpowiedzi tutaj jest nieco nieaktualna w stosunku do dzisiejszych najlepszych praktyk. Jako takie jest tutaj zastosowanie PBKDF2 / Rfc2898DeriveBytes
do przechowywania i weryfikacji haseł. Poniższy kod znajduje się w samodzielnej klasie w tym poście: Kolejny przykład przechowywania zasolonego skrótu hasła . Podstawy są naprawdę proste, więc tutaj jest podzielone:
KROK 1 Utwórz wartość soli za pomocą kryptograficznego PRNG:
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);
KROK 2 Utwórz Rfc2898DeriveBytes i uzyskaj wartość skrótu:
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
KROK 3 Połącz bajty soli i hasła do późniejszego wykorzystania:
byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);
KROK 4 Zamień połączoną sól + haszysz w sznurek do przechowywania
string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });
KROK 5 Zweryfikuj hasło wprowadzone przez użytkownika z zapisanym hasłem
/* 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();
Uwaga: W zależności od wymagań dotyczących wydajności określonej aplikacji, wartość 100000
można zmniejszyć. Minimalna wartość powinna wynosić około 10000
.
W oparciu o świetną odpowiedź csharptest.net napisałem w tym celu klasę:
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;
}
}
Stosowanie:
// Hash
var hash = SecurePasswordHasher.Hash("mypassword");
// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);
Przykładowy hash może wyglądać tak:
$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn
Jak widać, włączyłem również iteracje do skrótu, aby ułatwić użytkowanie i możliwość aktualizacji, jeśli zajdzie potrzeba aktualizacji.
Jeśli jesteś zainteresowany .net core, mam również wersję .net core w Code Review .
V1
i V2
jakiej metody weryfikacji potrzebujesz.
Używam skrótu i soli do szyfrowania hasła (jest to ten sam skrót, którego używa członkostwo w Asp.Net):
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);
}
}
Odpowiedzi @ csharptest.net i Christiana Gollhardta są świetne, bardzo dziękuję. Ale po uruchomieniu tego kodu na produkcji z milionami rekordów odkryłem wyciek pamięci. Klasy RNGCryptoServiceProvider i Rfc2898DeriveBytes są pochodnymi klasy IDisposable, ale ich nie usuwamy . Napiszę swoje rozwiązanie jako odpowiedź, jeśli ktoś potrzebuje wersji wyrzuconej.
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;
}
}
}
Stosowanie:
// Hash
var hash = SecurePasswordHasher.Hash("mypassword");
// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);
Myślę, że użycie KeyDerivation.Pbkdf2 jest lepsze niż Rfc2898DeriveBytes.
Przykład i wyjaśnienie: haszowanie haseł w 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=
*/
To jest przykładowy kod z artykułu. I to jest minimalny poziom bezpieczeństwa. Aby go zwiększyć, użyłbym zamiast parametru KeyDerivationPrf.HMACSHA1
KeyDerivationPrf.HMACSHA256 lub KeyDerivationPrf.HMACSHA512.
Nie rezygnuj z mieszania haseł. Istnieje wiele matematycznie uzasadnionych metod optymalizacji hakowania haseł. Konsekwencje mogą być katastrofalne. Gdy złoczyńca dostanie w swoje ręce tablicę haszującą haseł Twoich użytkowników, złamanie haseł byłoby dla niego stosunkowo łatwe, ponieważ algorytm jest słaby lub implementacja jest nieprawidłowa. Ma dużo czasu (czas x moc komputera) na łamanie haseł. Haszowanie haseł powinno być mocne kryptograficznie, aby zamienić „dużo czasu” w „ nierozsądną ilość czasu ”.
Jeszcze jeden punkt do dodania
Weryfikacja skrótu zajmuje trochę czasu (i jest dobra). Gdy użytkownik wpisze złą nazwę użytkownika, sprawdzenie, czy nazwa użytkownika jest nieprawidłowa, nie zajmuje czasu. Gdy nazwa użytkownika jest poprawna zaczynamy weryfikację hasła - to stosunkowo długi proces.
Dla hakera bardzo łatwo byłoby zrozumieć, czy użytkownik istnieje, czy nie.
Nie zwracaj natychmiastowej odpowiedzi, gdy nazwa użytkownika jest nieprawidłowa.
Nie trzeba dodawać: nigdy nie udzielaj odpowiedzi, co jest nie tak. Po prostu ogólne „Poświadczenia są nieprawidłowe”.
using
oświadczeniu lub wywołajClear()
go po zakończeniu korzystania z implementacji.