Документация Engee
Notebook

Шифрование и аутентификация текстовых данных

Введение

В современном цифровом мире защита информации при передаче по открытым каналам является одной из ключевых задач. В данном примере представлено симметричное шифрование текстового сообщения с одновременной проверкой его целостности. В основе решения лежит алгоритм 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 используется для генерации криптографически случайных значений, а именно соли и вектора инициализации.
In [ ]:
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.

In [ ]:
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
Out[0]:
зашифровать (generic function with 1 method)

Функция расшифровки

Функция расшифровать принимает строку Base64 и пароль, возвращает исходный текст либо выбрасывает ошибку:

  • выполняется декодирование Base64 и разбор на четыре части. соль занимает байты 1–8, iv16 9–24, тег последние 32 байта, всё между ними это шифр;
  • из соли и пароля заново формируются ключ шифрования и HMAC-ключ. При верном пароле они совпадут с использованными при шифровании;
  • по шифру и восстановленному HMAC-ключу вычисляется тег и сравнивается с переданным. При несовпадении функция немедленно выбрасывает ошибку, и расшифровка не выполняется. Это ключевой этап с точки зрения безопасности.
In [ ]:
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
Out[0]:
расшифровать (generic function with 1 method)

Исходные данные

Определим пароль, на основе которого будет формироваться ключ шифрования и зададим исходный текст секретного шифруемого сообщения.

In [ ]:
пароль = "qwerty123";
текст  = "Это секретное сообщение. Cтрого конфиденциально."
Out[0]:
"Это секретное сообщение. Cтрого конфиденциально."

Шифрование

Обращаясь к функции зашифровать(), выполним шифрование исходных текстовых данных.

In [ ]:
сообщение = зашифровать(текст, пароль);

Передача сообщения

Представим, что данные переданы по открытому текстовому каналу. Получатель увидит только зашифрованные данные в формате base64. Для расшифровки ему должен быть известен пароль.

In [ ]:
пароль = nothing
текст = nothing
display(сообщение)
"OVLUQ86U2ook0hIns1PSIKCpQK4HHIgprkSwyAJZ+SBA9W/xnjO3ziCcoN2FX8uCuTDGlKgyKQFobwfXXa6ijLRbOFy+83mFPTvm+ElL1fc0BZUzAnzAwRnVjkuTrFofFxk3RqWNykPCOqLDsbSrhErCrtIerlSP7u8GUpkSBuxalf7o3CDqwwCDzKyB5Bn3Q07m1kZFDvI="

Приём и расшифровка сообщения

Рассмотрим возможные дальнейшие сценарии:

  • введён неверный пароль;
  • введён верный пароль, выполнена подмена одного бита данных;
  • введён верный пароль, передано истинное сообщение.

Неверный пароль

При вводе неверного пароля, будет выводиться сообщение об ошибке.

In [ ]:
неверный_пароль = "qwerty12345"
текст = расшифровать(сообщение, неверный_пароль);
display(text)
ОШИБКА: Данные повреждены или подделаны!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] расшифровать(сообщение::String, пароль::String)
   @ Main ./In[9]:12
 [3] top-level scope
   @ In[39]:2
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Подмена сообщения

Смоделируем ситуацию искажения данных при передаче. Отобразим содержание бит в 25-м байте.

In [ ]:
байты = base64decode(сообщение)
bitstring(байты[25])
Out[0]:
"10101110"

Заменим в нём один бит.

In [ ]:
байты[25] ⊻= 0x01
bitstring(байты[25])
Out[0]:
"10101111"

И сделает подмену сообщения.

In [ ]:
подмена = base64encode(байты)
Out[0]:
"OVLUQ86U2ook0hIns1PSIKCpQK4HHIgpr0SwyAJZ+SBA9W/xnjO3ziCcoN2FX8uCuTDGlKgyKQFobwfXXa6ijLRbOFy+83mFPTvm+ElL1fc0BZUzAnzAwRnVjkuTrFofFxk3RqWNykPCOqLDsbSrhErCrtIerlSP7u8GUpkSBuxalf7o3CDqwwCDzKyB5Bn3Q07m1kZFDvI="

Сравним символы в передаваемом и зашифрованном сообщении.

In [ ]:
for i in 1:length(сообщение)
    if сообщение[i] != подмена[i]
        println("Позиция ", i, ": '", сообщение[i], "' → '", подмена[i], "'")
    end
end
Позиция 34: 'k' → '0'

В сообщении на 34-й позиции символ заменён на . При попытке расшифровки, даже при верном пароле, будем выводиться сообщение об ошибке.

In [ ]:
пароль = "qwerty123"
расшифровать(подмена, пароль)
ОШИБКА: Данные повреждены или подделаны!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] расшифровать(сообщение::String, пароль::String)
   @ Main ./In[9]:12
 [3] top-level scope
   @ In[42]:3
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Истинное сообщение и верный пароль

Когда пароль верный и передаваемые данные в целостности, выполняется расшифровка сообщения.

In [ ]:
пароль = "qwerty123"
текст = расшифровать(сообщение, пароль)
display(текст)
"Это секретное сообщение. Cтрого конфиденциально."

Заключение

В данном примере продемонстрирована полная схема защищённой передачи текстового сообщения. Показано, что шифрование без аутентификации не обеспечивает целостности данных, а добавление HMAC позволяет обнаружить даже изменение одного бита.

Описанные принципы лежат в основе множества реальных систем. Протокол TLS, обеспечивающий безопасность HTTPS, использует аналогичные схемы. Защищённые мессенджеры применяют симметричное шифрование с аутентификацией для каждого сообщения. Системы шифрования дисков, например LUKS или BitLocker, используют блочные шифры с аутентификацией. Протоколы защищённой электронной почты, VPN-туннели, системы электронного документооборота также опираются на эти принципы.

Очевидно, что никогда не следует использовать пароли вида "qwerty123", в реальности нужен пароль с как можно большим количеством символов с разным регистром, цифрами и спецсимволами, а лучше длинная парольная фраза или случайный 256-битный ключ. Пароль нельзя передавать по тому же каналу, хранить открыто или использовать повторно.

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