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
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.
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:
This will make sure PHP won't throw any stupid errors. See Best way to use PHP to encrypt and decrypt passwords?
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.
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.
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
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