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

