Як хеш-пароль


117

Я хотів би зберегти хеш пароля на телефоні, але я не впевнений, як це зробити. Я можу лише знайти способи шифрування. Як слід правильно розмістити пароль?

Відповіді:


62

ОНОВЛЕННЯ : ЦЕ ВІДПОВІДЬ СЕРІЙНО ВІДБУДЕНО . Будь ласка, використовуйте замість цього рекомендації https://stackoverflow.com/a/10402129/251311 .

Можна або використовувати

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

або

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

Щоб отримати dataяк байтовий масив, який ви могли використовувати

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

і повернути рядок з md5dataабоsha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
Я б дійсно рекомендував використовувати SHA1. MD5 - це ні-ні, якщо ви не підтримуєте сумісність із існуючою системою. Крім того, переконайтеся, що ви помістили його у usingвиписку або зателефонували Clear()до неї, коли закінчите за допомогою реалізації.
vcsjones

3
@vcsjones: Я не хочу тут священної війни, але md5достатньо хороший для майже всіх завдань. Його вразливість також стосується дуже конкретних ситуацій і майже вимагає від зловмисника багато знати про криптографію.
zerkms

4
@zerkms прийнято, але якщо немає причин для зворотної сумісності, немає ніяких причин використовувати MD5. "Краще перестрахуватися, ніж потім шкодувати".
vcsjones

4
Немає підстав використовувати MD5 в цей момент. Зважаючи на те, що час обчислень незначний, немає ніяких підстав використовувати MD5, крім сумісності з існуючими системами. Навіть якщо MD5 "досить хороший", з користувачем набагато безпечніше SHA не коштує. Я впевнений, що zerkms знають, що це коментар більше для запитувача.
Джеральд Девіс

11
Три великі помилки: 1) ASCII мовчки принижує паролі з незвичайними символами 2) Звичайна MD5 / SHA-1 / SHA-2 швидко. 3) Вам потрібна сіль. | Замість цього використовуйте PBKDF2, bcrypt або scrypt. PBKDF2 найпростіший у класі Rfc2898DeriveBytes (не впевнений, чи він присутній на WP7)
CodesInChaos

299

Більшість інших відповідей тут дещо застаріли з найкращими практиками сьогодні. Таким чином, тут використовується програма використання PBKDF2 / Rfc2898DeriveBytesдля зберігання та перевірки паролів. Наступний код є автономним класом у цій публікації: Ще один приклад того, як зберігати солоний хеш пароля . Основи дійсно прості, тому тут вони розбиті:

КРОК 1 Створіть значення солі за допомогою криптографічного PRNG:

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

КРОК 2 Створіть Rfc2898DeriveBytes і отримайте хеш-значення:

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

КРОК 3 Комбінуйте байт солі та пароля для подальшого використання:

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

КРОК 4 Перетворіть комбіновану сіль + хеш на рядок для зберігання

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

КРОК 5 Перевірте введений користувачем пароль проти збереженого пароля

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

Примітка. Залежно від вимог щодо продуктивності вашої конкретної програми значення 100000можна зменшити. Мінімальне значення має бути близько 10000.


8
@Daniel в основному повідомлення про використання чогось більш безпечного, ніж хеш-поодинці. Якщо ви просто хеш-пароль, навіть з сіллю, паролі ваших користувачів будуть порушені (і, ймовірно, продані / опубліковані), перш ніж у вас навіть з’явиться шанс сказати їм змінити його. Використовуйте наведений вище код, щоб зробити це важким для зловмисника, а не просто для розробника.
csharptest.net

2
@DatVM Ні, нова сіль щоразу, коли ви зберігаєте хеш. тому він поєднується з хешем для зберігання, щоб ви могли перевірити пароль.
csharptest.net

9
@CiprianJijie вся справа у тому, що ти не можеш цього робити.
csharptest.net

9
У випадку, якщо хтось виконує метод VerifyPassword, якщо ви хочете використовувати Linq і коротший виклик для булевого, це зробить: поверніть hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo

