Введение
Симметричное шифрование использует один и тот же ключ для шифрования и расшифровки данных — в статье разберём AES (Advanced Encryption Standard), наиболее распространённый современный алгоритм симметричного шифрования, через высокоуровневый EVP API OpenSSL, абстрагирующий конкретные детали реализации алгоритма.
Концепция
AES работает блоками фиксированного размера (16 байт), и режим работы (mode of operation) определяет, как обрабатывается сообщение, состоящее из множества таких блоков — режим CBC (Cipher Block Chaining) требует случайного вектора инициализации (IV) для каждого шифрования, а более современный GCM (Galois/Counter Mode) дополнительно даёт аутентификацию (проверку того, что зашифрованные данные не были изменены), что делает его предпочтительным выбором для большинства новых проектов по сравнению с «просто» CBC без отдельной аутентификации.
Пример кода: Симметричное шифрование
// aescipher.h — шифрование и расшифровка через AES-256-GCM (рекомендуемый современный режим)
#pragma once
#include <openssl/evp.h>
#include <QByteArray>
#include <stdexcept>
struct EncryptedData {
QByteArray ciphertext;
QByteArray iv; // вектор инициализации — должен передаваться/храниться вместе с шифротекстом
QByteArray tag; // аутентификационный тег GCM — для проверки целостности при расшифровке
};
EncryptedData encryptAesGcm(const QByteArray &plaintext, const QByteArray &key)
{
if (key.size() != 32) {
throw std::invalid_argument("Ключ AES-256 должен быть ровно 32 байта");
}
EncryptedData result;
result.iv.resize(12); // 12 байт — рекомендуемый размер IV для GCM
if (RAND_bytes(reinterpret_cast<unsigned char *>(result.iv.data()), result.iv.size()) != 1) {
throw std::runtime_error("Не удалось сгенерировать случайный IV");
}
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr,
reinterpret_cast<const unsigned char *>(key.constData()),
reinterpret_cast<const unsigned char *>(result.iv.constData()));
result.ciphertext.resize(plaintext.size() + EVP_MAX_BLOCK_LENGTH);
int outLength = 0;
EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char *>(result.ciphertext.data()), &outLength,
reinterpret_cast<const unsigned char *>(plaintext.constData()), plaintext.size());
int finalLength = 0;
EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char *>(result.ciphertext.data()) + outLength, &finalLength);
result.ciphertext.resize(outLength + finalLength);
result.tag.resize(16);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, result.tag.data());
EVP_CIPHER_CTX_free(ctx);
return result;
}
QByteArray decryptAesGcm(const EncryptedData &encrypted, const QByteArray &key)
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr,
reinterpret_cast<const unsigned char *>(key.constData()),
reinterpret_cast<const unsigned char *>(encrypted.iv.constData()));
QByteArray plaintext(encrypted.ciphertext.size(), 0);
int outLength = 0;
EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char *>(plaintext.data()), &outLength,
reinterpret_cast<const unsigned char *>(encrypted.ciphertext.constData()),
encrypted.ciphertext.size());
// Установка тега ПЕРЕД финализацией — OpenSSL проверит его и сообщит об ошибке при несовпадении
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, const_cast<char *>(encrypted.tag.constData()));
int finalLength = 0;
int success = EVP_DecryptFinal_ex(ctx, reinterpret_cast<unsigned char *>(plaintext.data()) + outLength, &finalLength);
EVP_CIPHER_CTX_free(ctx);
if (success != 1) {
throw std::runtime_error("Расшифровка не удалась — данные повреждены или ключ неверен");
}
plaintext.resize(outLength + finalLength);
return plaintext;
}
Пояснения к коду
EVP_aes_256_gcm() выбирает конкретный алгоритм и режим — переключение на другой режим (например, EVP_aes_256_cbc()) требует соответствующей корректировки остального кода (CBC не использует тег аутентификации вовсе, и его отсутствие в этом режиме не означает ошибку, а просто другую модель безопасности без встроенной проверки целостности). Уникальный, случайно сгенерированный для каждого шифрования IV (RAND_bytes) — обязательное требование GCM: повторное использование одного и того же IV с одним и тем же ключом катастрофически компрометирует безопасность всего режима GCM (а не просто снижает её немного), и IV (в отличие от самого ключа) не считается секретным — он передаётся/хранится открыто вместе с шифротекстом, что и показано структурой EncryptedData. Проверка тега аутентификации (EVP_CTRL_GCM_SET_TAG перед EVP_DecryptFinal_ex) — встроенная в сам режим GCM защита целостности, и неудачная расшифровка (success != 1) означает либо повреждение данных при передаче/хранении, либо (что значительно важнее с точки зрения безопасности) попытку подмены зашифрованных данных третьей стороной.
Подводные камни
- Повторное использование одного и того же IV с одним и тем же ключом для AES-GCM — это, возможно, самая серьёзная практическая ошибка при использовании GCM-режима: в отличие от многих других ошибок криптографии, приводящих к «просто» сниженной безопасности, повтор IV в GCM может позволить атакующему полностью восстановить ключ аутентификации и подделывать сообщения; IV должен генерироваться заново для каждого отдельного шифрования через криптографически качественный генератор случайных чисел (статья 378), как явно показано в примере.
- Игнорирование результата
EVP_DecryptFinal_ex()(проверки тега аутентификации) и использование расшифрованных данных независимо от результата этой проверки — пропуск этой проверки сводит на нет всю защиту целостности, которую даёт режим GCM, фактически превращая его обратно в шифрование без аутентификации; расшифрованные данные не должны использоваться приложением, если эта функция вернула признак неудачи. - Хранение ключа шифрования (
keyв примерах) непосредственно в исходном коде приложения или в незащищённом, легко доступном месте — аналогично подводным камням из gRPC-цикла (статья 329) и других мест цикла про безопасность, ключ должен либо запрашиваться у пользователя (производный от пароля через специальную функцию вывода ключа, key derivation function), либо храниться в защищённом системном хранилище (статья 377), а не быть зашит непосредственно в код или лежать в обычном текстовом конфигурационном файле. - Использование устаревших или ослабленных режимов работы AES (ECB-режим, в частности) — ECB шифрует каждый блок независимо без какого-либо механизма, связывающего блоки между собой, что приводит к тому, что одинаковые блоки исходных данных дают одинаковые блоки шифротекста, потенциально раскрывая структуру исходных данных даже без расшифровки (классическая иллюстрация — узнаваемые очертания изображения, «зашифрованного» в ECB-режиме остаются видны); для нового кода ECB не должен использоваться вовсе, в пользу GCM или, при необходимости совместимости со старыми системами, как минимум CBC с правильно сгенерированным IV.