12

In node.js, I use the build in function to encrypt data like that:

var text = "Yes";
var password = "123456";
var encrypt = crypto.createCipher('aes-256-cbc', password);
var encryptOutput1 = encrypt.update(text, 'base64', 'base64');
var encryptOutput2 = encrypt.final('base64');
var encryptedText = encryptOutput1 + encryptOutput2;

the output (encrypted text) is: OnNINwXf6U8XmlgKJj48iA==

Then I use decrypt it in PHP:

$encrypted = 'OnNINwXf6U8XmlgKJj48iA==';
(or $encrypted = base64_decode('OnNINwXf6U8XmlgKJj48iA==')  );
$dtext2 = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $encrypted, MCRYPT_MODE_CBC);
echo "Decrypted: $dtext2";

I will get some funny characters, which I can't decrypted it. I tried with/without base64_decode or MCRYPT_RIJNDAEL_128.. all fail.

Then I check how the encryption in PHP, it looks very different from the output from node.js.

$text = "Yes";
    $key = "123456"; 


    $eText = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_CBC);
    echo "Encrypted: $eText \n";
    echo "base64: " . base64_encode($eText) . " \n";

    $dtext1 = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $eText, MCRYPT_MODE_CBC);
    echo "Decrypted: $dtext1 \n\n";

It can encrypt and decrypt. and the encrypted data is : njCE/fk3pLD1/JfiQuyVa6w5H+Qb/utBIT3m7LAcetM=

which is very different from the output from node.js please advise how I can encrypt and decrypt between node.js & php. thanks. :)


@Mel here is what I have in PHP:

$text = "Yes";

$key = "32BytesLongKey560123456789ABCDEF"; 
$iv =  "sixteenbyteslong";

/* Open the cipher */
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');

/* Intialize encryption */
mcrypt_generic_init($td, $key, $iv);

/* Encrypt data */
$eText = mcrypt_generic($td, $text);

echo "Encrypted Data: $eText \n";
echo "base64: " . base64_encode($eText) . " \n";

/* Terminate encryption handler */
mcrypt_generic_deinit($td);

/* Initialize encryption module for decryption */
mcrypt_generic_init($td, $key, $iv);

/* Decrypt encrypted string */
$dText = mdecrypt_generic($td, $eText);

/* Terminate decryption handle and close module */
mcrypt_generic_deinit($td);
mcrypt_module_close($td);

/* Show string */
echo trim($dText) . "\n";

However, it still doesn't work.

The encrypted base 64 in PHP is: 80022AGM4/4qQtiGU5oJDQ== The encrypted base 64 in nodejs is: EoYRm5SCK7EPe847CwkffQ==

thus, i can't decrypt the nodejs one in PHP.

I wonder if it is because nodejs doesn't require $iv?

Nick Craver
  • 594,859
  • 130
  • 1,270
  • 1,139
murvinlai
  • 43,517
  • 50
  • 120
  • 169

7 Answers7

19

Seven months late, but I was struggling with this as well, and found a solution. Apparently, PHP pads the input with zero bytes to make its size a multiple of the block size. For example, using AES-128, the 14 byte input "contrabassists" will be padded with two zero bytes, like this:

"contrabassists\0\0"

A N*blocksize byte input is left alone.

The standard Node crypto functions, however, use a different padding scheme called PKCS5. PKCS5 doesn't add zeros, but adds the length of the padding, so again using AES-128, "contrabassists" would become:

"contrabassists\2\2"

Even a N*blocksize byte input gets padded in PKCS5. Otherwise, it's impossible to remove the padding after decoding. The input "spectroheliogram" would then become:

"spectroheliogram\16\16\16\16\16\16\16\16\16\16\16\16\16\16\16\16"

To make PHP m_crypt encryption compatible with Node decryption, you'll have to pad the input yourself:

$pad = $blocksize - (strlen($input) % $blocksize);
$input = $input . str_repeat(chr($pad), $pad);

The other way around, you'll have to read the last byte of the decoded data and cut off the padding yourself.

Example functions: (added 01-14-2012)

In PHP, this function would return AES-128 encrypted, hex encoded data that can be decrypted by Node:

function nodeEncrypt($data, $key, $iv) {
    $blocksize = 16; // AES-128
    $pad = $blocksize - (strlen($data) % $blocksize);
    $data = $data . str_repeat(chr($pad), $pad);
    return bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv));
}

In Node, the following would decrypt the data:

