javascriptc++encryptionopensslcryptography

Aes-256-GCM OpenSSL > nodeJS


I'm trying to decrypt data that have been encrypted using C++/OpenSSL Aes-256-GCM from nodejs javascript.

C++ encryption implementation:

#include <string>
#include <sstream>
#include <iomanip>
#include <vector>
#include <iostream>
#include <openssl/rand.h>
#include <openssl/bio.h>
#include <openssl/buffer.h> 

std::string base64Encode(const unsigned char* input, int length)
{
    BIO *bio, *b64;
    BUF_MEM *bufferPtr;

    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new(BIO_s_mem());
    bio = BIO_push(b64, bio);

    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    BIO_write(bio, input, length);
    BIO_flush(bio);
    BIO_get_mem_ptr(bio, &bufferPtr);
    
    std::string result(bufferPtr->data, bufferPtr->length);

    BIO_free_all(bio);

    return result;
}

std::string bytesToHexString(const unsigned char* data, int len)
{
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    for (int i = 0; i < len; ++i)
    {
        ss << std::setw(2) << static_cast<int>(data[i]);
    }
    return ss.str();
}

bool encrypt(std::string& data, const std::vector<unsigned char>& password)
{
    constexpr int KEY_SIZE        = 32;  // AES-256
    constexpr int IV_SIZE         = 16;
    constexpr int TAG_SIZE        = 16;
    constexpr int ITERATION_COUNT = 10000;

    std::vector<unsigned char> key(KEY_SIZE);
    std::vector<unsigned char> iv(IV_SIZE);    
    std::vector<unsigned char> salt(16);
    RAND_bytes(salt.data(), 16);
 
    // PBKDF2 for key derivation
    if (PKCS5_PBKDF2_HMAC(reinterpret_cast<const char*>(password.data()), password.size(),
        salt.data(), salt.size(), ITERATION_COUNT,
        EVP_sha256(), KEY_SIZE, key.data()) != 1)
        return false;

    if (RAND_bytes(iv.data(), IV_SIZE) != 1)
        return false;
    
    // encryption context
    std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);
    if (!ctx)
        return false;
    
    if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, key.data(), iv.data()) != 1)
        return false;

    std::vector<unsigned char> ciphertext(data.size() + EVP_MAX_BLOCK_LENGTH);
    int outLen1 = 0, outLen2 = 0;
    
    if (EVP_EncryptUpdate(ctx.get(), ciphertext.data(), &outLen1,
        reinterpret_cast<const unsigned char*>(data.data()),
        data.size()) != 1)
        return false;
    
    if (EVP_EncryptFinal_ex(ctx.get(), ciphertext.data() + outLen1, &outLen2) != 1)
        return false;
    
    //tag
    std::vector<unsigned char> tag(TAG_SIZE);
    if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, TAG_SIZE, tag.data()) != 1)
        return false;
    
    // debug:
    std::cout << "\nSalt:\n"           << bytesToHexString(salt.data(), salt.size())             << std::endl;
    std::cout << "\nPassword (hex):\n" << bytesToHexString(password.data(), password.size())     << std::endl;
    std::cout << "\nDerived key:\n"    << bytesToHexString(key.data(), KEY_SIZE)                 << std::endl;
    std::cout << "\nIV:\n"             << bytesToHexString(iv.data(),  IV_SIZE)                  << std::endl;
    std::cout << "\nTAG:\n"            << bytesToHexString(tag.data(), TAG_SIZE)                 << std::endl;
    std::cout << "\nCiphertext:\n"     << bytesToHexString(ciphertext.data(), outLen1 + outLen2) << std::endl;

    // combined: salt, IV, ciphertext, and tag
    std::vector<unsigned char> result;
    result.insert(result.end(), salt.begin(), salt.end());
    result.insert(result.end(), iv.begin(), iv.end());
    result.insert(result.end(), ciphertext.begin(), ciphertext.begin() + outLen1 + outLen2);
    result.insert(result.end(), tag.begin(), tag.end());
    
    data = base64Encode(result.data(), result.size());
    
    // erase buffers...
    return true;
}

int main()
{
    std::vector<unsigned char> password(9);
    RAND_bytes(password.data(), 9);

    std::string base64Password = base64Encode(password.data(), password.size());
    std::cout << "Password: " << base64Password << std::endl;

    std::string data = "Hello, World!";
    encrypt(data, password);

    std::cout << "\nEncrypted data:\n" << data;
    return 0;
}

javascript decryption implementation:

function decrypt(encryptedBase64, passwordBase64, SALT_SIZE) {
    const KEY_SIZE        = 32;  // AES-256
    const IV_SIZE         = 16;
    const TAG_SIZE        = 16;
    const ITERATION_COUNT = 10000;

    // Decode the base64 encrypted data
    const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');    
    // Decode the base64 password
    const passwordBuffer  = Buffer.from(passwordBase64, 'base64');

    // Extract salt, IV, ciphertext, and tag
    const salt       = encryptedBuffer.slice(0, SALT_SIZE);
    const iv         = encryptedBuffer.slice(SALT_SIZE, SALT_SIZE + IV_SIZE);
    const tag        = encryptedBuffer.slice(-TAG_SIZE);
    const ciphertext = encryptedBuffer.slice(SALT_SIZE + IV_SIZE, -TAG_SIZE);

    // Derive the key using PBKDF2
    const key = crypto.pbkdf2Sync(passwordBuffer, salt, ITERATION_COUNT, KEY_SIZE, 'sha256');

    console.log('\nSalt:\n',             salt.toString('hex'));
    console.log('\nPassword (hex):\n',   passwordBuffer.toString('hex'));
    console.log('\nDerived key:\n',      key.toString('hex'));
    console.log('\nIV:\n',               iv.toString('hex'));
    console.log('\nTAG:\n',              tag.toString('hex'));
    console.log('\nCiphertext:\n',       ciphertext.toString('hex'));

    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(tag);

    let decrypted = decipher.update(ciphertext);
    decipher.final()

    return decrypted.toString('utf8');
}


(async () =>
{
    const encryptedBase64 = 'p4lx3wi2bstgBR/puZBYOKcJU4Ua41spBbeR9etV9K7BaRWGsUqKUIymO+vaQ1PAlv/M1e9OxExVykGNzQ=='
    const password = 'Et5Q/1E6iTg5'
    try
    {
        const decryptedData = decrypt(encryptedBase64, password, 16)
        console.log('Decrypted data:', decryptedData)
    } catch (error)
    {
        console.error('Decryption failed:', error.message)
    }
}

I confirmed that on the javascript function everything matches the ones generated from the C++, i mean, the password, salt, iv, derived key, ciphertext, but the function fails at decipher.final() with the error:

Error: Unsupported state or unable to authenticate data
    at Decipheriv.final 

My javascript implementation was mostly based on this question.

What i'm missing?


Solution

  • You have forgotten to specify the IV/nonce size in the C code, which is why the default value of 12 bytes is used for GCM. This can be verified by using this length in the NodeJS code, e.g. with

    const iv = encryptedBuffer.slice(SALT_SIZE, SALT_SIZE + IV_SIZE - 4);
    

    With this change decryption is successful.


    As fix you should use a 12 bytes IV/nonce and adapt both codes accordingly. This is the recommended solution for performance and compatibility reasons.

    If you want to keep the 16 bytes IV/nonce for any reason, this length must be specified with

    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL);
    

    see here.