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

将AES加密从CTR模式切换为GCM模式时MAC校验失败问题求助

错误原因

javax.crypto.AEADBadTagException: mac check in GCM failed 是GCM模式的专属报错,本质是认证标签校验失败,常见触发原因如下:

  • GCM属于带认证的加密模式,加密时会生成一个12~16字节的认证标签(Tag),用于解密时校验密文完整性、密钥正确性。你原有CTR模式的代码逻辑没有存储、传递这个标签,导致解密时缺少校验凭据直接报错。
  • 参数配置不匹配:GCM需要使用GCMParameterSpec代替CTR用的IvParameterSpec,同时要指定标签长度,加密和解密的标签长度、IV长度必须完全一致。
  • 原有代码存在逻辑漏洞:PBEKeySpec(masterpw.toString().toCharArray()) 这里masterpw是ByteArray类型,直接调用toString()得到的是数组的内存地址字符串(类似[B@123456),不是你预期的密码内容,会导致加密解密生成的密钥不一致。
  • IV长度不符合GCM规范:GCM推荐使用12字节(96位)IV,你原有CTR用的16字节IV虽然可用,但会降低计算性能,部分实现也可能出现兼容问题。
修复方案

修改后的完整加密解密代码如下:

import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.PBEKeySpec
import android.util.Base64
import java.security.SecureRandom

// 常量定义,避免硬编码不一致
private const val GCM_IV_LENGTH = 12
private const val GCM_TAG_LENGTH = 16 // 对应128位标签长度
private const val SALT_LENGTH = 16
private const val PBKDF2_ITERATIONS = 10000 // 原有100次迭代太弱,建议至少10000次
private const val KEY_LENGTH = 256
private val CHARSET = Charsets.UTF_8

fun encryptAESBasic(input: String, password: String): String {
    val random = SecureRandom()
    // 生成盐
    val salt = ByteArray(SALT_LENGTH)
    random.nextBytes(salt)
    // 生成GCM专用IV
    val iv = ByteArray(GCM_IV_LENGTH)
    random.nextBytes(iv)

    // PBKDF2生成密钥,直接用原始密码即可
    val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
    val spec: PBEKeySpec = PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH)
    val secretKey: SecretKey = factory.generateSecret(spec)

    // 初始化GCM加密器
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv) // 标签长度单位为位
    cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)

    // 加密得到密文+标签的拼接结果(Android GCM实现默认把Tag追加到密文末尾)
    val cipherTextWithTag = cipher.doFinal(input.toByteArray(CHARSET))

    // 编码后拼接存储:IV + 盐 + 密文+标签
    val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
    val saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP)
    val cipherTextBase64 = Base64.encodeToString(cipherTextWithTag, Base64.NO_WRAP)
    return "$ivBase64-$saltBase64-$cipherTextBase64"
}

fun decryptAESBasic(text: String, password: String): String {
    val parts = text.split("-")
    require(parts.size == 3) { "加密文本格式错误" }

    // 解码各部分内容
    val iv = Base64.decode(parts[0], Base64.NO_WRAP)
    val salt = Base64.decode(parts[1], Base64.NO_WRAP)
    val cipherTextWithTag = Base64.decode(parts[2], Base64.NO_WRAP)

    // 生成和加密端一致的密钥
    val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
    val spec: PBEKeySpec = PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH)
    val secretKey: SecretKey = factory.generateSecret(spec)

    // 初始化GCM解密器
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv)
    cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)

    // 解密时会自动从密文末尾读取Tag校验
    val plainText = cipher.doFinal(cipherTextWithTag)
    return plainText.toString(CHARSET)
}

额外优化说明

  • 删掉了原有多余的getKey方法,PBKDF2本身支持任意长度的密码输入,不需要提前补位到32位,同时也避免了原来的ByteArray.toString()逻辑错误。
  • 把PBKDF2迭代次数从100调整为10000,过低的迭代次数会大幅降低暴力破解的成本,可根据设备性能酌情调整到更高数值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.09 10:27:02