function nodeDecrypt(data, key, iv) {
    var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
    var chunks = []
    chunks.push(decipher.update(data.toString(),'hex','binary'))
    chunks.push(decipher.final('binary'))
    return chunks.join('')
}

I haven't done the reverse yet, but it should be straightforward once you understand the padding scheme. I haven't made any assumptions about key/iv generation.

Michilus
  • 475
  • 3
  • 6
2

I'm just starting messing around with node.js but I think your problem is related to mismatching IVs. Try doing the following instead:

var encrypt = crypto.createCipheriv('aes-256-cbc', password, /* password.createHash('md5').toHex()*/);

PS: I'm not sure how to create an MD5 hash in node.js, you'll have to figure it out for yourself and change the above code accordingly.

And in PHP:

$decrypt = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, base64_decode($encrypted), MCRYPT_MODE_CBC, md5($key)), "\0");

This should make sure both implementations use the same initialization vector.

I also recommend that your make the following changes:

  • password: md5(original_password)
  • iv = md5(md5(original_password))

This will make sure PHP won't throw any stupid errors. See Best way to use PHP to encrypt and decrypt passwords?

Community
  • 1
  • 1
Alix Axel
  • 141,486
  • 84
  • 375
  • 483
  • thanks.. i will try .. but I don't see there is a function called crypto.createCipheriv in crypt API. is it from other npm package? – murvinlai May 18 '11 at 19:09
  • @murvinlai: Doesn't seem to be documented, but it's there: https://github.com/joyent/node/blob/9f0b1a9bc60f70b7c5c014743eb1edd69c36db76/lib/crypto.js#L118. – Alix Axel May 18 '11 at 19:13
1

I have another working example in this other post if it helps for anyone else.

If you make sure you use an 32 characters length "key/secret" and a 16 characters length IV in both PHP and Node, and base64 encryption encoding and utf8 message encoding in Node, then you should not have any issues with any differences in the padding schema.

Regards, Ignacio

Community
  • 1
  • 1
inieto
  • 589
  • 9
  • 17
1

I found couple of things which might be the reasons why decryption/encryption on PHP and Node.js are not the same.

PHP used MCRYPT_RIJNDAEL_256 algorythm. AES 256 is based on MCRYPT_RIJNDAEL_256, but is not the same. AES 256 this is encryption standart, but not algorythm.

If you trying to encrypt some thing by using standart simple functions ("mcrypt_encrypt" and "mcrypt_decrypt" on PHP for example), you can't see all of steps and you surely can't know why you can't decrypt that what you encrypted. It can be same for Node.js, because need to use function which can encrypt step by step to prevent substitution to default parameters.

To encrypt/decrypt some thing you need to know (to set):

encryption method (algorythm)
encryption mode (CBF, ECB, CBC...)
key to decryption
key lenght
initialisation vector lenght

And check it on the both sides. It should be the same. Also need to find right combination "encryption method" + "encryption mode" which surely working on the both sides.

My solution is RIJNDAEL_256 + ECB. You should install node-rijndael, because it uses RIJNDAEL_256 for sure. If not - my example will not work.

Here is Node.js example for encryption.

Install node-rijndael in some folder where should be two .js files.

r256.js - it is functions for encrypt/decrypt. I found it here.

var Rijndael = require('node-rijndael');

/**
 * Pad the string with the character such that the string length is a multiple
 * of the provided length.
 *
 * @param {string} string The input string.
 * @param {string} chr The character to pad with.
 * @param {number} length The base length to pad to.
 * @return {string} The padded string.
 */
function rpad(string, chr, length) {
  var extra = string.length % length;
  if (extra === 0)
    return string;

  var pad_length = length - extra;
  // doesn't need to be optimized because pad_length will never be large
  while (--pad_length >= 0) {
    string += chr;
  }
  return string;
}

/**
 * Remove all characters specified by the chr parameter from the end of the
 * string.
 *
 * @param {string} string The string to trim.
 * @param {string} chr The character to trim from the end of the string.
 * @return {string} The trimmed string.
 */
function rtrim(string, chr) {
  for (var i = string.length - 1; i >= 0; i--)
    if (string[i] !== chr)
      return string.slice(0, i + 1);

  return '';
}

/**
 * Encrypt the given plaintext with the base64 encoded key and initialization
 * vector.
 *
 * Null-pads the input plaintext. This means that if your input plaintext ends
 * with null characters, they will be lost in encryption.
 *
 * @param {string} plaintext The plain text for encryption.
 * @param {string} input_key Base64 encoded encryption key.
 * @param {string} input_iv Base64 encoded initialization vector.
 * @return {string} The base64 encoded cipher text.
 */
