Javascript 使用PHP(openssl_encrypt)进行加密,然后使用JS(CryptoJS)进行解密

Javascript 使用PHP(openssl_encrypt)进行加密,然后使用JS(CryptoJS)进行解密,javascript,php,encryption,cryptography,cryptojs,Javascript,Php,Encryption,Cryptography,Cryptojs,我第一次使用CryptoJS,我正在努力解密我在PHP中使用openssl\u encrypt()加密的字符串 PHP 5.6.13.0和CryptoJS 3.1.2 首先,我的PHP: $encryptHash=hash_pbkdf2(“sha256”、“0000”、“secret”、1000、32); 变量转储($encryptHash); $iv=openssl_随机_伪_字节(openssl_密码_iv_长度('aes-256-cbc'); var_转储(bin2hex($iv));

我第一次使用CryptoJS,我正在努力解密我在PHP中使用openssl\u encrypt()加密的字符串

PHP 5.6.13.0和CryptoJS 3.1.2


首先,我的PHP:

$encryptHash=hash_pbkdf2(“sha256”、“0000”、“secret”、1000、32);
变量转储($encryptHash);
$iv=openssl_随机_伪_字节(openssl_密码_iv_长度('aes-256-cbc');
var_转储(bin2hex($iv));
$encrypted=openssl_encrypt(“你好!这是我的字符串!”,'aes-256-cbc',$encryptHash,0,$iv);
var_dump($加密);
$encrypted=base64_encode($encrypted.:“.bin2hex($iv));
echo“\r\n”。$已加密;
这为我提供了以下输出:

string(32) "59b6ab46d379b89d794c87b74a511fbd"
string(32) "0aaff094b6dc29742cc98a4bac8bc8f9"
string(44) "xHIxg1HDUOqyhBmAaU2Sx3ct8GaKaeE5w4d1KM1yuDw="

eEhJeGcxSERVT3F5aEJtQWFVMlN4M2N0OEdhS2FlRTV3NGQxS00xeXVEdz06MGFhZmYwOTRiNmRjMjk3NDJjYzk4YTRiYWM4YmM4Zjk=

现在我的JS:

var encryptedString=“eehjegcxservt3f5aejtqwfvmln4m2n0oedhs2flrtv3ngqxs00xexvedz06mgfhzmyotrinmrjmjk3ndjyzk4ytriywm4ymm4zjk=”;
var key256Bits=CryptoJS.PBKDF2(“0000”,“secret”,{keySize:128/32,迭代次数:1000,hasher:CryptoJS.algo.SHA256});
var keyAsHex=key256Bits.toString(CryptoJS.enc.Hex);
/*keyAsHex=“59b6ab46d379b89d794c87b74a511fbd”*/
var rawData=atob(加密字符串);
var rawPieces=rawData.split(“:”);
var crypttext=rawPieces[0];
var iv=原材料[1];
/*crypttext=“xHIxg1HDUOqyhBmAaU2Sx3ct8GaKaeE5w4d1KM1yuDw=”*/
/*iv=“0aaff094b6dc29742cc98a4bac8bc8f9”*/
/*到目前为止还好吗*/
var plaintextArray=CryptoJS.AES.decrypt(
{ciphertext:CryptoJS.enc.Base64.parse(cryptotext)},
CryptoJS.enc.Hex.parse(keyAsHex),
{iv:CryptoJS.enc.Hex.parse(iv)}
);
/*明文数组:d.WordArray.n.extend.init
兆字节:-67
单词:数组[8]
0: 1419734786
1: -2048883413
2: -1709437124
3: 736946566
4: 718053567
5: -64039355
6: 1868905697
7: -910423965 */
var输出=CryptoJS.enc.Utf8.stringify(明文数组);
/*output=“”*/

如您所见,我的输出是一个空字符串。有人试图做类似的事情吗?我被难住了

编辑 原来我的钥匙长度不正确!以下是我正在使用的PHP(加密)和JS(解密)代码:


PHP:

$encryptHash=hash_pbkdf2(“sha256”、“0000”、“secret”、1000、32、true);
变量转储($encryptHash);
$iv=openssl_随机_伪_字节(openssl_密码_iv_长度(“aes-256-cbc”);
var_dump(四美元);
$encrypted=openssl_encrypt(“您好!这是一个测试!”,“aes-256-cbc”,$encryptHash,0,$iv);
var_dump($加密);
$encrypted=base64_encode($encrypted.:“.bin2hex($iv));
echo“\r\n”。$已加密;
给我以下信息:

string(32) "Y½FËy©ØyLçÀJQ▼¢▄▄êI╩öo§(NtÙת‼ç"
string(16) "àX§ $VÇ‼♣┘█²áÓßt"
string(44) "VIzzao8Wdo8HPM015v6c5Q77ervGUIVbL6ERKRXb0fU="

Vkl6emFvOFdkbzhIUE0wMTV2NmM1UTc3ZXJ2R1VJVmJMNkVSS1JYYjBmVT06ODU1ODE1MjAyNDU2ODAxMzA1ZDlkYmZkYTBlMGUxNzQ=

JS:

var encryptedString=“vkl6emfvodfkbzhiue0wmtv2nmm1utc3zxj2r1vjvmjmnkvss1jyyjbmvt06odu1ode1jayndu2odaxmza1zdlkymzkbluxnzq=”;
var key256Bits=CryptoJS.PBKDF2(“0000”,“secret”,{keySize:256/32,迭代次数:1000,hasher:CryptoJS.algo.SHA256});
var rawData=atob(加密字符串);
var rawPieces=rawData.split(“:”);
var crypttext=rawPieces[0];
var iv=CryptoJS.enc.Hex.parse(rawPieces[1]);
var cipherParams=CryptoJS.lib.cipherParams.create({ciphertext:CryptoJS.enc.Base64.parse(cryptotext)});
var plaintextArray=CryptoJS.AES.decrypt(
密码参数,
键256位,
{iv:iv}
);
var输出=CryptoJS.enc.Utf8.stringify(明文数组);
/*输出=='你好!这是一个测试*/

TL;灾难恢复-尝试使用32字节键而不是16字节键。

在撰写了一个早期的答案并最终将其删除后,我反驳了我自己关于这是一个填充问题的理论:-),现在我相当确定这个问题可能只是与键长度有关

在试图重现您的问题时,我无法使第一个密文块在使用
openssl\u encrypt
vs
CryptoJS
生成时相同。然后我把钥匙的长度增加了一倍,它就工作了

上面生成的密钥是32个字符,但转换后只有16个字节,所以尝试将其加倍,看看会发生什么

FWIW,下面是我用来测试密钥长度的PHP代码:

$data = "hello! this is a test!";
$method = 'aes-256-cbc';
$key = '59b6ab46d379b89d794c87b74a511fbd59b6ab46d379b89d794c87b74a511fbd';
$iv = '0aaff094b6dc29742cc98a4bac8bc8f9';

$e = openssl_encrypt( $data, $method, hex2bin( $key ), 0, hex2bin( $iv ));

echo 'Ciphertext: [', bin2hex( base64_decode( $e )), "]\n";
echo 'Key:        [', $key, "]\n";
echo 'Cleartext:  [', openssl_decrypt( $e, $method, hex2bin( $key ), 0, hex2bin( $iv )), "]\n";

// Test with openssl on the command line as well, just to be sure!
file_put_contents( 'clear.txt', $data );

$exec = "openssl enc -$method -e -in clear.txt -out encrypted.txt -base64 -nosalt -K $key -iv $iv";
exec ($exec);
$out = file_get_contents( 'encrypted.txt' );
echo 'Ciphertext: [', bin2hex( base64_decode(trim($out))), "]\n";
下面是兼容的JavaScript,我在Mac上使用
jsc
运行它:

var data = "hello! this is a test!";
var key = '59b6ab46d379b89d794c87b74a511fbd59b6ab46d379b89d794c87b74a511fbd';
var iv = '0aaff094b6dc29742cc98a4bac8bc8f9';

var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(data), CryptoJS.enc.Hex.parse(key), { iv: CryptoJS.enc.Hex.parse(iv) });

print( 'Ciphertext: [' + encrypted.ciphertext + ']' );
print( 'Key:        [' + encrypted.key + ']' );

cipherParams = CryptoJS.lib.CipherParams.create({ciphertext: CryptoJS.enc.Hex.parse(encrypted.ciphertext.toString())});
var decrypted = CryptoJS.AES.decrypt(cipherParams, CryptoJS.enc.Hex.parse(key), { iv: CryptoJS.enc.Hex.parse(iv) });

print( 'Cleartext:  [' + decrypted.toString(CryptoJS.enc.Utf8) + ']');
无论输入的长度如何,这两段代码都会生成相同的密文,这证实了填充策略在两个库之间是兼容的。但是,如果将密钥长度减半,密文将不再相同,这显然意味着解密也将不兼容

更新

我刚刚发现,默认情况下,
hash_pbkdf2()
返回ASCII十六进制字符串,因此在将其传递给
openssl_encrypt()
之前,您应该使用
hex2bin()将
$encryptHash
转换为二进制,或者将
hash_pbkdf2()
的最后一个参数设置为
true
,以获得原始输出

更新2

我刚刚确认,如果您进行以下更改,您的代码将正常工作:

在PHP中,将密钥大小从32字节更改为64字节,并在生成密钥时添加原始输出选项:

$encryptHash = hash_pbkdf2("sha256", "0000", "secret", 1000, 64, 1);
将JavaScript中的密钥长度从128位更改为256位:

var key256Bits  = CryptoJS.PBKDF2("0000", "secret", { keySize: 256/32, iterations: 1000, hasher: CryptoJS.algo.SHA256 });
希望这些更改在您尝试它们时能起作用。

关于错误的键长度和hash_pbkdf2中的
bool$binary=true
的想法也解决了我的所有问题,因为一旦开始深入研究这个问题就不那么明显了。我添加了我的解决方案和一些额外的解释,这样可以在查找所有这些信息时节省一些时间

我发现的另一个相当重要的细节是函数的
0
options'参数,这在用PHP加密和用JS解密时会带来更多的混乱。这对返回的数据格式有很大影响,使用CryptoJS时应该注意

第四个选项的参数设置为
0
,返回的数据由openssl_encrypt编码为Base64,因此需要在CryptoJS中从Base64解码两次。但是,
    const encryptedPlainText = "w7lnM1VEQwdZssE=";
    const passphrase = "obligate properly elective edge"; // don't save it here, get it from some other place
    const salt = "0c5597db78ac4aedf2bdfb1d4ce7935c270876284239b0ef48ba63d08ed164b5"; // 64 characters
    const iv = "MzcwNmY0ZjA4OWMyZjZmMmUwYWFmYTYxOTExNzBkYWU=";
    const parsedSalt = CryptoJS.enc.Hex.parse(salt); // or: CryptoJS.enc.Latin1.parse(salt);
    const parsedIV = CryptoJS.enc.Base64.parse(iv);

    const key = CryptoJS.PBKDF2(passphrase, parsedSalt, {
        hasher: CryptoJS.algo.SHA256,
        keySize: 256 / 32, // the length of the key is then 32 characters
        iterations: 1001,
    });
    // you can check the length in bytes like so:
    console.log("KEY (in bytes in Latin1):",
        CryptoJS.enc.Latin1.parse(CryptoJS.enc.Latin1.stringify(key))
      );
    // KEY (in bytes in Latin1):  t.init {words: Array(8), sigBytes: 32}

    console.log("KEY (toString in Latin1): ", key.toString(CryptoJS.enc.Latin1));
    // KEY (toString in Latin1):  ÎgŠƒ)·ƒÙ2™'ú¤ø€ÃM2eCYéI§J§Ä^

    // However in UTF-8 is's 64 characters, so keep this in mind:
    console.log("KEY (Utf8): ", key.toString());
    // ce670c8a7f8329b78306d9329927faa4f880c34d32654359e949a74aa77fc45e
    
    const decrypted = CryptoJS.AES.decrypt(
        {
          ciphertext: CryptoJS.enc.Base64.parse(encryptedPlainText),
        },
        key,
        {
          keySize: 32, // optional here, as it was set in CryptoJS.PBKDF2() above
          iv: parsedIV,
          mode: CryptoJS.mode.CTR,
          padding: CryptoJS.pad.NoPadding,
          /*
              Depending on the contents of the data you're encrypting (trailing spaces or alike),
              the padding can also be set to 'NoPadding' to avoid the additional
              characters or blocks of padding.

              See this post for explanation:
              https://stackoverflow.com/questions/48673427/cryptojs-with-hex-key-not-decrypting-properly
              I use `NoPadding`, since encryptedPlainText is already encoded into Base64.
              Look this post for more details on this topic:
              https://stackoverflow.com/questions/61717485/incorrect-decrypted-string-implemented-using-aes-ecb-nopadding-and-base-64-with/61737626
          */
        }
      );

    console.log("DECRYPTED TEXT:", decrypted);
    // DECRYPTED TEXT: t.init {words: Array(4), sigBytes: 11}
    // It corresponds to 1 byte per character as in `Latin1` encoding.
    // The `Lorem ipsum` text decrypted below is 11 bytes long in Latin1.
    // See: https://stackoverflow.com/questions/2708958/differences-between-utf8-and-latin1

    console.log("DECRYPTED (in UTF8):", CryptoJS.enc.Utf8.parse(decrypted));
    // DECRYPTED (UTF8): t.init {words: Array(6), sigBytes: 22}
    // It corresponds to the UTF8's 2 bytes per character.

    console.log("DECRYPTED (toString in Latin1):", decrypted.toString(CryptoJS.enc.Latin1));
    // DECRYPTED (toString in Latin1): Lorem ipsum