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
相关产品推荐
相关产品推荐

