1

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.

  • Original String: hi (hex equivalent would be 6869)
  • Encrypted Hex String: 7457
  • Decrypted Hex String: 2b0a684b
  • Re-Encrypted Hex String: 2e5c6d1dc7cfa554

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
David
  • 14,569
  • 34
  • 78
  • 107
  • 1
    Do you really need all these round trips to/from hex encoding? It seems like something is going wrong there. hex-encoding is mainly useful for human *viewing* of binary data like the results of encryption of randomly generated keys and IVs/counters. – President James K. Polk Dec 08 '22 at 03:02

1 Answers1

1

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:

  • The hex ouput that the openssl sha256 command generates, is prefixed with (stdin)= (at least on my distribution), so I just ran the binary hash of your password through xxd to get the key as a clean hex string.
  • The openssl enc command expects the IV to be in hex format, so I also ran that through xxd. (Since 16 bytes are required for the AES IV, your command would have resulted in an IV value of 111111111111111100000000000000, which I assume is not what you were aiming for.)

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.

Robby Cornelissen
  • 91,784
  • 22
  • 134
  • 156
  • Thanks you so much for this! I will need to take a little bit of time to make sure I grok it on my end, but I expect to accept this as the correct answer later today. – David Dec 08 '22 at 15:20