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?
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.