Правильное хеширование паролей

хеширование паролей

Наверняка вам известно, что хорошая система контроля доступа, основанная на вводе и проверке правильности пароля, никогда и нигде не сохраняет пароли в открытом виде, а проверяет введенный пользователем пароль с использованием хеш-суммы этого пароля. А очень хорошие системы еще и добавляют к ним «соль» — случайную строку, которая для каждого пользователя уникальна. В этой статье мы на практике рассмотрим вопросы правильного хеширования паролей, руководствуясь при этом актуальными российскими методическими рекомендациями.
Как выглядят записи в базе данных пользователей «хороших систем контроля доступа»? Примерно так (здесь видны имя учетной записи пользователя, значение соли и значение хеша):

Таким образом, на основе введенного пользователем пароля и соответствующего ему значения соли с помощью того или иного алгоритма вычисляется хеш. Далее он сравнивается с записанным в базе: если они совпадают, пользователь допускается в систему, если не совпадают, пользователю в допуске будет отказано. Все, в общем-то, совсем не сложно. Главный вопрос заключается в том, каким образом и каким алгоритмом считать этот самый хеш из значений введенного пароля и соли. Если как следует порыться в весьма объемном ворохе отечественных нормативно-методических документов, посвященных криптографии, то можно обнаружить документ, который поможет нам дать ответ на этот вопрос.

Документ называется «Рекомендации по стандартизации Р 50.1.111—2016. Информационная технология. Криптографическая защита информации. Парольная защита ключевой информации». Он разработан техническим комитетом по стандартизации ТК 26 «Криптографическая защита информации» и представляет собой расширение международного стандарта PKCS #5 Version 2.0 (Public Key Cryptography Standart: Password-Based Cryptography Specification): в процедуру хеширования, описанную в PKCS #5, внесена возможность использовать алгоритм из ГОСТ Р 34.11—2012 (да-да, это тот самый «Стрибог» — похоже, нынче без него никуда).

Общая схема и исходные данные

Основу алгоритма получения хеша составляет так называемая функция диверсификации PBKDF версии 2.0 (Password-Based Key Derivation Function). Данная функция реализуется путем применения псевдослучайной хеш-функции к строке, в нашем случае к паролю, вместе с солью, процесс повторяется большое число раз.

хеширование паролей
Общая схема выработки нужного хеша

В качестве псевдослучайной хеш-функции мы будем использовать функцию вычисления аутентификационного кода сообщения HMAC_GOST3411 (Hash-based Message Authentication Code) на основе, как ты уже догадался, хеш-функции «Стрибог». Исходные данные для алгоритма:

  • введенный пользователем пароль (длина не более 512 бит, или 64 байт);
  • значение соли (произвольной длины);
  • число итераций (минимально допустимое значение — 1000, максимально допустимое — 4 294 967 295);
  • необходимая длина вычисляемого хеша.

Псевдослучайная хеш-функция HMAC_GOST3411

Сама функция HMAC_GOST3411 описана в другом нормативно-методическом документе Р 50.1.113—2016 и включает в себя следующие этапы:

  • дополнение нулями введенного пароля до длины в 64 байт (конечно, в том случае, если его длина меньше этого значения);
  • побайтовое сложение по модулю 2 получившегося на предыдущем этапе дополненного пароля с 64-байтовой константой ipad, в которой каждый байт равен 0x36;
  • конкатенация получившегося на предыдущем этапе значения с солью и подсчет «стрибог»-хеша из полученной строки;
  • конкатенация результата побайтового xor дополненного пароля с 64-байтовой константой opad (значение каждого байта равно 0x5c) с получившимся на предыдущем этапе результатом;
  • подсчет «стрибог»-хеша из результата предыдущего этапа.
хеширование паролей
Схема функции HMAC_GOST3411

Перед тем как писать код самой функции HMAC_GOST3411, необходимо определить нужные константы ipad и opad и написать функцию подсчета «стрибог»-хеша байтового массива произвольной длины.

Определение констант

Поскольку в ходе подсчета значений хеш-сумм мы оперируем 64-байтовыми блоками, то зададим размер этого блока:

Константу ipad определим таким образом:

а константу opad таким:

Для экономии места здесь константы описаны не полностью, на самом деле каждая из них длиной по 64 байт.

Функция подсчета «стрибог»-хеша

Для начала скачаем и подключим нужные файлы, в которых реализованы базовые функции алгоритма «Стрибог»:

Далее напишем саму функцию. На вход функции подается указатель на строку с исходными данными, указатель на массив, куда будет записан искомый хеш, длина искомого хеша (512 или 256 бит) и длина массива исходных данных:

Определяем структуру CTX для хранения всего, что нужно при подсчете хеша, и выделяем для нее память:

Создаем промежуточный буфер и записываем в него исходную строку:

Считаем хеш-сумму и пишем результат в выходной байтовый массив:

Пишем непосредственно саму функцию HMAC_GOSTR3411

Объявим эту функцию таким образом:

На вход идут: указатель на байтовый массив с паролем, длина пароля, указатель на байтовый массив с солью, длина соли и указатель на байтовый массив, куда будем записывать результат вычислений.

Далее произведем дополнение пароля нулями и поксорим результат дополнения с ipad:

Присоединим к полученному соль и вычислим значение «стрибог»-хеша полученного массива:

