Различные статьи по Qt и c++

Симметричное шифрование: AES через OpenSSL EVP API в Qt-приложении

Введение

Симметричное шифрование использует один и тот же ключ для шифрования и расшифровки данных — в статье разберём 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.