Это вторая часть разговора об инфраструктуре шифрования нового поколения "Crypt Next Generation". В первой мы рассмотрели базовые сведения о шифровании данных, и режимы работы алгоритма AES. В том-что это действительно мотор поколения Next можно убедиться на сайте MSDN, где на невинный запрос описания какой-либо
Содержание:
1. Хеширование данных
Как правило, информацию хешируют для проверки её целостности. Отправитель пересчитывает хеш со-своей инфы, и вкладывает его значение в конверт с данными. Получатель на другом конце извлекает из конверта значение полученного хеша, и при помощи всё той-же функции повторно снимает хеш с принятых данных. Если при сравнении оба значения совпадут, значит не один бит информации не потерялся в каналах связи, и полученная инфа валидна.
Здесь нужно понимать, что расчёт хеша – это не шифрование данных, а своеобразная сумма всех байт, на которую накладываются сложные математические операции. По этой причине, хеширование является односторонней функцией, без возможности восстановления исходных данных.
Вне зависимости от размера информации на входе, мы всегда получаем хеш фиксированной длины. К примеру алгоритмы MD2[4,5] генерируют на выходе 16-байтные значения, а разрядность хеша SHA (Secure Hash Algorithm) указывается в его названии, т.е. для SHA-512 будет соответственно 512-бит или 64 байта (особняком стоит SHA1 с размером хеша 160-бит = 20 байт).
Возьмём к примеру пароль из четырёх символов "1234". Как его хранить в программе? Ясно, что не в открытом виде, а если зашифровать обычным ксором, то подбор его брутфорсом займёт пару минут. Поэтому мы снимаем с пароля хеш например SHA-256 (sha2) и на выходе получаем уже 32-байтное значение этого пароля. Так решаются сразу две проблемы: во-первых автоматически отваливается брутфорс любого рода, а во-вторых – скрывается от посторонних глаз пароль. Получить коллизию при 32-байтном значении нереально, а потому можно быть уверенным, что хеши двух паролей не совпадут.
Для программного его расчёта нам понадобятся аж шесть функций из библиотеки CNG Bcrypt.dll, т.к. в нашем распоряжении не законченные функции, а их примитивы. Конечно удобней было-бы собрать все в один флакон и обозвать набор элементарно BCryptHash() (кстати подобная функция есть и добавлена она только в Win10), но мы имеем дело с библиотекой низкого уровня, где каждую единицу приходится обрабатывать отдельно – это раскрепощает нас, хотя и требует взамен больше телодвижений.
Функция BCryptOpenAlgorithmProvider() возвращает дескриптор выбранного алгоритма. Поскольку мы собираемся хешировать данные, то передаём этой функции во-втором параметре указатель на Unicode-строку "SHA256". Подготавливает окружение для операции следующий примитив BCryptCreateHash(). При вызове, она запросит у нас память для временного своего объекта, так-что перед ней нужно будет узнать у провайдера размер этого объекта для данного алгоритма, при помощи BCryptGetProperty() с аргументом "OBJECT_LENGTH".
Теперь можно приступать непосредственно к процессу хеширования BCryptHashData() – функция ожидает адрес исходных данных и их размер. Финализирует операцию BCryptFinishHash(), которая сохраняет готовый к употреблению хеш в указанный нами буфер. По окончании, временный объект и всё окружение подлежит уничтожению функцией BCryptDestroyHash(), но если это последнее, что делает наша программа, ExitProcess() на выходе сама подчистит все выделенные системой ресурсы.
В своей демке, я посчитаю хеш введённого юзером пароля сразу двумя алгоритмами SHA-256 и MD5, чтобы можно было сравнить их значения:
Хешам находят разное применение на практике. К примеру аверы проверяют файлы на наличие блох только один раз, после чего сравнивают их хеши. Если файл заражён, он моментально всплывёт наружу – это быстрее, чем каждый раз повторять одну и ту-же процедуру по всей длине файлов.
2. AAD – дополнительные данные для хеша
Хеширование информации в чистом виде просуществовало довольно долго. Но однажды кому-то понадобилось не только проверять целость данных, но и идентифицировать их отправителя. К примеру получили мы конверт с вложенным хешем, но как узнать, что это хеш конкретно принятых данных, ведь отправитель (или третье лицо) может подставить вместо него любое значение от фонаря?
Одним из разумных решений стало добавление запроса на "ввод пароля" от отправителя. Теперь, на этапе хеширования информации, отправитель должен предоставить функции не только сами данные, но и дополнительную текстовую строку, чтобы однозначно идентифицировать себя – эту строку назвали AAD, или "Additional Authenticated Data". AAD можно рассматривать как внешнюю соль к хешу, и чтобы значения двух хешей совпали на передающей и принимающей стороне, получатель должен подтвердить оригинальный AAD (т.е. ввести пароль).
Так появились хеши с аутентификацией. Их тут-же взяли на вооружения сразу несколько режимов шифрования AES, известных под общим именем AEAD – "Authenticated Encryption with Associated Data" (аутентифицированное шифрование с присоединёнными данными). В этих режимах, алгоритм сначала шифрует исходные данные, после чего сразу-же вычисляет с них хеш. В контексте шифрования, такие хеши назвали МАС, что в развёрнутом виде означает "Message Authentification Code", или код аутентификации сообщения.
Таким образом, AEAD-режимы сочетают в себе две операции: непосредственно шифрование для конфиденциальности, и вычисление MAC для проверки целостности данных, с аутентификацией юзера. Это приводит к более низким вычислительным затратам, по сравнению с использованием отдельных функций шифрование + аутентификация. Здесь, первая часть сообщения шифруется, вторая часть в виде AAD остаётся открытой, и всё сообщение целиком накрыто аутентификацией. В настоящее время имеются несколько AEAD-режимов AES, но в тройке лидеров: OCB2 (Offset codebook), CCM (CBC+MAC), и герой данного чтива GCM (Galois Counter Mode, CTR+GMAC) как наиболее популярный.
Один из примеров использования AAD – это когда наше приложение служит прокси-сервером для развёртывания интерфейса с одним симметричным ключом и неограниченным кол-вом клиентов (причём каждый клиент находится в своём сегменте безопасности). Например, приложение может быть базой, в которую юзеры заносят данные личного характера. Когда одному из них нужно просмотреть свои записи, приложение может затребовать уникальное его имя в качестве AAD. В этом сценарии, функция AES откажет в расшифровке данных, если для крипт/декрипта не используется одно и то-же значение AAD. Параметр является опциональным в функции шифрования BCryptEncrypt(), однако он всегда принимает участие в процессе – если мы игнорируем его, просто используется пустая строка.
3. Алгоритм AES в режиме GCM
На рисунке ниже представлена схема режима AES/GCM где видно, что он состоит из двух самостоятельных модулей – это шифрование и аутентификация (серый блок). Обратите внимание на локацию доп.данных AAD, обозначенных здесь как "Auth-Data". Они поступают в модуль GMAC и не шифруются, подвергаясь только операции хеш. Результатом функций AEAD является зашифрованный текст + тег аутентификации МАС (синие блоки). Вместе с ключом, вектором IV и необязательным AAD, 16-байтный тег МАС необходим для расшифровки закрытого CipherText. Криптографы всегда рекомендуют использовать режимы AEAD для блочных алгоритмов шифрования с симметричным ключом:
Если присмотреться к этой схеме, то режим AES/GMAC есть ничто-иное как расширенная версия режима CTR (см.предыдущую часть статьи), к которому добавлен модуль GMAC для одновременного вычисления хеша:
Все блочные алгоритмы с счётчиком на борту, благодаря
Зоопарк режимов AES может иметь разные характеристики производительности. GCM в полной мере использует преимущества параллельной обработки на нескольких ядрах процессора, и его реализация эффективно использует конвейер команд CPU. А вот цепочечные режимы AES (типа CBC) напротив приводят к остановкам конвейера, и это снижает их производительность.
Чтобы увеличить скорость шифрования, начиная с процессоров "Xeon-56xx" Intel ввела специальный набор инструкций под названием AES-NI (New Instruction). Так появилась
Типичный интерфейс его программирования выглядит так:
• Шифрование
• Расшифровка
Кстати в литературе, мнение криптографов на счёт определения "вектор инициализации IV" расходятся. Дело в том, что термин применяется как в контексте шифрования, так и в руководствах по хешированию данных. Поэтому (если дотошно придираться к мелочам), здесь нужно внести ясность..
Для хэш-функций, вектор инициализации – это постоянная константа, которая никогда не меняется (полином, см.в
4. Практика – шифрование AES без аутентификации
Теперь к практической реализации AES..
Говорят, что качество имеет различные метрики, и это утверждение как-нельзя лучше подходит к выбору нами алгоритма шифрования. Если задача проста и не требует особой криптко-стойкости, то зачем нагружать программу лишним кодом, ведь чем он проще, тем стабильней работает софт. По этой причине, сначала рассмотрим обычное шифрование AES без тегов аутентификации МАС (в числе которых режимы CBC и CFB), а потом и AEAD-режимы CCM/GCM с дополнительными данными AAD.
Всё-что нужно режимам без МАС, это ключ шифрования и вектор инициализации IV. Как-правило, оба эти значения на подготовительном этапе генерируются рандомно, при этом мы можем выбрать один из трёх размеров ключей на своё усмотрение: 16, 24, 32 байта (для AES-128, 192, 256 соответственно). Поскольку в данных режимах нет внутреннего 4-байтного счётчика, то вектор IV должен быть строго размером 16 байт, чтобы он захватил весь первый блок AES:
Программная реализация шифрования включает в себя пять этапов по такой схеме:
На что здесь следует обратить особое внимание, так это на значение вектора IV. Дело в том, что согласно докам MSDN, функция шифрования BCryptEncrypt() может испортить его при переходе к следующему блоку AES, и если мы не хотим потерять исходные данные, то должны заблаговременно сделать резервную копию оригинального IV, чтобы в первоначальном виде передать его функции последующей расшифровки BCryptDecrypt(). Иначе мы получим невалидный вектор, и данные уйдут к праотцам. Отметьте сей факт красным фломастером!
Функция-же непосредственно шифрования BCryptEncrypt() принимает на грудь 10 параметров, из них в приоритете четвёртый "PaddingInfo" и последний "dwFlags". Два этих аргумента содержат информацию о заполнении 16-байтных блоков AES, и для режимов без аутентификации первый должен быть нуль, а во-втором нужно выставить флаг авто-выравнивания "BCRYPT_BLOCK_PADDING". Если его не указать, функция шифрования начнёт требовать от нас, чтобы мы подавали на вход кратные блоку AES данные, т.е. минимум 16 и дальше 32,48 и т.д. байт. Так-что паддинг ставим в единицу, что даст возможность вводить пароли произвольной длины.
В демке ниже продемонстрирован процесс шифрования и сразу-же расшифровки данных, обратной по назначению функцией BCryptDecrypt(). Параметры у них одинаковы и то-что мы передаём в функцию шифрования, нужно будет возвратить и в функцию декрипта (т.е. ключ, оригинальный IV и накрытые AES'ом данные). Поскольку блочные режимы AES увеличивают информацию на выходе и делают её кратной 16-байтному блоку, необходимо узнать их размер для аргумента "cbOutput". Для этого функция вызывается дважды – сначала вместо адреса приёмного буфа "pbOutput" подставляем нуль, и в переменной "pcbResult" получаем требуемый размер, который передаём во-второй вызов той-же функции. Это действительно как для крипта, так и для декрипта. Вот пример:
Как видим тут всё просто, но программисты предпочитают что-то позабористей, ведь за почти такое-же количество "приседаний" можно получить более лучший эффект. Достаточно в этом коде задействовать параметр "pPaddingInfo", как в нашем распоряжении оказываются дополнительные данные AAD, с возможностью получить тег-аутентификации МАС.
Если мы сталкиваемся с зашифрованными данными и их длина кратна 16-ти, то можно предположить, что это AES в блочном режиме. Если-же длина кратна восьми, как вариант перед нами DES.
4.1. Практика – шифрование AES-256 в режиме GCM
Режимы шифрования с аутентификацией GCM/CCM можно сравнить с безоборотным метанием ножа – они при любых обстоятельствах бьют точно в цель. Такие бонусы мы получаем благодаря структуре "BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO", указатель на которую передаётся в параметре(4) Padding.
C этой структурой связана одна мерзкая неприятность – по неизвестным причинам, её размер везде (на MSDN и хидере bcrypt.h) указывается на 8-байт меньше, чем этого требуют функции BCryptEncrypt/Decrypt(). Правится баг обычным добавление в хвост структуры ещё одного 8-байтного поля, которое я обозначил как резерв. Иначе крипто-функции выше возвращают ошибку
Функция BCryptEncrypt() способна за один выстрел шифровать данные, которые расположены сразу в нескольких разных буферах – такую схему назвали "связанный вызов" (активируется флагом BCRYPT_AUTH_MODE_IN_PROGRESS_FLAG). Если разработчики её предусмотрели, значит кому-то это нужно. В этой схеме мы получаем единую зашифрованную массу из всех буферов, и один тег-аутентификации МАС на всех. Вызовы такого рода больше исключение, чем правило и в большинстве случаях отмеченные в скобках поля сбрасываются в нуль (т.е.остаются в дефолте).
Таким образом, при стандартной схеме с одним буфером исходных данных, нам остаётся заполнить только каждую пару полей: "pbNonce", "pbAuthData" и "pbTag". При этом AuthData является опциональным полем (можно не использовать AAD, тогда в место него будет пустая строка), а остальные два обязательны. Если мы шифруем данные, то значение "pbTag" должно быть адресом приёмного буфера, куда функция вернёт сгенерированный на выходе МАС (тег-аутентификации). Соответственно при расшифровке – поле используется как указатель на источник.
Некоторых пояснений требует и поле "pbNonce" в этой структуре. Как уже говорилось ранее, в контексте шифрования, термины "Nonce" и "вектор IV" нужно воспринимать как оговорку по Фрейду – они обозначают одно и то-же. Однако здесь есть нюанс, который не следует сбрасывать со-счетов!
Если в двух словах, то когда мы шифруем алгоритмами без аутентификации не используя структуру "BCRYPT_AUTH_CIPHER_MODE_INFO" (см.предыдущий пример), указатель на вектор IV и его размер должны находится в пятом и шестом параметрах функции BCryptEncrypt(). Если-же планируем шифровать в режиме GCM с тегом МАС, то в самой функции параметры IV нужно обязательно сбросить в нуль, и поместить линк на буфер IV в поле "pbNonce" данной структуры. Очевидно, что нашаманили здесь мелкомягкие так, что без танцев с бубном не разберёшь.
Ну и немного практики..
Следуя предыдущему примеру, продемонстрируем опять шифрование и сразу восстановление данных.
Чтобы охватить всё вышеизложенное, помимо ввода пароля, я добавил и ввод дополнительных данных AAD. Если такое подтверждение не нужно, то просто жмякаем Enter и получаем пустую строку AAD. Но если мы шифруем при помощи доп.данных, то и при расшифровке нужно будет ввести валидную строку AAD. Кстати макс.размер этой строки равен 64 кБайт, поэтому функцию консольного ввода scanf() требуется заменить на gets() (первая читает ввод до первого пробела, что лишает нас возможности вводить предложения). Юзаем..
Если расчехлить отладчик и вскормить ему этот код, то нырнув в функцию BCryptEncrypt() по F7 можно обнаружить, что функция не использует преимущества аппаратного шифрования AES-NI, а весь алгоритм реализует исключительно обычными
Обратите внимание на формат результата шифрования – он включает в себя три составляющие: 12-байтный вектор IV, зашифрованный пароль, и в хвосте тег-аутентификации GМАС. А вот данные AAD как-правило не передаются принимающей стороне, иначе в них теряется смысл. Их нужно держать в тайне, накрыв протектом на уровне самой программы. Это-же относится и 32-байтному ключу шифрования AES. Рассмотрим, как справляется с этой задачей, например, браузер Хром.
5. Использование AEAD в браузерах
Поскольку высказывания ниже могут классифицироваться в рамках шпионажа, я не буду заострять внимание на мелочах, а лишь приведу общую картину. Достаточно будет сказать, что алгоритм проверенный и воркает исправно.
На предыдущем скрине, я не зря расположил результат шифрования AES/GCM в таком порядке. По сути, от перестановки мест слагаемых сумма не меняется. Однако интернет браузеры Chrome версии 80+ держат эту информацию в своих базах SQLite именно в таком виде, только добавляют к ним ещё и сигнатуру "v10". То-есть получаем сл.табличку:
Это типичный формат хранения хромом паролей и кукисов, базы которых можно найти по следующим адресам:
Открываем базу кукисов в каком-нибудь редакторе SQLite3 (типа SQLite Expert Personal), и обнаруживаем в ней две таблицы с названиями "cookies" и "meta". В первой имеется столбец "encrypted_value", где и хранятся зашифрованные алгоритмом AES-256/GCM непосредственно куки различных сайтов. Если щёлкнуть на пимпу любой строки в этом столбце, то перед нами откроется встроенный Hex-редактор, где будет красоваться соответствующий закриптованный кукис. Вот как это выглядит у меня:
В процессе расшифровки данного кукиса, нам нужно просто отбросить сигнатуру "v10" и передать всю оставшуюся информацию в свой код. Отметим, что Хром не использует данные AAD, тогда для достижении цели нам остаётся добыть только 32-байтный ключ шифрования. Как и следовало ожидать, он прописан там-же, только в файле Json по адресу:
Помимо ключа, в этом файле хранится всякое барахло,
а сам ключ в нём можно найти по Json-строке
За этой строкой и вплоть до обратной/фигурной скобки, будет лежать закодированный в Base64 ключ. Значит нам нужно сначала раскодировать его из Base64, а затем снять с него протект при помощи функции DPAPI CryptUnprotectData() (рассматривалась в этой статье). После всех этих операций мы получим ключ расшифровки кукисов алгоритмом AES-256/GCM, а так-как имеем уже IV, MAC и зашифрованные данные, то дело остаётся за малым.
6. Послесловие
Шифрование является основным методом защиты информации, однако в основе криптографии лежит не только крипт, но и ряд вспомогательных методов. В частности это не рассмотренная здесь "электронная подпись" (используется для подтверждения авторства передаваемых данных). Такие алгоритмы называют "сигнатурными", и в них подписывается обычно хэш данных, а не все данные целиком. Они используют два вида ключей – секретный для вычисления электронной подписи, и открытый для её проверки.
У нас не было-бы безопасного современного Интернета без работы В.Реймена, Д.Дэемен, Д.Виеги, Д.МакГрю и бесчисленного множества других криптографов и исследователей безопасности, которые сделали возможным использование AES в режиме GCM. Это по истине один из самых продвинутых на сегодняшний день алгоритмов шифрования, который прочно пустил свои корни во-все направления современной сферы IT.
В скрепке лежат три рассмотренных выше исполняемых файла, а так-же инклуд "вcrypt.inc" для сборки исходников.
Надеюсь ещё увидимся, удачи, пока!
Ссылка скрыта от гостей
из библиотеки CAPI, мелкософт встречает нас грозным предупреждением типа: -"Этот API устарел! Новое программное обеспечение должно использовать CNG, т.к. Microsoft может удалить этот API в будущих выпусках". Таким образом нас просто ставят перед фактом, подталкивая на изучение современных методов. Так не будем-же противиться этому..Содержание:
1. Хеширование информации;
2. AAD – дополнительные данные для хеша;
3. Алгоритм AES в режиме GCM;
4. Практика – шифрование с аутентификацией;
5. Использование AEAD в браузерах;
6. Выводы.
-------------------------------------------------------1. Хеширование данных
Как правило, информацию хешируют для проверки её целостности. Отправитель пересчитывает хеш со-своей инфы, и вкладывает его значение в конверт с данными. Получатель на другом конце извлекает из конверта значение полученного хеша, и при помощи всё той-же функции повторно снимает хеш с принятых данных. Если при сравнении оба значения совпадут, значит не один бит информации не потерялся в каналах связи, и полученная инфа валидна.
Здесь нужно понимать, что расчёт хеша – это не шифрование данных, а своеобразная сумма всех байт, на которую накладываются сложные математические операции. По этой причине, хеширование является односторонней функцией, без возможности восстановления исходных данных.
Вне зависимости от размера информации на входе, мы всегда получаем хеш фиксированной длины. К примеру алгоритмы MD2[4,5] генерируют на выходе 16-байтные значения, а разрядность хеша SHA (Secure Hash Algorithm) указывается в его названии, т.е. для SHA-512 будет соответственно 512-бит или 64 байта (особняком стоит SHA1 с размером хеша 160-бит = 20 байт).
Возьмём к примеру пароль из четырёх символов "1234". Как его хранить в программе? Ясно, что не в открытом виде, а если зашифровать обычным ксором, то подбор его брутфорсом займёт пару минут. Поэтому мы снимаем с пароля хеш например SHA-256 (sha2) и на выходе получаем уже 32-байтное значение этого пароля. Так решаются сразу две проблемы: во-первых автоматически отваливается брутфорс любого рода, а во-вторых – скрывается от посторонних глаз пароль. Получить коллизию при 32-байтном значении нереально, а потому можно быть уверенным, что хеши двух паролей не совпадут.
Для программного его расчёта нам понадобятся аж шесть функций из библиотеки CNG Bcrypt.dll, т.к. в нашем распоряжении не законченные функции, а их примитивы. Конечно удобней было-бы собрать все в один флакон и обозвать набор элементарно BCryptHash() (кстати подобная функция есть и добавлена она только в Win10), но мы имеем дело с библиотекой низкого уровня, где каждую единицу приходится обрабатывать отдельно – это раскрепощает нас, хотя и требует взамен больше телодвижений.
Функция BCryptOpenAlgorithmProvider() возвращает дескриптор выбранного алгоритма. Поскольку мы собираемся хешировать данные, то передаём этой функции во-втором параметре указатель на Unicode-строку "SHA256". Подготавливает окружение для операции следующий примитив BCryptCreateHash(). При вызове, она запросит у нас память для временного своего объекта, так-что перед ней нужно будет узнать у провайдера размер этого объекта для данного алгоритма, при помощи BCryptGetProperty() с аргументом "OBJECT_LENGTH".
C-подобный:
;//
;// Common algorithm Unicode identifiers.
;// (du = Define Unicode)
;//
BCRYPT_3DES_112_ALGORITHM du '3DES_112',0
BCRYPT_3DES_ALGORITHM du '3DES',0
BCRYPT_AES_ALGORITHM du 'AES',0
BCRYPT_AES_CMAC_ALGORITHM du 'AES-CMAC',0
BCRYPT_AES_GMAC_ALGORITHM du 'AES-GMAC',0
BCRYPT_AES_XTS_ALGORITHM du 'XTS-AES',0
BCRYPT_CAPI_KDF_ALGORITHM du 'CAPI_KDF',0
BCRYPT_DES_ALGORITHM du 'DES',0
BCRYPT_DESX_ALGORITHM du 'DESX',0
BCRYPT_DH_ALGORITHM du 'DH',0
BCRYPT_DSA_ALGORITHM du 'DSA',0
BCRYPT_ECDH_ALGORITHM du 'ECDH',0
BCRYPT_ECDH_P256_ALGORITHM du 'ECDH_P256',0
BCRYPT_ECDH_P384_ALGORITHM du 'ECDH_P384',0
BCRYPT_ECDH_P521_ALGORITHM du 'ECDH_P521',0
BCRYPT_ECDSA_ALGORITHM du 'ECDSA',0
BCRYPT_ECDSA_P256_ALGORITHM du 'ECDSA_P256',0
BCRYPT_ECDSA_P384_ALGORITHM du 'ECDSA_P384',0
BCRYPT_ECDSA_P521_ALGORITHM du 'ECDSA_P521',0
BCRYPT_MD2_ALGORITHM du 'MD2',0
BCRYPT_MD4_ALGORITHM du 'MD4',0
BCRYPT_MD5_ALGORITHM du 'MD5',0
BCRYPT_PBKDF2_ALGORITHM du 'PBKDF2',0
BCRYPT_RC2_ALGORITHM du 'RC2',0
BCRYPT_RC4_ALGORITHM du 'RC4',0
BCRYPT_RNG_ALGORITHM du 'RNG',0
BCRYPT_RNG_DUAL_EC_ALGORITHM du 'DUALECRNG',0
BCRYPT_RNG_FIPS186_DSA_ALGORITHM du 'FIPS186DSARNG',0
BCRYPT_RSA_ALGORITHM du 'RSA',0
BCRYPT_RSA_SIGN_ALGORITHM du 'RSA_SIGN',0
BCRYPT_SHA1_ALGORITHM du 'SHA1',0
BCRYPT_SHA256_ALGORITHM du 'SHA256',0
BCRYPT_SHA384_ALGORITHM du 'SHA384',0
BCRYPT_SHA512_ALGORITHM du 'SHA512',0
BCRYPT_SP800108_CTR_HMAC_ALGORITHM du 'SP800_108_CTR_HMAC',0
BCRYPT_SP80056A_CONCAT_ALGORITHM du 'SP800_56A_CONCAT',0
BCRYPT_TLS1_1_KDF_ALGORITHM du 'TLS1_1_KDF',0
BCRYPT_TLS1_2_KDF_ALGORITHM du 'TLS1_2_KDF',0
Теперь можно приступать непосредственно к процессу хеширования BCryptHashData() – функция ожидает адрес исходных данных и их размер. Финализирует операцию BCryptFinishHash(), которая сохраняет готовый к употреблению хеш в указанный нами буфер. По окончании, временный объект и всё окружение подлежит уничтожению функцией BCryptDestroyHash(), но если это последнее, что делает наша программа, ExitProcess() на выходе сама подчистит все выделенные системой ресурсы.
В своей демке, я посчитаю хеш введённого юзером пароля сразу двумя алгоритмами SHA-256 и MD5, чтобы можно было сравнить их значения:
C-подобный:
format pe console
entry start
;//------------
section '.inc' data readable
include 'win32ax.inc'
include 'equates\bcrypt.inc'
;//------------
.data
pcbResult dd 0
passLen dd 0
hashBuff rb 64 ;//<---- готовый продукт хеша
hashObject rb 1024 ;//<---- буф под временный объект
align 16
;//<----- данные SHA-256
shaAlgHndl dd 0
shaHndl dd 0
shaLen dd 0
shaObjLen dd 0
;//<----- данные MD5
md5AlgHndl dd 0
md5Hndl dd 0
md5Len dd 0
md5ObjLen dd 0
buff db 0
;//------------
.code
start: invoke SetConsoleTitle,<'*** CNG Hash example ***',0>
cinvoke printf,<10,' SHA-256 and MD5 hash generate',\
10,' =======================================',10,0>
;// Получить дескриптор алгоритма SHA-512, и собрать о нём инфу
invoke BCryptOpenAlgorithmProvider,shaAlgHndl,BCRYPT_SHA256_ALGORITHM,0,0
invoke BCryptGetProperty,[shaAlgHndl],BCRYPT_HASH_LENGTH, shaLen, 4, pcbResult,0
invoke BCryptGetProperty,[shaAlgHndl],BCRYPT_OBJECT_LENGTH, shaObjLen, 4, pcbResult,0
;// Получить дескриптор MD5, и собрать о нём инфу
invoke BCryptOpenAlgorithmProvider,md5AlgHndl,BCRYPT_MD5_ALGORITHM,0,0
invoke BCryptGetProperty,[md5AlgHndl],BCRYPT_HASH_LENGTH, md5Len,4, pcbResult,0
invoke BCryptGetProperty,[md5AlgHndl],BCRYPT_OBJECT_LENGTH, md5ObjLen,4, pcbResult,0
mov eax,[shaLen] ;// длина хеша SHA в байтах
mov ebx,eax
shl ebx,3 ;// ..и в битах (х8)
cinvoke printf,<10,' SHA-256 information ************',\
10,' Hash len.........: %03d byte (%d bit)',\
10,' Temp object len..: %03d byte',10,0>,eax,ebx,[shaObjLen]
mov eax,[md5Len]
mov ebx,eax
shl ebx,3
cinvoke printf,<10,' MD5 information ****************',\
10,' Hash len.........: %03d byte (%d bit)',\
10,' Temp object len..: %03d byte',10,0>,eax,ebx,[md5ObjLen]
;//***********************************************
;// Запрашиваем у юзера пасс в свой буфер
cinvoke printf,<10,' =======================================',\
10,' Type pass..........: ',0>
cinvoke scanf,<'%s',0>,buff
invoke lstrlen,buff ;//<---- вычислить его длину
mov [passLen],eax
;//***** Хеширование данных из буфера ***************
invoke BCryptCreateHash,[shaAlgHndl],shaHndl,hashObject,[shaObjLen],0,0,0
invoke BCryptHashData,[shaHndl],buff,[passLen],0 ;//<---- исходные данные в буфере "buff"
invoke BCryptFinishHash,[shaHndl],hashBuff,[shaLen],0 ;//<---- результат в буфере "hashBuff"
invoke BCryptDestroyHash,[shaHndl]
cinvoke printf,<10,' Pass len...........: %02d byte',\
10,' Pass SHA-256 hash..: ',0>,[passLen]
mov ecx,[shaLen]
mov esi,hashBuff
call PrintHexString
;//********************
invoke BCryptCreateHash,[md5AlgHndl],md5Hndl,hashObject,[md5ObjLen],0,0,0
invoke BCryptHashData,[md5Hndl],buff,[passLen],0
invoke BCryptFinishHash,[md5Hndl],hashBuff,[md5Len],0
invoke BCryptDestroyHash,[md5Hndl]
cinvoke printf,<10,' Pass MD5 hash......: ',0>
mov ecx,[md5Len]
mov esi,hashBuff
call PrintHexString
@exit: cinvoke _getch
cinvoke exit,0
;//------------
;//----- Процедура вывода Hex-строки на консоль --------------
;//----- на входе: ESI = указатель на данные, ECX = длина
proc PrintHexString
@@: xor eax,eax
lodsb
push ecx esi
cinvoke printf,<'%02x',0>,eax
pop esi ecx
loop @b
ret
endp
;//------------
section '.idata' import data readable
library kernel32,'kernel32.dll',msvcrt,'msvcrt.dll',bcrypt,'bcrypt.dll'
include 'api\kernel32.inc'
include 'api\msvcrt.inc'
include 'api\bcrypt.inc'
Хешам находят разное применение на практике. К примеру аверы проверяют файлы на наличие блох только один раз, после чего сравнивают их хеши. Если файл заражён, он моментально всплывёт наружу – это быстрее, чем каждый раз повторять одну и ту-же процедуру по всей длине файлов.
2. AAD – дополнительные данные для хеша
Хеширование информации в чистом виде просуществовало довольно долго. Но однажды кому-то понадобилось не только проверять целость данных, но и идентифицировать их отправителя. К примеру получили мы конверт с вложенным хешем, но как узнать, что это хеш конкретно принятых данных, ведь отправитель (или третье лицо) может подставить вместо него любое значение от фонаря?
Одним из разумных решений стало добавление запроса на "ввод пароля" от отправителя. Теперь, на этапе хеширования информации, отправитель должен предоставить функции не только сами данные, но и дополнительную текстовую строку, чтобы однозначно идентифицировать себя – эту строку назвали AAD, или "Additional Authenticated Data". AAD можно рассматривать как внешнюю соль к хешу, и чтобы значения двух хешей совпали на передающей и принимающей стороне, получатель должен подтвердить оригинальный AAD (т.е. ввести пароль).
Так появились хеши с аутентификацией. Их тут-же взяли на вооружения сразу несколько режимов шифрования AES, известных под общим именем AEAD – "Authenticated Encryption with Associated Data" (аутентифицированное шифрование с присоединёнными данными). В этих режимах, алгоритм сначала шифрует исходные данные, после чего сразу-же вычисляет с них хеш. В контексте шифрования, такие хеши назвали МАС, что в развёрнутом виде означает "Message Authentification Code", или код аутентификации сообщения.
Таким образом, AEAD-режимы сочетают в себе две операции: непосредственно шифрование для конфиденциальности, и вычисление MAC для проверки целостности данных, с аутентификацией юзера. Это приводит к более низким вычислительным затратам, по сравнению с использованием отдельных функций шифрование + аутентификация. Здесь, первая часть сообщения шифруется, вторая часть в виде AAD остаётся открытой, и всё сообщение целиком накрыто аутентификацией. В настоящее время имеются несколько AEAD-режимов AES, но в тройке лидеров: OCB2 (Offset codebook), CCM (CBC+MAC), и герой данного чтива GCM (Galois Counter Mode, CTR+GMAC) как наиболее популярный.
Один из примеров использования AAD – это когда наше приложение служит прокси-сервером для развёртывания интерфейса с одним симметричным ключом и неограниченным кол-вом клиентов (причём каждый клиент находится в своём сегменте безопасности). Например, приложение может быть базой, в которую юзеры заносят данные личного характера. Когда одному из них нужно просмотреть свои записи, приложение может затребовать уникальное его имя в качестве AAD. В этом сценарии, функция AES откажет в расшифровке данных, если для крипт/декрипта не используется одно и то-же значение AAD. Параметр является опциональным в функции шифрования BCryptEncrypt(), однако он всегда принимает участие в процессе – если мы игнорируем его, просто используется пустая строка.
3. Алгоритм AES в режиме GCM
На рисунке ниже представлена схема режима AES/GCM где видно, что он состоит из двух самостоятельных модулей – это шифрование и аутентификация (серый блок). Обратите внимание на локацию доп.данных AAD, обозначенных здесь как "Auth-Data". Они поступают в модуль GMAC и не шифруются, подвергаясь только операции хеш. Результатом функций AEAD является зашифрованный текст + тег аутентификации МАС (синие блоки). Вместе с ключом, вектором IV и необязательным AAD, 16-байтный тег МАС необходим для расшифровки закрытого CipherText. Криптографы всегда рекомендуют использовать режимы AEAD для блочных алгоритмов шифрования с симметричным ключом:
Если присмотреться к этой схеме, то режим AES/GMAC есть ничто-иное как расширенная версия режима CTR (см.предыдущую часть статьи), к которому добавлен модуль GMAC для одновременного вычисления хеша:
Все блочные алгоритмы с счётчиком на борту, благодаря
Ссылка скрыта от гостей
по сути превращаются в потоковый шифр. Поэтому важно, чтобы для каждого блока AES использовался не повторяющийся IV, для чего собственно и служит счётчик. В отличии от блочных шифров, поточные не увеличивают исходные данные после шифрования – это относится и к AES/GCM. Зоопарк режимов AES может иметь разные характеристики производительности. GCM в полной мере использует преимущества параллельной обработки на нескольких ядрах процессора, и его реализация эффективно использует конвейер команд CPU. А вот цепочечные режимы AES (типа CBC) напротив приводят к остановкам конвейера, и это снижает их производительность.
Чтобы увеличить скорость шифрования, начиная с процессоров "Xeon-56xx" Intel ввела специальный набор инструкций под названием AES-NI (New Instruction). Так появилась
Ссылка скрыта от гостей
шифрования AES и хеширования SHA. Несмотря на то, что в данный набор входят всего несколько инструкций, они относятся к разряду SIMD, когда одна инструкция выполняет сразу несколько микроопераций (Single Instruction, Multiple Data). Благодаря тому, что каждая такая инструкция проделывает свою часть глобальной работы (AES включает в себя 4-этапа) – это позволило добиться 5-кратного увеличения скорости. Таким образом, если вам нужен быстрый режим шифрования с массой дополнений, используйте AES/GCM.Типичный интерфейс его программирования выглядит так:
• Шифрование
- Вход: открытый текст + ключ + IV + опционально AAD в виде открытого текста, который не будет зашифрован, но будет защищён аутентификацией.
- Выход: зашифрованный текст + тег аутентификации МАС.
• Расшифровка
- Вход: зашифрованный текст + ключ + IV + МАС + AAD (если он использовался во время шифрования).
- Выход: открытый текст или ошибка, если МАС или AAD не соответствуют зашифрованному тексту.
Кстати в литературе, мнение криптографов на счёт определения "вектор инициализации IV" расходятся. Дело в том, что термин применяется как в контексте шифрования, так и в руководствах по хешированию данных. Поэтому (если дотошно придираться к мелочам), здесь нужно внести ясность..
Для хэш-функций, вектор инициализации – это постоянная константа, которая никогда не меняется (полином, см.в
Ссылка скрыта от гостей
). Для блочных-же шифров, одним из обязательных условий является уникальный IV для каждого блока AES – такие векторы назвали Nonce, или одноразовое не повторяющееся значение N(once)
. Это вносит путаницу и делает свойства двух терминов неоднозначными. Но т.к. IV в промышленных процедурах хеширования зарыт глубоко внутри и его хвост не торчит снаружи, то им можно пренебречь. Таким образом остаётся только шифрование, где термины IV/nonce превращаются в синонимы, даже на уровне документации MSDN.4. Практика – шифрование AES без аутентификации
Теперь к практической реализации AES..
Говорят, что качество имеет различные метрики, и это утверждение как-нельзя лучше подходит к выбору нами алгоритма шифрования. Если задача проста и не требует особой криптко-стойкости, то зачем нагружать программу лишним кодом, ведь чем он проще, тем стабильней работает софт. По этой причине, сначала рассмотрим обычное шифрование AES без тегов аутентификации МАС (в числе которых режимы CBC и CFB), а потом и AEAD-режимы CCM/GCM с дополнительными данными AAD.
Всё-что нужно режимам без МАС, это ключ шифрования и вектор инициализации IV. Как-правило, оба эти значения на подготовительном этапе генерируются рандомно, при этом мы можем выбрать один из трёх размеров ключей на своё усмотрение: 16, 24, 32 байта (для AES-128, 192, 256 соответственно). Поскольку в данных режимах нет внутреннего 4-байтного счётчика, то вектор IV должен быть строго размером 16 байт, чтобы он захватил весь первый блок AES:
Программная реализация шифрования включает в себя пять этапов по такой схеме:
1. BCryptOpenAlgorithmProvider() с аргументом "AES_ALGORITHM" – возвращает дескриптор алгоритма;
2. BCryptSetProperty() с аргументом "CHAIN_MODE_CBC" – устанавливает в свойствах дескриптора режим CBC;
3. BCryptGenRandom() – сгенерировать случайные ключ и вектор IV;
4. BCryptGenerateSymmetricKey() – получить дескриптор симметричного ключа шифрования;
5. BCryptEncrypt() – зашифровать данные ключом и вектором IV.
На что здесь следует обратить особое внимание, так это на значение вектора IV. Дело в том, что согласно докам MSDN, функция шифрования BCryptEncrypt() может испортить его при переходе к следующему блоку AES, и если мы не хотим потерять исходные данные, то должны заблаговременно сделать резервную копию оригинального IV, чтобы в первоначальном виде передать его функции последующей расшифровки BCryptDecrypt(). Иначе мы получим невалидный вектор, и данные уйдут к праотцам. Отметьте сей факт красным фломастером!
Функция-же непосредственно шифрования BCryptEncrypt() принимает на грудь 10 параметров, из них в приоритете четвёртый "PaddingInfo" и последний "dwFlags". Два этих аргумента содержат информацию о заполнении 16-байтных блоков AES, и для режимов без аутентификации первый должен быть нуль, а во-втором нужно выставить флаг авто-выравнивания "BCRYPT_BLOCK_PADDING". Если его не указать, функция шифрования начнёт требовать от нас, чтобы мы подавали на вход кратные блоку AES данные, т.е. минимум 16 и дальше 32,48 и т.д. байт. Так-что паддинг ставим в единицу, что даст возможность вводить пароли произвольной длины.
C-подобный:
NTSTATUS BCryptEncrypt ;// <----- 0 = OK!
hKey dd 0 ;// дескриптор ключа от BCryptGenerateSymmetricKey()
pbInput dd 0 ;// буфер с открытым текстом
cbInput dd 0 ;// размер буфера
pPaddingInfo dd 0 ;// инфа о заполнении (только асим и аутен)
pbIV dd 0 ;// адрес IV
cbIV dd 0 ;// размер IV
pbOutput dd 0 ;// адрес приёмного буфа (нуль = вычислить размер)
cbOutput dd 0 ;// размер
pcbResult dd 0 ;// получает кол-во байт на выходе
dwFlags dd 0 ;// нуль для ССМ и GCM
• pPaddingInfo
Указатель на структуру, содержащую информацию о заполнении. Этот параметр используется только с асимметричными ключами и режимами аутентифицированного шифрования. Если используется Auth-режим, параметр должен указывать на структуру "BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO". Если используются асимметричные ключи, тип структуры определяется значением "dwFlags". В противном случае параметр должен иметь значение нуль.
В демке ниже продемонстрирован процесс шифрования и сразу-же расшифровки данных, обратной по назначению функцией BCryptDecrypt(). Параметры у них одинаковы и то-что мы передаём в функцию шифрования, нужно будет возвратить и в функцию декрипта (т.е. ключ, оригинальный IV и накрытые AES'ом данные). Поскольку блочные режимы AES увеличивают информацию на выходе и делают её кратной 16-байтному блоку, необходимо узнать их размер для аргумента "cbOutput". Для этого функция вызывается дважды – сначала вместо адреса приёмного буфа "pbOutput" подставляем нуль, и в переменной "pcbResult" получаем требуемый размер, который передаём во-второй вызов той-же функции. Это действительно как для крипта, так и для декрипта. Вот пример:
C-подобный:
format pe console
entry start
;//------------
section '.inc' data readable
include 'win32ax.inc'
include 'equates\bcrypt.inc'
;//------------
.data
pcbResult dd 0
passLen dd 0
keyRandom rb 32
ivRandom rb 16 ;// оригинальный IV,
ivBackup rb 16 ;// ..и его бэкап для передачи в декрипт.
;//<-------- данные AES-256/CBC
align 16
aesAlgHndl dd 0
aesKeyHndl dd 0
aesHndl dd 0
aesBlockLen dd 0
aesObjLen dd 0
align 16
encryptBuff rb 128 ;// под результат шифрования
decryptBuff rb 128 ;// под результат расшифровки
buff db 0 ;// пароль для шифрования
;//------------
.code
start: invoke SetConsoleTitle,<'*** CNG. Crypt-Next-Generation ***',0>
cinvoke printf,<10,' AES-256/CBC password crypt example',\
10,' ========================================',10,0>
;// Получить дескриптор AES-256/CBC, и собрать об алгоритме инфу.
;// Для этого в свойствах дескриптора нужно выставить флаг "BCRYPT_CHAIN_MODE_CBC"
invoke BCryptOpenAlgorithmProvider,aesAlgHndl,BCRYPT_AES_ALGORITHM,0,0
invoke BCryptSetProperty,[aesAlgHndl],BCRYPT_CHAINING_MODE, BCRYPT_CHAIN_MODE_CBC,30,0
invoke BCryptGetProperty,[aesAlgHndl],BCRYPT_BLOCK_LENGTH, aesBlockLen,4,pcbResult,0
invoke BCryptGetProperty,[aesAlgHndl],BCRYPT_OBJECT_LENGTH, aesObjLen,4, pcbResult,0
mov eax,[aesBlockLen]
mov ebx,eax
shl ebx,3
cinvoke printf,<10,' Crypt block len....: %03d byte (%d bit)',\
10,' Temp object len....: %03d byte',0>,eax,ebx,[aesObjLen]
;// Сгенерим 32-байт рандом для ключа
invoke BCryptGenRandom,0,keyRandom,32,BCRYPT_USE_SYSTEM_PREFERRED_RNG
cinvoke printf,<10,' 32-byte crypt key..: ',0>
mov ecx,32
mov esi,keyRandom
call PrintHexString
;// Сгенерим 16-байт рандом для вектора IV
invoke BCryptGenRandom,0,ivRandom,16,BCRYPT_USE_SYSTEM_PREFERRED_RNG
cinvoke printf,<10,' 16-byte crypt IV...: ',0>
mov ecx,16
mov esi,ivRandom
push esi ecx
call PrintHexString
;// Функция Encrypt() портит оригин.IV в буфере, поэтому сделаем его бэкап для расшифровки
mov edi,ivBackup
pop ecx esi
rep movsb
;//************* ШИФРОВАНИЕ **********************************
;// Запрашиваем пасс юзера в свой буфер
cinvoke printf,<10,10,' ========================================',\
10,' Type pass............: ',0>
cinvoke scanf,<'%s',0>,buff
invoke lstrlen,buff
mov [passLen],eax
;// Получим дескриптор ключа AES-256/CBC
invoke BCryptGenerateSymmetricKey,[aesAlgHndl],aesKeyHndl,0,0,keyRandom,32,0
;// Первый вызов, чтобы узнать размер выходных данных (в переменную "pcbResult")
invoke BCryptEncrypt,[aesKeyHndl],buff,[passLen],0,ivRandom,16,\
0,0,pcbResult,BCRYPT_BLOCK_PADDING
;// Второй вызов - непосредственно шифрование в буфер "encryptBuff"
invoke BCryptEncrypt,[aesKeyHndl],buff,[passLen],0,ivRandom,16,\
encryptBuff,[pcbResult],pcbResult,BCRYPT_BLOCK_PADDING
cinvoke printf,<10,' Plain pass size..: %02d byte',\
10,' Cipher pass size..: %02d byte',\
10,' Pass cipher text..: %s',0>,[passLen],[pcbResult],encryptBuff
;//************* РАСШИФРОВКА **********************************
;// Соответственно чтобы обратно расшифровать пароль,
;// нужно вызвать BCryptDecrypt() передав ей: ключ, оригинальный IV, и адрес данных
cinvoke printf,<10,10,' ========================================',\
10,' Decrypt password *****',0>
;// Расшифровка - получим дескриптор ключа из значения
invoke BCryptGenerateSymmetricKey,[aesAlgHndl],aesKeyHndl,0,0,keyRandom,32,0
;// Запросим в EAX размер зашифрованных данных
invoke lstrlen,encryptBuff
push eax
;// Первый вызов, чтобы узнать размер выходных данных (в переменную "pcbResult")
invoke BCryptDecrypt,[aesKeyHndl],encryptBuff,eax,0,ivBackup,16,\
0,0,pcbResult,BCRYPT_BLOCK_PADDING
;// Второй вызов - непосредственно расшифровка в буфер "decryptBuff"
pop eax
invoke BCryptDecrypt,[aesKeyHndl],encryptBuff,eax,0,ivBackup,16,\
decryptBuff,[pcbResult],pcbResult,BCRYPT_BLOCK_PADDING
cinvoke printf,<10,' Plain pass size...: %02d byte',\
10,' Pass plain text...: %s',0>,[pcbResult],decryptBuff
@exit: cinvoke _getch
cinvoke exit,0
;//------------
;//----- Процедура вывода Hex-строки на консоль --------------
;//----- на входе: ESI = указатель на данные, ECX = длина
proc PrintHexString
@@: xor eax,eax
lodsb
push ecx esi
cinvoke printf,<'%02x',0>,eax
pop esi ecx
loop @b
ret
endp
;//------------
section '.idata' import data readable
library kernel32,'kernel32.dll',msvcrt,'msvcrt.dll',bcrypt,'bcrypt.dll'
include 'api\kernel32.inc'
include 'api\msvcrt.inc'
include 'api\bcrypt.inc'
Как видим тут всё просто, но программисты предпочитают что-то позабористей, ведь за почти такое-же количество "приседаний" можно получить более лучший эффект. Достаточно в этом коде задействовать параметр "pPaddingInfo", как в нашем распоряжении оказываются дополнительные данные AAD, с возможностью получить тег-аутентификации МАС.
Если мы сталкиваемся с зашифрованными данными и их длина кратна 16-ти, то можно предположить, что это AES в блочном режиме. Если-же длина кратна восьми, как вариант перед нами DES.
4.1. Практика – шифрование AES-256 в режиме GCM
Режимы шифрования с аутентификацией GCM/CCM можно сравнить с безоборотным метанием ножа – они при любых обстоятельствах бьют точно в цель. Такие бонусы мы получаем благодаря структуре "BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO", указатель на которую передаётся в параметре(4) Padding.
C этой структурой связана одна мерзкая неприятность – по неизвестным причинам, её размер везде (на MSDN и хидере bcrypt.h) указывается на 8-байт меньше, чем этого требуют функции BCryptEncrypt/Decrypt(). Правится баг обычным добавление в хвост структуры ещё одного 8-байтного поля, которое я обозначил как резерв. Иначе крипто-функции выше возвращают ошибку
"INVALID_PARAMETR"
с кодом 0xC000000d
. Вот её исправленное описание:
C-подобный:
struct BCRYPT_AUTH_CIPHER_MODE_INFO
cbSize dd 64 ;// размер данной структуры (должен быть мин.64 байт для х32)
dwInfoVer dd 1 ;// версия структуры, всегда = 1
pbNonce dd 0 ;// линк на буфер, содержащий IV (dq для х64)
cbNonce dd 0 ;// размер IV = 12 байт
pbAuthData dd 0 ;// линк на буфер с данными-аутентификации AAD (dq для х64)
cbAuthData dd 0 ;//
pbTag dd 0 ;// линк на буфер для кода-аутентификации (dq для х64)
cbTag dd 0 ;//
pbMacContext dd 0 ;// линк на буфер для МАС, (только при связанном вызове, dq для х64)
cbMacContext dd 0 ;//
cbAAD dd 0 ;// AAD (только при связанном вызове)
cbData dq 0 ;// длина данных полезной нагрузки (только при связанном вызове)
dwFlags dd 0 ;// флаг связывания вызовов = 0
Reserved dq 0 ;// <------------------ (!)добавить, чтобы на х32 размер стал 64 байт!
ends
Функция BCryptEncrypt() способна за один выстрел шифровать данные, которые расположены сразу в нескольких разных буферах – такую схему назвали "связанный вызов" (активируется флагом BCRYPT_AUTH_MODE_IN_PROGRESS_FLAG). Если разработчики её предусмотрели, значит кому-то это нужно. В этой схеме мы получаем единую зашифрованную массу из всех буферов, и один тег-аутентификации МАС на всех. Вызовы такого рода больше исключение, чем правило и в большинстве случаях отмеченные в скобках поля сбрасываются в нуль (т.е.остаются в дефолте).
Таким образом, при стандартной схеме с одним буфером исходных данных, нам остаётся заполнить только каждую пару полей: "pbNonce", "pbAuthData" и "pbTag". При этом AuthData является опциональным полем (можно не использовать AAD, тогда в место него будет пустая строка), а остальные два обязательны. Если мы шифруем данные, то значение "pbTag" должно быть адресом приёмного буфера, куда функция вернёт сгенерированный на выходе МАС (тег-аутентификации). Соответственно при расшифровке – поле используется как указатель на источник.
Некоторых пояснений требует и поле "pbNonce" в этой структуре. Как уже говорилось ранее, в контексте шифрования, термины "Nonce" и "вектор IV" нужно воспринимать как оговорку по Фрейду – они обозначают одно и то-же. Однако здесь есть нюанс, который не следует сбрасывать со-счетов!
Если в двух словах, то когда мы шифруем алгоритмами без аутентификации не используя структуру "BCRYPT_AUTH_CIPHER_MODE_INFO" (см.предыдущий пример), указатель на вектор IV и его размер должны находится в пятом и шестом параметрах функции BCryptEncrypt(). Если-же планируем шифровать в режиме GCM с тегом МАС, то в самой функции параметры IV нужно обязательно сбросить в нуль, и поместить линк на буфер IV в поле "pbNonce" данной структуры. Очевидно, что нашаманили здесь мелкомягкие так, что без танцев с бубном не разберёшь.
Ну и немного практики..
Следуя предыдущему примеру, продемонстрируем опять шифрование и сразу восстановление данных.
Чтобы охватить всё вышеизложенное, помимо ввода пароля, я добавил и ввод дополнительных данных AAD. Если такое подтверждение не нужно, то просто жмякаем Enter и получаем пустую строку AAD. Но если мы шифруем при помощи доп.данных, то и при расшифровке нужно будет ввести валидную строку AAD. Кстати макс.размер этой строки равен 64 кБайт, поэтому функцию консольного ввода scanf() требуется заменить на gets() (первая читает ввод до первого пробела, что лишает нас возможности вводить предложения). Юзаем..
C-подобный:
format pe console
entry start
;//------------
section '.inc' data readable
include 'win32ax.inc'
include 'equates\bcrypt.inc'
;//------------
.data
pcbResult dd 0
passLen dd 0
keyRandom rb 32
ivRandom rb 16
ivBackup rb 16
tagBuff rb 16
chainMode rb 64
;//<----- данные AES-256/GCM
align 16
aesKeyHndl dd 0
aesAlgHndl dd 0
aesHndl dd 0
aesBlockLen dd 0
aesObjLen dd 0
align 16
authInfo BCRYPT_AUTH_CIPHER_MODE_INFO
tagLen BCRYPT_AUTH_TAG_LENGTHS_STRUCT
align 16
authLen dd 0
authData rb 128 ;//<---- буфер под строку AAD
encryptBuff rb 128 ;//<---- буфер под зашифрованные данные
decryptBuff rb 128 ;//<---- сюда расшифруем
buff rb 0 ;//<---- пароль юзера
;//------------
.code
start: invoke SetConsoleTitle,<'*** CNG. Crypt-Next-Generation ***',0>
cinvoke printf,<10,' AES-256/GCM password crypt example',\
10,' =========================================',10,0>
;// Получить дескриптор AES-256/GCM, и собрать о нём инфу.
;// В свойствах дескриптора ставим флаг "BCRYPT_CHAIN_MODE_GCM"
invoke BCryptOpenAlgorithmProvider,aesAlgHndl,BCRYPT_AES_ALGORITHM,0,0
invoke BCryptSetProperty,[aesAlgHndl],BCRYPT_CHAINING_MODE, BCRYPT_CHAIN_MODE_GCM,30,0
invoke BCryptGetProperty,[aesAlgHndl],BCRYPT_BLOCK_LENGTH, aesBlockLen,4,pcbResult,0
invoke BCryptGetProperty,[aesAlgHndl],BCRYPT_OBJECT_LENGTH, aesObjLen,4, pcbResult,0
invoke BCryptGetProperty,[aesAlgHndl],BCRYPT_AUTH_TAG_LENGTH,tagLen,4*3,pcbResult,0
invoke BCryptGetProperty,[aesAlgHndl],BCRYPT_CHAINING_MODE,chainMode,64,pcbResult,0 ;//<--- запрос текущего режима
mov eax,[aesBlockLen]
mov ebx,eax
shl ebx,3
cinvoke printf,<10,' AES-256/GCM information ********',\
10,' Chaining mode......: %ls',\
10,' Crypt block len....: %03d byte (%d bit)',\
10,' Temp object len....: %03d byte',\
10,' Auth-tag len....: %d...%d byte, increment %d byte',0>,\
chainMode,eax,ebx,[aesObjLen],\
[tagLen.dwMinLen],[tagLen.dwMaxLen],[tagLen.dwIncrement]
;// Сгенерим рандомы Key(32) + IV(12) байт, и сохраним IV в резервный бэкап
cinvoke printf,<10,10,' 12-byte IV.........: ',0>
invoke BCryptGenRandom,0,ivRandom,12,BCRYPT_USE_SYSTEM_PREFERRED_RNG
mov ecx,12
mov esi,ivRandom
push ecx esi
call PrintHexString
pop esi ecx
mov edi,ivBackup
rep movsb
cinvoke printf,<10,' 32-byte Key........: ',0>
invoke BCryptGenRandom,0,keyRandom,32,BCRYPT_USE_SYSTEM_PREFERRED_RNG
mov ecx,32
mov esi,keyRandom
call PrintHexString
;//***********************************************
;// Запрашиваем в свои буферы пароль и "AAD"
cinvoke printf,<10,10,' ================ ENCRYPT =================',\
10,' Type pass............: ',0>
cinvoke gets,buff
invoke lstrlen,buff
mov [passLen],eax
cinvoke printf,<' Type AAD.............: ',0>
cinvoke gets,authData
invoke lstrlen,authData
mov [authLen],eax
;//************* ШИФРОВАНИЕ **********************
;// Запросить дескриптор ключа AES-256/GCM
invoke BCryptGenerateSymmetricKey,[aesAlgHndl],aesKeyHndl,0,0,keyRandom,32,0
;// Заполнить структуру "BCRYPT_AUTH_CIPHER_MODE_INFO"
mov [authInfo.pbNonce],ivRandom
mov [authInfo.cbNonce],12
mov [authInfo.pbTag],tagBuff
mov [authInfo.cbTag],16
mov eax,[authLen]
mov [authInfo.pbAuthData],authData
mov [authInfo.cbAuthData],eax
invoke BCryptEncrypt,[aesKeyHndl],buff,[passLen],authInfo,0,0,\
encryptBuff,[passLen],pcbResult,0
cinvoke printf,<10,' 12-byte origin IV....: ',0>
mov ecx,12
mov esi,ivBackup
call PrintHexString
cinvoke printf,<10,' Encrypted pass.......: %s',0>,encryptBuff
cinvoke printf,<10,' 16-byte GMAC.........: ',0>
mov ecx,16
mov esi,tagBuff
call PrintHexString
;//************* РАСШИФРОВКА **********************
;// Очистить введённые данные в буфере AAD
mov edi,authData
mov ecx,128/4
xor eax,eax
rep stosd
;// Запрос юзеру на подтверждение AAD
cinvoke printf,<10,10,' ================ DECRYPT =================',\
10,' Type origin AAD......: ',0>
cinvoke gets,authData
invoke lstrlen,authData
mov [authLen],eax
;// Узнать длину зашифрованного пароля
invoke lstrlen,encryptBuff
mov [passLen],eax
;// Заполнить структуру "BCRYPT_AUTH_CIPHER_MODE_INFO"
mov [authInfo.pbNonce],ivBackup
mov [authInfo.cbNonce],12
mov [authInfo.pbTag],tagBuff
mov [authInfo.cbTag],16
mov eax,[authLen]
mov [authInfo.pbAuthData],authData
mov [authInfo.cbAuthData],eax
;// Запросить дескриптор ключа AES-256/GCM
invoke BCryptGenerateSymmetricKey,[aesAlgHndl],aesKeyHndl,0,0,keyRandom,32,0
;// Расшифровать пароль!
invoke BCryptDecrypt,[aesKeyHndl],encryptBuff,[passLen],authInfo,0,0,\
decryptBuff,[passLen],pcbResult,0
or eax,eax
je @f
cinvoke printf,<10,' Decrypt ERROR! Get valid data. ',0>
jmp @exit
@@: cinvoke printf,<' Ok! Decrypted pass...: %s',0>,decryptBuff
@exit: cinvoke _getch
cinvoke exit,0
;//------------
;//----- Процедура вывода Hex-строки на консоль ---------
;//----- на входе: ESI = указатель на данные, ECX = длина
proc PrintHexString
@@: xor eax,eax
lodsb
push ecx esi
cinvoke printf,<'%02x',0>,eax
pop esi ecx
loop @b
ret
endp
;//------------
section '.idata' import data readable
library kernel32,'kernel32.dll',msvcrt,'msvcrt.dll',bcrypt,'bcrypt.dll'
include 'api\kernel32.inc'
include 'api\msvcrt.inc'
include 'api\bcrypt.inc'
Если расчехлить отладчик и вскормить ему этот код, то нырнув в функцию BCryptEncrypt() по F7 можно обнаружить, что функция не использует преимущества аппаратного шифрования AES-NI, а весь алгоритм реализует исключительно обычными
MOV
, XOR
и прочими. То есть почему-то Intel ввела инструкции AES-NI, а мелкософт не использует их. Может на это есть свои причины?Обратите внимание на формат результата шифрования – он включает в себя три составляющие: 12-байтный вектор IV, зашифрованный пароль, и в хвосте тег-аутентификации GМАС. А вот данные AAD как-правило не передаются принимающей стороне, иначе в них теряется смысл. Их нужно держать в тайне, накрыв протектом на уровне самой программы. Это-же относится и 32-байтному ключу шифрования AES. Рассмотрим, как справляется с этой задачей, например, браузер Хром.
5. Использование AEAD в браузерах
Поскольку высказывания ниже могут классифицироваться в рамках шпионажа, я не буду заострять внимание на мелочах, а лишь приведу общую картину. Достаточно будет сказать, что алгоритм проверенный и воркает исправно.
На предыдущем скрине, я не зря расположил результат шифрования AES/GCM в таком порядке. По сути, от перестановки мест слагаемых сумма не меняется. Однако интернет браузеры Chrome версии 80+ держат эту информацию в своих базах SQLite именно в таком виде, только добавляют к ним ещё и сигнатуру "v10". То-есть получаем сл.табличку:
Это типичный формат хранения хромом паролей и кукисов, базы которых можно найти по следующим адресам:
• Пароли – AppData\Local\Google\Chrome\User Data\Default\Login Data
• Кукисы – AppData\Local\Google\Chrome\User Data\Default\Cookies
Открываем базу кукисов в каком-нибудь редакторе SQLite3 (типа SQLite Expert Personal), и обнаруживаем в ней две таблицы с названиями "cookies" и "meta". В первой имеется столбец "encrypted_value", где и хранятся зашифрованные алгоритмом AES-256/GCM непосредственно куки различных сайтов. Если щёлкнуть на пимпу любой строки в этом столбце, то перед нами откроется встроенный Hex-редактор, где будет красоваться соответствующий закриптованный кукис. Вот как это выглядит у меня:
В процессе расшифровки данного кукиса, нам нужно просто отбросить сигнатуру "v10" и передать всю оставшуюся информацию в свой код. Отметим, что Хром не использует данные AAD, тогда для достижении цели нам остаётся добыть только 32-байтный ключ шифрования. Как и следовало ожидать, он прописан там-же, только в файле Json по адресу:
• AppData\Local\Google\Chrome\User Data\Local State
Помимо ключа, в этом файле хранится всякое барахло,
а сам ключ в нём можно найти по Json-строке
"os_crypt":{"encrypted_key":
За этой строкой и вплоть до обратной/фигурной скобки, будет лежать закодированный в Base64 ключ. Значит нам нужно сначала раскодировать его из Base64, а затем снять с него протект при помощи функции DPAPI CryptUnprotectData() (рассматривалась в этой статье). После всех этих операций мы получим ключ расшифровки кукисов алгоритмом AES-256/GCM, а так-как имеем уже IV, MAC и зашифрованные данные, то дело остаётся за малым.
6. Послесловие
Шифрование является основным методом защиты информации, однако в основе криптографии лежит не только крипт, но и ряд вспомогательных методов. В частности это не рассмотренная здесь "электронная подпись" (используется для подтверждения авторства передаваемых данных). Такие алгоритмы называют "сигнатурными", и в них подписывается обычно хэш данных, а не все данные целиком. Они используют два вида ключей – секретный для вычисления электронной подписи, и открытый для её проверки.
У нас не было-бы безопасного современного Интернета без работы В.Реймена, Д.Дэемен, Д.Виеги, Д.МакГрю и бесчисленного множества других криптографов и исследователей безопасности, которые сделали возможным использование AES в режиме GCM. Это по истине один из самых продвинутых на сегодняшний день алгоритмов шифрования, который прочно пустил свои корни во-все направления современной сферы IT.
В скрепке лежат три рассмотренных выше исполняемых файла, а так-же инклуд "вcrypt.inc" для сборки исходников.
Надеюсь ещё увидимся, удачи, пока!