Поксорим дополненный пароль с opad, соединим с хешем, вычисленным на предыдущем шаге, и второй раз посчитаем «стрибог»-хеш. Результат запишем в HMAC:

Основа функции PBKDF в виде HMAC_GOSTR3411 написана, можно приступать к реализации непосредственно самой PBKDF.

Password-Based Key Derivation Function

Первым делом необходимо определить количество 64-байтовых блоков в искомом хеше, для чего установленную ранее длину нужного нам хеша от пароля и соли делим на 64 и округляем результат до ближайшего целого в большую сторону (к примеру, если требуемая длина искомого хеша равна 100 байт, то количество блоков будет равно двум, в стандарте про округление в большую сторону напрямую не сказано, однако это надо иметь в виду). Количество блоков искомого хеша определяет число циклов, каждый из которых включает в себя некоторое число итераций C (об этом числе мы уже говорили выше).

Результатом первой итерации будет результат вычисления HMAC_GOSTR3411 от пароля и значения соли с присоединенным к нему значением текущего номера цикла в байтовом представлении (напомню, номер цикла изменяется в пределах от единицы до N, где N — число блоков в искомом хеше). Результатом последующих итераций будет результат вычисления HMAC_GOSTR3411 от пароля и значения HMAC_GOSTR3411, полученного на предыдущей итерации. Далее результаты каждой итерации побайтно ксорятся между собой, и в итоге мы получаем результат вычислений одного цикла T(i), где i лежит в пределах от единицы до N (число блоков искомого хеша).

хеширование паролей
Схема i-го цикла функции PBKDF (в данном случае i = 1)

Функция вычисления значения первой итерации для [crayon-662c04084af0d298526000-i/]-го цикла

Объявим функцию так:

На вход подаем указатель на байтовый массив с паролем, размер этого пароля, указатель на массив с солью, размер соли, номер цикла i, а также указатель на байтовый массив для записи результата. Далее соль и значение номера цикла (в четырехбайтовом представлении) сливаем в один массив и считаем HMAC_GOSTR3411 от значений пароля и объединенного массива с солью и номером текущего цикла:

Функция вычисления значения последующих итераций для [crayon-662c04084af12850847328-i/]-го цикла

Объявление данной функции выглядит следующим образом:

На входе у нас: значение пароля (вернее, указатель на байтовый массив с этим значением), длина пароля, указатель на массив с результатом предыдущей итерации и указатель на байтовый массив для записи результата работы функции.

В тело самой функции поместим всего одну строку (что она делает, думаю, понятно):

Ксорим результаты всех итераций

Для этого действия объявим функцию F:

На вход функции подаем указатель на массив с паролем, длину пароля, указатель на соль, длину соли, число итераций, номер текущего блока (или номер текущего цикла) и указатель на место, куда будем писать результат. Далее напишем саму функцию:

Итак, все для того, чтобы проделать один цикл расчета хеша, у нас есть. Осталось разобрать, что делать с полученными в каждом цикле результатами расчета и как из них получить искомый хеш.

Собираем результаты расчетов каждого цикла и определяем окончательный хеш

После прохода всех циклов мы получим множество значений вида {T(1), T(2), ..., T(N)}, каждое из которых представляет собой результат вычислений одного из циклов. То есть T(1) — результат вычислений первого цикла, T(2) — второго и так далее. Число же циклов, напомню, у нас определяется числом 64-байтовых блоков в искомом хеше. Для того чтобы из полученного множества определить нужное нам значение хеша, необходимо просто выполнить конкатенацию всех значений T из полученного множества (привести все эти значения к одному массиву) и отделить от получившегося массива столько байтов, какую длину искомого хеша нам надо получить (то есть произвести усечение полученного массива справа на лишнее число байтов).

алгоритмы хеширования паролей
Конкатенация массивов и усечение до нужного размера. DK — нужный нам хеш, R — функция усечения)

Исходя из этого, напишем итоговую функцию, которая и будет считать все, что нам нужно (а именно значение хеша от пароля и соли). На вход поступают: указатели на пароль, соль и значения их размеров; число итераций; нужная нам длина хеша; указатель, куда поместим итоговый результат:

Определяем количество блоков (и, соответственно, число циклов расчета):

Резервируем нужные места в памяти:

Делаем нужное количество циклов и сливаем результаты всех циклов в один общий длинный массив:

Отсекаем лишнее и пишем результат в нужное место:

Заключение

Вот и все. Результат работы написанной нами функции PBKDF_2 с исходными данными из пятого контрольного примера методических рекомендаций Р 50.1.111—2016 (с. 9) показан на рисунке ниже.

алгоритмы хеширования паролей
Время расчета хеша составило порядка 15 секунд

Время расчета хеша при количестве итераций 4096 составило около пятнадцати секунд, что вполне способно очень серьезно усложнить злоумышленнику подбор пароля как прямым перебором, так и с помощью «радужных таблиц» и прочего непотребства.

Думается, написанная нами функция хеширования паролей на основе российского алгоритма «Стрибог» наряду с scrypt, bcrypt, crypt, SHA-2 и прочими алгоритмами подсчета хеша займет свое место в вашей коллекции алгоритмов хеширования паролей, а при выборе алгоритма для своего очередного проекта вы будете знать, что алгоритмам, придуманным за пределами Российской Федерации, есть вполне достойная отечественная альтернатива.

Ссылки

Понравилась статья? Поделиться с друзьями:
Добавить комментарий