AES 在 Node 中加密.js在 PHP 中解密. 失败


AES encrypt in Node.js Decrypt in PHP. Fail.

在node.js中,我使用内置函数来加密数据,如下所示:

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;

输出(加密文本)为:OnNINwXf6U8XmlgKJj48iA==

然后我在 PHP 中使用解密它:

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

我会得到一些有趣的角色,我无法解密它。 我尝试过有/没有base64_decode或MCRYPT_RIJNDAEL_128。都失败了。

然后我检查 PHP 中的加密方式,它看起来与 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";

加密的数据是:njCE/fk3pLD1/JfiQuyVa6w5H+Qb/utBIT3m7LAcetM=

这与node的输出非常不同.js请告知如何在node.js和php之间加密和解密。 谢谢。:)


@Mel这是我在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";

但是,它仍然不起作用。

PHP 中的加密基数 64 为:80022AGM4/4qQtiGU5oJDQ==nodejs 中的加密基数 64 为:EoYRm5SCK7EPe847CwkffQ==

因此,我无法在PHP中解密nodejs。

我想知道是不是因为nodejs不需要$iv?

晚了七个月,但我也在为此苦苦挣扎,并找到了解决方案。显然,PHP 用零字节填充输入,使其大小是块大小的倍数。例如,使用 AES-128,14 字节输入"低音提琴手"将填充两个零字节,如下所示:

"contrabassists'0'0"

N*块大小的字节输入是独立的。

但是,标准节点加密函数使用称为 PKCS5 的不同填充方案。PKCS5 不添加零,而是添加填充的长度,因此再次使用 AES-128,"低音提琴手"将变为:

"contrabassists'2'2"

即使是 N*块大小的字节输入也会填充在 PKCS5 中。否则,解码后无法删除填充。输入的"光谱日光图"将变为:

"spectroheliogram'16'16'16'16'16'16'16'16'16'16'16'16'16'16'16'16"

为了使 PHP m_crypt 加密与 Node 解密兼容,您必须自己填充输入:

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

相反,您必须读取解码数据的最后一个字节并自己切断填充。

示例函数:(2012 年 1 月 14 日新增)

在 PHP 中,此函数将返回 AES-128 加密的十六进制编码数据,这些数据可由 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));
}

在 Node 中,以下内容将解密数据:

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('')
}

我还没有做相反的事情,但是一旦您了解了填充方案,它应该很简单。我没有对密钥/iv 生成做出任何假设。

我刚刚开始弄乱节点.js但我认为您的问题与不匹配的 IV 有关。请尝试执行以下操作:

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

PS:我不确定如何在node.js中创建MD5哈希,您必须自己弄清楚并相应地更改上述代码。

在 PHP 中:

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

这应该确保两个实现使用相同的初始化向量。

我还建议您进行以下更改:

  • 密码: MD5(original_password)
  • iv = md5(
  • md5(original_password))

这将确保PHP不会抛出任何愚蠢的错误。请参阅使用 PHP 加密和解密密码的最佳方法?

如果

对其他人有帮助,我在另一篇文章中还有另一个工作示例。

如果确保在 PHP 和 Node 中使用长度为

32 个字符的"密钥/机密"和长度为 16 个字符的 IV,并在 Node 中使用base64加密编码和utf8消息编码,则填充架构中的任何差异应该没有任何问题。

问候Ignacio

AES 是 rijndael,固定大小为 16 字节 IV。详情请见此处。不能用于解密。更重要的是,我也无法使用 openssl 解密您的字符串:

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

或者使用 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));

输出:

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

因此,节点.js似乎正在使用一些未记录的功能来创建 IV,我认为无法在 node.js 中提供 IV。

我发现了一些事情,这可能是PHP和Node.js上的解密/加密不同的原因。

PHP使用MCRYPT_RIJNDAEL_256算法。AES 256 基于 MCRYPT_RIJNDAEL_256,但并不相同。AES 256 这是加密标准,但不是算法。

如果您尝试使用标准简单函数(例如PHP上的"mcrypt_encrypt"和"mcrypt_decrypt")加密某些内容,则无法看到所有步骤,并且肯定无法知道为什么无法解密加密的内容。对于 Node.js 可以相同,因为需要使用可以逐步加密的函数以防止替换默认参数。

要加密/解密您需要知道的某些内容(设置):

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

并在两侧检查它。它应该是相同的。还需要找到正确的组合"加密方法"+"加密模式",这肯定对双方都有效。

我的解决方案是RIJNDAEL_256+欧洲央行。你应该安装 node-rijndael,因为它肯定使用RIJNDAEL_256。如果没有 - 我的例子将不起作用。

这是 Node.js 加密示例

将node-rijndael安装在应该有两个.js文件的文件夹中。

R256.js - 它是加密/解密的功能。我在这里找到了它。

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;

加密.js - 这是加密的示例。

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});

这是用于解密的 PHP 示例

<?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);

附言我不知道为什么,但是 Node.js 忽略了 IV(在我的示例中),因此密码将始终相同。PHP 总是使用 IV,它应该是严格的长度,所以 PHP 总是返回不同的密码。但是我反过来尝试了它(通过PHP加密并通过Node.js解密),它可以工作。

Node.js 正在用你的输入密码做一些魔术来派生一个密钥和 iv。很难看出这在PHP中是如何工作的,除非PHP做完全相同的键和iv派生魔法。

你为什么不使用createCipheriv来代替。使用基于密码的密钥派生函数从密码创建密钥。例如:

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

这样的函数在更高版本的 Node 中可用.js

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

也提供一个好的 iv;你可以使用 crypto.randomBytes 创建一个。如果您控制键和 iv 参数,那么您将更容易确定是否可以将数据往返到 PHP。

您不能只对密码进行哈希处理以生成 iv。对于每个加密消息,iv应该是不同的,否则它是无用的。

另外,您告诉 Node.js您的输入字符串"Yes"是 Base64 编码的,但我认为它实际上是 ASCII 或 UTF-8。

如果您坚持使用MCRYPT_RIJNDAEL_256的第三方库,请知道 256 指定块大小,而不是密钥大小。AES 使用 128 位的固定块大小,openssl 不实现更通用的 Rijndael 算法。为了规避这一点,我发布了一个绑定到libmcrypt的模块,就像PHP一样。这是一个非常有限的用例,但它确保它与 256 位块大小的 rijndael 兼容。

如果你在 PHP 中使用它

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

你可以在 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