Android/Java与Web/JavaScript的AES-GCM跨平台加解密兼容问题
解决Android与Web跨平台AES-GCM加解密不兼容问题
Hey 兄弟,我来帮你搞定这个跨平台加解密的坑!你遇到的AEADBadTagException本质就是两端生成的认证标签对不上,导致解密时MAC校验失败。咱们先捋清楚问题根源,再给你调整好代码。
问题根源分析
AES-GCM本身是跨平台兼容的标准,但你遇到的问题大概率出在这几个细节上:
- Base64编码不一致:Android默认的Base64编码可能带换行符(比如
Base64.DEFAULT),而Web端用的是标准无换行的Base64,直接导致解密时字节数组对不上。 - 密钥/IV的字节一致性:可能你在传递密钥或IV时,不小心做了不必要的字符串编码转换,导致两端实际使用的字节数组不一样。
- 加密后数据的处理:虽然AES-GCM标准是
密文+16字节认证标签,但要确保两端都是直接输出这个组合体,没有额外的拼接或截断操作。
修正后的代码实现
Android端代码调整
重点统一Base64编码为标准无换行格式,同时简化密钥处理逻辑:
import android.util.Base64 fun encryptAES(textToEncrypt: String, secretKey: ByteArray, iv: ByteArray): String? { return try { val secretKeySpec = SecretKeySpec(secretKey, "AES") val cipher = Cipher.getInstance("AES/GCM/NoPadding") val parameterSpec = GCMParameterSpec(128, iv) // 和Web端保持128位标签长度一致 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, parameterSpec) // 加密结果是密文+认证标签的组合字节数组 val cipherTextWithTag = cipher.doFinal(textToEncrypt.toByteArray(Charsets.UTF_8)) // 使用NO_WRAP确保Base64无换行,和Web端对齐 Base64.encodeToString(cipherTextWithTag, Base64.NO_WRAP) } catch (e: Exception) { e.printStackTrace() null } } fun decryptAES(cipherString: String, secretKey: ByteArray, iv: ByteArray): String? { return try { val secretKeySpec = SecretKeySpec(secretKey, "AES") val cipher = Cipher.getInstance("AES/GCM/NoPadding") val parameterSpec = GCMParameterSpec(128, iv) // 解码时同样用NO_WRAP避免格式错误 val cipherTextWithTag = Base64.decode(cipherString, Base64.NO_WRAP) cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, parameterSpec) val plainTextByteArray = cipher.doFinal(cipherTextWithTag) String(plainTextByteArray, Charsets.UTF_8) } catch (e: Exception) { e.printStackTrace() null } }
注意:实际使用时要确保
secretKey和iv参数非空,这里简化了可空性处理。
Web端代码调整
确保密钥以raw格式正确导入,同时统一Base64处理逻辑:
// 导入raw格式的AES密钥,和Android端的SecretKeySpec对应 async function importAesKey(rawKeyBytes) { return window.crypto.subtle.importKey( "raw", rawKeyBytes, { name: "AES-GCM" }, true, ["encrypt", "decrypt"] ); } // 加密函数 async function encryptAES(plainText, secretKeyBytes, iv) { const encoder = new TextEncoder(); const plainTextBuffer = encoder.encode(plainText); const secretKey = await importAesKey(secretKeyBytes); // 加密结果是包含密文+认证标签的ArrayBuffer const cipherBuffer = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, secretKey, plainTextBuffer ); // 转为标准无换行的Base64字符串 return btoa(String.fromCharCode(...new Uint8Array(cipherBuffer))); } // 解密函数 async function decryptAES(cipherString, secretKeyBytes, iv) { const decoder = new TextDecoder(); // 将Base64字符串转回ArrayBuffer const cipherBuffer = Uint8Array.from(atob(cipherString), c => c.charCodeAt(0)).buffer; const secretKey = await importAesKey(secretKeyBytes); const plainTextBuffer = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, secretKey, cipherBuffer ); return decoder.decode(plainTextBuffer); }
关键注意事项
- IV的规范使用:AES-GCM的IV必须是随机唯一的(推荐用12字节长度,符合NIST标准),每次加密都要生成新的IV,加密后要把IV和密文一起传递给解密端(比如用分隔符拼接两者的Base64字符串)。
- 密钥安全:绝对不要硬编码密钥,要通过安全的方式传递或生成(比如使用密钥协商算法)。
- 字节一致性:密钥和IV一定要以字节数组的形式保持一致,不要随意转成UTF-8字符串传递,直接用Base64编码后传递最稳妥。
这样调整后,两端的加解密逻辑就完全对齐了,应该能解决MAC校验失败的问题!
内容的提问来源于stack exchange,提问作者NotABot
相关产品推荐
相关产品推荐

