Aes-256-GCM OpenSSL > nodeJS

问题描述 投票:0回答:1

我正在尝试解密使用

C++/OpenSSL
Aes-256-GCM
来自
nodejs
javascript 加密的数据。

C++加密实现:

#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解密实现:

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)
    }
}

我确认,在

javascript
函数上,所有内容都与从
C++
生成的内容匹配,我的意思是,
password, salt, iv, derived key, ciphertext
,但该函数在
decipher.final()
处失败 出现错误:

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

我的javascript实现主要基于这个问题

我缺少什么?

javascript c++ encryption openssl cryptography
1个回答
0
投票

您忘记在 C 代码中指定 IV/nonce 大小,这就是 GCM 使用默认值 12 字节的原因。这可以通过在 NodeJS 代码中使用这个长度来验证,例如与

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

通过此更改解密成功。


作为修复,您应该使用 12 字节 IV/nonce 并相应地调整这两个代码。出于性能和兼容性原因,这是推荐的解决方案。

如果您出于任何原因想要保留 16 字节 IV/nonce,则必须使用

指定此长度
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL)

参见这里

© www.soinside.com 2019 - 2024. All rights reserved.