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) } } }