В предыдущей части мы познакомились с тем, что такое симметричное и ассиметричное шифрование, посмотрели на алгоритм ассиметричного шифрования на основе задачи о рюкзаке, кратко пробежались по его реализации на C#. В конце была упомянута мысль, что эту серию статей можно продолжить и на коленке собрать безопасный простой мессенджер. Как вы помните, ассиметричное шифрование позволяет установить безопасное соединение "в одну сторону", - чтобы сервер мог безопасно отправить клиенту ответ, понадобится либо создать пару приватный-публичный ключ на клиенте, либо выкручиваться каким-то другим способом. Чтобы всё усложнить (и получить больше опыта!), мы выберем второй вариант и реализуем популярный алгоритм симметриченого шифрования AES на C++. В этой второй части мы кратко пробежимся по реализации, посмотрим, как запускать тесты при помощи фреймворка googletest в сборочной системе autoconf, и попробуем скомпилировать наш проект в WebAssembly. Пристёгивайтесь, будет жарко! ❌ Полного описания алгоритма AES. ✅ Скажу, где искать информацию. ❌ Кучи кода. ✅ Лишь пробегусь по классам. ❌ Исчерпывающего справочника по autoconf. ✅ Тут только про то, как тестировать при помощи него проекты с точки зрения пользователя вашего кода. ❌ Как настроить autoconf, чтобы он использовал компилятор emscripten, а не gcc. ✅ Мы соберем C++ библиотеку отдельно, не меняя configure.ac или Makefile.am. ❌ Установка autoconf. ✅ Рассмотрим опции для пользователей Windows.  Ещё раз: наша долгосрочная задача — сделать безопасный мессенджер. Его устройство будет простым до невозможности: пользователи смогут зарегистрироваться, отправить другому пользователю сообщение и получить список всех своих пришедших сообщений. Есть клиент (веб‑ или android‑приложение, etc), и есть сервер, где все сообщения будут храниться. Мы хотим всё сделать безопасно, потому применим такой подход — при начале общения клиент запрашивает у сервера публичный ключ K1 (или этот ключ встроен в клиент), затем генерирует ключ K2 алгоритма симметричного шифрования, ключ K2 шифрует публичным ключом сервера K1 и отправляет серверу. Сервер принимает и дешифрует — получает ключ симметричного шифрования K2. Дальнейшее общение между клиентом и сервером происходит при помощи определенного алгоритма симметричного шифрования, ключ к которому (K2) есть у каждой стороны. Теперь не только клиент может безопасно передавать свои данные серверу, но и сервер — клиенту. AES — хороший алгоритм симметричного шифрования, именно его мы и возьмём для своего мессенджера. На хабре как минимум 3 статьи, которые о нём рассказывают, так что кратко: появился он как победитель в соревновании США, где надо было придумать стойкий, быстрый и простой блочный алгоритм шифрования, который ещё можно было бы и реализовать эффективно (параллельно). Тогда пара ребят, фамилия одного из которых начинается на R, придумали этот алгоритм, и он выиграл. С тех пор он стал популярным стандартом, что отражено в его названии. Итак, и сервер, и клиент должны уметь работать с 2мя вещами — с выбранным нами алгоритмом ассиметричного шифрования (который мы реализовали в предыдущей части — здесь мы будем называть его BackpackCrypto) и с алгоритмом AES. Первое мы уже сделали в C#, но это не означает, что все наше приложение (которое будет состоять из нескольких частей) будет написано именно на нём. Давайте, чтобы было интереснее, AES сделаем на C++. Это позволит нам выиграть в оптимизации (наверное). Так или иначе, сейчас мы быстро рассмотрим, на каких языках мы можем написать приложение: Сервер можно будет организовать в микросервисной архитектуре, что позволит нам писать части бэкенда на разных языках. Клиентом может выступать как html5-приложение, написанное на Godot, так и Android-приложение, но во втором случае проблем будет больше (простого способа вызвать C# из java я не нашёл). Теперь мы разобрали, что может быть нашим клиентом. В этой статье: Кратко пройдёмся по реализации AES в С++. Посмотрим, как можно собирать и тестировать код при помощи autoconf (вместо CMake). Посмотрим, как можно собирать C++ библиотеки в WebAssebly, чтобы потом вызывать их из JavaScript при помощи emscripten. Тем самым мы убедимся, что сможем написать веб-клиент, у которого будет доступ к написанному нами 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 статические функции — расшифровка и шифровка.  Из-за того, что мы не стали говнокодить, а разбили функциональность на несколько классов, мы теперь можем протестировать каждую операцию, убедиться, что она выполняет то, что должна, а код шифровки и дешифровки (в последнем классе) выглядит очень просто: Красота, не так ли? Написание этого кода заняло у меня не один день: пришлось искать информацию, читать некоторые противоречащие друг другу источники. Конечно, вы для своей реализации можете открыть официальную специализацию и прочитать её. Если вам лень и вы хотите быстро вникнуть в то, как AES работает (а зная это, вы легко сможете реализовать его самостоятельно), то вот вам несколько полезных ссылок: https://habr.com/ru/post/534620/ Разбирает алгоритм, описывает преимущества и атаки. Как начало (вход в тему) подойдёт, но как полноценный документ, из которого можно понять всё на 100% - нет. https://habr.com/ru/post/112733/ Очень подробно описаны операции над байтами (сложение и умножение), показано, как можно вычислить матрицу SBox. Единственная статья, где правильно сказано, как делать дешифровку. https://bit.nmu.org.ua/ua/student/metod/cryptology/лекция 9.pdf Документ, где в доступном виде рассказано, как строка байтов располагается в матрицах-блоках. Хороший плюс - рассказано, как реализовать операции MixColumns и InvMixColumns. https://www.youtube.com/watch?v=CxU4ROAYGzs Видос, в котором наглядно показаны все шаги шифрования (без дешифровки, к несчастью). Операции расширения ключа показаны тоже очень наглядно. Ну и самое главное - отсюда можно взять примеры и протестировать у себя, чтобы убедиться, что каждая операция в отдельности работает корректно. Реализация base64 на C++ расположена здесь. Как считать матрицу SBox самому, а не копипастить в свой код — здесь. Ну и последнее — все операции над блоком обратимы, а это значит, что для дешифрования надо выполнить их в обратном порядке (применяя сначала последний раундовый ключ, потом предпоследний и так далее до основного).   В этой секции кратко расскажу про то, как собирать проекты, использующие эту сборочную систему. Autoconf по сути за вас составляет make‑файлы, которые вы затем в обычном порядке можете использовать, чтобы собрать своё приложение.  Инфа по этой сборочной системе (краткое введение) — здесь. Как внедрить googletest — здесь. Я не буду сравнивать CMake и autoconf, а лишь скажу, что CMake позволяет собирать проекты на разных платформах, а autoconf доступен лишь пользователям линукса. Тогда надо установить линукс). Тут есть несколько дополнительных вариантов, например, поставить виртуальную машину. Но можно даже проще — собрать код в докере! Для этого не нужно будет долго скачивать образ всей ОС, а лишь образ докера, но вы вряд ли сможете как‑то просто извлечь собранные бинарники из контейнера.  Для этого - устанавливаете докер, после чего заходите в новый контейнер в терминал и пользуетесь им как настоящим линуксом: Вы получите оболочку чуть-чуть попроще чем bash, но логика та же самая - выполняйте команды и смотрите результат. Все команды дальше я тестировал в этом окружении, так что вы можете их проверить как на Linux, так и на Windows. Для примера я покажу, как я собираю и тестирую свой же (этот) проект. Скачаем с git-а: Очистим проект от лишних файлов и создадим ./configure: Теперь создадим Makefile: Всё! Осталось только запустить тесты: Осталось самое интересное - мы попробуем собрать библиотеку в WebAssembly, а потом вызвать функции из JavaScript. Нам понадобится сам проект Emscripten. Как удобно, но я уже рассказывал про его установку в статье, где мы собирали Godot из исходников. Сейчас шаги не будут сильно отличаться — поставим ту же версию 1.39.9. Мы не будем использовать всю C++ библиотеку в JavaScript, вместо этого мы определим лишь те функции, которые хотим вызывать. Это позволит нам контролировать, что должно попасть в JavaScript, а что - нет. Статические функции будут выполнять роль обёртки над библиотекой, что позволит нам поменять типы параметров с std::string на const char *. В корневой папке проекта (где лежат файлы Makefile.am & configure.ac) создаём папку wasm. Здесь мы будем хранить файлы веб-страницы. Заходим в эту папку и создаём текстовый файл aes.cpp: На 2й строчке мы указали, где искать заголовочные файлы с вызываемыми функциями. Параметрами функции принимаем сообщение и base64-ключ. Возвращаем зашифрованную строку. Чтобы экспортировать функцию в JavaScript и вызвать её оттуда по нормальному имени без танцов с бубнами (по типу Module.__Z3myFunctionName()) есть 2 пути:  Пометить функцию для экспорта прямо в исходниках при помощи EMSCRIPTEN_KEEPALIVE из <emscripten/emscripten.h> (как мы сделали только что), Или указать функцию для экспорта в опциях компилятора (через EXPORTED_FUNCTIONS). Если вы ранее выполнили команду 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++ упадёт из‑за ошибок линковки. В моём случае все файлы библиотеки относительно корневой папки проекта располагались в include/smatrix/aes/, так что я воспользовался wildcard, который позволяет отобрать все файлы с указанным расширением. После компиляции в текущей папке появились файлы a.out.js и a.out.wasm.  Мы бы могли руками заружать a.out.wasm файл и вообще не пользоваться a.out.js, но тогда возникнет исключение, потому что файл wasm получился слишком большой (> 4 Кб). Благо, у нас есть другая опция - использовать скрипт a.out.js, после чего получить верхнеуровневый доступ к модулю. В этой же папке создадим index.html страничку, которая будет писать в консоль результат шифровки: При помощи Module.cwrap получаем саму функцию, которую можем вызывать. Module.cwrap требует аргументы: 'DoCipher' — имя функции для обёртки. 'string' — тип возвращаемого значения.  — типы аргументов. Осталось только проверить, что страничка работает правильно. Запускаем сервер: В браузере открываем localhost:8000 и сразу открываем средства разработчика. Если всё правильно, то в окне появится alert, а результат в консоли должен быть такой, как на картинке: Сегодня мы разобрали реализацию AES на C++, посмотрели, как запускать тесты при помощи autoconf и узнали, что C++ код можно экспортировать в JavaScript. В следующих статьях этой серии (если они будут) мы посмотрим ещё много чего интересного, а может, сделаем свой мессенджер! Посмотрите одну их моих предыдущих статей, где я рассказываю, как поднять свой GitLab на RedOs.  Пишите в личку (в диалоги на хабре) и я скину код архивом или ссылкой. Введение в wasm. Если впервые сталкиваетесь с этой технологией — очень хорошая обзорная статья. Emscripten Tutorial. Compiling a C/C++ Module to WebAssembly. Компилятор может выдавать целые html файлы с терминалом для откладки, при этом начальные html шаблоны можно конфигурировать. Больше возможностей компиляции. Ccall & cwrap. Официальная документация Emscripten. Типы данных JavaScript. Чтобы посмотреть, как они называются. std::string в JavaScript. Ответ на stackoverflow. Спасибо за внимание.  Самый крутой прогер на деревне.