使用 PHP (openssl_encrypt) 发出加密,然后使用 JS (CryptoJS) 解密

Issue encrypting with PHP (openssl_encrypt), then decrypting with JS (CryptoJS)

我第一次使用 CryptoJS,我正在努力解密我在 PHP.[=18 中使用 openssl_encrypt() 加密的字符串=]

PHP 5.6.13.0 和 CryptoJS 3.1.2


首先,我的 PHP:

$encryptHash = hash_pbkdf2("sha256", "0000", "secret", 1000, 32);
var_dump($encryptHash);

$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
var_dump(bin2hex($iv));

$encrypted = openssl_encrypt("hello! this is my string!", 'aes-256-cbc', $encryptHash, 0, $iv);
var_dump($encrypted);

$encrypted = base64_encode($encrypted.":".bin2hex($iv));
echo "\r\n".$encrypted;

这给了我以下输出:

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

eEhJeGcxSERVT3F5aEJtQWFVMlN4M2N0OEdhS2FlRTV3NGQxS00xeXVEdz06MGFhZmYwOTRiNmRjMjk3NDJjYzk4YTRiYWM4YmM4Zjk=

现在我的 JS:

var encryptedString = "eEhJeGcxSERVT3F5aEJtQWFVMlN4M2N0OEdhS2FlRTV3NGQxS00xeXVEdz06MGFhZmYwOTRiNmRjMjk3NDJjYzk4YTRiYWM4YmM4Zjk=";

var key256Bits  = CryptoJS.PBKDF2("0000", "secret", { keySize: 128/32, iterations: 1000, hasher: CryptoJS.algo.SHA256 });
var keyAsHex = key256Bits.toString(CryptoJS.enc.Hex);

/* keyAsHex = "59b6ab46d379b89d794c87b74a511fbd" */

var rawData = atob(encryptedString);
var rawPieces = rawData.split(":");

var crypttext = rawPieces[0];
var iv = rawPieces[1];

/* crypttext = "xHIxg1HDUOqyhBmAaU2Sx3ct8GaKaeE5w4d1KM1yuDw=" */
/* iv = "0aaff094b6dc29742cc98a4bac8bc8f9" */

/* So far so good? */

var plaintextArray = CryptoJS.AES.decrypt(
  { ciphertext: CryptoJS.enc.Base64.parse(crypttext) },
  CryptoJS.enc.Hex.parse(keyAsHex),
  { iv: CryptoJS.enc.Hex.parse(iv) }
);

/* plaintextArray: d.WordArray.n.extend.init
    sigBytes: -67
    words: Array[8]
        0: 1419734786
        1: -2048883413
        2: -1709437124
        3: 736946566
        4: 718053567
        5: -64039355
        6: 1868905697
        7: -910423965 */

var output = CryptoJS.enc.Utf8.stringify(plaintextArray);

/* output = "" */

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

编辑

原来我的密钥长度不正确!这是我的工作 PHP(加密)和 JS(解密)代码:


PHP:

$encryptHash = hash_pbkdf2("sha256", "0000", "secret", 1000, 32, true);
var_dump($encryptHash);

$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length("aes-256-cbc"));
var_dump($iv);

$encrypted = openssl_encrypt("hello! this is a test!", "aes-256-cbc", $encryptHash, 0, $iv);
var_dump($encrypted);

$encrypted = base64_encode($encrypted.":".bin2hex($iv));
echo "\r\n".$encrypted;

给我以下内容:

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

Vkl6emFvOFdkbzhIUE0wMTV2NmM1UTc3ZXJ2R1VJVmJMNkVSS1JYYjBmVT06ODU1ODE1MjAyNDU2ODAxMzA1ZDlkYmZkYTBlMGUxNzQ=

JS:

var encryptedString = "Vkl6emFvOFdkbzhIUE0wMTV2NmM1UTc3ZXJ2R1VJVmJMNkVSS1JYYjBmVT06ODU1ODE1MjAyNDU2ODAxMzA1ZDlkYmZkYTBlMGUxNzQ=";

var key256Bits  = CryptoJS.PBKDF2("0000", "secret", { keySize: 256/32, iterations: 1000, hasher: CryptoJS.algo.SHA256 });

var rawData = atob(encryptedString);
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(crypttext)});

var plaintextArray = CryptoJS.AES.decrypt(
  cipherParams,
  key256Bits,
  { iv: iv }
);

var output = CryptoJS.enc.Utf8.stringify(plaintextArray);

/* output === 'hello! this is a test!' */

TL;DR - 尝试使用 32 字节密钥而不是 16 字节密钥。

