我正在尝试从 Node.js 解密使用 C++ OpenSSL ChaCha20 加密的数据,但标准 Node.js 加密模块不直接支持没有 Poly1305 的 ChaCha20。
我尝试使用JsChaCha20库,它没有失败,但解密的数据不正确,
decrypt
函数返回:'bAۄ���@��'
我试图弄清楚这是否是我的代码或 JsChaCha20 库的问题,甚至是 OpenSSL 实现和库之间的不匹配。
// https://github.com/thesimj/js-chacha20/blob/master/src/jschacha20.js
import crypto from 'crypto'
import JSChaCha20 from './jschacha20.js'
function decrypt(data, password)
{
if (!data.length)
return false
// Generate a salt from the password
const pwhash = crypto.createHash('sha256').update(password).digest()
const salt = Buffer.from(pwhash)
const NONCE_SIZE = 12
const KEY_SIZE = 32
// Derive key from password
const key = crypto.createHash('sha256').update(password).digest()
// Derive nonce
const context = Buffer.concat([Buffer.from(password), salt])
const hash = crypto.createHash('sha256').update(context).digest()
const nonce = hash.slice(0, NONCE_SIZE)
// Log values for comparison
console.log('\nkey\n', key.toString('base64'))
console.log('\nnonce\n', nonce.toString('base64'))
console.log('\nsalt\n', salt.toString('base64'))
console.log('\ncontext\n', context.toString('base64'))
try
{
const decipher = new JSChaCha20(key, nonce)
let decrypted = decipher.decrypt(Buffer.from(data))
decrypted = Buffer.from(decrypted).toString('utf8')
return decrypted
}
catch (error)
{
console.error(error.message)
return false
}
}
let encrypted = "b1OnS61xyC/D0Dc="
encrypted = Buffer.from(encrypted, 'base64')
const decrypted = decrypt(encrypted, "password")
我的 C++ OpenSSL ChaCha20 实现:
#include <openssl/sha.h>
#include <openssl/buffer.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
bool encrypt(std::string& data, std::string& password)
{
if (data.empty())
return false;
// Generate a salt from the password
unsigned char pwhash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(password.data()), password.size(), pwhash);
std::vector<unsigned char> salt(pwhash, pwhash + 32);
constexpr size_t NONCE_SIZE = 12;
constexpr size_t KEY_SIZE = 32;
// Derive key from password
unsigned char key[KEY_SIZE];
SHA256(reinterpret_cast<const unsigned char*>(password.data()), password.size(), key);
// Derive nonce
std::vector<unsigned char> context;
context.reserve(password.size() + salt.size());
context.insert(context.end(), password.begin(), password.end());
context.insert(context.end(), salt.begin(), salt.end());
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(context.data(), context.size(), hash);
std::vector<unsigned char> nonce(hash, hash + NONCE_SIZE);
// Encrypt with derived nonce
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
if (!ctx) return false;
if (EVP_EncryptInit_ex(ctx, EVP_chacha20(), nullptr, key, nonce.data()) != 1) {
EVP_CIPHER_CTX_free(ctx);
return false;
}
std::vector<char> ciphertext(data.size(), 0);
int ciphertextLen;
if (EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()), &ciphertextLen,
reinterpret_cast<const unsigned char*>(data.data()), data.size()) != 1) {
EVP_CIPHER_CTX_free(ctx);
return false;
}
EVP_CIPHER_CTX_free(ctx);
ciphertext.resize(ciphertextLen);
// Print key, nonce, salt, and context in base64
auto toBase64 = [](const unsigned char* data, size_t len) -> std::string {
BIO* b64 = BIO_new(BIO_f_base64());
BIO* bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_write(bio, data, len);
BIO_flush(bio);
BUF_MEM* bufferPtr;
BIO_get_mem_ptr(bio, &bufferPtr);
std::string result(bufferPtr->data, bufferPtr->length - 1); // Exclude the null terminator
BIO_free_all(bio);
return result;
};
std::cout << "\nkey\n" << toBase64(key, sizeof(key));
std::cout << "\nnonce\n" << toBase64(nonce.data(), nonce.size());
std::cout << "\nsalt\n" << toBase64(salt.data(), salt.size());
std::cout << "\ncontext\n" << toBase64(context.data(), context.size());
int encDataSize = 4 * ((data.length() + 2) / 3);
std::vector<unsigned char> b64(encDataSize + 1); // +1 for the terminating null that EVP_EncodeBlock adds on
EVP_EncodeBlock(b64.data(), reinterpret_cast<const unsigned char*>(ciphertext.data()), ciphertext.size());
data = std::string(b64.begin(), b64.end());
return true;
}
int main()
{
std::string data = "Hello World";
std::string password = "password";
encrypt(data, password);
}
key, nonce, salt, contenxt
所有这些都与 C++ 函数中的值匹配。
我从密码中派生随机数,所以我不需要将其附加到密码中,我知道这不安全,但我试图不增加数据大小,这就是为什么我尝试 ChaCha20 而不是 Poly1305,因为 Poly1305 还附加了一个标签。
您没有正确使用
EVP_chacha20()
的随机数。来自文档:
EVP_chacha20()
ChaCha20 流密码。密钥长度为256位,IV长度为128位。前 32 位由一个小尾数顺序的计数器组成,后跟一个 96 位随机数。例如随机数:
000000000000000000000002
初始计数器为 42(十六进制为 2a)将表示为:
2a000000000000000000000000000002
你的
nonce
对应的是IV,根据描述应该是16字节长。您正在使用 12 个字节。这是用 0xFD 隐式填充的(尽管我不清楚为什么使用 0xFD)。结果 IV 的前 4 个字节用作小端顺序的计数器,其余的是实际的随机数。
JavaScript 库允许单独指定随机数和计数器。这需要进行以下更改:
...
const nonceCtr = Buffer.concat([nonce, Buffer.from('fdfdfdfd', 'hex')])
const ctr = nonceCtr.subarray(0, 4).readUInt32LE(0)
const iv = nonceCtr.subarray(4)
try
{
const decipher = new JSChaCha20(key, iv, ctr)
...
至此,解密成功。
请注意,C 代码也应该修复。应应用 16 字节 IV,这将被解释为 4 字节(小端)计数器和 12 字节随机数的串联。然后应在 JavaScript 代码中相应地使用这些值。