javascriptqtencryptioncrypt

Encryption JavaScript to-from C++ and QT


Yes, there is 100 topics about it. Yes, most of them are snippets/parts of code that answer the specific problem and don't actually help.

So perhaps this topic would help provide a "complete" solution for symmetric and maybe if someone would be willing to help with asymmetric private/public key example.

So here are pre-reqs:

JavaScript:

npm install crypto

C++:

https://github.com/QuasarApp/Qt-AES/tree/master and Qt

Now in order to do encryption the tutorial on this page works quite well: Example 2 > https://www.geeksforgeeks.org/node-js-crypto-createdecipheriv-method/?ref=lbp

Now as far as I can tell, say we create our Key - password :

const password = 'mySuperFancyPassword';

// Defining key
export const key = crypto.scryptSync(password, 'salt', 32);

This password is not the same as the one we would made in C++ using:

QAESEncryption encryption(QAESEncryption::AES_256, QAESEncryption::CBC,QAESEncryption::PKCS7);
QString key("mySuperFancyPassword");
QByteArray hashKey = QCryptographicHash::hash(key.toLocal8Bit(), QCryptographicHash::Sha256);
QByteArray decodeText = encryption.decode(jsByteArray, hashKey , jsIv);

Because Qt-AES takes Hash rather than whatever crypto.scryptSync() produces.

I suppose the question is: how can I match these two passwords? If I were to pass JavaScript key-hex to C++ and convert it to byte array (auto key = QByteArray::fromHex(hexByte)) C++ library will decompile the string properly and with PKCS7 padding it will match JavaScript.

Now I know that I should use OpenSSL as that is standard, but every time I look at it I want to cry.

So this library here seems to be very dummy friendly so far. However, if anyone is interested in OpenSSL, there is this interesting "file" > https://github.com/soroush/qtz-security/blob/dev/lib/src/crypto.cpp

That shows how to do it OpenSSL but I get error 0 in

error_code = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);

indecryptRawData(const QByteArray& input, const QByteArray& rawKey, const QByteArray& rawIV). So same issue, black magic! I did match my EVP_aes_256_cbc settings between JS and C++ in second library.

Bottom line, I need to convert the KEY to properly match between C++ and JavaScript. Either that, or I need to use second lib OpenSSL? But I take its the same issue of salt/key generation.


Update

Big thanks to @absolute.madness for his solution!

Also, I found another way of "partially" solving the problem.

I found out that crypto has PKCS5_PBKDF2_HMAC support too! So here is a proposed workflow for that one, however even though I can send from JavaScript > C++, I can't send C++ > JavaScript using the QAESEncryption library due to (I think) incorrect padding. As I crash at decrypted = Buffer.concat([decrypted, decipher.final()]); .final() statement I think.

Here is JavaScript & C++ code that I got working up to 50%.

JS:

// Defining password
const password: string = process.env.KEY_LICENSE_GENERIC! as string

// Defining key
var key: Buffer

crypto.pbkdf2(password, 'salt_', 10000, 32,
    'sha256', (err, derivedKey) => {
        if (err) {
            throw new Error();
        }
        key = derivedKey
    })

const iv = crypto.randomBytes(16);

export function encrypt2(text: string) {

    // Creating Cipheriv with its parameter
    let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);

    // Updating text
    let encrypted = cipher.update(text);
    // Using concatenation iv + encrypt + enging & padding?
    encrypted = Buffer.concat([iv, encrypted, cipher.final()]);

    return encrypted.toString('hex')
}

// A decrypt  function
export function decrypt2(text: string) {
    let rawData = Buffer.from(text, 'hex');
    if (rawData.length > 16) {
        let iv = rawData.subarray(0, 16) // We put IV as 1st 16 bytes.
        let encr = rawData.subarray(16, rawData.length)

        // Creating Decipher
        let decipher = crypto.createDecipheriv(
            'aes-256-cbc', Buffer.from(key), iv);

        // Updating encrypted text
        let decrypted = decipher.update(encr);
        decrypted = Buffer.concat([decrypted, decipher.final()]);

        return decrypted.toString()
    }
    return ""
}

c++


#include <openssl/rand.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>

