125 lines
5.2 KiB
Kotlin
125 lines
5.2 KiB
Kotlin
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)
|
||
}
|
||
}
|
||
}
|
||
|