docs: 添加管理平台标准加密算法 Kotlin 参考实现
This commit is contained in:
124
docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt
Normal file
124
docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
42
docs/工具箱端-授权对接指南/utils/DateUtil.kt
Normal file
42
docs/工具箱端-授权对接指南/utils/DateUtil.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class DateUtil {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 获取当前时间戳
|
||||||
|
fun getCurrentTimestamp(): Long {
|
||||||
|
return System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
fun formatDate(date: Date, format: String = "yyyy-MM-dd HH:mm:ss"): String {
|
||||||
|
val sdf = SimpleDateFormat(format)
|
||||||
|
return sdf.format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析日期字符串
|
||||||
|
fun parseDate(dateString: String, format: String = "yyyy-MM-dd HH:mm:ss"): Date? {
|
||||||
|
val sdf = SimpleDateFormat(format)
|
||||||
|
return try {
|
||||||
|
sdf.parse(dateString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算两个日期之间的天数差
|
||||||
|
fun getDaysBetweenDates(date1: Date, date2: Date): Long {
|
||||||
|
val diff = Math.abs(date1.time - date2.time)
|
||||||
|
return diff / (24 * 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间并格式化为 yyyy-MM-dd_HH-mm-ss
|
||||||
|
fun getCurrentFormattedTime(format: String = "yyyy-MM-dd_HH-mm-ss"): String {
|
||||||
|
val sdf = SimpleDateFormat(format)
|
||||||
|
return sdf.format(Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
docs/工具箱端-授权对接指南/utils/DeviceSignatureUtil.kt
Normal file
129
docs/工具箱端-授权对接指南/utils/DeviceSignatureUtil.kt
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备签名工具类
|
||||||
|
* 用于生成和验证设备报告签名
|
||||||
|
*
|
||||||
|
* 签名算法:HMAC-SHA256
|
||||||
|
* 签名数据(严格顺序):
|
||||||
|
* sign_payload = taskId + inspectionId +
|
||||||
|
* SHA256(assets.json) +
|
||||||
|
* SHA256(vulnerabilities.json) +
|
||||||
|
* SHA256(weakPasswords.json) +
|
||||||
|
* SHA256(漏洞评估报告.html)
|
||||||
|
*
|
||||||
|
* 安全设计说明:
|
||||||
|
* - 使用 HMAC-SHA256 提供消息认证,防止伪造和篡改
|
||||||
|
* - 签名包含 taskId 和 inspectionId,确保签名与特定任务绑定
|
||||||
|
* - 包含多个报告文件的 SHA256,确保报告内容完整性
|
||||||
|
* - 只有拥有正确 licence + fingerprint 的设备才能生成有效签名
|
||||||
|
*/
|
||||||
|
object DeviceSignatureUtil {
|
||||||
|
|
||||||
|
private const val HMAC_ALGORITHM = "HmacSHA256"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名数据文件列表(严格顺序)
|
||||||
|
*/
|
||||||
|
data class SignatureFileHashes(
|
||||||
|
val assetsJsonSha256: String,
|
||||||
|
val vulnerabilitiesJsonSha256: String,
|
||||||
|
val weakPasswordsJsonSha256: String,
|
||||||
|
val reportHtmlSha256: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成设备签名
|
||||||
|
*
|
||||||
|
* @param key 派生密钥(32字节)
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param inspectionId 检查ID
|
||||||
|
* @param fileHashes 各个文件的 SHA256 哈希值(hex字符串)
|
||||||
|
* @return Base64 编码的签名
|
||||||
|
*/
|
||||||
|
fun generateSignature(
|
||||||
|
key: ByteArray,
|
||||||
|
taskId: String,
|
||||||
|
inspectionId: Long,
|
||||||
|
fileHashes: SignatureFileHashes
|
||||||
|
): String {
|
||||||
|
return try {
|
||||||
|
// 组装签名数据(严格顺序):
|
||||||
|
// taskId + inspectionId + SHA256(assets.json) + SHA256(vulnerabilities.json) +
|
||||||
|
// SHA256(weakPasswords.json) + SHA256(漏洞评估报告.html)
|
||||||
|
val signatureData = buildString {
|
||||||
|
append(taskId)
|
||||||
|
append(inspectionId)
|
||||||
|
append(fileHashes.assetsJsonSha256)
|
||||||
|
append(fileHashes.vulnerabilitiesJsonSha256)
|
||||||
|
append(fileHashes.weakPasswordsJsonSha256)
|
||||||
|
append(fileHashes.reportHtmlSha256)
|
||||||
|
}
|
||||||
|
val dataBytes = signatureData.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
|
||||||
|
// 使用 HMAC-SHA256 计算签名
|
||||||
|
val mac = Mac.getInstance(HMAC_ALGORITHM)
|
||||||
|
val secretKey = SecretKeySpec(key, HMAC_ALGORITHM)
|
||||||
|
mac.init(secretKey)
|
||||||
|
val signatureBytes = mac.doFinal(dataBytes)
|
||||||
|
|
||||||
|
// Base64 编码返回
|
||||||
|
Base64.getEncoder().encodeToString(signatureBytes)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "生成设备签名失败: taskId=$taskId, inspectionId=$inspectionId" }
|
||||||
|
throw RuntimeException("生成设备签名失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证设备签名
|
||||||
|
*
|
||||||
|
* @param key 派生密钥(32字节)
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param inspectionId 检查ID
|
||||||
|
* @param fileHashes 各个文件的 SHA256 哈希值(hex字符串)
|
||||||
|
* @param expectedSignature Base64 编码的期望签名
|
||||||
|
* @return true 如果签名匹配,false 否则
|
||||||
|
*/
|
||||||
|
fun verifySignature(
|
||||||
|
key: ByteArray,
|
||||||
|
taskId: String,
|
||||||
|
inspectionId: Long,
|
||||||
|
fileHashes: SignatureFileHashes,
|
||||||
|
expectedSignature: String
|
||||||
|
): Boolean {
|
||||||
|
return try {
|
||||||
|
val calculatedSignature = generateSignature(key, taskId, inspectionId, fileHashes)
|
||||||
|
// 使用时间安全的比较,防止时序攻击
|
||||||
|
MessageDigest.isEqual(
|
||||||
|
Base64.getDecoder().decode(expectedSignature),
|
||||||
|
Base64.getDecoder().decode(calculatedSignature)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "验证设备签名失败: taskId=$taskId, inspectionId=$inspectionId" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算文件的 SHA256 哈希值(hex字符串)
|
||||||
|
*
|
||||||
|
* @param fileContent 文件内容
|
||||||
|
* @return SHA256 哈希值的 hex 字符串
|
||||||
|
*/
|
||||||
|
fun calculateSha256(fileContent: ByteArray): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hashBytes = digest.digest(fileContent)
|
||||||
|
return hashBytes.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
12
docs/工具箱端-授权对接指南/utils/DistributedIdUtil.kt
Normal file
12
docs/工具箱端-授权对接指南/utils/DistributedIdUtil.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
object DistributedIdUtil {
|
||||||
|
fun generateId(platformId: Long, localId: Long): Long {
|
||||||
|
require(platformId in 0..0xFFFF) { "platformId must be 0-65535" }
|
||||||
|
val safeLocalId = localId and 0xFFFFFFFFFFFF
|
||||||
|
return (platformId shl 48) or safeLocalId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parsePlatform(id: Long): Long = id ushr 48
|
||||||
|
fun parseLocal(id: Long): Long = id and 0xFFFFFFFFFFFF
|
||||||
|
}
|
||||||
18
docs/工具箱端-授权对接指南/utils/HashUtil.kt
Normal file
18
docs/工具箱端-授权对接指南/utils/HashUtil.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
object HashUtil {
|
||||||
|
fun calculateFileHash(inputStream: InputStream): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var bytesRead: Int
|
||||||
|
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
digest.update(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
66
docs/工具箱端-授权对接指南/utils/HkdfUtil.kt
Normal file
66
docs/工具箱端-授权对接指南/utils/HkdfUtil.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||||
|
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
|
||||||
|
import org.bouncycastle.crypto.params.HKDFParameters
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HKDF (HMAC-based Key Derivation Function) 工具类
|
||||||
|
* 用于从 licence + fingerprint 派生设备签名密钥
|
||||||
|
*
|
||||||
|
* 安全设计说明:
|
||||||
|
* - 使用 HKDF 而非直接哈希,提供更好的密钥分离和扩展性
|
||||||
|
* - Salt 固定为 "AUTH_V3_SALT",确保同一输入产生相同密钥
|
||||||
|
* - Info 参数用于区分不同用途的密钥派生(device_report_signature)
|
||||||
|
* - 输出长度 32 字节(256位),适用于 HMAC-SHA256
|
||||||
|
*/
|
||||||
|
object HkdfUtil {
|
||||||
|
|
||||||
|
private const val SALT = "AUTH_V3_SALT"
|
||||||
|
private const val INFO = "device_report_signature"
|
||||||
|
private const val KEY_LENGTH = 32 // 32 bytes = 256 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 HKDF 派生密钥(使用默认 salt 和 info)
|
||||||
|
*
|
||||||
|
* @param input 输入密钥材料(licence + fingerprint)
|
||||||
|
* @return 派生出的密钥(32字节)
|
||||||
|
*/
|
||||||
|
fun deriveKey(input: String): ByteArray {
|
||||||
|
return deriveKey(input, SALT, INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 HKDF 派生密钥(支持自定义 salt 和 info)
|
||||||
|
*
|
||||||
|
* @param input 输入密钥材料(licence + fingerprint)
|
||||||
|
* @param salt Salt 值(用于密钥派生)
|
||||||
|
* @param info Info 值(用于区分不同用途的密钥)
|
||||||
|
* @param keyLength 输出密钥长度(默认32字节)
|
||||||
|
* @return 派生出的密钥
|
||||||
|
*/
|
||||||
|
fun deriveKey(input: String, salt: String, info: String, keyLength: Int = KEY_LENGTH): ByteArray {
|
||||||
|
return try {
|
||||||
|
val inputBytes = input.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val saltBytes = salt.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val infoBytes = info.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
|
||||||
|
val hkdf = HKDFBytesGenerator(SHA256Digest())
|
||||||
|
val params = HKDFParameters(inputBytes, saltBytes, infoBytes)
|
||||||
|
hkdf.init(params)
|
||||||
|
|
||||||
|
val derivedKey = ByteArray(keyLength)
|
||||||
|
hkdf.generateBytes(derivedKey, 0, keyLength)
|
||||||
|
|
||||||
|
derivedKey
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "HKDF 密钥派生失败: input=$input, salt=$salt, info=$info" }
|
||||||
|
throw RuntimeException("HKDF 密钥派生失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
50
docs/工具箱端-授权对接指南/utils/JwtUtil.kt
Normal file
50
docs/工具箱端-授权对接指南/utils/JwtUtil.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Jwts
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm
|
||||||
|
import io.jsonwebtoken.security.Keys
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JwtUtil(
|
||||||
|
@Value("\${jwt.secret}")
|
||||||
|
private val secretKey: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
// 生成签名 Key(HS256)
|
||||||
|
private val signingKey: SecretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Token
|
||||||
|
* */
|
||||||
|
fun generateToken(subject: String, claims: Map<String, Any> = emptyMap(), expireDays: Long = 7): String {
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val expiration = now.plusDays(expireDays)
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(subject)
|
||||||
|
.setClaims(claims)
|
||||||
|
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
|
||||||
|
.setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
|
||||||
|
.signWith(signingKey, SignatureAlgorithm.HS256)
|
||||||
|
.compact()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Token 获取 Claims
|
||||||
|
*/
|
||||||
|
fun parseToken(token: String): Map<String, Any> {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(signingKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.body
|
||||||
|
}
|
||||||
|
}
|
||||||
22
docs/工具箱端-授权对接指南/utils/RegionUtil.kt
Normal file
22
docs/工具箱端-授权对接指南/utils/RegionUtil.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import kotlin.text.substring
|
||||||
|
|
||||||
|
object RegionUtil {
|
||||||
|
fun getLevel(code: String?): String {
|
||||||
|
if (code == null || code.length != 6) {
|
||||||
|
return "无效编码"
|
||||||
|
}
|
||||||
|
|
||||||
|
val province = code.substring(0, 2)
|
||||||
|
val city = code.substring(2, 4)
|
||||||
|
val county = code.substring(4, 6)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
city == "00" && county == "00" -> "province"
|
||||||
|
city != "00" && county == "00" -> "city"
|
||||||
|
county != "00" -> "county"
|
||||||
|
else -> "未知级别"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
115
docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtil.kt
Normal file
115
docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtil.kt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA-OAEP 解密工具类
|
||||||
|
* 用于设备身份首次绑定时解密设备信息
|
||||||
|
*
|
||||||
|
* 使用场景:设备使用平台的公钥加密数据,平台使用私钥解密
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
class RsaOaepDecryptionUtil(
|
||||||
|
@Value("\${device.encrypt.privateKey:}")
|
||||||
|
private val privateKeyBase64: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val keyFactory = KeyFactory.getInstance("RSA")
|
||||||
|
private val cipherAlgorithm = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
|
||||||
|
|
||||||
|
// 缓存私钥,避免每次解密都重新加载
|
||||||
|
private val privateKey by lazy {
|
||||||
|
if (privateKeyBase64.isBlank()) {
|
||||||
|
throw IllegalStateException("RSA私钥未配置,无法解密设备信息")
|
||||||
|
}
|
||||||
|
val privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64)
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(privateKeyBytes)
|
||||||
|
keyFactory.generatePrivate(keySpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (privateKeyBase64.isBlank()) {
|
||||||
|
logger.warn { "RSA私钥未配置,设备授权解密功能可能无法使用" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用RSA-OAEP解密设备信息
|
||||||
|
* @param encryptedData Base64编码的加密数据
|
||||||
|
* @return 解密后的JSON字符串
|
||||||
|
*/
|
||||||
|
fun decrypt(encryptedData: String): String {
|
||||||
|
if (privateKeyBase64.isBlank()) {
|
||||||
|
throw IllegalStateException("RSA私钥未配置,无法解密设备信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// 创建新的Cipher实例(Cipher不是线程安全的)
|
||||||
|
val cipher = Cipher.getInstance(cipherAlgorithm)
|
||||||
|
|
||||||
|
// 初始化解密器
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
|
||||||
|
// Base64解码加密数据
|
||||||
|
val encryptedBytes = Base64.getDecoder().decode(encryptedData)
|
||||||
|
|
||||||
|
// 解密数据
|
||||||
|
val decryptedBytes = cipher.doFinal(encryptedBytes)
|
||||||
|
|
||||||
|
// 返回解密后的字符串
|
||||||
|
String(decryptedBytes, StandardCharsets.UTF_8)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "RSA-OAEP解密设备信息失败" }
|
||||||
|
throw RuntimeException("RSA-OAEP解密设备信息失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用平台公钥加密数据
|
||||||
|
*
|
||||||
|
* @param plainText 原始JSON字符串(设备信息)
|
||||||
|
* @param publicKeyBase64 平台公钥(Base64)
|
||||||
|
* @return Base64编码的密文
|
||||||
|
*/
|
||||||
|
fun encrypt1(
|
||||||
|
plainText: String,
|
||||||
|
publicKeyBase64: String
|
||||||
|
): String {
|
||||||
|
try {
|
||||||
|
val publicKey = loadPublicKey(publicKeyBase64)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance(cipherAlgorithm)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||||
|
|
||||||
|
val encryptedBytes = cipher.doFinal(
|
||||||
|
plainText.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(encryptedBytes)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "RSA-OAEP 加密失败" }
|
||||||
|
throw RuntimeException("RSA-OAEP 加密失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPublicKey(base64Key: String): PublicKey {
|
||||||
|
val keyBytes = Base64.getDecoder().decode(base64Key)
|
||||||
|
val keySpec = X509EncodedKeySpec(keyBytes)
|
||||||
|
return keyFactory.generatePublic(keySpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
103
docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtilV2.kt
Normal file
103
docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtilV2.kt
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
|
||||||
|
object RsaOaepCryptoUtil {
|
||||||
|
|
||||||
|
private const val cipherAlgorithm =
|
||||||
|
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
|
||||||
|
|
||||||
|
private val keyFactory = KeyFactory.getInstance("RSA")
|
||||||
|
|
||||||
|
fun encrypt(
|
||||||
|
plainText: String,
|
||||||
|
publicKeyBase64: String
|
||||||
|
): String {
|
||||||
|
val publicKey = loadPublicKey(publicKeyBase64)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance(cipherAlgorithm)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||||
|
|
||||||
|
val encryptedBytes = cipher.doFinal(
|
||||||
|
plainText.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
)
|
||||||
|
return Base64.getEncoder().encodeToString(encryptedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(
|
||||||
|
encryptedData: String,
|
||||||
|
privateKeyBase64: String
|
||||||
|
): String {
|
||||||
|
val privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64)
|
||||||
|
val keySpec = PKCS8EncodedKeySpec(privateKeyBytes)
|
||||||
|
val privateKey = keyFactory.generatePrivate(keySpec)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance(cipherAlgorithm)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
|
|
||||||
|
val decryptedBytes = cipher.doFinal(
|
||||||
|
Base64.getDecoder().decode(encryptedData)
|
||||||
|
)
|
||||||
|
return String(decryptedBytes, StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPublicKey(base64Key: String): PublicKey {
|
||||||
|
val keyBytes = Base64.getDecoder().decode(base64Key)
|
||||||
|
val keySpec = X509EncodedKeySpec(keyBytes)
|
||||||
|
return keyFactory.generatePublic(keySpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object Test {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
|
||||||
|
keyPairGenerator.initialize(2048)
|
||||||
|
val keyPair = keyPairGenerator.generateKeyPair()
|
||||||
|
|
||||||
|
// val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded)
|
||||||
|
// val privateKey = Base64.getEncoder().encodeToString(keyPair.private.encoded)
|
||||||
|
|
||||||
|
val publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB"
|
||||||
|
val privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDMOVm8wNVov5+OXTkeLXzYk4BQBo3iCH2s4X7U2Ep87gnp7QcvLyUG7KWncDjGhOLJR6M2bbaHR2oCANI+dj/FlHvo84VMPWygevYtoUI3wkBtD3o/yATvAL2qmhOyxW8hVEftBtV3brQnp4PRLPBzH1yDn3VFI3r2kyl7khVGLxP30eGqXr/Cdkc5E+vXx4RIs5j3eNGyQvNzSrXyvrxFGhMmJB/71gLy5vmIqnusKNWc+LVRshiZeYvTy2TmaSxgDQ2pZABrWh8rHH/21C0H25OOFPG5O73hdT2OUZTzupodmz6SmprwIBthgkVtI/XEfChnOlTaOnDZoXbYcFjHAgMBAAECggEBAKUDagjj3wwmWWwU00QZCRmOBU2kHhyXM8Tu5yZgJb/QLt5/ESEE/OwdZrRmLtnpIb31TmF6QNQJ1hQMSdUCgwDIu1489mXl89GvyR6RgAnBwYDd0q+alLHxbU9d87CtauAJU5ynuvAn2RV5ez8XCqpamswXmg/lXUPDIO+h1+K+UJPwYFhzHL3WpZQXHofcSUCRn0JONbz7c9OwVFYe0v1g18wwAvIAsEEimcnXFZaIJ/md2md0Jk4+LOFUsbgCM5skNTooXarDiHGCRFMJXxvYpavC6hhhcWfh3K2ydHMhFdF70ZOs169ShDtYq4i/hkaZ+p7kFVo8Z6oxVUpC8IECgYEA+OQxfc2iGMD9uus/r1yjUx0kdhYKik202tE53C4ikAn490+Lb8qcY/kykFj721MHBc73ijq/ZNU21JxyMx4T1jbdvl+5Kv9EUFAQmsJuTSudC1Dud3MlUgYEtVUhau2WOexUIRyCda6V4NUYkJG4vLRyMloPprF3xThmJRgG9qcCgYEA0g6QrU1e/DjADSpymRfqoOX4ASp7G++CRHTB2NZvA6ZxnWoEX59TO3mW4z4fgJkIwevkPUA9nfO2bRQWph+aom5qAczsPmZ2OpR9cjel63AIV1AboUV2Gr/H4IfHs7qGX7JWmW1SnZyvvGuI5MdBHEcysJO+L5V1OVyFRzvj8OECgYAO21A4+jVa1OpQZgp/JUB6jZrHkbk/WDQbe7HAeuCFSJMb8BuaqLV9IjrqcuVVyjb5Gcmc7rTOCAwl1NDcTEdS2iOSYZRkBKjHQoA7PK/o21mce1BAwRbRNprBWDuObnAxNPIwp8sBy1IXAaFdv9UPLpZCey3D/YPwudUfEbgYsQKBgQCZax3sFYh8ew56Dzin7Dnnzk72uwozexkP2p8COovWhKiSqi4LkRh/Zez4iBUGHb+xsxJ+Uf8u8CObQ4LPTmHopPAz5HHfmYJcgrukwlQiwy60ZsPnZA5AtzXLHiCTenZOSrjJUnl2uEv6OChBv+4kMzQol5/erTBy9so5Htr6wQKBgQC3apeFD/x+0FjABleVITzGyAWj/Kxl/OOiL4dQAYW49wVD/j0ujG3CvbK0GHZFwy/Ju+pWlHISbiZiKYko/4GBtp+JwxG9fFmbHdl4BZSTwfQMYdNq8hN+0hBfWwcceIhbEZcFLIvG/ZhkcT3yh874XRn1A5V/AR8W9YFH1EWYwQ=="
|
||||||
|
|
||||||
|
// val plainText = "{\n" +
|
||||||
|
// " \"taskId\": 723047797139586052,\n" +
|
||||||
|
// " \"licence\": \"LIC-8F2A-XXXX\",\n" +
|
||||||
|
// " \"fingerprint\": \"FP-2c91e9f3\",\n" +
|
||||||
|
// " \"enterpriseId\": \"1173040813421105152\",\n" +
|
||||||
|
// " \"inspectionId\": \"702286470691215417\",\n" +
|
||||||
|
// " \"summary\": \"1\"\n" +
|
||||||
|
// "}"
|
||||||
|
|
||||||
|
val plainText = "{\n" +
|
||||||
|
" \"licence\": \"lic-1234567890\",\n" +
|
||||||
|
" \"fingerprint\": \"e19c60d21c544c1118e3b633eae1bb935e2762ebddbc671b60b8b61c65c05d1c\"\n" +
|
||||||
|
"}"
|
||||||
|
|
||||||
|
val encryptedText =
|
||||||
|
RsaOaepCryptoUtil.encrypt(plainText, publicKey)
|
||||||
|
|
||||||
|
val decryptedText =
|
||||||
|
RsaOaepCryptoUtil.decrypt(encryptedText, privateKey)
|
||||||
|
|
||||||
|
println("Plain Text: $plainText")
|
||||||
|
|
||||||
|
println("Public Key: $publicKey")
|
||||||
|
println("Private Key: $privateKey")
|
||||||
|
|
||||||
|
|
||||||
|
println("Encrypted: $encryptedText")
|
||||||
|
println("Decrypted: $decryptedText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
120
docs/工具箱端-授权对接指南/utils/TaskEncryptionUtil.kt
Normal file
120
docs/工具箱端-授权对接指南/utils/TaskEncryptionUtil.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.*
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务加密工具类
|
||||||
|
* 使用 licence + fingerprint 作为密钥对任务数据进行 AES-256-GCM 对称加密
|
||||||
|
*
|
||||||
|
* GCM 模式提供认证加密,比 ECB 模式更安全
|
||||||
|
* 加密数据格式:IV(12字节) + 加密数据 + 认证标签(16字节)
|
||||||
|
*/
|
||||||
|
object TaskEncryptionUtil {
|
||||||
|
|
||||||
|
private const val ALGORITHM = "AES"
|
||||||
|
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||||
|
private const val GCM_IV_LENGTH = 12 // GCM 推荐使用 12 字节 IV
|
||||||
|
private const val GCM_TAG_LENGTH = 16 // GCM 认证标签长度(128位)
|
||||||
|
private const val KEY_LENGTH = 32 // AES-256 密钥长度(256位 = 32字节)
|
||||||
|
|
||||||
|
private val secureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 licence + fingerprint 加密任务数据(AES-256-GCM)
|
||||||
|
* @param data 待加密的数据(JSON字符串)
|
||||||
|
* @param licence 授权码
|
||||||
|
* @param fingerprint 硬件指纹
|
||||||
|
* @return Base64编码的加密数据(包含IV + 加密数据 + 认证标签)
|
||||||
|
*/
|
||||||
|
fun encrypt(data: String, licence: String, fingerprint: String): String {
|
||||||
|
return try {
|
||||||
|
// 使用 licence + fingerprint 生成密钥
|
||||||
|
val key = generateKey(licence, fingerprint)
|
||||||
|
|
||||||
|
// 生成随机 IV(12字节)
|
||||||
|
val iv = ByteArray(GCM_IV_LENGTH)
|
||||||
|
secureRandom.nextBytes(iv)
|
||||||
|
|
||||||
|
// 创建加密器
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
val parameterSpec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv) // 标签长度以位为单位
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec)
|
||||||
|
|
||||||
|
// 加密数据
|
||||||
|
val encryptedBytes = cipher.doFinal(data.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
|
||||||
|
// 组合:IV + 加密数据(包含认证标签)
|
||||||
|
val combined = ByteArray(iv.size + encryptedBytes.size)
|
||||||
|
System.arraycopy(iv, 0, combined, 0, iv.size)
|
||||||
|
System.arraycopy(encryptedBytes, 0, combined, iv.size, encryptedBytes.size)
|
||||||
|
|
||||||
|
// 返回 Base64 编码的加密数据
|
||||||
|
Base64.getEncoder().encodeToString(combined)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "AES-256-GCM加密任务数据失败" }
|
||||||
|
throw RuntimeException("加密任务数据失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 licence + fingerprint 解密任务数据(AES-256-GCM)
|
||||||
|
* @param encryptedData Base64编码的加密数据(包含IV + 加密数据 + 认证标签)
|
||||||
|
* @param licence 授权码
|
||||||
|
* @param fingerprint 硬件指纹
|
||||||
|
* @return 解密后的数据(JSON字符串)
|
||||||
|
*/
|
||||||
|
fun decrypt(encryptedData: String, licence: String, fingerprint: String): String {
|
||||||
|
return try {
|
||||||
|
// 使用 licence + fingerprint 生成密钥
|
||||||
|
val key = generateKey(licence, fingerprint)
|
||||||
|
|
||||||
|
// Base64 解码
|
||||||
|
val combined = Base64.getDecoder().decode(encryptedData)
|
||||||
|
|
||||||
|
// 分离 IV 和加密数据
|
||||||
|
if (combined.size < GCM_IV_LENGTH) {
|
||||||
|
throw IllegalArgumentException("加密数据格式错误:数据长度不足")
|
||||||
|
}
|
||||||
|
|
||||||
|
val iv = combined.sliceArray(0 until GCM_IV_LENGTH)
|
||||||
|
val cipherText = combined.sliceArray(GCM_IV_LENGTH until combined.size)
|
||||||
|
|
||||||
|
// 创建解密器
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
val parameterSpec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec)
|
||||||
|
|
||||||
|
// 解密数据(GCM 会自动验证认证标签)
|
||||||
|
val decryptedBytes = cipher.doFinal(cipherText)
|
||||||
|
|
||||||
|
// 返回解密后的字符串
|
||||||
|
String(decryptedBytes, StandardCharsets.UTF_8)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error(e) { "AES-256-GCM解密任务数据失败" }
|
||||||
|
throw RuntimeException("解密任务数据失败: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 licence + fingerprint 生成 AES-256 密钥(256位 = 32字节)
|
||||||
|
* 使用 SHA-256 哈希的全部32字节作为密钥
|
||||||
|
*/
|
||||||
|
private fun generateKey(licence: String, fingerprint: String): SecretKeySpec {
|
||||||
|
val combined = "$licence$fingerprint"
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hash = digest.digest(combined.toByteArray(StandardCharsets.UTF_8))
|
||||||
|
|
||||||
|
// 使用全部32字节作为 AES-256 密钥
|
||||||
|
return SecretKeySpec(hash, ALGORITHM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
134
docs/工具箱端-授权对接指南/utils/ZipVerifierUtil.kt
Normal file
134
docs/工具箱端-授权对接指南/utils/ZipVerifierUtil.kt
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package top.tangyh.lamp.filing.utils
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.bouncycastle.openpgp.*
|
||||||
|
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
|
||||||
|
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.Security
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
object ZipVerifierUtil {
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
// @JvmStatic
|
||||||
|
// fun main(args: Array<String>) {
|
||||||
|
// verifyZip("signed.zip", "public.key")
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 ZIP 文件
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun verifyZip(zipPath: String, pubkeyContent: String):Boolean {
|
||||||
|
|
||||||
|
println(Security.getProviders().joinToString { it.name })
|
||||||
|
val publicKey = readPublicKey(
|
||||||
|
ByteArrayInputStream(pubkeyContent.toByteArray())
|
||||||
|
)
|
||||||
|
|
||||||
|
val zip = ZipFile(zipPath)
|
||||||
|
|
||||||
|
// 1. 读取 manifest.json
|
||||||
|
val manifestEntry = zip.getEntry("META-INF/manifest.json")
|
||||||
|
?: throw RuntimeException("manifest.json is missing!")
|
||||||
|
val manifestJson = zip.getInputStream(manifestEntry).readAllBytes().toString(Charsets.UTF_8)
|
||||||
|
|
||||||
|
// 2. 读取 signature.asc
|
||||||
|
val sigEntry = zip.getEntry("META-INF/signature.asc")
|
||||||
|
?: throw RuntimeException("signature.asc is missing!")
|
||||||
|
val signature = zip.getInputStream(sigEntry).readAllBytes()
|
||||||
|
|
||||||
|
// 3. 使用 OpenPGP 验证签名
|
||||||
|
val ok = verifyDetachedSignature(publicKey, manifestJson.toByteArray(), signature)
|
||||||
|
if (!ok) throw RuntimeException("PGP signature invalid!")
|
||||||
|
|
||||||
|
// 4. 校验 manifest 里每个文件的 SHA-256
|
||||||
|
val mapper = ObjectMapper()
|
||||||
|
val manifest = mapper.readValue(manifestJson, Map::class.java)
|
||||||
|
val files = manifest["files"] as? Map<String, String>
|
||||||
|
?: throw RuntimeException("Invalid manifest.json: missing 'files'")
|
||||||
|
|
||||||
|
for ((name, expectedHash) in files) {
|
||||||
|
val entry = zip.getEntry(name)
|
||||||
|
?: throw RuntimeException("文件不存在: $name")
|
||||||
|
|
||||||
|
val data = zip.getInputStream(entry).readAllBytes()
|
||||||
|
val hash = sha256Hex(data)
|
||||||
|
|
||||||
|
if (!hash.equals(expectedHash, ignoreCase = true)) {
|
||||||
|
throw RuntimeException("Hash mismatch: $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun sha256Hex(data: ByteArray): String {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
return bytesToHex(md.digest(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bytesToHex(bytes: ByteArray): String {
|
||||||
|
return bytes.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun readPublicKey(keyIn: InputStream): PGPPublicKey {
|
||||||
|
val keyRings = PGPPublicKeyRingCollection(
|
||||||
|
PGPUtil.getDecoderStream(keyIn),
|
||||||
|
JcaKeyFingerprintCalculator()
|
||||||
|
)
|
||||||
|
|
||||||
|
for (keyRing in keyRings) {
|
||||||
|
for (key in keyRing) {
|
||||||
|
if (key.isEncryptionKey || key.isMasterKey) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw IllegalArgumentException("Can't find public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun verifyDetachedSignature(
|
||||||
|
key: PGPPublicKey,
|
||||||
|
data: ByteArray,
|
||||||
|
sigBytes: ByteArray
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val decoder = PGPUtil.getDecoderStream(ByteArrayInputStream(sigBytes))
|
||||||
|
val factory = PGPObjectFactory(decoder, JcaKeyFingerprintCalculator())
|
||||||
|
|
||||||
|
val message = factory.nextObject()
|
||||||
|
?: throw IllegalArgumentException("Invalid signature file")
|
||||||
|
|
||||||
|
val sigList = when (message) {
|
||||||
|
is PGPSignatureList -> message
|
||||||
|
is PGPCompressedData -> {
|
||||||
|
val compressedFactory = PGPObjectFactory(
|
||||||
|
message.dataStream,
|
||||||
|
JcaKeyFingerprintCalculator()
|
||||||
|
)
|
||||||
|
val compressedObj = compressedFactory.nextObject()
|
||||||
|
compressedObj as? PGPSignatureList
|
||||||
|
?: throw IllegalArgumentException("Invalid PGP signature (not signature list)")
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
throw IllegalArgumentException("Unsupported PGP signature format: ${message::class.java}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sig = sigList[0]
|
||||||
|
|
||||||
|
sig.init(JcaPGPContentVerifierBuilderProvider().setProvider("BC"), key)
|
||||||
|
sig.update(data)
|
||||||
|
|
||||||
|
return sig.verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user