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

PHP openssl_encrypt与JS crypto.subtle.encrypt(AES-256-GCM)输出不一致

问题原因与解决方案

16字节差异的来源

这16字节是AES-GCM模式的认证标签(Authentication Tag)。GCM属于AEAD(认证加密带关联数据)类加密算法,加密过程会生成一个标签用于解密时验证数据完整性和真实性,默认长度为16字节。

两边的核心差异在于:

  • JavaScript的crypto.subtle.encrypt在GCM模式下,返回的加密结果是密文内容 + 16字节认证标签的拼接体。
  • PHP的openssl_encrypt在GCM模式下,默认仅返回密文内容,认证标签需要通过$tag参数单独获取,不会自动追加到密文中。

这就是两边加密后Base64串长度差16字节的原因,也是解密失败的根源。

跨语言适配方案

需要统一两端的加密输出格式:密文 + 16字节认证标签(或标签在前,只要两端一致即可),以下是具体实现:

PHP端适配代码

加密逻辑

$plaintext = "需要加密的内容";
$password = "固定密码";
$salt = "固定盐值";
$iv = "固定IV(12字节,GCM推荐长度)";

// 从密码和盐值推导密钥(用PBKDF2,和JS保持一致)
$key = hash_pbkdf2("sha256", $password, $salt, 10000, 32, true);

// 加密,获取密文和tag
$ciphertext = openssl_encrypt(
    $plaintext,
    "aes-256-gcm",
    $key,
    OPENSSL_RAW_DATA,
    $iv,
    $tag
);

// 将密文和tag拼接,再转Base64
$encrypted = base64_encode($ciphertext . $tag);
echo $encrypted;

解密逻辑

$encrypted = "JS加密后的Base64串";
$password = "固定密码";
$salt = "固定盐值";
$iv = "固定IV(12字节)";

// 解码Base64,拆分密文和tag
$decoded = base64_decode($encrypted);
$tag = substr($decoded, -16);
$ciphertext = substr($decoded, 0, -16);

// 推导密钥
$key = hash_pbkdf2("sha256", $password, $salt, 10000, 32, true);

// 解密
$plaintext = openssl_decrypt(
    $ciphertext,
    "aes-256-gcm",
    $key,
    OPENSSL_RAW_DATA,
    $iv,
    $tag
);

echo $plaintext;

JavaScript端适配代码

加密逻辑

async function encrypt() {
    const plaintext = "需要加密的内容";
    const password = "固定密码";
    const salt = new TextEncoder().encode("固定盐值");
    const iv = new TextEncoder().encode("固定IV(12字节)");

    // 从密码和盐值推导密钥(PBKDF2,和PHP一致)
    const keyMaterial = await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode(password),
        { name: "PBKDF2" },
        false,
        ["deriveKey"]
    );
    const key = await crypto.subtle.deriveKey(
        {
            name: "PBKDF2",
            salt: salt,
            iterations: 10000,
            hash: "SHA-256"
        },
        keyMaterial,
        { name: "AES-GCM", length: 256 },
        true,
        ["encrypt", "decrypt"]
    );

    // 加密,得到密文+tag的Uint8Array
    const encryptedBuffer = await crypto.subtle.encrypt(
        { name: "AES-GCM", iv: iv },
        key,
        new TextEncoder().encode(plaintext)
    );

    // 转Base64
    const encrypted = btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer)));
    console.log(encrypted);
}

解密逻辑

async function decrypt(encrypted) {
    const password = "固定密码";
    const salt = new TextEncoder().encode("固定盐值");
    const iv = new TextEncoder().encode("固定IV(12字节)");

    // 解码Base64为Uint8Array
    const decoded = new Uint8Array(atob(encrypted).split("").map(c => c.charCodeAt(0)));

    // 推导密钥
    const keyMaterial = await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode(password),
        { name: "PBKDF2" },
        false,
        ["deriveKey"]
    );
    const key = await crypto.subtle.deriveKey(
        {
            name: "PBKDF2",
            salt: salt,
            iterations: 10000,
            hash: "SHA-256"
        },
        keyMaterial,
        { name: "AES-GCM", length: 256 },
        true,
        ["encrypt", "decrypt"]
    );

    // 解密
    const decryptedBuffer = await crypto.subtle.decrypt(
        { name: "AES-GCM", iv: iv },
        key,
        decoded
    );

    const plaintext = new TextDecoder().decode(decryptedBuffer);
    console.log(plaintext);
}

注意事项

  • IV必须是12字节长度(GCM模式的推荐值,也支持其他长度但12字节最安全高效),测试时可固定,生产环境需随机生成并和密文一起传输。
  • 密钥推导算法的参数必须完全一致(包括迭代次数、哈希算法、密钥长度)。
  • 认证标签长度默认16字节,不要修改,否则两端无法兼容。

内容的提问来源于stack exchange,提问作者Michel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 07:37:39