PHP用phpseclib实现AES-256-GCM加密,JS解密失败问题排查
PHP(phpseclib)AES-256-GCM加密与JS crypto.subtle解密适配问题修复
问题场景
需要实现PHP通过phpseclib进行AES-256-GCM加密,再用JS的crypto.subtle解密。初始代码用示例数据能正常解密,但替换为PHP生成的真实密钥、IV和密文后,浏览器抛出Uncaught (in promise) DOMException: The operation failed for an operation-specific reason错误,在线加密工具却能正常解密。
初始代码
PHP加密/解密代码
<?php require 'vendor/autoload.php'; use phpseclib3\Crypt\AES; use phpseclib3\Crypt\Random; function generateSharedKey() { return base64_encode(Random::string(16)); } function encryptMessage($messaggio_da_criptare, $chiave) { $aes = new AES('gcm'); $aes->setKey(base64_decode($chiave)); $iv=base64_encode(openssl_random_pseudo_bytes(12)); $aes->setNonce($iv); $testo_cifrato = $aes->encrypt($messaggio_da_criptare); $tag = $aes->getTag(); $dati_criptati = ['messaggio_criptato_con_tag' => base64_encode($testo_cifrato.$tag),'iv' => $iv]; return $dati_criptati; } function decryptMessage($messaggio_criptato_con_tag, $iv, $chiave) { $aes = new AES('gcm'); $aes->setKey(base64_decode($chiave)); $aes->setNonce($iv); $combined = base64_decode($messaggio_criptato_con_tag); $tagLength = 16; $tag = substr($combined, -$tagLength); $encryptedMessage = substr($combined, 0, -$tagLength); $aes->setTag($tag); $decryptedMessage = $aes->decrypt($encryptedMessage); return $decryptedMessage; } $messaggio = "Hello, this is a secret message!"; echo "待加密示例消息: ".$messaggio."<br>"; $chiave = generateSharedKey(); echo "密钥: ".$chiave."<br>"; $dati_criptati = encryptMessage($messaggio, $chiave); echo "包含标签的加密消息: ".$dati_criptati["messaggio_criptato_con_tag"]."<br>"; echo "IV: ".$dati_criptati["iv"]."<br>"; echo "解密结果: ".decryptMessage($dati_criptati["messaggio_criptato_con_tag"], $dati_criptati["iv"], $chiave); ?>
HTML/JS解密代码
<!DOCTYPE html> <html> <script> var phpkey = "TWw4QCkeZEnXoCDkI1GEHQ=="; var phpiv = "CRKTyQoWdWB2n56f"; var phpmessageencrypted = "7K+HAB7Ch9V4jJ1XJPM0sANXA2ocJok="; (async () => { function _base64ToArrayBuffer(base64) { var binary_string = atob(base64); var len = binary_string.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes.buffer; } async function _importKeyAes(key) { return await window.crypto.subtle.importKey("raw",key,{ name: "AES-GCM" },false,["encrypt", "decrypt"]); } async function decryptMessageSymetric(key, data, iv) { var keyArrayBuffer = _base64ToArrayBuffer(key); var keyt = await _importKeyAes(keyArrayBuffer); var ivt = new TextEncoder().encode(iv); var datat = _base64ToArrayBuffer(data); var result = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: ivt, tagLength: 128 },keyt,datat); return new TextDecoder().decode(result); } var result = await decryptMessageSymetric(phpkey, phpmessageencrypted, phpiv); console.log(result); })(); </script> </html>
问题原因
初始代码的核心问题在于密钥的生成与传递逻辑不匹配:
- PHP端生成的密钥是
Random::string(16)生成的16字节随机二进制数据,经Base64编码后输出。这类二进制转Base64的字符串可能包含+、/、=等特殊字符,在HTML输出或传递给JS时容易被转义,导致JS拿到的密钥与PHP实际使用的密钥不一致。 - 示例数据是手动输入的可打印Base64字符串,不存在转义问题,因此能正常解密;而真实生成的密钥因特殊字符转义,导致解密时密钥不匹配,触发DOMException。
修复后的完整代码
PHP代码
<?php require 'vendor/autoload.php'; use phpseclib3\Crypt\AES; function generateSharedKey() { // 生成仅包含可打印字符的16位密钥,避免传递时转义问题 $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-;:.'; return substr(str_shuffle($characters), 0, 16); } function encryptMessage($message, $key) { $aes = new AES('gcm'); $aes->setKey($key); // 直接使用字符串作为密钥(字节与JS端处理后一致) $iv=base64_encode(openssl_random_pseudo_bytes(12)); $aes->setNonce($iv); $encrypted_message = $aes->encrypt($message); $tag = $aes->getTag(); $encrypted_data = ['encrypted_message_with_tag' => base64_encode($encrypted_message.$tag),'iv' => $iv]; return $encrypted_data; } function decryptMessage($encrypted_message_with_tag, $iv, $key) { $aes = new AES('gcm'); $aes->setKey($key); $aes->setNonce($iv); $combined = base64_decode($encrypted_message_with_tag); $tagLength = 16; $tag = substr($combined, -$tagLength); $encryptedMessage = substr($combined, 0, -$tagLength); $aes->setTag($tag); $decryptedMessage = $aes->decrypt($encryptedMessage); return $decryptedMessage; } $message = "Hello, this is a secret message!"; echo "明文消息: ".$message."<br>"; $key = generateSharedKey(); echo "密钥: ".$key."<br>"; $encrypted_data = encryptMessage($message, $key); echo "包含标签的加密消息: ".$encrypted_data["encrypted_message_with_tag"]."<br>"; echo "IV: ".$encrypted_data["iv"]."<br>"; echo "解密结果: ".decryptMessage($encrypted_data["encrypted_message_with_tag"], $encrypted_data["iv"], $key); ?>
HTML/JS代码
<!DOCTYPE html> <html> <script> // 替换为PHP生成的真实数据 var phpSecretKey = "joSigfZlF9cu2rBK"; var phpEncryptedMessageWithTag = "FwzV1FjL6T8AGydI+j/ybRbqGzVpnmz633Sj34pwMCPpo1dcb7brp4WilGqWsivD"; var phpIV = "xgwZ1lV5XKoCGurU"; // 建议每次加密生成新IV,提升安全性 (async () => { function _base64ToArrayBuffer(base64) { var binary_string = atob(base64); var len = binary_string.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes.buffer; } async function _importKeyAes(key) { return await window.crypto.subtle.importKey( "raw", key, { name : "AES-GCM" }, false, ["encrypt", "decrypt"] ); } async function decryptMessageSymetric(key, data, iv) { // 将密钥字符串转Base64再解码,得到与PHP端一致的字节数组 var keyArrayBuffer = _base64ToArrayBuffer(btoa(key)); var keyt = await _importKeyAes(keyArrayBuffer); var ivt = new TextEncoder().encode(iv); var datat = _base64ToArrayBuffer(data); var result = await window.crypto.subtle.decrypt( { name : "AES-GCM", iv : ivt, tagLength : 128 }, keyt, datat ); return new TextDecoder().decode(result); } // 执行解密并输出结果 var result = await decryptMessageSymetric(phpSecretKey, phpEncryptedMessageWithTag, phpIV); document.write(result); })(); </script> </html>
内容的提问来源于stack exchange,提问作者Marcello Kad
相关产品推荐
相关产品推荐

