Хэш-пароль пароля по умолчанию для ASP.NET Identity - как он работает и является ли он безопасным?

163

Мне интересно, достаточно ли безопасен пароль хэшер, который по умолчанию реализован в UserManager, который поставляется с MVC 5 и ASP.NET Identity Framework? И если да, то не могли бы вы объяснить мне, как это работает?

Интерфейс IPasswordHasher выглядит так:

public interface IPasswordHasher
{
    string HashPassword(string password);
    PasswordVerificationResult VerifyHashedPassword(string hashedPassword, 
                                                       string providedPassword);
}

Как вы можете видеть, это не требует соли, но упоминается в этой теме: « Хэширование идентификационного пароля Asp.net », что оно действительно замаскирует его за кулисами. Поэтому мне интересно, как это сделать? И откуда эта соль?

Меня беспокоит то, что соль статична, что делает ее небезопасной.

Андре Снед Кок
источник
Я не думаю, что это прямо отвечает на ваш вопрос, но Брок Аллен написал здесь о некоторых ваших проблемах => brockallen.com/2013/10/20/…, а также написал библиотеку управления и аутентификации пользователей с открытым исходным кодом, которая имеет различные такие функции, как сброс пароля, хеширование и т. д. github.com/brockallen/BrockAllen.MembershipReboot
Shiva
@Shiva Спасибо, я посмотрю в библиотеке и видео на странице. Но я бы предпочел не иметь дело с внешней библиотекой. Нет, если я смогу избежать этого.
Андре Снеде Кок
2
К вашему сведению: эквивалент переполнения стека для безопасности. Поэтому, хотя вы часто получите хороший / правильный ответ здесь. Эксперты на security.stackexchange.com, особенно комментируют: «Безопасно ли?» Я задал подобный вопрос, а глубина и качество ответа были удивительными.
Фил Соади
@philsoady Спасибо, это имеет смысл, конечно, я уже на нескольких других "подфорумах", если я не получу ответ, я могу использовать, я перейду к securiry.stackexchange.com. И спасибо за совет!
Андре Снед Кок

Ответы:

227

Вот как работает реализация по умолчанию ( ASP.NET Framework или ASP.NET Core ). Для получения хэша используется функция получения ключа со случайной солью. Соль включена в состав продукции KDF. Таким образом, каждый раз, когда вы «хэшируете» один и тот же пароль, вы получаете разные хэши. Чтобы проверить хеш, выходные данные разделяются на соль и остальные, и KDF снова запускается на пароле с указанной солью. Если результат соответствует остальной части исходного вывода, хеш проверяется.

Хэш:

public static string HashPassword(string password)
{
    byte[] salt;
    byte[] buffer2;
    if (password == null)
    {
        throw new ArgumentNullException("password");
    }
    using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
    {
        salt = bytes.Salt;
        buffer2 = bytes.GetBytes(0x20);
    }
    byte[] dst = new byte[0x31];
    Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
    Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
    return Convert.ToBase64String(dst);
}

Проверка:

public static bool VerifyHashedPassword(string hashedPassword, string password)
{
    byte[] buffer4;
    if (hashedPassword == null)
    {
        return false;
    }
    if (password == null)
    {
        throw new ArgumentNullException("password");
    }
    byte[] src = Convert.FromBase64String(hashedPassword);
    if ((src.Length != 0x31) || (src[0] != 0))
    {
        return false;
    }
    byte[] dst = new byte[0x10];
    Buffer.BlockCopy(src, 1, dst, 0, 0x10);
    byte[] buffer3 = new byte[0x20];
    Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
    using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8))
    {
        buffer4 = bytes.GetBytes(0x20);
    }
    return ByteArraysEqual(buffer3, buffer4);
}
Андрей Савиных
источник
7
Так что, если я правильно понимаю, HashPasswordфункция возвращает оба в одной строке? И когда вы проверяете это, он снова разделяет его, снова хэширует и хэширует входящий пароль открытого текста вместе с солью из разбиения и сравнивает его с исходным хешем?
Андре Снед Кок
9
@ AndréSnedeHansen, точно. И я тоже рекомендую вам спрашивать либо о безопасности, либо о криптографии SE. Часть «это безопасно» может быть адресована лучше в этих соответствующих контекстах.
Андрей Савиных
1
@shajeerpuzhakkal, как описано в ответе выше.
Андрей Савиных
3
@AndrewSavinykh Я знаю, поэтому я спрашиваю - какой смысл? Чтобы код выглядел умнее? ;) Потому что подсчет вещей с использованием десятичных чисел НАМНОГО более интуитивен (в конце концов, у нас 10 пальцев - по крайней мере, у большинства из нас), поэтому объявление числа с помощью шестнадцатеричных чисел кажется ненужным запутыванием кода.
Эндрю Сайрул
1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- это то, что вам нужно сделать. после этого resultсодержит истину.
Андрей Савиных
43