2
@ csharptest.net Які розміри масивів ви рекомендуєте? чи впливає розмір масиву набагато сильніше на безпеку? Я не так багато знаю про хешування / криптографію
lennyy

71

На основі чудової відповіді csharptest.net я написав для цього клас:

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

Використання:

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

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

Зразковий хеш може бути таким:

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

Як бачите, я також включив ітерації в хеш для простого використання та можливість оновити це, якщо нам потрібно оновити.


Якщо вас цікавить .net core, у мене також є версія .net core на Code Review .


1
Просто для того, щоб переконатися, що якщо ви оновите хешируючий двигун, ви збільшили б розділ V1 свого хеша і відключили його?
Майк Коул

1
Так, це план. Ви б потім вирішити , на основі V1і V2який метод вам потрібно підтвердження.
Крістіан Голхардт

Дякуємо за відповідь та клас. Я реалізую це, як ми говоримо.
Майк Коул

2
Так @NelsonSilva. Це через сіль .
Крістіан Голхардт

1
З усією копією / вставкою цього коду (включаючи мене), я сподіваюся, що хтось заговорить, і публікація буде переглянута, якщо з цим буде виявлена ​​проблема! :)
pettys

14

Я використовую хеш і сіль для шифрування пароля (це той самий хеш, який використовує членство 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 для використання простого SHA-1, який швидко. Використовуйте функцію виведення повільного ключа, наприклад PBKDF2, bcrypt або scrypt.
CodesInChaos

1
  1. Створіть сіль,
  2. Створіть хеш-пароль із сіллю
  3. Збережіть і хеш, і сіль
  4. розшифрувати паролем і сіллю ... тому розробники не можуть розшифрувати пароль
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

Відповіді @ csharptest.net та Крістіана Голхардта чудові, дуже дякую. Але після запуску цього коду на виробництві з мільйонами записів я виявив, що відбувається витік пам'яті. Класи RNGCryptoServiceProvider та Rfc2898DeriveBytes є похідними від IDisposable, але ми не розпоряджаємось ними. Я напишу своє рішення як відповідь, якщо комусь потрібно з розміщеною версією.

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

    }
}

Використання:

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

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

0

Я думаю, що використання KeyDerivation.Pbkdf2 краще, ніж Rfc2898DeriveBytes.

Приклад та пояснення: Hash паролі в 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=
 */

Це зразок коду зі статті. І це мінімальний рівень безпеки. Для його збільшення я б використав замість параметра KeyDerivationPrf.HMACSHA1

KeyDerivationPrf.HMACSHA256 або KeyDerivationPrf.HMACSHA512.

Не робіть компромісів щодо хешування паролів. Існує багато математично обгрунтованих методів оптимізації злому хешу паролів. Наслідки можуть бути згубними. Як тільки зловмисник зможе отримати свою таблицю хешування паролів ваших користувачів, йому буде відносно легко зламати паролі, заданий алгоритм слабкий або впровадження невірно. У нього є багато часу (час х комп'ютерної потужності), щоб зламати паролі. Хешування паролів має бути криптографічно сильним, щоб перетворити "багато часу" на " необгрунтовану кількість часу ".

Ще один момент, який потрібно додати

Перевірка хешу потребує часу (і це добре). Коли користувач вводить неправильне ім’я користувача, не потрібно проводити час, щоб перевірити, чи є ім'я користувача неправильним. Коли ім’я користувача правильне, ми починаємо перевірку пароля - це процес досить тривалий.

Для хакера було б дуже легко зрозуміти, чи існує користувач чи ні.

Переконайтеся, що не повертайте негайну відповідь, коли ім’я користувача невірно.

Зайве говорити: ніколи не давай відповіді, що не так. Просто загальне "Посвідчення помиляються".


1
До речі, попередня відповідь stackoverflow.com/a/57508528/11603057 є невірною та шкідливою. Це приклад хешування, а не хешування паролів. Повинно бути ітерацією псевдовипадкової функції під час процесу виведення ключів. Немає. Я не можу коментувати це або знижувати (моя низька репутація). Будь ласка, не пропустіть невірні відповіді!
Альберт Любарський
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.