node.jsbashopensslencryption-symmetric

Cannot decrypt with node.js encryptedstring using openssl in bash (symmetric encryption)


I'm symmetrically encrypting data using node.js code, that I'll decrypt using the openssl command in a bash script. I have encrypt/decrypt functions in both environments that work as expected, but encrypting in one and decrypting in the other seems not to work.

I tried to make sure the methods used are completely the same (using aes-256-cbc, 10000 iterations, encoding, ...) but methods are still incompatible.

Encrypt/decrypt in node:

const crypto = require('crypto');
const { Buffer } = require('buffer');

function encrypt(text, password) {
    const salt = crypto.randomBytes(16);
    const key = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256');
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    let encrypted = cipher.update(text, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    const result = Buffer.concat([salt, iv, Buffer.from(encrypted, 'base64')]).toString('base64');
    return result;
}

function decrypt(encryptedText, password) {
    const input = Buffer.from(encryptedText, 'base64');
    const salt = input.slice(0, 16);
    const iv = input.slice(16, 32);
    const encrypted = input.slice(32);
    const key = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256');
    const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
    let decrypted = decipher.update(encrypted, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

> encrypt('mysecrettext', 'mysecretkey')
'YRJnPLMis7e4Gj9mFDjwNPAPW6xn2EnsqRx/rcuUilLxJNbjKTC6g7HIoOaWCvZp'
> decrypt('YRJnPLMis7e4Gj9mFDjwNPAPW6xn2EnsqRx/rcuUilLxJNbjKTC6g7HIoOaWCvZp', 'mysecretkey')
'mysecrettext'

Encrypt/decrypt in Bash:

$ echo mysecrettext|openssl aes-256-cbc -a -A -salt -iter 10000 -pbkdf2 -k mysecretkey -e|b
ase64 -w0
VTJGc2RHVmtYMTl5c3NsY0s5azBMZ2wzK1ZocjNNeUhKM0d5Yll0aUtFVT0=
$(echo VTJGc2RHVmtYMTl5c3NsY0s5azBMZ2wzK1ZocjNNeUhKM0d5Yll0aUtFVT0=|base64 -d)|openssl aes-256-cbc -a -salt -iter 10000 -pbkdf2 -k mysecretkey -d
mysecrettext

But decrypting node's output:

$ echo $(echo YRJnPLMis7e4Gj9mFDjwNPAPW6xn2EnsqRx/rcuUilLxJNbjKTC6g7HIoOaWCvZp|base64 -d)|openssl aes-256-cbc -a -salt -iter 10000 -pbkdf2 -k mysecretkey -d
error reading input file

What is missing for these methods to be able to decrypt each other's data?

Tried to make sure the encryption/decryption methods and parameters in both environments are correct counterparts

Update: (@Denel)

This works also in node

const crypto = require('crypto');
const { Buffer } = require('buffer');

function encrypt(text, password) {
    const salt = crypto.randomBytes(8);
    const key = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256');
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    let encrypted = cipher.update(text, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    const result = Buffer.concat([Buffer.concat([Buffer.from('Salted__', 'utf8'), salt]), iv, Buffer.from(encrypted, 'base64')]).toString('base64');
    return result;
}

> encrypt('mysecrettext', 'mysecretkey')
'U2FsdGVkX18+fTvgLNXdePnaE3b/p+v5OXPOwx0euku7lUt3OpOS55ALFM5+FDMb'
> Buffer.from('U2FsdGVkX18+fTvgLNXdePnaE3b/p+v5OXPOwx0euku7lUt3OpOS55ALFM5+FDMb', 'utf8').toString('base64')
'VTJGc2RHVmtYMTgrZlR2Z0xOWGRlUG5hRTNiL3ArdjVPWFBPd3gwZXVrdTdsVXQzT3BPUzU1QUxGTTUrRkRNYg=='

I found I had to base64-encode the node output a secont time to translate U2FsdGVkX1 into VTJGc2RHVm, which my openssl command expects (ie. 'Salted__', twice base64-encoded). And close but no cigar in bash :

echo $(echo VTJGc2RHVmtYMTgrZlR2Z0xOWGRlUG5hRTNiL3ArdjVPWFBPd3gwZXVrdTdsVXQzT3BPUzU1QUxGTTUrRkRNYg==|base64 -d)|openssl aes-256-cbc -a -salt -iter 10000 -pbkdf2 -k mysecretkey -d
WU�j9���������imysecrettext

Solution

  • I think the order and the implementation of your code do not match how the OpenSSL library does it.

    For starters, when using the -salt flag in the OpenSSL library, it will include the Salted__ header at the beginning of your text. OpenSSL also expects this header when trying to decrypt with the -salt flag, and it does not seem to be included in your node.js implementation.

    However, I am not sure if prepending the salt and the IV is what the OpenSSL library does as well, I was under the impression that only the salt is prepended, which would also be encoded into base64 (which your implementation also does not do)

    I am not keen on the details, so you should take another good look at the OpenSSL documentation yourself.

    Edit:

    Good news, I had a lot of free time and looked into how the IV and key are generated. OpenSSL uses a custom "KDF" generator for their IV and key, the specific details of these seem to vary, but I found an interesting post that explains the idea behind it. Be careful though, it's an outdated post so it is not accurate in the details.

    The bad news, this is not included in the nodes.js crypto library, but you are lucky, I wrote a working version for you (only decrypt, I think you can figure out the encrypt yourself :)).

    function decrypt(input, password) {
        // Convert inputs to bytes
        const inputBytes = Buffer.from(input, 'base64');
        
        const salt = inputBytes.slice(8, 16);
        const encryptedData = inputBytes.slice(16); 
    
        const iterations = 10000;
        const keyLen = 32; 
        const ivLen = 16; 
    
        const derivedKey = crypto.pbkdf2Sync(password, salt, iterations, keyLen + ivLen, 'sha256');
        const key = derivedKey.slice(0, keyLen);
        const iv = derivedKey.slice(keyLen);
    
        console.log('password:', password);
        console.log('salt:', salt.toString('hex'));
        console.log('key:', key.toString('hex'));
        console.log('iv:', iv.toString('hex'));
    
        // Create a decipher object
        const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
        let decrypted = decipher.update(encryptedData, null, 'utf8');
        decrypted += decipher.final('utf8');
        console.log('decrypted:', decrypted)
    }
    

    This implementation differs from the answers in the post I mentioned above, but it gives the same output as the OpenSSL library! I tested using the -p flag, which outputs the salt, key, and IV used before de- or encrypting.

    Output from OpenSSL:

    $ echo "U2FsdGVkX1/drfR8wADgiQn8VgS8zYmI6UBIzXWOQos=" | openssl aes-256-cbc -pbkdf2 -iter 10000 -a -salt -k "mysecretkey" -p -d
    salt=DDADF47CC000E089
    key=3BAD713C8B20E5C12C40870A27979321EB9E7EA4C7BE3EF7BC5219E03E1ED777
    iv =6816DCD240F6E01D115136903B1558FB
    mysecrettext
    

    Output from the above code:

    $ node test.js 
    password: mysecretkey
    salt: ddadf47cc000e089
    key: 3bad713c8b20e5c12c40870a27979321eb9e7ea4c7be3ef7bc5219e03e1ed777
    iv: 6816dcd240f6e01d115136903b1558fb
    decrypted: mysecrettext
    

    This was a big learning experience, also for me haha!