Простое асинхронное шифрование в Qt

в 13:18, , рубрики: openssl, qt, rsa, информационная безопасность, шифрование

В рамках одного из своих проектов я решил сделать сделать асинхронное шифрование.

Вся работа была разбита не несколько этапов:

  1. Генерация ключей
  2. Чтение и загрузка ключей
  3. Шифрование одной строки
  4. Расшифровывания одной строки
  5. Шифрование текста произвольной длинны
  6. Расшифровывание данных произвольной длинны

image

1. Генерация ключей

С генерацией мне помогла эта статья, поэтому расписывать всё в деталях не буду.

Итак, что я добавил:

  1. Я отказался за ненадобностью от визарда, то есть конструктора страницы и фактически перенёс код в свои функции.
  2. Основная функция генерации в моём случае возвращает JsonObject в котором находятся ключи.
  3. Функция теперь занимается генерацией публичного и приватного ключей.

Пример получения ключей:

        keys["public"] =cert.publicKey().toPem().toStdString().c_str();
        keys["private"] = pkey.toPem().toStdString().c_str();

2. Чтение и загрузка ключей

Для чтения ключей я на форме расположил 2 виджета Text_Edit.

Изначально хотел использовать Line_Edit, но как оказалось этот виджет некорректно возвращает ключ, из-за чего он не загружается. Причиной послужил переход на новую строку или символ "n", который возвращался как отдельные символы "" и «n».

Ключ ввести можно по разному. Из файла, из строки типа (const char), или из виджета использовав преобразование:

    QString pub_key,priv_key;
    pub_key = ui->textEdit_3->toPlainText();
    priv_key = ui->textEdit_5->toPlainText();
      // LOAD PUBLIC KEY
      crypt_class::rsaPubKey = crypt_class::loadPUBLICKeyFromString( pub_key.toStdString().c_str() ) ;
      // LOADR PRIVATE KEY
      crypt_class::rsaPrivKey = crypt_class::loadPRIVATEKeyFromString( priv_key.toStdString().c_str()  ) ;

Считанный ключ отправляется в соответствующую функцию загрузки:

/**
 * @brief crypt_class::loadPUBLICKeyFromString
 * @param publicKeyStr
 * @return
 * функция загрузки публичного ключа
 */
RSA* crypt_class::loadPUBLICKeyFromString( const char* publicKeyStr )
{
  BIO* bio = BIO_new_mem_buf( (void*)publicKeyStr, -1 ) ;
  BIO_set_flags( bio, BIO_FLAGS_BASE64_NO_NL ) ;
  RSA* rsaPubKey = PEM_read_bio_RSA_PUBKEY( bio, NULL, NULL, NULL ) ;
  if( !rsaPubKey ){
      qDebug()<< "ERROR: Could not load PUBLIC KEY!  PEM_read_bio_RSA_PUBKEY FAILED: %sn";
      return NULL;
  }
    BIO_free( bio ) ;
    return rsaPubKey ;
}

/**
 * @brief crypt_class::loadPRIVATEKeyFromString
 * @param privateKeyStr
 * @return
 * Функция загрузки приватного ключа
 */
RSA* crypt_class::loadPRIVATEKeyFromString( const char* privateKeyStr )
{
  BIO *bio = BIO_new_mem_buf( (void*)privateKeyStr, -1 );
  RSA* rsaPrivKey = PEM_read_bio_RSAPrivateKey( bio, NULL, NULL, NULL ) ;
  if ( !rsaPrivKey ){
      qDebug()<< "ERROR: Could not load PRIVATE KEY!  PEM_read_bio_RSAPrivateKey FAILED: %sn";
      return NULL;
  }
  BIO_free( bio ) ;
  return rsaPrivKey ;
}

Если всё прошло без ошибок, то в переменных что ниже будут загружены наши ключи.

crypt_class::rsaPrivKey
crypt_class::rsaPubKey

3. Шифрование одной строки

Для шифрования я использовал функционал openssl.

int RSA_public_encrypt(int flen, unsigned char *from, unsigned char *to, RSA *rsa, int padding);

Изначально все входящие и исходящие данные были в (unsigned char *), что очень упрощало отработку алгоритма и процесс дебаггинга. В дальнейшем я работал со строками QString и байтовыми наборами в QByteArray, а конвертировал в (unsigned char *) уже непосредственно при входе функцию шифрования.

