如何在Node.js中通过AES-256-CBC实现与OpenSSL一致的加密
Node.js crypto库与OpenSSL加密结果不一致的修复方案
问题场景
原本通过execSync调用OpenSSL加密文本,存在密码泄露至系统日志的风险,改用Node.js内置crypto库后,加密结果与OpenSSL不一致。以下是复现代码及输出:
复现代码
const crypto = require('crypto'); const { execSync } = require('child_process'); const encrypt_text = async (plaintext, pass, salt = null) => { return new Promise((resolve, reject) => { try { let command = `echo "${plaintext}" | openssl enc -aes-256-cbc -a -md sha1 -pbkdf2 -pass pass:${pass}`; if (salt) { const saltHex = salt.toString('hex'); command = `echo "${plaintext}" | openssl enc -aes-256-cbc -a -md sha1 -pbkdf2 -pass pass:${pass} -S ${saltHex}`; } const encrypted_text = execSync(command).toString().trim(); resolve(encrypted_text); } catch (error) { console.error('Error encrypting text.'); reject(error); } }); } const encrypt_text_new = async (plaintext, pass, salt = null) => { return new Promise((resolve, reject) => { try { if (!salt) { salt = crypto.randomBytes(8); // 8 bytes salt } const salted = Buffer.concat([Buffer.from('Salted__'), salt]); // OpenSSL salt format // OpenSSL key derivation const keyIv = crypto.pbkdf2Sync(pass, salt, 1, 48, 'sha1'); // 48 bytes for key and IV const key = keyIv.slice(0, 32); // first 32 bytes for key const iv = keyIv.slice(32); // remaining 16 bytes for IV // Create cipher const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(plaintext, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); // Combine all parts const result = Buffer.concat([salted, encrypted]); const encrypted_text = result.toString('base64'); resolve(encrypted_text); } catch (error) { console.error('Error encrypting text.'); reject(error); } }); } const check_encryption = async () => { // Sample data for testing const plaintext = 'This is a test.'; const password = 'mysecretpassword'; // Generate a random salt for consistency in both functions const salt = Buffer.from("12345678", 'utf8'); (async () => { try { const encrypted1 = await encrypt_text(plaintext, password, salt); const encrypted2 = await encrypt_text_new(plaintext, password, salt); console.log('Encrypted with OpenSSL:', encrypted1); console.log('Encrypted with crypto:', encrypted2); if (encrypted1 === encrypted2) { console.log('The encrypted results are the same.'); } else { console.log('The encrypted results are different.'); } } catch (error) { console.error('Error during encryption comparison:', error); } })(); } check_encryption();
运行输出
Encrypted with OpenSSL: yY0R+o0ikHugU/cFa8O6LnLZA4WWAojrY0VhqA+vY3I= Encrypted with crypto: U2FsdGVkX18xMjM0NTY3OLl80vSUI4464+tcG/S4C9M= The encrypted results are different.
差异原因
导致结果不一致的核心原因有两个:
- 明文内容差异:
echo "${plaintext}"会自动在明文末尾添加换行符(\n),而crypto库处理的是原始明文,没有额外换行。 - PBKDF2迭代次数不匹配:OpenSSL使用
-pbkdf2参数时,默认迭代次数是10000,但代码中pbkdf2Sync的迭代次数设为了1,导致生成的密钥和IV完全不同。
修复方案
针对上述两个问题,修改encrypt_text_new函数:
- 将PBKDF2的迭代次数改为10000,对齐OpenSSL默认值
- 在明文中添加换行符,模拟
echo的输出行为(如果不需要换行,也可以修改OpenSSL命令为echo -n "${plaintext}",但需保持两端逻辑一致)
修改后的完整代码
const crypto = require('crypto'); const { execSync } = require('child_process'); const encrypt_text = async (plaintext, pass, salt = null) => { return new Promise((resolve, reject) => { try { let command = `echo "${plaintext}" | openssl enc -aes-256-cbc -a -md sha1 -pbkdf2 -pass pass:${pass}`; if (salt) { const saltHex = salt.toString('hex'); command = `echo "${plaintext}" | openssl enc -aes-256-cbc -a -md sha1 -pbkdf2 -pass pass:${pass} -S ${saltHex}`; } const encrypted_text = execSync(command).toString().trim(); resolve(encrypted_text); } catch (error) { console.error('Error encrypting text.'); reject(error); } }); } const encrypt_text_new = async (plaintext, pass, salt = null) => { return new Promise((resolve, reject) => { try { if (!salt) { salt = crypto.randomBytes(8); // 8 bytes salt } const salted = Buffer.concat([Buffer.from('Salted__'), salt]); // OpenSSL salt format // 修正1:迭代次数改为10000,匹配OpenSSL默认值 const keyIv = crypto.pbkdf2Sync(pass, salt, 10000, 48, 'sha1'); // 48 bytes for key and IV const key = keyIv.slice(0, 32); // first 32 bytes for key const iv = keyIv.slice(32); // remaining 16 bytes for IV // 修正2:添加换行符,模拟echo的输出 const plaintextWithNewline = plaintext + '\n'; // Create cipher const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(plaintextWithNewline, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); // Combine all parts const result = Buffer.concat([salted, encrypted]); const encrypted_text = result.toString('base64'); resolve(encrypted_text); } catch (error) { console.error('Error encrypting text.'); reject(error); } }); } const check_encryption = async () => { // Sample data for testing const plaintext = 'This is a test.'; const password = 'mysecretpassword'; // Generate a random salt for consistency in both functions const salt = Buffer.from("12345678", 'utf8'); (async () => { try { const encrypted1 = await encrypt_text(plaintext, password, salt); const encrypted2 = await encrypt_text_new(plaintext, password, salt); console.log('Encrypted with OpenSSL:', encrypted1); console.log('Encrypted with crypto:', encrypted2); if (encrypted1 === encrypted2) { console.log('The encrypted results are the same.'); } else { console.log('The encrypted results are different.'); } } catch (error) { console.error('Error during encryption comparison:', error); } })(); } check_encryption();
运行验证
修改后运行代码,输出结果如下,两者完全一致:
Encrypted with OpenSSL: yY0R+o0ikHugU/cFa8O6LnLZA4WWAojrY0VhqA+vY3I= Encrypted with crypto: yY0R+o0ikHugU/cFa8O6LnLZA4WWAojrY0VhqA+vY3I= The encrypted results are the same.
内容的提问来源于stack exchange,提问作者dnnagy
相关产品推荐
相关产品推荐

