Search code examples
javascriptphpnode.jsencryptionnode-crypto

PHP AES Encryption into NodeJS using crypto module


My task is to follow the given and working PHP encryption into node.js. using any node module packages. I dont need to do the decryption because its already existing in their API and i just need to pass the encypted value to their API for decryption which is in PHP. I tried using node-crypto and seperate md5 module. here is the pseudo code:

  • Data Encryption Algorithm
    1. Create a 16-byte random salt and hash. The salt is newly created every time you call the API.
    2. Hash the encryption key provided.
    3. Encrypt using 'AES-128-CBC' and use the hashed salt value as vector and hashed encryption key.
    4. Prefix salt and append the encrypted data.
    5. Do Base64 encoding.
    6. JSON encode and post request

I think I'm almost done just few steps to get a successful response here is my current node.js code

Node:

const reqBody = {
    "username": "jCpVyf3VEt",
    "password": "eGD6TWKmnn",
    "account_no": "0030300155398",
    "tran_date": "08/06/2019 10:30:45",
    "reference_no": "12328ALHYGZC20",
    "area": "JENRA DAU"
};
const Serialize = require('php-serialize')
const md5 = require('md5');
//encrypt
const crypto = require('crypto'),
  algorithm = 'aes-128-cbc',
  key = 'IfZDGbVDHTxlJIkK',
  inputEncoding = 'utf8',
  outputEncoding = 'base64';

function encrypt(data, key) {
  let salt = crypto.randomBytes(16);
  let hKey = md5(key);
  let iv = md5(salt);
  let serialized = Serialize.serialize(data);
  let cipher = crypto.createCipheriv(algorithm, Buffer.from(hKey, 'hex'), Buffer.from(iv, 'hex'));
  let crypted = cipher.update(serialized, inputEncoding, outputEncoding);

  crypted += cipher.final(outputEncoding);

  let encrypted = salt.toString('base64') + crypted.toString();

  return encrypted;
}
encrypt(JSON.stringify(reqBody), key);

here is the working php code:

$data = json_encode([
  'username' => "jCpVyf3VEt",
  'password' => "eGD6TWKmnn",
  'account_no' => "0030300155398",
  'tran_date' => "08/06/2019 10:30:45",
  'reference_no' => "12328ALHYGZC20",
  'area' => "JENRA DAU"]);

function encrypt( $data) {
  $key = md5("IfZDGbVDHTxlJIkK", true);
  $cipher = "aes-128-cbc";
  $salt = openssl_random_pseudo_bytes(16);
  $iv = md5( $salt, true);
  $encrypted_bin =  $salt . openssl_encrypt( serialize( $data ), $cipher, $key, true, $iv);
  $encrypted_str = base64_encode( $encrypted_bin);

  return $encrypted_str;

}

echo encrypt($data);

for testing purpose here is the PHP code from their API for decryption:

$data = 'LI5BJJw1PEhWellnjKEt3g9oaHs8uDDknBT2qDNI7Rfs644+IjobOaFxlrIrOvDm7dkASRsOTu4Yuxzi4I5q29QoE5huH6y4/XZXsResZjLPidv1ToTnhB2UKXH5rX/g/8Od7ljO6VLVAS7zx+94xeOgtpP/idkkpDi1fRNGvnOkl1c6fcyVhwl2Pv+ijKSK9+ou+54dfQrCng2uBzKC6RrHY3lvP7ktsSvtnkXFqksrpjfJ2gnMH6sMIMzru1+D';

function decrypt($encrypted) {
 $cipher = "aes-128-cbc";
 $key = md5("IfZDGbVDHTxlJIkK", true);
    $data = base64_decode($encrypted);
    $salt = substr($data, 0, 16);
    $iv = md5($salt, true);
    $decrypted_bin = openssl_decrypt(substr($data, 16, strlen($data)), $cipher, $key, true, $iv);

    if($decrypted_bin === false) {
    return json_encode([ -102 => "Authentication Failed"]);
    }

    return unserialize( $decrypted_bin);
}
echo decrypt($data);

Running the PHP encryption code result a success response from the PHP decryption. But when I run my Node.js encryption I'm able to get an encrypted data but when I test the encrypted data from my Node.js and send the encrypted value into the PHP decryption code the result is authentication error. seems I'm not able to translate the PHP encryption algo into Node .js


Solution

  • This is a very interesting one.. I think the main issue is the method of concatenating our salt and encrypted data in Node.js.

    I found the following code worked nicely, it's giving us the same result as the PHP code.

    Note that I'm using a fixed salt here (decoding from a fixed base64 string), since this gives us a deterministic output. You should probably switch to using crypto.randomBytes for this in production.

    Also, PHP encodes "/" as "\/" in json output, see json_encode. This can be configured of course in PHP, (using the flag JSON_UNESCAPED_SLASHES in json_encode), but I suspect it can't be changed in your case. This is why I am replacing "/" with "\/" in the json plaintext.

    const reqBody = {
        "username": "jCpVyf3VEt",
        "password": "eGD6TWKmnn",
        "account_no": "0030300155398",
        "tran_date": "08/06/2019 10:30:45",
        "reference_no": "12328ALHYGZC20",
        "area": "JENRA DAU"
    };
    const Serialize = require('php-serialize')
    const md5 = require('md5');
    //encrypt
    const crypto = require('crypto'),
    algorithm = 'aes-128-cbc',
    key = 'IfZDGbVDHTxlJIkK',
    inputEncoding = 'utf8',
    outputEncoding = 'base64';
    
    function encrypt(input, key, salt) {
        let serialized = Serialize.serialize(input);
        let iv = md5(salt);
        let hKey = md5(key);
    
        let cipher = crypto.createCipheriv(algorithm, Buffer.from(hKey, 'hex'), Buffer.from(iv, 'hex'));
        let crypted = cipher.update(serialized, inputEncoding);
        let encrypted = Buffer.concat([salt, crypted, cipher.final()]);
        return encrypted.toString(outputEncoding);
    }
    
    // We must escape forward slashes here, since this is how PHP will behave. 
    let data = JSON.stringify(reqBody).replace(/\//ig, "\\/");
    
    // We can use any random salt here, e.g. crypto.randomBytes For this example we'll use the same as the existing encrypted data. 
    let salt = Buffer.from('LI5BJJw1PEhWellnjKEt3g==', 'base64');
    console.log("Encrypted data: ", encrypt(data, key, salt));