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

Асимметричное шифрование: RSA, генерация ключей, шифрование/расшифровка

Введение

Симметричное шифрование (статья 373) требует, чтобы обе стороны заранее владели одним и тем же секретным ключом — асимметричное шифрование решает проблему первоначального безопасного обмена этим ключом через пару математически связанных ключей: публичный (можно свободно передавать кому угодно) и приватный (должен храниться в секрете). В статье разберём RSA — наиболее распространённый алгоритм асимметричного шифрования.

Концепция

RSA позволяет зашифровать данные публичным ключом так, что расшифровать их может только владелец соответствующего приватного ключа — это решает задачу безопасной передачи данных без необходимости заранее обмениваться секретным ключом по какому-либо защищённому каналу (классическая проблема «курицы и яйца» симметричного шифрования). На практике RSA значительно медленнее AES и ограничен по объёму данных, которые можно зашифровать за один раз (зависит от размера ключа) — поэтому типичная практическая схема (гибридное шифрование) использует RSA только для шифрования случайного, короткого симметричного ключа, которым затем шифруются собственно данные через AES (статья 373), сочетая преимущества обоих подходов.

Пример кода: Асимметричное шифрование

// Генерация пары RSA-ключей через OpenSSL EVP API
#include <openssl/evp.h>
#include <openssl/pem.h>

EVP_PKEY *generateRsaKeyPair()
{
    EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
    EVP_PKEY_keygen_init(ctx);
    EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048); // 2048 бит — разумный минимум для большинства задач на сегодня

    EVP_PKEY *keyPair = nullptr;
    EVP_PKEY_keygen(ctx, &keyPair);
    EVP_PKEY_CTX_free(ctx);

    return keyPair;
}

// Сохранение ключей в файлы PEM-формата для последующего использования/передачи публичного ключа
void saveKeysToFiles(EVP_PKEY *keyPair, const QString &privateKeyPath, const QString &publicKeyPath)
{
    FILE *privFile = fopen(privateKeyPath.toStdString().c_str(), "wb");
    PEM_write_PrivateKey(privFile, keyPair, nullptr, nullptr, 0, nullptr, nullptr);
    fclose(privFile);

    FILE *pubFile = fopen(publicKeyPath.toStdString().c_str(), "wb");
    PEM_write_PUBKEY(pubFile, keyPair);
    fclose(pubFile);
}
// Гибридное шифрование — практическая схема, сочетающая RSA и AES (статья 373)
struct HybridEncryptedPackage {
    QByteArray encryptedAesKey;  // случайный AES-ключ, зашифрованный публичным RSA-ключом получателя
    EncryptedData aesEncryptedData; // сами данные, зашифрованные AES-GCM (статья 373)
};

HybridEncryptedPackage hybridEncrypt(const QByteArray &plaintext, EVP_PKEY *recipientPublicKey)
{
    // Шаг 1: генерация случайного AES-ключа для ЭТОГО конкретного сообщения
    QByteArray aesKey(32, 0);
    RAND_bytes(reinterpret_cast<unsigned char *>(aesKey.data()), aesKey.size());

    // Шаг 2: шифрование самих данных через AES-GCM — быстро, без ограничения по объёму
    HybridEncryptedPackage package;
    package.aesEncryptedData = encryptAesGcm(plaintext, aesKey);

    // Шаг 3: шифрование самого AES-ключа через RSA публичным ключом получателя —
    // только владелец соответствующего приватного ключа сможет его расшифровать
    EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(recipientPublicKey, nullptr);
    EVP_PKEY_encrypt_init(ctx);
    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING); // OAEP — современный, безопасный padding

    size_t encryptedKeyLength;
    EVP_PKEY_encrypt(ctx, nullptr, &encryptedKeyLength,
                     reinterpret_cast<const unsigned char *>(aesKey.constData()), aesKey.size());

    package.encryptedAesKey.resize(encryptedKeyLength);
    EVP_PKEY_encrypt(ctx, reinterpret_cast<unsigned char *>(package.encryptedAesKey.data()), &encryptedKeyLength,
                     reinterpret_cast<const unsigned char *>(aesKey.constData()), aesKey.size());
    package.encryptedAesKey.resize(encryptedKeyLength);

    EVP_PKEY_CTX_free(ctx);
    return package;
}

Пояснения к коду

EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) задаёт размер ключа в битах — больший размер даёт большую криптографическую стойкость, но за счёт значительно более медленных операций шифрования/расшифровки и большего размера самого ключа; 2048 бит — практический минимум, рекомендуемый для большинства новых систем на текущий момент (с пониманием, что эта рекомендация со временем может измениться по мере роста вычислительных мощностей потенциальных атакующих). hybridEncrypt() показывает практическую схему, объединяющую обе статьи цикла — данные шифруются быстрым AES со случайным, одноразовым ключом, а сам этот короткий ключ (а не сами потенциально большие данные) шифруется значительно более медленным, но решающим проблему обмена ключами RSA — именно так на практике реализовано большинство современных протоколов (включая TLS, статья 376), а не «чистое» RSA-шифрование напрямую самих данных.

Подводные камни

  • Попытка непосредственного RSA-шифрования больших объёмов данных вместо гибридной схемы — RSA имеет жёсткое математическое ограничение на максимальный размер данных, которые можно зашифровать за одну операцию (зависящее от размера ключа и используемого padding), и попытка зашифровать данные, превышающие этот предел, просто не сработает; кроме того, даже для данных в пределах допустимого размера RSA значительно медленнее AES, что делает гибридную схему практически предпочтительной почти всегда, а не только при превышении предела размера.
  • Использование устаревшего padding-режима PKCS1v15 вместо современного OAEP (RSA_PKCS1_OAEP_PADDING в примере) для RSA-шифрования — старый PKCS1v15 уязвим для определённых криптографических атак (Bleichenbacher’s attack и его варианты), и для нового кода OAEP — однозначно предпочтительный выбор padding-схемы для RSA-шифрования (важно не путать с padding для RSA-подписей, статья 375, где исторически использовались несколько иные схемы).
  • Использование слишком малого размера ключа RSA (1024 бита и менее) для нового проекта — такие размеры ключей считаются недостаточно стойкими против современных вычислительных мощностей и не должны использоваться для новых систем; минимальный практический размер для большинства современных задач — 2048 бита, а для систем с особо высокими требованиями к долгосрочной безопасности данных — 3072 или 4096 бит.
  • Незащищённое хранение или передача сгенерированного приватного ключа (как в простом примере saveKeysToFiles(), записывающем приватный ключ без какого-либо пароля-защиты, что показано через nullptr для параметра пароля шифрования ключа в PEM_write_PrivateKey) — для реального использования приватный ключ должен либо храниться в защищённом системном хранилище (статья 377), либо как минимум быть дополнительно зашифрован паролем при сохранении в файл (что PEM_write_PrivateKey поддерживает через соответствующие, ненулевые параметры алгоритма шифрования и пароля).