Как хешировать пароль


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

300

Большинство других ответов здесь несколько устарели с сегодняшней передовой практикой. Таким образом, вот приложение использования PBKDF2 / Rfc2898DeriveBytesдля хранения и проверки паролей. Следующий код находится в отдельном классе в этом посте: Еще один пример того, как хранить соленый хэш пароля . Основы действительно просты, поэтому вот они:

ШАГ 1 Создайте значение соли с криптографическим ГПСЧ:

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 и более короткий вызов для логического значения, это должно сделать: return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Хесу Кастильо

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

72

Основываясь на отличном ответе 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, у меня также есть версия ядра .net на Code Review .


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

1
Да, это план. Вы бы затем решить , на основе V1и V2какой метод вам нужно подтверждение.
Кристиан Голлхардт

Спасибо за ответ и класс. Я реализую это, как мы говорим.
Майк Коул

2
Да @NelsonSilva. Это из-за соли .
Кристиан Голлхардт

1
Со всем копированием / вставкой этого кода (включая меня), я надеюсь, что кто-то выскажется, и сообщение будет исправлено, если с ним будет обнаружена проблема! :)
pettys

14

Я использую хеш и соль для шифрования моего пароля (это тот же хеш, что и в Asp.Net Membership):

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

2
  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

Я думаю, что использование KeyDerivation.Pbkdf2 лучше, чем Rfc2898DeriveBytes.

Пример и объяснение: хеш-пароли в 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 неверен и вреден. Это пример хеширования, а не хеширования пароля. Должны быть итерациями псевдослучайной функции в процессе получения ключа. Здесь нет. Я не могу это комментировать или отрицать (моя низкая репутация). Пожалуйста, не пропустите неверные ответы!
Альберт Любарский

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);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.