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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:49:54