Jak zaszyfrować hasło


117

Chciałbym przechowywać skrót hasła w telefonie, ale nie wiem, jak to zrobić. Mogę tylko znaleźć metody szyfrowania. Jak poprawnie zaszyfrować hasło?

Odpowiedzi:


62

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ć datatablicę bajtów, możesz użyć

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

i odzyskać ciąg z md5datalubsha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
NAPRAWDĘ poleciłbym używanie SHA1. MD5 to nie-nie, chyba że zachowujesz wsteczną zgodność z istniejącym systemem. Ponadto upewnij się, że umieściłeś go w usingoświadczeniu lub wywołaj Clear()go po zakończeniu korzystania z implementacji.
vcsjones

3
@vcsjones: Nie chcę tutaj świętej wojny, ale md5jest 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.
zerkms

4
Podjęto punkt @zerkms, ale jeśli nie ma powodu do wstecznej kompatybilności, nie ma powodu, aby używać MD5. "Lepiej dmuchać na zimne".
vcsjones

4
Na tym etapie nie ma powodu, aby używać MD5. Biorąc pod uwagę, że czas obliczeń jest nieistotny, nie ma powodu, aby używać MD5 poza kompatybilnością z istniejącymi systemami. Nawet jeśli MD5 jest „dostatecznie dobre”, użytkownik nie musi ponosić kosztów znacznie bezpieczniejszego SHA. Jestem pewien, że zerkmowie wiedzą, że komentarz jest bardziej dla pytającego.
Gerald Davis

11
Trzy duże błędy: 1) ASCII po cichu degraduje hasła z nietypowymi znakami 2) Zwykły MD5 / SHA-1 / SHA-2 jest szybki. 3) Potrzebujesz soli. | Zamiast tego użyj PBKDF2, bcrypt lub scrypt. PBKDF2 jest najłatwiejszy w klasie Rfc2898DeriveBytes (brak pewności, czy jest obecny w WP7)
CodesInChaos

299

Większość pozostałych odpowiedzi tutaj jest nieco nieaktualna w stosunku do dzisiejszych najlepszych praktyk. Jako takie jest tutaj zastosowanie PBKDF2 / Rfc2898DeriveBytesdo 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ść 100000można zmniejszyć. Minimalna wartość powinna wynosić około 10000.


8
@Daniel w zasadzie post dotyczy używania czegoś bezpieczniejszego niż sam hash. Jeśli po prostu zaszyfrujesz hasło, nawet z solą, hasła użytkowników zostaną naruszone (i prawdopodobnie sprzedane / opublikowane), zanim będziesz miał szansę powiedzieć im, aby je zmienili. Użyj powyższego kodu, aby utrudnić atak napastnikowi, a nie jest to łatwe dla programisty.
csharptest.net

2
@DatVM Nie, nowa sól za każdym razem, gdy przechowujesz hash. dlatego jest połączony z hashem do przechowywania, abyś mógł zweryfikować hasło.
csharptest.net

9
@CiprianJijie chodzi o to, że nie powinieneś być w stanie.
csharptest.net

9
W przypadku, gdy ktoś wykonuje metodę VerifyPassword, jeśli chciałbyś użyć Linq i krótszego wywołania wartości logicznej, zrobiłoby to: return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo

2
@ csharptest.net Jakie rodzaje rozmiarów tablic są zalecane? czy rozmiar tablicy i tak ma duży wpływ na bezpieczeństwo? Nie wiem zbyt wiele o haszowaniu / kryptografii
lennyy

71

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 .


1
Tylko po to, aby sprawdzić, czy jeśli zaktualizujesz silnik mieszający, zwiększysz sekcję V1 swojego skrótu i ​​wyłączysz z niego klucz?
Mike Cole

1
Tak, taki jest plan. Następnie zdecydowałbyś na podstawie V1i V2jakiej metody weryfikacji potrzebujesz.
Christian Gollhardt

Dzięki za odpowiedź i klasę. Wdrażam to tak, jak mówimy.
Mike Cole

2
Tak @NelsonSilva. To z powodu soli .
Christian Gollhardt

1
Mam nadzieję, że po całym kopiowaniu / wklejaniu tego kodu (w tym mnie) ktoś się odezwie i post zostanie poprawiony, jeśli zostanie znaleziony problem! :)
pettys

14

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

16
-1 do używania zwykłego SHA-1, który jest szybki. Użyj powolnej funkcji wyprowadzania klucza, takiej jak PBKDF2, bcrypt lub scrypt.
CodesInChaos

1
  1. Stwórz sól,
  2. Utwórz hasło mieszające za pomocą soli
  3. Oszczędzaj haszysz i sól
  4. odszyfruj za pomocą hasła i soli ... więc programiści nie mogą odszyfrować hasła
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

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

0

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”.


1
BTW, poprzednia odpowiedź stackoverflow.com/a/57508528/11603057 nie jest poprawna i szkodliwa. To jest przykład haszowania, a nie haszowania hasła. Muszą być iteracje funkcji pseudolosowej podczas procesu wyprowadzania klucza. Nie ma. Nie mogę tego skomentować ani zagłosować (moja niska reputacja). Nie przegap niepoprawnych odpowiedzi!
Albert Lyubarsky
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.