QByteArray generateKey(const QByteArray &phrase, bool encode, const int iterations) {
    const int length = 32;
    QByteArray salt("salt_");
    unsigned char key[length];

    PKCS5_PBKDF2_HMAC(
            phrase.data(), phrase.size(),
            (const unsigned char *) (salt.data()), salt.size(),
            iterations, EVP_sha256(),
            length, key
    );

    return encode ? QByteArray((const char *) (key), length).toBase64(QByteArray::Base64UrlEncoding) : QByteArray((const char *) (key), length);
}
QByteArray randomBytes(int size) {
    QByteArray bytes(size, char(0));
    if (RAND_bytes((unsigned char *) (bytes.data()), bytes.size()) != 1) {
        QRandomGenerator::securelySeeded().fillRange((quint32 *) (bytes.data()), bytes.size() / sizeof(quint32));
    }
    return bytes;
}


void decrypt(){
    QByteArray hexEnc = reply.readAll(); // QNetworkReply*
    QByteArray enc = QByteArray::fromHex(hexEnc.toUtf8());
    auto iv = enc.mid(0, 16);
    enc = enc.mid(16, enc.size());

    QAESEncryption encryption(QAESEncryption::AES_256, 
    QAESEncryption::CBC,QAESEncryption::PKCS7);
    QByteArray decodeText = encryption.decode(enc, generateKey("Fancy 
    password", false, 10000), iv);

    /// Remove padding, I think this is missing when we encrypt.
    QString decodedString = QString(encryption.removePadding(decodeText ));

}

void encrypt(){
    auto iv = randomBytes(16);

    auto encrypted = encryption.encode("Hello test code", 
    generateKey("Fancy password", false, 10000), iv); // bad encrypt, js will crash.
}

Solution

  • You cannot just use SHA-256 to match scrypt key derivation algorithm, obviously. Scrypt is defined in RFC 7914 and it's not (as of yet) implemented in Qt via its interfaces. OpenSSL (used by Qt) supports it on the other hand. I added 2 implementations of the Node.js example1 which you reference: the first one uses OpenSSL & Qt-AES, the second uses pure OpenSSL. Initially, I got an error from EVP_DecryptFinal_ex similar to what you described. When I started to debug it turned out that EVP_DecodeBlock was returning incorrect size when decoding from base64. After using EVP_DecodeInit/EVP_DecodeUpdate/EVP_DecodeFinal to handle base64 instead of EVP_DecodeBlock as was suggested here the error was gone.

    I include the c++ code which roughly translates js-code from example 1 to c++ (I used OpenSSL 1.1.1q for testing):

    #include <QDebug>
    #include <openssl/aes.h>
    #include <openssl/evp.h>
    #include <openssl/kdf.h>
    
    #include "qaesencryption.h"
    
    void error(const char *msg)
    {
      qCritical(msg);
    }
    
    #define ERROR(msg) \
    { \
      qCritical(msg); \
      return; \
    }
    
    // scrypt key derivation function/algorithm, see also
    // https://www.openssl.org/docs/man1.1.1/man7/scrypt.html
    // returns -1 on error and 1 on success
    int scrypt_kdf(unsigned char *key, size_t *keylen,
                   const unsigned char *pass, size_t passlen,
                   const unsigned char *salt, size_t saltlen,
                   uint64_t N = 16384, uint64_t r = 8, uint64_t p = 1)
    {
      // Note, default values for N, r, p are taken from
      // https://nodejs.org/api/crypto.html#cryptoscryptsyncpassword-salt-keylen-options
      EVP_PKEY_CTX *kctx;
      int ret = 1;
      kctx = EVP_PKEY_CTX_new_id(EVP_PKEY_SCRYPT, NULL);
    
      if(EVP_PKEY_derive_init(kctx) <= 0)
      {
        error("EVP_PKEY_derive_init failed");
        ret = -1;
      }
      if(1 == ret && EVP_PKEY_CTX_set1_pbe_pass(kctx, pass, passlen) <= 0)
      {
        error("EVP_PKEY_CTX_set1_pbe_pass failed");
        ret = -1;
      }
      if(1 == ret && EVP_PKEY_CTX_set1_scrypt_salt(kctx, salt, saltlen) <= 0)
      {
        error("EVP_PKEY_CTX_set1_scrypt_salt failed");
        ret = -1;
      }
      if(1 == ret && EVP_PKEY_CTX_set_scrypt_N(kctx, N) <= 0)
      {
        error("EVP_PKEY_CTX_set_scrypt_N failed");
        ret = -1;
      }
      if (1 == ret && EVP_PKEY_CTX_set_scrypt_r(kctx, 8) <= 0)
      {
        error("EVP_PKEY_CTX_set_scrypt_r failed");
        ret = -1;
      }
      if (1 == ret && EVP_PKEY_CTX_set_scrypt_p(kctx, 1) <= 0)
      {
        error("EVP_PKEY_CTX_set_scrypt_p failed");
        ret = -1;
      }
      if (1 == ret && EVP_PKEY_derive(kctx, key, keylen) <= 0)
      {
        error("EVP_PKEY_derive failed");
        ret = -1;
      }
    
      EVP_PKEY_CTX_free(kctx);
      return ret;
    }
    
    // we use OpenSSL for scrypt key derivation algorithm and Qt/Qt-AES for decryption
    void example1_openssl_and_qt(void)
    {
      unsigned char key[24];
      size_t sz_key = sizeof(key);
      const char password[] = "bncaskdbvasbvlaslslasfhj";
      const char salt[] = "GfG";
      QByteArray iv(16, char(0));
      QByteArray encrypted = QByteArray::fromBase64("MfHwhG/WPv+TIbG/qM78qA==");
      // you can also try
    //  encrypted = QByteArray::fromBase64(
    //        "j9QsjAFxuIAK0zvi5Iq2Z2+mo44RRpR2VMnJTNS7Ey0IkPjsGSJ+A+OPuvAqGO77Ww"
    //        "S2rI0dnJVREkFz0v8hug==");
    
      if(scrypt_kdf(
           key, &sz_key, reinterpret_cast<const unsigned char*>(password),
           sizeof(password)-1, reinterpret_cast<const unsigned char*>(salt),
           sizeof(salt)-1) <= 0)
      {
        ERROR("Key derivation failed");
      }
      OPENSSL_assert(sz_key == sizeof(key));
    
      QAESEncryption encryption(QAESEncryption::AES_192, QAESEncryption::CBC,
                                QAESEncryption::PKCS7);
      QByteArray decrypted = encryption.decode(
            encrypted, QByteArray(reinterpret_cast<char*>(key), sizeof(key)), iv);
      qDebug() << decrypted;
    }
    
    // we use qt only for base64 decoding
    void example1_pure_openssl(void)
    {
      int len; // general purpose length variable, used in EVP_*Update/EVP_*Final
      EVP_ENCODE_CTX *b64ctx;
      unsigned char key[24];
      size_t sz_key = sizeof(key);
      EVP_CIPHER_CTX *dctx;
      const char password[] = "bncaskdbvasbvlaslslasfhj";
      const char salt[] = "GfG";
      unsigned char iv[16] = { 0 }; // 16 zero bytes
      char encrypted_b64[] = "MfHwhG/WPv+TIbG/qM78qA==";
      // you can also try
    //  char encrypted_b64[] = "j9QsjAFxuIAK0zvi5Iq2Z2+mo44RRpR2VMnJTNS7Ey0IkPjsG"
    //                         "SJ+A+OPuvAqGO77WwS2rI0dnJVREkFz0v8hug==";
    
      // Note, base64 encoding is supposed to be b64size = (size + 2) / 3 * 4
      // characters long, where size is the size of the encoded string, therefore
      // the following assert checks that the size is correct and thus the size
      // of the maximum decoded string size can be calculated as
      // max_size = 3 * b64size / 4
      // https://stackoverflow.com/questions/13378815/base64-length-calculation
      OPENSSL_assert((sizeof(encrypted_b64) - 1) % 4 == 0);
      unsigned char encrypted[3 * (sizeof(encrypted_b64) - 1) / 4];
      unsigned char decrypted[sizeof(encrypted) + 1]; // +1 for terminating 0
      int sz_decoded, sz_decrypted;
    
      // Note, do not use EVP_DecodeBlock for decoding from base64 as it returns
      // wrong decoded length and ignores padding, see
      // https://github.com/openssl/openssl/issues/17197
      b64ctx = EVP_ENCODE_CTX_new();
      EVP_DecodeInit(b64ctx);
      if(EVP_DecodeUpdate(b64ctx, encrypted, &sz_decoded,
                          (const unsigned char*)encrypted_b64,
                          sizeof (encrypted_b64) - 1) < 0)
      {
        EVP_ENCODE_CTX_free(b64ctx);
        ERROR("EVP_DecodeUpdate failed");
      }
      if(EVP_DecodeFinal(b64ctx, encrypted + sz_decoded, &len) <= 0)
      {
        EVP_ENCODE_CTX_free(b64ctx);
        ERROR("EVP_DecodeFinal failed");
      }
      sz_decoded += len;
      EVP_ENCODE_CTX_free(b64ctx);
    
      OPENSSL_assert(sz_decoded <= sizeof(encrypted));
    
      if(scrypt_kdf(
           key, &sz_key, (const unsigned char*)password, sizeof(password)-1,
           (const unsigned char*)salt, sizeof(salt)-1) <= 0)
      {
        ERROR("Key derivation failed");
      }
      OPENSSL_assert(sz_key == sizeof(key));
    
      dctx = EVP_CIPHER_CTX_new();
      if (EVP_DecryptInit_ex(dctx, EVP_aes_192_cbc(), NULL, key, iv) <= 0)
      {
        EVP_CIPHER_CTX_free(dctx);
        ERROR("EVP_DecryptInit_ex failed");
      }
      if(EVP_CIPHER_CTX_set_key_length(dctx, 24) <= 0)
      {
        EVP_CIPHER_CTX_free(dctx);
        ERROR("EVP_CIPHER_CTX_set_key_length failed");
      }
      if(EVP_DecryptUpdate(dctx, decrypted, &sz_decrypted,
                           encrypted, sz_decoded) <= 0)
      {
        EVP_CIPHER_CTX_free(dctx);
        ERROR("EVP_DecryptUpdate failed");
      }
      if(EVP_DecryptFinal_ex(dctx, decrypted + sz_decrypted, &len) <= 0)
      {
        EVP_CIPHER_CTX_free(dctx);
        ERROR("EVP_DecryptFinal_ex failed");
      }
      EVP_CIPHER_CTX_free(dctx);
      sz_decrypted += len;
    
      // do not forget the null terminator
      decrypted[sz_decrypted] = 0;
      qDebug() << (const char*)decrypted;
    }
    
    int main(void)
    {
      qDebug() << "example1_openssl_and_qt decryption:";
      example1_openssl_and_qt();
      qDebug() << "example1_pure_openssl decryption:";
      example1_pure_openssl();
      return 0;
    }
    

    I also attach the code I used to generate the additional encrypted data:

    const crypto = require('crypto');
    
    const algorithm = 'aes-192-cbc';
    const password = 'bncaskdbvasbvlaslslasfhj';
    const plaintext = 'Lorem ipsum dolor sit amet, consectetur adipiscing';
    
    const key = crypto.scryptSync(password, 'GfG', 24);
    const iv = Buffer.alloc(16, 0);
    
    const cipher = crypto.createCipheriv(algorithm, key, iv);
    
    const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
    
    console.log(encrypted.toString('base64'));
    

    UPD

    C++

    void pbkdf2withsha256_pure_openssl(void)
    {
      int len; // general purpose length variable, used in EVP_*Update/EVP_*Final
      EVP_ENCODE_CTX *b64ctx;
      const int sz_key = 32;
      unsigned char key[sz_key];
    
      // Note, base64 encoding size is supposed to be b64size = (size + 2) / 3 * 4
      // characters long, where size is the size of the source string
      // https://stackoverflow.com/questions/13378815/base64-length-calculation
      unsigned char key_b64[(sz_key + 2) / 3 * 4 + 1];
    
      int sz_key_b64;
      const char password[] = "myPassw0rd";
      const unsigned char salt[] = "mySalt";
    
      if(PKCS5_PBKDF2_HMAC(password, sizeof(password) - 1, salt, sizeof(salt) - 1,
                           10000, EVP_sha256(), sz_key, key) < 1)
      {
        ERROR("PKCS5_PBKDF2_HMAC failed");
      }
    
      b64ctx = EVP_ENCODE_CTX_new();
      EVP_EncodeInit(b64ctx);
      if(EVP_EncodeUpdate(b64ctx, key_b64, &sz_key_b64, key, sz_key) < 0)
      {
        EVP_ENCODE_CTX_free(b64ctx);
        ERROR("EVP_DecodeUpdate failed");
      }
      EVP_EncodeFinal(b64ctx, key_b64 + sz_key_b64, &len);
      sz_key_b64 += len;
      EVP_ENCODE_CTX_free(b64ctx);
    
      qDebug() << (const char*)key_b64;
    }
    

    JS

    crypto = require ('crypto');
    
    crypto.pbkdf2('myPassw0rd', 'mySalt', 10000, 32,
        'sha256', (err, key) => {
            if (err) throw new Error();
            console.log(key.toString('base64'))
        })