javascriptnode.jscryptographyaesnode-forge

When encrypting, decrypting, then re-encrypting a string with node-forge's AES impl, why are the encrypted and re-encrypted strings different?


I am trying to use node-forge to decrypt strings encrypted by another application. After decrypting I am not getting the original strings back, so I decided to put together the following SSCCE that encrypts a string, decrypts it, then re-encrypts it. The results I get don't make sense.

Questions:

  1. First and foremost, what am I doing wrong? i.e. why is the decrypted hex different from the original hex, and why is the re-encrypted hex different from the encrypted hex?

  2. All of the code examples in the node-forge docs get the decrypted output as hex. What's up with this? I want plain text back i.e. 'hi'. How do I ask the library to give me text instead (calling decypher.output.toString() results in an error.)

  3. My ultimate goal is to be able to decrypt the output of: echo -n "hi" | openssl enc -aes-256-ctr -K $(echo -n redacted12345678 | openssl sha256) -iv 1111111111111111 -a -A -nosalt using a javascript library. Any advice on how to do that would be greatly appreciated.

SSCCE:

var forge = require('node-forge'); //npm install node-forge

//Inital data
var data = 'hi';
var iv = '1111111111111111';
var password = 'redacted12345678';

var md = forge.md.sha256.create();
md.update(password)
var keyHex = md.digest().toHex();
var key = Buffer.from(keyHex, 'hex').toString()

var cipher = forge.cipher.createCipher('AES-CTR', key);
cipher.start({iv: iv});
cipher.update(forge.util.createBuffer(data));
cipher.finish(); 
var encrypted = cipher.output.toHex()

console.log("encrypted: " + encrypted) //encrypted: 7457

var decipher = forge.cipher.createDecipher('AES-CTR', key)
decipher.start({iv: iv});
decipher.update(forge.util.createBuffer(encrypted));
decipher.finish(); 
var decrypted = decipher.output.toHex()

console.log("decrypted: " + decrypted) //decrypted: 2b0a684b

var recipher = forge.cipher.createCipher('AES-CTR', key);
recipher.start({iv: iv});
recipher.update(forge.util.createBuffer(decrypted));
recipher.finish(); 
var reencrypted = recipher.output.toHex()

console.log("reencrypted: " + reencrypted) //reencrypted: 2e5c6d1dc7cfa554

Solution

  • I've rewritten the OpenSSL command you're trying to mimic as follows:

    echo -n "hi" | openssl enc -aes-256-ctr \
        -K $(echo -n redacted12345678 | openssl sha256 -binary | xxd -p -c 256) \
        -iv $(echo -n 1111111111111111 | xxd -p) -a -A -nosalt
    

    The changes I made are due to the following:

    Executing this yields the following base64 output for the encrypted string:

    JAA=
    

    To replicate the same, I modified your JavaScript code as follows:

    const forge = require('node-forge');
    
    const data = 'hi', iv = '1111111111111111', password = 'redacted12345678';
    
    const key = forge.md.sha256.create().update(password).digest().getBytes();
    
    const cipher = forge.cipher.createCipher('AES-CTR', key);
    cipher.start({ iv });
    cipher.update(forge.util.createBuffer(data));
    cipher.finish(); 
    const encryptedBytes = cipher.output.getBytes();
    const encryptedBase64 = forge.util.encode64(encryptedBytes);
    
    console.log("encrypted: " + encryptedBase64);
    
    const decipher = forge.cipher.createDecipher('AES-CTR', key)
    decipher.start({ iv });
    decipher.update(forge.util.createBuffer(encryptedBytes));
    decipher.finish(); 
    const decryptedBytes = decipher.output.getBytes();
    const decryptedString = forge.util.encodeUtf8(decryptedBytes);
    
    console.log("decrypted: " + decryptedString);
    
    const recipher = forge.cipher.createCipher('AES-CTR', key);
    recipher.start({ iv });
    recipher.update(forge.util.createBuffer(decryptedBytes));
    recipher.finish(); 
    const reencryptedBytes = recipher.output.getBytes();
    const reencryptedBase64 = forge.util.encode64(reencryptedBytes);
    
    console.log("reencrypted: " + reencryptedBase64);
    

    Which generates matching output:

    encrypted: JAA=
    decrypted: hi
    reencrypted: JAA=
    

    In essence, everything works correctly when the entire encryption/decryption operation is done using raw bytes, and only converting from/to hex, base64 or UTF-8 string when processing input or presenting output.