В предыдущей части мы познакомились с тем, что такое симметричное и ассиметричное шифрование, посмотрели на алгоритм ассиметричного шифрования на основе задачи о рюкзаке, кратко пробежались по его реализации на C#. В конце была упомянута мысль, что эту серию статей можно продолжить и на коленке собрать безопасный простой мессенджер. Как вы помните, ассиметричное шифрование позволяет установить безопасное соединение "в одну сторону", - чтобы сервер мог безопасно отправить клиенту ответ, понадобится либо создать пару приватный-публичный ключ на клиенте, либо выкручиваться каким-то другим способом. Чтобы всё усложнить (и получить больше опыта!), мы выберем второй вариант и реализуем популярный алгоритм симметриченого шифрования AES на C++. В этой второй части мы кратко пробежимся по реализации, посмотрим, как запускать тесты при помощи фреймворка googletest в сборочной системе autoconf, и попробуем скомпилировать наш проект в WebAssembly. Пристёгивайтесь, будет жарко!
Чего не будет в этой статье:
❌ Полного описания алгоритма AES. |
✅ Скажу, где искать информацию. |
❌ Кучи кода. |
✅ Лишь пробегусь по классам. |
❌ Исчерпывающего справочника по autoconf. |
✅ Тут только про то, как тестировать при помощи него проекты с точки зрения пользователя вашего кода. |
❌ Как настроить autoconf, чтобы он использовал компилятор emscripten, а не gcc. |
✅ Мы соберем C++ библиотеку отдельно, не меняя |
❌ Установка autoconf. |
✅ Рассмотрим опции для пользователей Windows. |
Вступление
Клиент-сервер
Ещё раз: наша долгосрочная задача — сделать безопасный мессенджер. Его устройство будет простым до невозможности: пользователи смогут зарегистрироваться, отправить другому пользователю сообщение и получить список всех своих пришедших сообщений. Есть клиент (веб‑ или android‑приложение, etc), и есть сервер, где все сообщения будут храниться. Мы хотим всё сделать безопасно, потому применим такой подход — при начале общения клиент запрашивает у сервера публичный ключ K1 (или этот ключ встроен в клиент), затем генерирует ключ K2 алгоритма симметричного шифрования, ключ K2 шифрует публичным ключом сервера K1 и отправляет серверу. Сервер принимает и дешифрует — получает ключ симметричного шифрования K2. Дальнейшее общение между клиентом и сервером происходит при помощи определенного алгоритма симметричного шифрования, ключ к которому (K2) есть у каждой стороны. Теперь не только клиент может безопасно передавать свои данные серверу, но и сервер — клиенту.
Что такое AES?
AES — хороший алгоритм симметричного шифрования, именно его мы и возьмём для своего мессенджера. На хабре как минимум 3 статьи, которые о нём рассказывают, так что кратко: появился он как победитель в соревновании США, где надо было придумать стойкий, быстрый и простой блочный алгоритм шифрования, который ещё можно было бы и реализовать эффективно (параллельно). Тогда пара ребят, фамилия одного из которых начинается на R, придумали этот алгоритм, и он выиграл. С тех пор он стал популярным стандартом, что отражено в его названии.
Какой язык выбрать?
Итак, и сервер, и клиент должны уметь работать с 2мя вещами — с выбранным нами алгоритмом ассиметричного шифрования (который мы реализовали в предыдущей части — здесь мы будем называть его BackpackCrypto) и с алгоритмом AES. Первое мы уже сделали в C#, но это не означает, что все наше приложение (которое будет состоять из нескольких частей) будет написано именно на нём. Давайте, чтобы было интереснее, AES сделаем на C++. Это позволит нам выиграть в оптимизации (наверное). Так или иначе, сейчас мы быстро рассмотрим, на каких языках мы можем написать приложение:
Сервер можно будет организовать в микросервисной архитектуре, что позволит нам писать части бэкенда на разных языках. Клиентом может выступать как html5-приложение, написанное на Godot, так и Android-приложение, но во втором случае проблем будет больше (простого способа вызвать C# из java я не нашёл).
Теперь мы разобрали, что может быть нашим клиентом. В этой статье:
Кратко пройдёмся по реализации AES в С++.
Посмотрим, как можно собирать и тестировать код при помощи autoconf (вместо CMake).
Посмотрим, как можно собирать C++ библиотеки в WebAssebly, чтобы потом вызывать их из JavaScript при помощи emscripten. Тем самым мы убедимся, что сможем написать веб-клиент, у которого будет доступ к написанному нами AES.
AES
Алгоритм
AES — алгоритм блочного шифрования. Ключ может быть длины 128 или 256 бит. Возьмём первый вариант. Алгоритм разбивает исходный текст на блоки, после чего к каждому блоку применяется несколько одинаковых раундов преобразования, которые также требуют значение ключа. Для этого ключ вносится в таблицу байт 4×4. По ключу при помощи несложных операций образуется ещё 10 раундовых ключей. Для каждого раунда над блоком (помимо первого) используется свой раундовый ключ. Операция получения раундовых ключей по основному ключу детерминирована и может быть выполнена как при шифровке данных, так и при дешифровке. Раундовые ключи одинаковы для всех блоков. Каждый блок исходного текста подвергается одним и тем же изменениям, после чего все блоки сворачиваются обратно в строку и получается набор байт, который и является не чем иным как шифртекстом. Иными словами, два одинаковых блока plain‑текста будут преобразованы в два других одинаковых блока шифртекста.
Реализация
Для преобразований удобно сделать отдельный класс матрицы 4x4. Поскольку после шифрования получатся сырые байты, которые неудобно будет передавать по сети, воспользуемся конвертацией в base64.
Кратко пройдусь по классам, которые получились в результате:
class Matrix
— матрица с шаблонным типом элементов, для создания которой можно сделать свои аллокаторы.struct Err
— структура с ошибками, которые будет выбрасывать код библиотеки в нехороших ситуациях.class ElemPtr
&struct MatrixPtr
— эти вещи нужны для того, чтобы к элементам матрицы можно было обращаться при помощи операции[]
. Это было необязательно, но для будущего применения классаMatrix
может быть полезно.struct Allocator
— абстрактная структура аллокатора, если для матрицы вам нужен свой аллокатор, надо будет реализовывать эти методы.-
struct ArrayAllocator : public Allocator
— реализация аллокатора, которая хранит все элементы матрицы в массиве.На этом мы подготовили почву для непосредственно AES — теперь мы можем использовать матрицу с выбранным аллокатором, кидать ошибки и обращаться к элементам матрицы удобным для кода способом.
class Byte final
— обёртка надunsigned char
, которая реализует операции над полем + получение обратного элемента, что и требуется для раундовых преобразований и вычисления матрицы SBox.-
class StateMatrix : public Matrix<Byte>
— матрица состояния 4×4, здесь реализация всех раундовых преобразований шифровки и дешифровки:ShiftRows & InvShiftRows
— сдвиг строк.SubBytes & InvSubBytes
— замена байт.MixColumns & InvMixColumns
— линейное преобразование.void Add(const StateMatrix&);
— побитно складывается с другой матрицей.
class SBoxMatrix : public Matrix<Byte>
— матрица, которая используется для хранения байтовых отображений в следующих 2х классах.class SBox final & class InvSBox final
— классы, которые вычисляют и хранят соответственно матрицы SBox и InvSBox.class MixColumns final
— как и предыдущие 2 предоставляет сугубо статические методы. Здесь хранятся матрицы для линейных преобразований и реализована сама логика этих и обратных операций. КлассStateMatrix
использует этот и предыдущие классы, чтобы выполнить раундовые операции над блоком.class RconMatrix : protected Matrix<Byte>
— эта матрица используется в алгоритме расписания ключей, она прячет все операции над матрицей, предоставляя только возможность получить столбец по индексу.class Key final
— загружает ключ из base64-строки в матрицу 4×4 и сразу считает расписание ключей. АгрегируетStateMatrix
.-
Файл
"base64.h"
— содержит 2 статические функции для преобразования байтовых строк в base64-строки и обратно.Все операции готовы! Осталось, собственно, преобразовать исходный текст блоки и вызвать функции для каждого блока:
class Unroller final
— класс, который предоставляет статические функции для развертывания строки в вектор матриц и наоборот — сворачивания вектора матриц состояний в строку.class AdvancedEncryptionStandard final
— класс, в котором 2 статические функции — расшифровка и шифровка.
Из-за того, что мы не стали говнокодить, а разбили функциональность на несколько классов, мы теперь можем протестировать каждую операцию, убедиться, что она выполняет то, что должна, а код шифровки и дешифровки (в последнем классе) выглядит очень просто:
#include "../Err.h"
#include "AES.h"
#include "Key.h"
#include "StateMatrix.h"
#include "Unroller.h"
using namespace SM::AES;
using namespace std;
string AdvancedEncryptionStandard::Cipher(string plain_text, string key_base64)
{
Key key = Key(key_base64.c_str());
vector<StateMatrix> blocks = Unroller::ToBlocksPlain(plain_text.c_str());
for (size_t block_i = 0; block_i < blocks.size(); block_i++) {
StateMatrix& block = blocks[block_i];
// First round:
block.Add(key.GetKey());
// 9 rounds of ciphering:
for (int round_i = 0; round_i < key.ROUNDS_COUNT - 1; round_i++) {
block.SubBytes();
block.ShiftRows();
block.MixColumns();
block.Add(key.GetRoundKey(round_i));
}
// Final round:
block.SubBytes();
block.ShiftRows();
block.Add(key.GetRoundKey(key.ROUNDS_COUNT - 1));
}
return Unroller::ToStringBase64(blocks);
}
Красота, не так ли?
Ресурсы
Написание этого кода заняло у меня не один день: пришлось искать информацию, читать некоторые противоречащие друг другу источники. Конечно, вы для своей реализации можете открыть официальную специализацию и прочитать её. Если вам лень и вы хотите быстро вникнуть в то, как AES работает (а зная это, вы легко сможете реализовать его самостоятельно), то вот вам несколько полезных ссылок:
Разбирает алгоритм, описывает преимущества и атаки. Как начало (вход в тему) подойдёт, но как полноценный документ, из которого можно понять всё на 100% - нет. |
|
Очень подробно описаны операции над байтами (сложение и умножение), показано, как можно вычислить матрицу SBox. Единственная статья, где правильно сказано, как делать дешифровку. |
|
https://bit.nmu.org.ua/ua/student/metod/cryptology/лекция 9.pdf |
Документ, где в доступном виде рассказано, как строка байтов располагается в матрицах-блоках. Хороший плюс - рассказано, как реализовать операции MixColumns и InvMixColumns. |
Видос, в котором наглядно показаны все шаги шифрования (без дешифровки, к несчастью). Операции расширения ключа показаны тоже очень наглядно. Ну и самое главное - отсюда можно взять примеры и протестировать у себя, чтобы убедиться, что каждая операция в отдельности работает корректно. |
Ещё пара советов:
Реализация base64 на C++ расположена здесь.
Как считать матрицу SBox самому, а не копипастить в свой код — здесь.
Ну и последнее — все операции над блоком обратимы, а это значит, что для дешифрования надо выполнить их в обратном порядке (применяя сначала последний раундовый ключ, потом предпоследний и так далее до основного).
string AdvancedEncryptionStandard::Decipher(string cipher_base64_text, string key_base64) {
Key key = Key(key_base64.c_str());
vector<StateMatrix> blocks = Unroller::ToBlocksBase64(cipher_base64_text.c_str());
for (size_t block_i = 0; block_i < blocks.size(); block_i++) {
StateMatrix& block = blocks[block_i];
// Inversion of final round:
block.Add(key.GetRoundKey(key.ROUNDS_COUNT - 1));
block.InvShiftRows();
block.InvSubBytes();
// 9 rounds of deciphering:
for (int round_i = key.ROUNDS_COUNT - 2; round_i >= 0; round_i--) {
block.Add(key.GetRoundKey(round_i));
block.InvMixColumns();
block.InvShiftRows();
block.InvSubBytes();
}
// Inversion of first round:
block.Add(key.GetKey());
}
return Unroller::ToStringPlain(blocks);
}
Сборочная система autoconf
В этой секции кратко расскажу про то, как собирать проекты, использующие эту сборочную систему.
Autoconf по сути за вас составляет make‑файлы, которые вы затем в обычном порядке можете использовать, чтобы собрать своё приложение.
Я не буду сравнивать CMake и autoconf, а лишь скажу, что CMake позволяет собирать проекты на разных платформах, а autoconf доступен лишь пользователям линукса.
Я - пользователь windows!
Тогда надо установить линукс). Тут есть несколько дополнительных вариантов, например, поставить виртуальную машину. Но можно даже проще — собрать код в докере! Для этого не нужно будет долго скачивать образ всей ОС, а лишь образ докера, но вы вряд ли сможете как‑то просто извлечь собранные бинарники из контейнера.
Для этого - устанавливаете докер, после чего заходите в новый контейнер в терминал и пользуетесь им как настоящим линуксом:
docker run -it --entrypoint /bin/sh prantlf/alpine-make-gcc:latest
Вы получите оболочку чуть-чуть попроще чем bash, но логика та же самая - выполняйте команды и смотрите результат. Все команды дальше я тестировал в этом окружении, так что вы можете их проверить как на Linux, так и на Windows.
Для примера я покажу, как я собираю и тестирую свой же (этот) проект. Скачаем с git-а:
apk update && apk add git # если у вас ОС alpine и вы хотите поставить git
git clone --recursive https://url.git
cd sparse-matrix/
Очистим проект от лишних файлов и создадим ./configure
:
autoreconf -fisv
ls ./configure # проверить, что файл появился
Теперь создадим Makefile:
./configure
ls Makefile # проверить, что файл появился
Всё! Осталось только запустить тесты:
make check
cat tests/test.log # посмотреть инфу по пройденным тестам
Emscripten
Осталось самое интересное - мы попробуем собрать библиотеку в WebAssembly, а потом вызвать функции из JavaScript.
Установка
Нам понадобится сам проект Emscripten. Как удобно, но я уже рассказывал про его установку в статье, где мы собирали Godot из исходников. Сейчас шаги не будут сильно отличаться — поставим ту же версию 1.39.9
.
Статические функции
Мы не будем использовать всю C++ библиотеку в JavaScript, вместо этого мы определим лишь те функции, которые хотим вызывать. Это позволит нам контролировать, что должно попасть в JavaScript, а что - нет. Статические функции будут выполнять роль обёртки над библиотекой, что позволит нам поменять типы параметров с std::string
на const char *
.
В корневой папке проекта (где лежат файлы Makefile.am
& configure.ac
) создаём папку wasm
. Здесь мы будем хранить файлы веб-страницы. Заходим в эту папку и создаём текстовый файл aes.cpp
:
#include <string>
#include "../include/smatrix/aes/AES.h"
#include <emscripten/emscripten.h>
#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif
EXTERN EMSCRIPTEN_KEEPALIVE const char * DoCipher(char * message, char * key) {
return SM::AES::AdvancedEncryptionStandard::Cipher(std::string(message), std::string(key)).c_str();
}
На 2й строчке мы указали, где искать заголовочные файлы с вызываемыми функциями. Параметрами функции принимаем сообщение и base64-ключ. Возвращаем зашифрованную строку.
Чтобы экспортировать функцию в JavaScript и вызвать её оттуда по нормальному имени без танцов с бубнами (по типу Module.__Z3myFunctionName()
) есть 2 пути:
Пометить функцию для экспорта прямо в исходниках при помощи
EMSCRIPTEN_KEEPALIVE
из<emscripten/emscripten.h>
(как мы сделали только что),Или указать функцию для экспорта в опциях компилятора (через
EXPORTED_FUNCTIONS
).
Сборка в wasm
Если вы ранее выполнили команду source ./emsdk_env.sh
в папке с Emscripten, то в PATH
появится парочка команд. Одной из них — em++
— мы и воспользуемся, чтобы собрать wasm‑файл. Вот краткое описание некоторых флагов, которые принимает этот компилятор:
-std=c++20
— версия C++.-Wno-braced-scalar-init
— можно передать некоторые аргументы, как у обычного компилятора. Тут отключается warning.-s STANDALONE_WASM
— сделать wasm‑файл максимально портируемым (но при использовании в веб‑странице можно получить ошибку!).-s EXPORTED_FUNCTIONS="['_DoCipher']"
— экспортировать функцию.-s EXPORTED_RUNTIME_METHODS="['cwrap']"
— служебные функции wasm, к которым мы хотим иметь доступ из JavaScript.-fsanitize=address
— подробные сообщения об ошибках с памятью. Помогает хотя бы локализовать возникновение segfault.
em++ -Wno-braced-scalar-init -std=c++20 -s EXPORTED_RUNTIME_METHODS="['cwrap']" aes.cpp ../include/smatrix/aes/*.cpp
Первым указываем файл со статическими функциями, затем все остальные модули, которые необходимо собрать, чтобы статические функции не потеряли реализации. Если их не указать, скорее всего, em++
упадёт из‑за ошибок линковки. В моём случае все файлы библиотеки относительно корневой папки проекта располагались в include/smatrix/aes/
, так что я воспользовался wildcard, который позволяет отобрать все файлы с указанным расширением.
После компиляции в текущей папке появились файлы a.out.js
и a.out.wasm
.
Веб-страница
Мы бы могли руками заружать a.out.wasm
файл и вообще не пользоваться a.out.js
, но тогда возникнет исключение, потому что файл wasm получился слишком большой (> 4 Кб). Благо, у нас есть другая опция - использовать скрипт a.out.js
, после чего получить верхнеуровневый доступ к модулю.
В этой же папке создадим index.html
страничку, которая будет писать в консоль результат шифровки:
<html>
<body>
<script src="a.out.js"></script>
Some text.
<script>
Module.onRuntimeInitialized = function() {
alert("Check console!");
const myStrMessage = 'Hello, world!';
const myKey = 'K34VFiiu0qar9xWICc9PPA==';
var DoCipher = Module.cwrap('DoCipher', 'string', ['string', 'string']);
const result = DoCipher(myStrMessage, myKey);
console.log(result);
}
</script>
</body>
</html>
При помощи Module.cwrap
получаем саму функцию, которую можем вызывать. Module.cwrap
требует аргументы:
'DoCipher'
— имя функции для обёртки.'string'
— тип возвращаемого значения.['string', 'string']
— типы аргументов.
Запуск сервера
Осталось только проверить, что страничка работает правильно. Запускаем сервер:
python3 -m http.server
В браузере открываем localhost:8000 и сразу открываем средства разработчика. Если всё правильно, то в окне появится alert, а результат в консоли должен быть такой, как на картинке:
Итоги
Сегодня мы разобрали реализацию AES на C++, посмотрели, как запускать тесты при помощи autoconf и узнали, что C++ код можно экспортировать в JavaScript. В следующих статьях этой серии (если они будут) мы посмотрим ещё много чего интересного, а может, сделаем свой мессенджер!
Если вы не знаете, где хранить свой код:
Посмотрите одну их моих предыдущих статей, где я рассказываю, как поднять свой GitLab на RedOs.
Если вам нужен проект, который получился в итоге:
Пишите в личку (в диалоги на хабре) и я скину код архивом или ссылкой.
Полезные ссылки
Введение в wasm. Если впервые сталкиваетесь с этой технологией — очень хорошая обзорная статья.
Compiling a C/C++ Module to WebAssembly. Компилятор может выдавать целые html файлы с терминалом для откладки, при этом начальные html шаблоны можно конфигурировать.
Больше возможностей компиляции.
Ccall & cwrap. Официальная документация Emscripten.
Типы данных JavaScript. Чтобы посмотреть, как они называются.
std::string в JavaScript. Ответ на stackoverflow.
Спасибо за внимание.
Комментарии (8)
BigGiraffe
00.00.0000 00:00+1Сервер принимает и дешифрует — получает ключ симметричного шифрования K2.
С ключом не дешифруют, а расшифровывают :) Знакомые мне криптографы очень обижаются, когда путают два этих понятия. Терминология с википедии не даст соврать.
CadMan77
00.00.0000 00:00+1Недавно меня начала интересовать тема шифрования. Увидел тут цикл ваших статей, обрадовался и решил внимательно прочитать его с самого начала. Судя по ссылкам в статьях самым началом цикла является "задача о рюкзаке". Вот на попытке чтения статьи описывающей задачу моё знакомство с циклом и застопорилось.
Статья про задачу не выглядит законченной...
UPDATE:
Vivaldi for Android почему-то не показал спец-знаки.
emaxx
А где же главный совет - никогда не используйте наколеночные реализации криптографии с реальными данными?
В криптографии много граблей, которые невозможно заметить без знания кучи теории и стандартных атак, или хотя бы без детального анализа общепризнанных реализаций. Конкретно для AES наглядный пример в https://www.winmill.com/blog/2022/08/05/incorrect-aes-implementation-leaves-system-vulnerable/, или можно посмотреть на старые уязвимости типа CVE-2016-7440 в wolfSSL.
sci_nov
А есть ли где-нибудь систематизированное описание "граблей" на русском языке?
Number571
Думаю "Практическая криптография" от Шнайера и Фергюсона подойдёт.
sci_nov
Шнайер + Фергюсон = Шнеерсон :).
emaxx
Так в том и дело, что люди годами учат матчасть, изучают "эволюцию" алгоритмов, открытые раньше бреши и тренируются ломать чужие кривые протоколы или реализации. Это не то, что можно изучить за пару выходных или за чтением одной книжки, какой бы хорошей она ни была.
Я не говорю, что играться самому с криптографией это плохо. Как разминка для ума и как игрушка для экспериментов это отлично. Но в продакшен, к реальным людям и реальным данным, без аудитов или проверенных реализаций нельзя.
sci_nov
При наличии образования можно и изучить, там ничего сверхъестественного нет, просто эти знания да, надо накопить опытом, не всё сразу очевидно.