Поскольку в наши дни ASP.NET является открытым исходным кодом, вы можете найти его на GitHub: AspNet.Identity 3.0 и AspNet.Identity 2.0 .

Из комментариев:

/* =======================
 * HASHED PASSWORD FORMATS
 * =======================
 * 
 * Version 2:
 * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
 * (See also: SDL crypto guidelines v5.1, Part III)
 * Format: { 0x00, salt, subkey }
 *
 * Version 3:
 * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
 * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
 * (All UInt32s are stored big-endian.)
 */
Knelis
источник
Да, и стоит отметить, что есть дополнения к алгоритму, который показывает zespri.
Андре Снеде Коцк
1
Источник на GitHub - Asp.Net.Identity 3.0, который все еще находится в предварительном выпуске. Источник хэш-функции 2.0 находится на CodePlex
Дэвид
1
Новейшую реализацию можно найти по адресу github.com/dotnet/aspnetcore/blob/master/src/Identity/… сейчас. Они заархивировали другой репозиторий;)
FranzHuber23
32

Я понимаю принятый ответ и проголосовал за него, но подумал, что дам ответ моего мирянина здесь ...

Создание хэша

  1. Соль генерируется случайным образом с помощью функции Rfc2898DeriveBytes, которая генерирует хэш и соль. Входными данными для Rfc2898DeriveBytes являются пароль, размер создаваемой соли и количество итераций хеширования, которые необходимо выполнить. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. Затем соль и хеш смешиваются (сначала соль, затем хеш) и кодируются в виде строки (поэтому соль кодируется в хеше). Этот закодированный хэш (который содержит соль и хэш) затем сохраняется (обычно) в базе данных против пользователя.

Проверка пароля по хешу

Чтобы проверить пароль, который вводит пользователь.

  1. Соль извлекается из сохраненного хешированного пароля.
  2. Соль используется для хеширования введенного пользователем пароля с использованием перегрузки Rfc2898DeriveBytes, которая принимает соль вместо ее генерации. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. Затем сохраненный хэш и тестовый хеш сравниваются.

Хеш

Под покровом хеш генерируется с использованием хеш-функции SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Эта функция итеративно вызывается 1000 раз (в стандартной реализации Identity)

Почему это безопасно

  • Случайные соли означают, что злоумышленник не может использовать предварительно сгенерированную таблицу хэшей, чтобы попытаться взломать пароли. Им нужно будет создать хеш-таблицу для каждой соли. (Предполагая, что хакер также скомпрометировал вашу соль)
  • Если 2 пароля идентичны, они будут иметь разные хэши. (то есть злоумышленники не могут определить «общие» пароли)
  • Итеративный вызов SHA1 1000 раз означает, что злоумышленник также должен сделать это. Идея состоит в том, что, если у них нет времени на суперкомпьютер, у них не будет достаточно ресурсов для взлома пароля из хэша. Это значительно замедлит время создания хеш-таблицы для данной соли.
