I used to encrypt text in nodejs by invoking openssl via execSync, but it's a bad practice, because it could leak password to system logs. Now I want to use the crypto
library, but I get inconsistent results.
Here's a minimal code to reproduce:
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();
Will output:
Encrypted with OpenSSL: yY0R+o0ikHugU/cFa8O6LnLZA4WWAojrY0VhqA+vY3I=
Encrypted with crypto: U2FsdGVkX18xMjM0NTY3OLl80vSUI4464+tcG/S4C9M=
The encrypted results are different.
How to get nodejs encryption consistent with openssl?
The following changes are required so that the crypto code generates the same ciphertext as the OpenSSL statement:
-pbkdf2
(this value can be changed with -iter
); in the crypto code an iteration count of 1 is used. This value must therefore be changed to 10,000:
const keyIv = crypto.pbkdf2Sync(pass, salt, 10000, 48, 'sha1');
\n
) is automatically appended at the end. To achieve the same result in the crypto code, a \n
must be appended to the plaintext:
let encrypted = cipher.update(plaintext + '\n', 'utf8');
If the appending of the line break is not intended, it can be avoided with echo -n
(then of course the crypto code does not need to be modified).Salted__|<salt>
is not applied (this is apparently the case for more modern OpenSSL versions when -S
is specified). The concatenation must therefore be omitted in the crypto code:
const result = encrypted; // Buffer.concat([salted, encrypted]);
With these changes, the crypto code returns the same ciphertext as the OpenSSL statement.