function encrypt(plaintext, input_key, input_iv) {
  var rijndael = new Rijndael(input_key, {
    mode: Rijndael.MCRYPT_MODE_ECB,
    encoding: 'base64',
    iv: input_iv
  });
console.log("Rijndael.blockSize", Rijndael.blockSize);
  var padded = rpad(plaintext, '\0', Rijndael.blockSize);

  return rijndael.encrypt(padded, 'binary', 'base64');
}

/**
 * Decrypt the given ciphertext with the base64 encoded key and initialization
 * vector.
 *
 * Reverses any null-padding on the original plaintext.
 *
 * @param {string} ciphertext The base64 encoded ciphered text to decode.
 * @param {string} input_key Base64 encoded encryption key.
 * @param {string} input_iv Base64 encoded initialization vector.
 * @param {string} The decrypted plain text.
 */
function decrypt(ciphertext, input_key, input_iv) {
  var rijndael = new Rijndael(input_key, {
    mode: Rijndael.MCRYPT_MODE_ECB,
    encoding: 'base64',
    iv: input_iv
  });
console.log('lol', rijndael.decrypt(ciphertext, 'base64', 'binary'));
  return rtrim(rijndael.decrypt(ciphertext, 'base64', 'binary'), '\0');
}

exports.decrypt = decrypt;
exports.encrypt = encrypt;

encrypt.js - it is example for encryption.

var crypto = require('crypto');

var key = new Buffer('theonetruesecretkeytorulethemall', 'utf-8').toString('base64'); //secret key to decrypt

var iv = crypto.randomBytes(32).toString('base64');

console.log({"key":key, "iv":iv});
var rijndael = require('./r256'); 
var plaintext = 'lalala'; //text to encrypt

var ciphertext = rijndael.encrypt(plaintext, key, iv);
console.log({"ciphertext":ciphertext});

Here is PHP example for decryption.

<?php
echo "<PRE>";
$mcrypt_method = MCRYPT_RIJNDAEL_256;
$mcrypt_mode = MCRYPT_MODE_ECB;
$mcrypt_iv = '123456'; //needed only for encryption, but needed for mcrypt_generic_init, so for decryption doesn't matter what is IV, main reason it is IV can exist.
$mcrypt_key = 'theonetruesecretkeytorulethemall';
$data_to_decrypt = base64_decode('ztOS/MQgJyKJNFk073oyO8KklzNJxfEphu78ok6iRBU='); //node.js returns base64 encoded cipher text


$possible_methods = array_flip(mcrypt_list_algorithms());

if(empty($possible_methods[$mcrypt_method]))
{
    echo "method $mcrypt_method is impossible".PHP_EOL;
    exit();
}

$possible_modes = array_flip(mcrypt_list_modes());
if(empty($possible_modes[$mcrypt_mode]))
{
    echo "mode $mcrypt_mode is impossible".PHP_EOL;
    exit();
}

if(!@mcrypt_get_block_size($mcrypt_method, $mcrypt_mode))
{
    echo "method $mcrypt_method does not support mode $mcrypt_mode".PHP_EOL;
    exit();
}

$mcrypt = mcrypt_module_open($mcrypt_method,'', $mcrypt_mode, '');

$ivsize = mcrypt_enc_get_iv_size($mcrypt);

if($ivsize != strlen($mcrypt_iv))
{
    $mcrypt_iv = str_pad($mcrypt_iv, $ivsize, '#');
}

if($ivsize < strlen($mcrypt_iv))
{
    $mcrypt_iv=substr($mcrypt_iv,0,$ivsize);
}

$keysize = mcrypt_enc_get_key_size($mcrypt);
if($keysize != strlen($mcrypt_key))
{
    $mcrypt_key = str_pad($mcrypt_key, $keysize, '#');
}

if($keysize < strlen($mcrypt_key))
{
    $mcrypt_key=substr($mcrypt_key,0,$keysize);
}


$mcrypt_isblock = (int)mcrypt_enc_is_block_mode($mcrypt);
$mcrypt_blocksize = mcrypt_enc_get_block_size($mcrypt);
$mcrypt_method = mcrypt_enc_get_algorithms_name($mcrypt);
$mcrypt_mode = mcrypt_enc_get_modes_name($mcrypt);

echo "used method=$mcrypt_method  \nmode=$mcrypt_mode \niv=$mcrypt_iv \nkey=$mcrypt_key \nkey with blocksize=$mcrypt_blocksize \nisblock=$mcrypt_isblock".PHP_EOL;

