Шифрование текстовых данных
Шифрование и аутентификация текстовых данных
Введение
В современном цифровом мире защита информации при передаче по открытым каналам является одной из ключевых задач. В данном примере представлено симметричное шифрование текстового сообщения с одновременной проверкой его целостности. В основе решения лежит алгоритм AES-256 в режиме CBC в сочетании с кодом аутентификации HMAC-SHA256.
Что такое AES
Advanced Encryption Standard (AES) представляет собой симметричный блочный шифр, принятый как стандарт Национальным институтом стандартов и технологий США в 2001 году. Симметричным он называется потому, что для шифрования и расшифровки используется один и тот же ключ. Блочным потому, что оперирует блоками фиксированного размера. В случае AES размер блока составляет 128 бит (16 байт), а длина ключа может быть 128, 192 или 256 бит. В данном примере используется максимальная длина 256 бит, обеспечивающая наиболее высокий уровень стойкости. AES заменил устаревший DES и сегодня применяется в TLS, Wi-Fi, системах шифрования дисков и множестве других приложений.
Проблема простого блочного шифрования
Если шифровать каждый блок независимо, такой режим называется Electronic Codebook (ECB). У него есть фундаментальный недостаток: одинаковые блоки открытого текста превращаются в одинаковые блоки шифротекста. Это позволяет наблюдателю делать выводы о структуре данных, не зная ключа. Классический пример: зашифрованное в ECB изображение всё ещё различимо по контурам. Поэтому в реальных задачах используются режимы, устраняющие эту проблему.
CBC
Cipher Block Chaining (CBC) представляет собой режим, в котором каждый блок открытого текста перед шифрованием складывается по операции XOR с предыдущим блоком шифротекста. Для первого блока вместо предыдущего используется вектор инициализации (IV). Благодаря сцеплению одинаковые блоки открытого текста дают разные блоки шифротекста. Однако CBC сам по себе обеспечивает только конфиденциальность и не защищает от изменения данных.
HMAC
Hash-based Message Authentication Code (HMAC) представляет собой механизм проверки целостности и подлинности. Он принимает ключ и данные, а на выходе даёт короткое значение фиксированной длины, называемое тегом. Вычислить правильный тег может только владелец ключа. Любое изменение данных приводит к совершенно другому тегу. Если нарушитель изменит хотя бы один бит, но не знает ключа HMAC, он не сможет подделать тег.
Authenticated Encryption
Схема, объединяющая шифрование и аутентификацию, называется Authenticated Encryption. Она даёт сразу два свойства: конфиденциальность и целостность. Простое шифрование без аутентификации второй гарантии не даёт. Более того, CBC без аутентификации уязвим к атакам, позволяющим постепенно расшифровывать сообщение без ключа. Добавление HMAC и проверка тега до расшифровки полностью закрывают эту уязвимость. В данном примере используется схема encrypt-then-MAC: сначала данные шифруются, затем по шифротексту вычисляется тег. При приёме сначала проверяется тег, и только при совпадении выполняется расшифровка.
Присоединение библиотек
Присоединим необходимые библиотеки:
- Nettle представляет собой обёртку над библиотекой GNU Nettle и предоставляет реализацию AES, режима CBC, HMAC и вспомогательных операций с дополнением блоков;
- Base64 отвечает за кодирование и декодирование в формат Base64. Поскольку шифротекст представляет собой произвольные байты, его нельзя напрямую передавать по текстовым каналам. Base64 преобразует байты в последовательность печатных символов, пригодную для электронной почты, мессенджеров, JSON и других текстовых протоколов;
- Random используется для генерации криптографически случайных значений, а именно соли и вектора инициализации.
using Nettle, Base64, Random
Функция шифрования
Функция зашифровать принимает текст и пароль, возвращает строку Base64 со всеми данными для расшифровки.
соль это случайные 8 байт, добавляемые к паролю, чтобы одинаковые пароли давали разные ключи. Не секретна, передаётся открыто.
Ключ формируется функцией gen_key32_iv16 из пароля и соли. Его длина 32 байта (256 бит) для AES-256. Функция детерминирована, поэтому получатель восстанавливает ключ сам.
iv16 это случайные 16 байт для первого блока CBC. Должен быть уникальным для каждого сообщения, поэтому генерируется случайно. Передаётся открыто.
HMAC-ключ это отдельный секретный ключ для тега, получаемый хешированием соли и пароля через SHA-256. Ключи шифрования и аутентификации не должны совпадать.
SHA-256 это хеш-функция, преобразующая данные в 256 бит. Малейшее изменение входа даёт совершенно другой результат.
Шифрование выполняется через encrypt с AES-256 и режимом CBC. Поскольку AES работает блоками по 16 байт, применяется дополнение add_padding_PKCS5(): в конец добавляются байты, равные их количеству. Результат называется шифротекстом.
Тег это HMAC-SHA256 по шифротексту длиной 32 байта. Он доказывает, что шифротекст не изменён. При приёме тег вычисляется заново; при несовпадении расшифровка отклоняется.
Все компоненты объединяются в пакет: соль, iv16, шифр, тег. Порядок позволяет однозначно разделить пакет, так как размеры фиксированы. Пакет кодируется в Base64.
function зашифровать(текст, пароль)
соль = rand(UInt8, 8)
(ключ, _) = gen_key32_iv16(Vector{UInt8}(пароль), соль)
iv16 = rand(UInt8, 16)
hmac_ключ = hex2bytes(hexdigest("sha256", соль, Vector{UInt8}(пароль)))
шифр = encrypt(Encryptor("AES256", ключ), :CBC, iv16, add_padding_PKCS5(Vector{UInt8}(текст), 16))
тег = hex2bytes(hexdigest("sha256", hmac_ключ, шифр))
return base64encode(vcat(соль, iv16, шифр, тег))
end
Функция расшифровки
Функция расшифровать принимает строку Base64 и пароль, возвращает исходный текст либо выбрасывает ошибку:
- выполняется декодирование Base64 и разбор на четыре части.
сользанимает байты 1–8,iv169–24,тегпоследние 32 байта, всё между ними этошифр; - из соли и пароля заново формируются ключ шифрования и HMAC-ключ. При верном пароле они совпадут с использованными при шифровании;
- по шифру и восстановленному HMAC-ключу вычисляется тег и сравнивается с переданным. При несовпадении функция немедленно выбрасывает ошибку, и расшифровка не выполняется. Это ключевой этап с точки зрения безопасности.
function расшифровать(сообщение, пароль)
принято = base64decode(сообщение)
соль = принято[1:8]
iv16 = принято[9:24]
шифр = принято[25:end-32]
тег = принято[end-31:end]
(ключ, _) = gen_key32_iv16(Vector{UInt8}(пароль), соль)
hmac_ключ = hex2bytes(hexdigest("sha256", соль, Vector{UInt8}(пароль)))
if hex2bytes(hexdigest("sha256", hmac_ключ, шифр)) != тег
error("ОШИБКА: Данные повреждены или подделаны!")
end
return String(trim_padding_PKCS5(decrypt(Decryptor("AES256", ключ), :CBC, iv16, шифр)))
end
Исходные данные
Определим пароль, на основе которого будет формироваться ключ шифрования и зададим исходный текст секретного шифруемого сообщения.
пароль = "qwerty123";
текст = "Это секретное сообщение. Cтрого конфиденциально."
Шифрование
Обращаясь к функции зашифровать(), выполним шифрование исходных текстовых данных.
сообщение = зашифровать(текст, пароль);
Передача сообщения
Представим, что данные переданы по открытому текстовому каналу. Получатель увидит только зашифрованные данные в формате base64. Для расшифровки ему должен быть известен пароль.
пароль = nothing
текст = nothing
display(сообщение)
Приём и расшифровка сообщения
Рассмотрим возможные дальнейшие сценарии:
- введён неверный пароль;
- введён верный пароль, выполнена подмена одного бита данных;
- введён верный пароль, передано истинное сообщение.
Неверный пароль
При вводе неверного пароля, будет выводиться сообщение об ошибке.
неверный_пароль = "qwerty12345"
текст = расшифровать(сообщение, неверный_пароль);
display(text)
Подмена сообщения
Смоделируем ситуацию искажения данных при передаче. Отобразим содержание бит в 25-м байте.
байты = base64decode(сообщение)
bitstring(байты[25])
Заменим в нём один бит.
байты[25] ⊻= 0x01
bitstring(байты[25])
И сделает подмену сообщения.
подмена = base64encode(байты)
Сравним символы в передаваемом и зашифрованном сообщении.
for i in 1:length(сообщение)
if сообщение[i] != подмена[i]
println("Позиция ", i, ": '", сообщение[i], "' → '", подмена[i], "'")
end
end
В сообщении на 34-й позиции символ заменён на . При попытке расшифровки, даже при верном пароле, будем выводиться сообщение об ошибке.
пароль = "qwerty123"
расшифровать(подмена, пароль)
Истинное сообщение и верный пароль
Когда пароль верный и передаваемые данные в целостности, выполняется расшифровка сообщения.
пароль = "qwerty123"
текст = расшифровать(сообщение, пароль)
display(текст)
Заключение
В данном примере продемонстрирована полная схема защищённой передачи текстового сообщения. Показано, что шифрование без аутентификации не обеспечивает целостности данных, а добавление HMAC позволяет обнаружить даже изменение одного бита.
Описанные принципы лежат в основе множества реальных систем. Протокол TLS, обеспечивающий безопасность HTTPS, использует аналогичные схемы. Защищённые мессенджеры применяют симметричное шифрование с аутентификацией для каждого сообщения. Системы шифрования дисков, например LUKS или BitLocker, используют блочные шифры с аутентификацией. Протоколы защищённой электронной почты, VPN-туннели, системы электронного документооборота также опираются на эти принципы.
Очевидно, что никогда не следует использовать пароли вида "qwerty123", в реальности нужен пароль с как можно большим количеством символов с разным регистром, цифрами и спецсимволами, а лучше длинная парольная фраза или случайный 256-битный ключ. Пароль нельзя передавать по тому же каналу, хранить открыто или использовать повторно.
Надёжная защита сообщения складывается из нескольких элементов: стойкого алгоритма шифрования, правильно выбранного режима, механизма аутентификации, корректного формирования ключей и дисциплины обращения с паролями.