Наттрасс
источник
Спасибо за ваше объяснение. В разделе «Создание хеша 2». Вы упоминаете, что соль и хеш смешиваются вместе, знаете ли вы, хранится ли это в PasswordHash в таблице AspNetUsers. Соль хранится где-нибудь для меня, чтобы увидеть?
unicorn2
1
@ unicorn2 Если вы посмотрите на ответ Андрея Савиных ... В разделе о хешировании похоже, что соль хранится в первых 16 байтах массива байтов, который закодирован в Base64 и записан в базу данных. Вы сможете увидеть эту строку в кодировке Base64 в таблице PasswordHash. Все, что вы можете сказать о строке Base64, это то, что примерно первая треть ее - это соль. Значимая соль - это первые 16 байтов декодированной версии Base64 полной строки, хранящейся в таблице PasswordHash
Nattrass
@Nattrass, мое понимание хэшей и солей довольно элементарно, но если соль легко извлекается из хешированного пароля, то какой смысл солить в первую очередь. Я думал, что соль должна была быть дополнительным входом в алгоритм хэширования, который не мог быть легко угадан.
Южный
1
@NSouth Уникальная соль делает хэш уникальным для данного пароля. Таким образом, два одинаковых пароля будут иметь разные хеш-коды. Доступ к вашему хешу и соли все еще не заставит злоумышленника запомнить ваш пароль. Хеш не обратим. Они все равно должны будут перебирать все возможные пароли. Уникальная соль просто означает, что хакер не может определить общие пароли, выполняя частотный анализ конкретных хешей, если им удалось получить всю вашу пользовательскую таблицу.
Наттрасс
8

Для таких, как я, новичок в этом, вот код с const и реальный способ сравнить байты []. Я получил весь этот код из stackoverflow, но определил константы, чтобы значения могли быть изменены, а также

// 24 = 192 bits
    private const int SaltByteSize = 24;
    private const int HashByteSize = 24;
    private const int HasingIterationsCount = 10101;


    public static string HashPassword(string password)
    {
        // http://stackoverflow.com/questions/19957176/asp-net-identity-password-hashing

        byte[] salt;
        byte[] buffer2;
        if (password == null)
        {
            throw new ArgumentNullException("password");
        }
        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, SaltByteSize, HasingIterationsCount))
        {
            salt = bytes.Salt;
            buffer2 = bytes.GetBytes(HashByteSize);
        }
        byte[] dst = new byte[(SaltByteSize + HashByteSize) + 1];
        Buffer.BlockCopy(salt, 0, dst, 1, SaltByteSize);
        Buffer.BlockCopy(buffer2, 0, dst, SaltByteSize + 1, HashByteSize);
        return Convert.ToBase64String(dst);
    }

    public static bool VerifyHashedPassword(string hashedPassword, string password)
    {
        byte[] _passwordHashBytes;

        int _arrayLen = (SaltByteSize + HashByteSize) + 1;

        if (hashedPassword == null)
        {
            return false;
        }

        if (password == null)
        {
            throw new ArgumentNullException("password");
        }

        byte[] src = Convert.FromBase64String(hashedPassword);

        if ((src.Length != _arrayLen) || (src[0] != 0))
        {
            return false;
        }

        byte[] _currentSaltBytes = new byte[SaltByteSize];
        Buffer.BlockCopy(src, 1, _currentSaltBytes, 0, SaltByteSize);

        byte[] _currentHashBytes = new byte[HashByteSize];
        Buffer.BlockCopy(src, SaltByteSize + 1, _currentHashBytes, 0, HashByteSize);

        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, _currentSaltBytes, HasingIterationsCount))
        {
            _passwordHashBytes = bytes.GetBytes(SaltByteSize);
        }

        return AreHashesEqual(_currentHashBytes, _passwordHashBytes);

    }

    private static bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
    {
        int _minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
        var xor = firstHash.Length ^ secondHash.Length;
        for (int i = 0; i < _minHashLength; i++)
            xor |= firstHash[i] ^ secondHash[i];
        return 0 == xor;
    }

В своем пользовательском ApplicationUserManager вы задаете свойству PasswordHasher имя класса, который содержит приведенный выше код.

kfrosty
источник
Для этого .. _passwordHashBytes = bytes.GetBytes(SaltByteSize); Я думаю, вы имели в виду это _passwordHashBytes = bytes.GetBytes(HashByteSize);.. Не имеет значения в вашем сценарии, поскольку оба имеют одинаковый размер, но в целом ..
Акшата