Для выравнивания длинны ключа я использовал RSA_PKCS1_PADDING, что соответствует стандарту PKCS#1. При использовании этого режима размер буфера from должен быть не меньше (RSA_size(rsa) — 11), т.е. на 11 байт меньше размера ключа. Размер выходных данных всегда будет кратен длине ключа.

4. Дешифрование одной строки

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

int RSA_private_decrypt(int flen, unsigned char *from, unsigned char *to, RSA *rsa, int padding);

С одной строкой, пока её длина не превышала (RSA_size(rsa) — 11) всё было достаточно просто, но мне хотелось большего.

5. Шифрование текста произвольной длины

Для этого я поместил функцию шифрования в цикл while (size>0). За size взял длину входящей строки. При каждой итерации уменьшал size на RSA_size(rsa) — 11). Зашифрованные данные методом append записывал в QByteArray. Для собственного удобства в конце перевёл все данные из QByteArray в HEX и записал в строку, которую вернул.

/**
 * @brief crypt_class::rsaEncrypt
 * @param pubKey RSA
 * @param str source data in string
 * @return
 * шифруем данные и конвертируем в HEX для удобства работы со строками
 */
QString crypt_class::rsaEncrypt(RSA *pubKey, QString str)
{
  int size = str.length();
  unsigned char* tmp = (unsigned char*)malloc(RSA_size(pubKey)) ;
  memset(tmp,0,RSA_size(pubKey));
  QByteArray *binary;
  QByteArray tempar;
  QString element;
  int str_size_to_crypt=((RSA_size(pubKey)-11)/2)-1;
  int cyc=0;
  while (size>0){
         if( size>str_size_to_crypt){element = str.mid(cyc*str_size_to_crypt, str_size_to_crypt);}else{element = str.mid(cyc*str_size_to_crypt, str.length());}
         RSA_public_encrypt( RSA_size(pubKey)-11, (const unsigned char*)element.toStdString().c_str(), tmp, pubKey, RSA_PKCS1_PADDING );
         binary = new QByteArray((char*)tmp,RSA_size(pubKey));
         size -= str_size_to_crypt;
         tempar.append(*binary);
         delete binary;
         cyc++;
 }
  free(tmp);
  return tempar.toHex();
}

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

Решение оказалось простым — количество байт затрачиваемое на кодирование символов не равно количеству самих символов. Поэтому для успешного кодирования разного текста был сделан вот такой костыль, который будет терять текст если символы в нём кодируются больше чем по 2 байта (например азиатские языки):

int str_size_to_crypt=((RSA_size(pubKey)-11)/2)-1;

В дальнейшем планирую строку делить побайтово, что-бы максимально укладываться в (RSA_size(pubKey)-11) и шифровать текст в любых кодировках без потерь.

6. Расшифровывание данных произвольной длины

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

Все зашифрованные строки имеют равную длину. То есть достаточно взять длину ByteArray как size, и в каждой итерации отнимать RSA_size(pivateKey), пока size не станет меньше нуля.

QString crypt_class::rsaDecrypt( RSA *privKey,QString HextData)
{
    QByteArray encryptedData;
    for (int i=0; i<HextData.length()/2; i++){
        QString str = HextData.mid(i*2,2);
        int iVal = str.toInt(NULL,16);
          encryptedData[i]=iVal;
    }
    int size=encryptedData.length();
    unsigned char* decryptedBin = (unsigned char*)malloc(RSA_size(privKey)) ;
    memset(decryptedBin,0,RSA_size(privKey));
    int i=0;
    QString *string;
    QString out;
    unsigned char* ptr = (unsigned char*)encryptedData.constData();
    int cyc=0;
    while (size>0){
        if (RSA_private_decrypt( RSA_size(privKey), ptr, decryptedBin, privKey, RSA_PKCS1_PADDING )==-1){ qDebug() << "ERROR";}
        string = new QString( reinterpret_cast< char* >( decryptedBin ) );
        ptr += RSA_size(privKey);
        out.append(*string);
        delete string;
        size -=  RSA_size(privKey);
        cyc++;
    }
    free(decryptedBin);
  return out ;
}

Итоги

Код во некоторых местах не оптимален и требует доработок. Готовые классы уже сейчас можно подключать в другие проекты и использовать, а следовательно своей цели я достиг.

Само шифрование в Qt достаточно простое, чтобы с ним мог справиться новичок за пару дней.
Единственные сложности возникли при работе с (unsigned char *) типом данных и управлением памяти вручную.

Исходный код проекта на GitHub

Автор: Nic Weiss

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js