if(mcrypt_generic_init($mcrypt,$mcrypt_key,$mcrypt_iv)< 0)
{
    echo "mcrypt_generic_init failed...".PHP_EOL;
    exit();
}


$result = mdecrypt_generic($mcrypt, $data_to_decrypt);

echo PHP_EOL."decryption result|".$result.'|';

mcrypt_generic_deinit($mcrypt);

P.S. I don't know why, but Node.js ignores IV (in my example), so cipher will be always the same. PHP always uses IV and it should be strict lenght, so PHP returns diffirent ciphers always. But i tried it the other way round (encrypt by PHP and decrypt by Node.js) and it works.

madlopt
  • 387
  • 3
  • 7
1

AES is rijndael with fixed size 16 byte IV. Details here. Can't be used to decrypt. More importantly, I can't decrypt your string using openssl either:

% openssl aes-256-cbc -d -in dec.txt -a
enter aes-256-cbc decryption password:
bad magic number

Or using php:

$encrypted = 'OnNINwXf6U8XmlgKJj48iA==';
$text = 'Yes';
$pw = '123456';
$decrypted = @openssl_decrypt($encrypted, 'aes-256-cbc', $pw);
var_dump($decrypted);
var_dump(@openssl_encrypt($text, 'aes-256-cbc', $pw, FALSE, $pw));
var_dump(@openssl_encrypt($text, 'aes-256-cbc', $pw));

Output:

bool(false)
string(24) "xrYdu2UyJfxhhEHAKWv30g=="
string(24) "findrYaZVpZWVhEgOEVQwQ=="

So it seems that node.js is using some undocumented feature to create the IV and I see no way to provide the IV in node.js.

Mel
  • 5,748
  • 1
  • 13
  • 12
  • do i have to keep the key 32 byte? – murvinlai May 18 '11 at 16:43
  • it doesn't work. I have tried encrypted with AES-256 in node.js. then decrypt with RJ-128 with the same 32bytes Key, and a 16 bytes iv. still fail. – murvinlai May 18 '11 at 17:45
  • The encrypted data is base64 encoded after encrypted. is that why it make the difference? – murvinlai May 18 '11 at 19:16
  • and I tried command line. Even I encrypted the data in openssl command line. The encrypted code is still different from the one in PHP (even without iv) ... headache.. :( – murvinlai May 18 '11 at 19:43
0

If you're stuck with a third-party library which uses MCRYPT_RIJNDAEL_256, know that the 256 specifies the block size, not the key size. AES uses a fixed block-size of 128 bits, and openssl does not implement more generic Rijndael algorithms. To circumvent this I published a module that binds to libmcrypt, just as PHP does. It's a pretty limited use-case, but it ensures it will be compatible with 256-bit block size rijndael.

If you're using this in PHP

mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $plaintext, MCRYPT_MODE_ECB);
mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $ciphertext, MCRYPT_MODE_ECB);

You can do the same in Node:

var rijndael = require('node-rijndael');

// straight through (must be buffers)
rijndael.encrypt(plaintext, key);
rijndael.decrypt(ciphertext, key);

// or bound (can take a string for the key and an encoding)
var rijn = rijndael(key);
rijn.encrypt(plaintext); // gotta be a buffer again for implementation simplicity
rijn.decrypt(ciphertext);

node-rijndael on GitHub

node-rijndael on npm

skeggse
  • 5,220
  • 10
  • 52
  • 76
0

Node.js is doing some magic with your input password to derive a key and iv. It's hard to see how that would work in PHP unless PHP does exactly the same key and iv derivation magic.

Why don't you use createCipheriv instead. Use a password-based key derivation function to create a key from the password. For example:

http://en.wikipedia.org/wiki/PBKDF2

Such a function is available in later versions of Node.js

http://nodejs.org/docs/latest/api/crypto.html#crypto_crypto_pbkdf2_password_salt_iterations_keylen_callback

Provide a good iv as well; you can create one using crypto.randomBytes. If you control the key and iv parameters then you will have a much easier time determining if you can round-trip your data to PHP.

You can't just hash the password to generate an iv. The iv is supposed to be different for every encrypted message, otherwise it is useless.

Also, you are telling Node.js that your input string "Yes" is Base64 encoded, but I think it's really ASCII or UTF-8.

Tauren
  • 25,387
  • 37
  • 126
  • 165
kgilpin
  • 2,071
  • 14
  • 18