在编写了一个较早的答案并最终将其删除后,证明了我自己关于这是填充问题的理论 :-),我现在相当确定问题可能只是与密钥长度有关。

在尝试重现您的问题时,我无法使使用 openssl_encryptCryptoJS 生成的第一个密文块相同。然后我将密钥的长度加倍,它起作用了。

上面生成的密钥是 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() returns ASCII 十六进制字符串,因此您应该在将 $encryptHash 传递给 [=20 之前使用 hex2bin() 将其转换为二进制=] 或将 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 的想法解决了我所有的问题,因为好吧,因为一旦开始深入研究物质,它就不那么明显了。我正在添加我的解决方案和一些额外的解释,这样可以在查找所有这些信息时节省一些时间。

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

将第 4 个选项的参数设置为 0,返回的数据由 openssl_encrypt 编码为 Base64,因此需要对其进行解码两次来自 CryptoJS 中的 Base64。但是,将该选项设置为 OPENSSL_RAW_DATA 后,数据不会在 PHP.

中隐式编码为 Base64

PHP7.4:

    $plainTextToEncrypt = "Lorem ipsum";
    $passphrase = "obligate properly elective edge"; // from: https://www.useapassphrase.com
    $iterations = 1001;
    $salt = random_bytes(32);
    // It doesn't really matter how many characters the `salt` is.
    $hexSalt = bin2hex($salt);
    var_dump($hexSalt); 
    // string (64): 0c5597db78ac4aedf2bdfb1d4ce7935c270876284239b0ef48ba63d08ed164b5
    
    $key = hash_pbkdf2("sha256", $passphrase, $hexSalt, $iterations, 32, true);
    var_dump($key);
    // in raw as string (32) it then looks like this: �g��)���2�'�����M2eCY�I�J��^

    // More readable 64 lowercase hexits long key:
    $keyToHex = bin2hex($key);
    var_dump($keyToHex);
    // string (64): ce670c8a7f8329b78306d9329927faa4f880c34d32654359e949a74aa77fc45e

    /*
       By the way, $keyToHex is the same as the following 32 characters long
       raw binary $key if generated as 64 lowercase hexits long key (as it's discussed
       in the previous post in the 'UPDATE' section):
    */
    $keyIn64Hexits = hash_pbkdf2("sha256", $passphrase, $hexSalt, $iterations, 64, false);
    var_dump($keyIn64Hexits);
    // string (64):  ce670c8a7f8329b78306d9329927faa4f880c34d32654359e949a74aa77fc45e

    /*
        DON'T confuse yourself here! Below I encrypt with the raw binary $key, which is
        32 characters long, and with the `OPENSSL_RAW_DATA` option in `openssl_encrypt()`.
    */

    $cipher = 'aes-256-ctr';
    if (in_array($cipher, openssl_get_cipher_methods()))
    {
        $ivLen = openssl_cipher_iv_length($cipher);
        var_dump($ivLen);
        // int (16) <--- should be of the appropriate length used in the encryption algorithm of your choice!
        $iv = random_bytes($ivLen);
        var_dump($iv);
        // string (16): 7��������a��
        $ivInHex = bin2hex($iv); // <--- In CryptoJS I'm working with the Hex variant once it's decoded from Base64 (see the 2nd code block with my JS)
        var_dump($ivInHex);
        // string (32): 3706f4f089c2f6f2e0aafa6191170dae
        $iv64 = base64_encode($ivInHex);
        // $iv64 in Base64 looks like this: MzcwNmY0ZjA4OWMyZjZmMmUwYWFmYTYxOTExNzBkYWU=

        $encryptedData = openssl_encrypt($plainTextToEncrypt, $cipher, $key, OPENSSL_RAW_DATA, $ivInHex);
        // with `OPENSSL_RAW_DATA` you'll get the raw binary data: ùg3UDCY��
        // bin2hex($encryptedData) looks like: c3b967335544430759b2c1
        // base64_encode($encryptedData) looks like: w7lnM1VEQwdZssE= <--- btw, this is how one-time encoding to Base64 looks like

        // Let's prepare it for transport
        $data = array("ciphertext" => base64_encode($encryptedData), "iv" => $iv64, "salt" => $hexSalt);
        // Whatever you're doing with the encrypted data later, e.g.:
        // return json_encode($data);
    }

CryptoJS 4.0.0

    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:
              
              I use `NoPadding`, since encryptedPlainText is already encoded into Base64.
              Look this post for more details on this topic:
              
          */
        }
      );

    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: 

    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