Как правильно обрабатывать пароли в С#

Хорошо известно, что С# string довольно небезопасен, он не закреплен в ОЗУ, сборщик мусора может перемещать его, копировать, оставлять несколько следов в ОЗУ, а ОЗУ можно поменять местами и быть доступным как файл, который нужно прочитать, не говоря уже о нескольких других известных фактах.

Чтобы смягчить эту проблему, Microsoft придумала SecureString. Дело в том, что какой способ его использовать?

Я спрашиваю об этом, потому что рано или поздно вам придется извлечь внутреннюю строку. И да, SecureString позволяет нам писать один char за один раз и дает нам некоторые другие незначительные обычаи, скрывая его в секрете, но иногда нам нужна целая строка сразу или что-то в этом роде. До сих пор все реализации, которые я вижу, используют в конце концов регулярный string, с извлеченным и расшифрованным контентом из SecureString, который, как я думаю (или надеюсь), можно избежать в некоторых случаях, даже если он может включать в себя сложный код .NET string.

В моем случае я использую библиотеку для хэширования паролей под названием BCrypt. Он использует обычный string для вычисления хэшей, который, как я думаю, далек от идеала. Я не нашел никаких библиотек, которые бы принимали SecureString напрямую, поэтому у меня нет большого выбора, но используйте его.

В настоящее время я запускаю код, например:

SecureString password; // This usually comes from a PasswordBox Property
IntPtr passwordBSTR = Marshal.SecureStringToBSTR(password);

string insecurePassword = Marshal.PtrToStringBSTR(passwordBSTR);
Marshal.ZeroFreeBSTR(passwordBSTR);
passwordBSTR = default(IntPtr);
password.Clear();

// Use the insecurePassword....usually as:
bool result = BCrypt.Net.BCrypt.Verify(insecurePassword, user.Password); // This hashes the provided password and compares it with another BCrypt hash.

insecureString = string.Empty; // hoping for the GC to act now, fingers crossed.

// use result and etc...

Таким образом, пароль raw string будет скопирован несколько раз для метода BCrypt и, возможно, еще больше времени внутри него.

Это вызывает у меня некоторые вопросы:

1 - Есть строки BSTR, такие как строки C в смысле управления памятью?. Они прикреплены в ОЗУ, и я могу ими манипулировать и стирать их, как я считаю нужным, так что это будет более безопасно использовать их на С# или переписывать с помощью кода на С++, чтобы я мог устранить многие мои трюки .NET string (например, проверить, является ли строка пароля нулевой или пробельной или имеет правильную длину или имеет желаемый пароль прочность)?

2 - Если вопрос № 1 ответ "да", тогда следует потратить некоторое время на то, чтобы передать строку BSTR на код взаимодействия С++ для вычисления хеша и проверить?

3 - Является ли мой более поздний код правильным или я что-то упустил при манипуляции SecureString?

Только для того, чтобы быть ясным: Мое основное намерение с этим вопросом - оценить, должен ли я передать весь код обработки пароля на С++ из моего С# (возможно, используя С++/CLI или interop) для достижения более безопасный код, чем предлагает С#. Этот сценарий действительно касается меня для хэширования строки пароля, поскольку будут созданы и скопированы несколько строк с секретной информацией пользователя, разбросанной по всей ОЗУ.

В качестве примера: я мог бы сделать такой метод, как:

public string SecureHash(SecureString password)

И этот метод будет извлекать значение SecureString и передавать его как строку BSTR (или другую опцию извлечения, я, честно говоря, не знаю преимуществ между ними), методу хэширования С++, поэтому мы можем контролировать все места, где наш пароль находится в памяти.

Я прошу, чтобы у меня не было огромной работы по рефакторингу моего кода, и в конце концов выяснилось, что все это было ни к чему, потому что я использовал SecureStrings неправильно все время, и есть уже решения для этого, или ложные как это бывает иногда, поскольку я не знаю, как строки BSTR реализованы в памяти или другие SecureStrings параметры извлечения (Unicode, ANSI и т.д.).

Ответ 1

  • Marshal.SecureStringToBSTR() создаст строку BSTR (т.е. массив байтов) в неуправляемой памяти и вернет указатель на нее; это означает, что базовое значение привязано. Само значение представляет собой последовательность символов Юникода, представляющую вашу строку, с префиксом длины строки и последующие два последующих нулевых символа (https://msdn.microsoft.com/en-us/library/windows/desktop/ms221069(v=vs.85).aspx). С другой стороны, Marshal.PtrToStringBSTR() вернет объект .Net System.String, который является небезопасным.
  • Я бы сказал, что ваши варианты:

    • Преобразуйте объект SecureString в неуправляемый объект, а затем перейдите к любому неуправляемая библиотека, которая вычисляет хэши
    • Используйте SecureString повсюду в вашем стеке, в вашем случае это означает переписать BCrypt.Net для работы с объектами SecureString (библиотека с открытым исходным кодом и очень маленькая: https://bcrypt.codeplex.com/SourceControl/latest#BCrypt.Net/BCrypt.cs) или найти альтернативную управляемую реализацию. Но так как SecureString не предоставляет методы для чтения своих данных, вам в любом случае нужно будет преобразовать его в, по всей видимости, неуправляемую строку в любом случае (вы можете использовать эти неуправляемые данные из управляемого кода).
  • Вам не хватает правильного удаления SecureString (либо внутри "using", либо try/finally).

Ответ 2

Почему бы не использовать однонаправленную крипту (HASH)? Сохраните случайную соль с зашифрованным паролем и нажмите на это значение 2. Таким образом, вы можете склеить пароль, который хотите проверить, с той же солью и сравнить зашифрованные значения. Я бы рекомендовал SHA-256 или сильнее.

Что касается безопасности, это почти тот же метод, который использует Microsoft в членстве С# AspNet. Поскольку это довольно стандартный отраслевой стандарт с Dot Net, этот уровень безопасности также должен работать и на вас.

Вот хороший источник, чтобы посмотреть, как это сделать: http://www.obviex.com/samples/hash.aspx

Ответ 3

Как насчет метода расширения для SecureString:

using System;
using System.Runtime.InteropServices;

public static class SecureStringExtensions 
{
    public static T Decrypt<T>(this SecureString ss, Func<string, T> handler) 
    {
        if (handler == null)
        {
            throw new ArgumentNullException(nameof(handler));
        }

        var ptr = Marshal.SecureStringToBSTR(ss);
        try {
            return handler(Marshal.PtrToStringBSTR(ptr));
        } finally {
            Marshal.ZeroFreeBSTR(ptr);
        }
    }
}

Таким образом, дешифрованная строка остается в стеке и быстро перезаписывается будущими переменными стека.