В рамках одного из своих проектов я решил сделать сделать асинхронное шифрование.
Вся работа была разбита не несколько этапов:
- Генерация ключей
- Чтение и загрузка ключей
- Шифрование одной строки
- Расшифровывания одной строки
- Шифрование текста произвольной длинны
- Расшифровывание данных произвольной длинны
1. Генерация ключей
С генерацией мне помогла эта статья, поэтому расписывать всё в деталях не буду.
Итак, что я добавил:
- Я отказался за ненадобностью от визарда, то есть конструктора страницы и фактически перенёс код в свои функции.
- Основная функция генерации в моём случае возвращает JsonObject в котором находятся ключи.
- Функция теперь занимается генерацией публичного и приватного ключей.
Пример получения ключей:
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