Pull to refresh

Мой защищённый контейнер

C++
При разработке проекта на C++ возникла необходимость создать защищённый контейнер. Вернее, кросс-платформенные на уровне исходного кода классы, поддерживающие защищённое хранение информации от нескольких байт до гигабайтов, что делает необходимым поддержку потокового шифрования/дешифрования.
Не имея альтернативы нанять на данном этапе профессионального криптографа, приступил к созданию велосипеда.
Данная статья написана для открытого обсуждения предлагаемых решений людьми, близкими к криптографии, а также для тех, кто будет проходить этот путь с нуля, как экономия их времени и сил.

В ходе изучения и анализа информации были выбраны OpenSSL (отличная скорость работы судя по сравнению с другими библиотеками, не испытал трудностей при включении в проект, кроссплатформенность), а также алгоритм шифрования AES (Rijndael) с ключом 128, 192, 256 на выбор, как признанный (соответствующим комитетом сами знаете кого) современный стандарт шифрования.

Самый первый вопрос — криптографически стойкий генератор (псевдо)случайных чисел и его установка (сидирование). Только он годен для генерации, например, криптостойкого ключа. Алгоритм генератора, вполне логично, взят из OpenSSL, а вот с сидированием вопрос сложнее. POSIX системы, у которых есть /dev/*random, снимают с меня ответственность за случайный seed. Что касается Windows, то в OpenSSL можно взять набор сообщений к окну (лучше) и содержимое экрана (хуже). Несмотря на это, решение с сообщениями в окно мне не очень понравилось, так как это лишние проблемы пользователю. Я остановился на копии экрана плюс свои пять копеек. Они заключались в том, что я использую генерацию самим Windows уникального GUID, так что в seed перед копией экрана я добавляю некий крокодил из GetTickCount() XOR Системное_время XOR GUID c небольшими сдвигами и преобразованиями.
Кстати говоря, в реальном тесте на Windows XP SP2 x64 генератор OpenSSL заявил что уже достаточно сидирован по умолчанию. Почему — я не знаю, но в качестве потенциальной альтернативы решение предложено (и активируется на системах, где потребуется дополнительное сидирование).

Идём дальше. Предполагая, что сам ключ хранится отдельно, таким образом, контейнер должен содержать всю необходимую информацию для расшифровки. В случае AES в потоковом (cbc) режиме, необходимо хранить начальный вектор инициализации (iv0), а также точную длину данных, так как алгоритм работает с блоками по 16 байт (128 бит).

Что касается хранения начального вектора, то здесь существует стандарт RFC 3394, однако, при внимательном изучении, оказалось, что в нём используется постоянное значение начального вектора инициализации. Вместе с этим, в книге «An introduction to cryptography» чётко написано, что начальный вектор ни в коем случае не должен быть постоянным (здесь). Поэтому я отказался от этого алгоритма в пользу создания вектора инициализации криптографически стойким генератором случайных чисел, что конечно хуже чем инициализация дополнительным случайным сидом, но гораздо лучше чем постоянный вектор.

Длина начального вектора выбрана 16 байт, равной длине блока данных.

Перейдём к заголовку. Чтобы сделать длину заголовка кратной 16 байтам (размер блок AES), вспоминаем длину вектора 16 байт, тогда все дополнительные данные лучше уместить в другие 16 байт. 8 байт занял размер (ага, большие данные). Оставшиеся 8 байт я отдал под магическое число, с некоторой вероятностью гарантирующее, что ключ подобран правильно. И здесь я тоже попытался уйти от заранее заготовленного магического числа (M). Моя идея состоит в следующем: я беру заранее заготовленное 4-х байтное число-константу. Генерирую два стойких случайных 4-х байтных A1 и A2. При этом первое число я оставляю как есть, а второе изменяю с тем условием, что
A1 + A2' = M

Тогда
A2' = A2 + D

и
D = M — (A1 + A2)


При этом, для определения правильности пароля, я расшифровываю первый 16-ти байтный блок заголовка, беру два первых dword-а, суммирую и смотрю что получилось. Вероятность совпадения значений невелика, но конечна. В моём случае это вполне приемлимо (если неверен ключ, то это уже проблемы злоумышленника что он получил ложно-расшифрованные данные).

Выравнивание размера данных по 16-байтам я делаю наиболее трививально, путём дописывания в конец нужного числа случайных байтов, которые также создаю криптографически стойким генератором OpenSSL.

Таким образом, я избавляюсь от постоянных чисел в исходных данных и (как было показано выше) постоянного вектора инициализации.
Дальше дело техники: 48-байтный заголовок я кодирую AES в блочном режиме (ecb), потом идут блоки данных, шифрованные в потоковом режиме (cbc).

image

P.S. Что можно бы было улучшить:
+ сделать более случайным начальный сид в Windows (и POSIX?)
+ создавать начальный вектор инициализации при помощи сида, а не генератором
+ возможно, лучше бы было шифровать заголовок в том же потоковом режиме как часть данных
+ сделать более надёжной проверку правильности ключа
+…?
Tags:криптографияшифрованиеaesopensslзащищённый контейнер
Hubs: C++
Total votes 6: ↑5 and ↓1 +4
Views2.5K

Comments 8

Only those users with full accounts are able to leave comments. Log in, please.

Popular right now

C# Developer. Professional
April 30, 202165,000 ₽OTUS
Web-разработчик на Python
April 15, 202149,000 ₽OTUS
Машинное обучение
April 15, 202156,000 ₽Нетология
JavaScript Developer. Professional
April 15, 202172,500 ₽OTUS