You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

问题原因

初始代码的核心问题在于密钥的生成与传递逻辑不匹配:

  1. PHP端生成的密钥是Random::string(16)生成的16字节随机二进制数据,经Base64编码后输出。这类二进制转Base64的字符串可能包含+、/、=等特殊字符,在HTML输出或传递给JS时容易被转义,导致JS拿到的密钥与PHP实际使用的密钥不一致。
  2. 示例数据是手动输入的可打印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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 03:08:10