Files
fullstack-starter/docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt

125 lines
5.2 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package top.tangyh.lamp.filing.utils
import io.github.oshai.kotlinlogging.KotlinLogging
import java.nio.charset.StandardCharsets
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
private val logger = KotlinLogging.logger {}
/**
* AES-256-GCM 加密解密工具类
*
* 安全设计说明:
* - 使用 AES-256-GCM 提供认证加密AEAD
* - GCM 模式自动提供认证标签tag防止数据被篡改
* - IV初始化向量长度为 12 字节96位符合 GCM 推荐
* - 认证标签长度为 16 字节128位提供强认证
* - 加密数据格式IV (12字节) + Ciphertext (变长) + Tag (16字节)
*
* 为什么第三方无法伪造:
* - 只有拥有正确 licence + fingerprint 的设备才能派生正确的 AES 密钥
* - GCM 模式会验证认证标签,任何篡改都会导致解密失败
* - 即使第三方获取了加密数据,也无法解密(缺少密钥)
*/
object AesGcmUtil {
private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val IV_LENGTH = 12 // 12 bytes = 96 bits (GCM 推荐)
private const val TAG_LENGTH = 16 // 16 bytes = 128 bits (GCM 认证标签长度)
private const val GCM_TAG_LENGTH_BITS = TAG_LENGTH * 8 // 128 bits
/**
* 解密 AES-256-GCM 加密的数据
*
* @param encryptedData Base64 编码的加密数据格式iv + ciphertext + tag
* @param key AES 密钥32字节
* @return 解密后的明文UTF-8 字符串)
* @throws RuntimeException 如果解密失败(密钥错误、数据被篡改等)
*/
fun decrypt(encryptedData: String, key: ByteArray): String {
return try {
// 1. Base64 解码
val encryptedBytes = Base64.getDecoder().decode(encryptedData)
// 2. 提取 IV、密文和认证标签
if (encryptedBytes.size < IV_LENGTH + TAG_LENGTH) {
throw IllegalArgumentException("加密数据长度不足,无法提取 IV 和 Tag")
}
val iv = encryptedBytes.copyOfRange(0, IV_LENGTH)
val tag = encryptedBytes.copyOfRange(encryptedBytes.size - TAG_LENGTH, encryptedBytes.size)
val ciphertext = encryptedBytes.copyOfRange(IV_LENGTH, encryptedBytes.size - TAG_LENGTH)
// 3. 创建 SecretKeySpec
val secretKey = SecretKeySpec(key, ALGORITHM)
// 4. 创建 GCMParameterSpec包含 IV 和认证标签长度)
val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)
// 5. 初始化 Cipher 进行解密
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
// 6. 执行解密GCM 模式会自动验证认证标签)
// 如果认证标签验证失败,会抛出异常
val decryptedBytes = cipher.doFinal(ciphertext + tag)
// 7. 转换为 UTF-8 字符串
String(decryptedBytes, StandardCharsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) {
logger.error(e) { "AES-GCM 认证标签验证失败,数据可能被篡改或密钥错误" }
throw RuntimeException("解密失败:认证标签验证失败,数据可能被篡改或密钥错误", e)
} catch (e: Exception) {
logger.error(e) { "AES-GCM 解密失败" }
throw RuntimeException("解密失败: ${e.message}", e)
}
}
/**
* 加密数据(用于测试或客户端实现参考)
*
* @param plaintext 明文数据
* @param key AES 密钥32字节
* @return Base64 编码的加密数据格式iv + ciphertext + tag
*/
fun encrypt(plaintext: String, key: ByteArray): String {
return try {
// 1. 生成随机 IV
val iv = ByteArray(IV_LENGTH)
java.security.SecureRandom().nextBytes(iv)
// 2. 创建 SecretKeySpec
val secretKey = SecretKeySpec(key, ALGORITHM)
// 3. 创建 GCMParameterSpec
val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)
// 4. 初始化 Cipher 进行加密
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
// 5. 执行加密
val plaintextBytes = plaintext.toByteArray(StandardCharsets.UTF_8)
val encryptedBytes = cipher.doFinal(plaintextBytes)
// 6. 组装IV + Ciphertext + Tag
// GCM 模式会将认证标签附加到密文末尾
val ciphertext = encryptedBytes.copyOfRange(0, encryptedBytes.size - TAG_LENGTH)
val tag = encryptedBytes.copyOfRange(encryptedBytes.size - TAG_LENGTH, encryptedBytes.size)
val result = iv + ciphertext + tag
// 7. Base64 编码返回
Base64.getEncoder().encodeToString(result)
} catch (e: Exception) {
logger.error(e) { "AES-GCM 加密失败" }
throw RuntimeException("加密失败: ${e.message}", e)
}
}
}