You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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.

差异原因

导致结果不一致的核心原因有两个:

  1. 明文内容差异:echo "${plaintext}"会自动在明文末尾添加换行符(\n),而crypto库处理的是原始明文,没有额外换行。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:30:57