433 lines
17 KiB
Kotlin
433 lines
17 KiB
Kotlin
package top.tangyh.lamp.filing.controller.compress
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper
|
||
import io.swagger.annotations.Api
|
||
import io.swagger.annotations.ApiOperation
|
||
import io.swagger.annotations.ApiParam
|
||
import org.springframework.validation.annotation.Validated
|
||
import org.springframework.web.bind.annotation.*
|
||
import top.tangyh.basic.annotation.log.WebLog
|
||
import top.tangyh.basic.base.R
|
||
import top.tangyh.lamp.filing.dto.management.UploadInspectionFileV2Request
|
||
import top.tangyh.lamp.filing.utils.AesGcmUtil
|
||
import top.tangyh.lamp.filing.utils.HkdfUtil
|
||
import top.tangyh.lamp.filing.utils.PgpSignatureUtil
|
||
import java.util.*
|
||
|
||
/**
|
||
* 加密测试工具类
|
||
*
|
||
* 用于生成加密后的 encrypted 数据,测试 uploadInspectionFileV2Encrypted 接口
|
||
*
|
||
* 使用说明:
|
||
* 1. 调用 /compression/test/generateEncrypted 接口
|
||
* 2. 传入 licence、fingerprint、taskId 和明文数据
|
||
* 3. 获取加密后的 Base64 字符串
|
||
* 4. 使用返回的 encrypted 数据测试 uploadInspectionFileV2Encrypted 接口
|
||
*/
|
||
@Validated
|
||
@RestController
|
||
@RequestMapping("/compression/test")
|
||
@Api(value = "EncryptionTest", tags = ["加密测试工具"])
|
||
class EncryptionTestController {
|
||
|
||
private val objectMapper = ObjectMapper()
|
||
|
||
companion object {
|
||
private const val DEFAULT_PGP_PRIVATE_KEY = """-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||
|
||
lFgEaSZqXBYJKwYBBAHaRw8BAQdARzZ5JXreuTeTgMFwYcw0Ju7aCWmXuUMmQyff
|
||
5vmN8RQAAP4nli0R/MTNtgx9+g5ZPyAj8XSAnjHaW9u2UJQxYhMIYw8XtBZpdHRj
|
||
PGl0dGNAaXR0Yy5zaC5jbj6IkwQTFgoAOxYhBG8IkI1kmkNpEu8iuqWu91t6SEzN
|
||
BQJpJmpcAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEKWu91t6SEzN
|
||
dSQBAPM5llVG0X6SBa4YM90Iqyb2jWvlNjstoF8jjPVny1CiAP4hIOUvb686oSA0
|
||
OrS3AuICi7X/r+JnSo1Z7pngUA3VC5xdBGkmalwSCisGAQQBl1UBBQEBB0BouQlG
|
||
hIL0bq7EbaB55s+ygLVFOfhjFA8E4fwFBFJGVAMBCAcAAP98ZXRGgzld1XUa5ZGx
|
||
cTE+1qGZY4E4BVIeqkVxdg5tqA64iHgEGBYKACAWIQRvCJCNZJpDaRLvIrqlrvdb
|
||
ekhMzQUCaSZqXAIbDAAKCRClrvdbekhMzcaSAQDB/4pvDuc7SploQg1fBYobFm5P
|
||
vxguByr8I+PrYWKKOQEAnaeXT4ipi1nICXFiigztsIl2xTth3D77XG6pZUU/Zw8=
|
||
=/k1H
|
||
-----END PGP PRIVATE KEY BLOCK-----"""
|
||
|
||
private const val DEFAULT_PGP_PASSPHRASE = ""
|
||
}
|
||
|
||
/**
|
||
* 生成加密数据请求 DTO
|
||
*/
|
||
data class GenerateEncryptedRequest(
|
||
@ApiParam(value = "授权码", required = true)
|
||
val licence: String,
|
||
|
||
@ApiParam(value = "硬件指纹", required = true)
|
||
val fingerprint: String,
|
||
|
||
@ApiParam(value = "任务ID", required = true)
|
||
val taskId: String,
|
||
|
||
@ApiParam(value = "企业ID", required = true)
|
||
val enterpriseId: Long,
|
||
|
||
@ApiParam(value = "检查ID", required = true)
|
||
val inspectionId: Long,
|
||
|
||
@ApiParam(value = "摘要信息", required = true)
|
||
val summary: String
|
||
)
|
||
|
||
/**
|
||
* 生成加密数据响应 DTO
|
||
*/
|
||
data class GenerateEncryptedResponse(
|
||
val encrypted: String,
|
||
val requestBody: UploadInspectionFileV2Request,
|
||
val plaintext: String,
|
||
val keyDerivationInfo: KeyDerivationInfo
|
||
)
|
||
|
||
/**
|
||
* 密钥派生信息
|
||
*/
|
||
data class KeyDerivationInfo(
|
||
val ikm: String,
|
||
val salt: String,
|
||
val info: String,
|
||
val keyLength: Int,
|
||
val keyHex: String
|
||
)
|
||
|
||
/**
|
||
* 生成加密数据
|
||
*
|
||
* 模拟工具箱端的加密逻辑:
|
||
* 1. 使用 HKDF-SHA256 派生 AES 密钥
|
||
* - ikm = licence + fingerprint
|
||
* - salt = taskId
|
||
* - info = "inspection_report_encryption"
|
||
* - length = 32 bytes
|
||
*
|
||
* 2. 使用 AES-256-GCM 加密数据
|
||
* - 格式:IV (12字节) + Ciphertext + Tag (16字节)
|
||
* - Base64 编码返回
|
||
*
|
||
* @param request 生成加密数据请求
|
||
* @return 加密后的数据和完整的请求体
|
||
*/
|
||
@ApiOperation(value = "生成加密数据", notes = "生成加密后的 encrypted 数据,用于测试 uploadInspectionFileV2Encrypted 接口")
|
||
@PostMapping("/generateEncrypted")
|
||
@WebLog(value = "'生成加密数据:'", request = false)
|
||
fun generateEncrypted(
|
||
@RequestBody request: GenerateEncryptedRequest
|
||
): R<GenerateEncryptedResponse> {
|
||
return try {
|
||
// 1. 组装明文数据(JSON格式)
|
||
val timestamp = System.currentTimeMillis()
|
||
val plaintextMap = mapOf(
|
||
"enterpriseId" to request.enterpriseId.toString(),
|
||
"inspectionId" to request.inspectionId.toString(),
|
||
"summary" to request.summary,
|
||
"timestamp" to timestamp
|
||
)
|
||
val plaintext = objectMapper.writeValueAsString(plaintextMap)
|
||
|
||
// 2. 使用 HKDF-SHA256 派生 AES 密钥
|
||
// ikm = licence + fingerprint
|
||
// salt = taskId(工具箱从二维码获取,平台从请求获取)
|
||
// info = "inspection_report_encryption"(固定值)
|
||
// length = 32 bytes
|
||
val ikm = "${request.licence}${request.fingerprint}"
|
||
val salt = request.taskId.toString()
|
||
val info = "inspection_report_encryption"
|
||
val keyLength = 32
|
||
|
||
val aesKey = HkdfUtil.deriveKey(ikm, salt, info, keyLength)
|
||
|
||
// 3. 使用 AES-256-GCM 加密数据
|
||
val encrypted = AesGcmUtil.encrypt(plaintext, aesKey)
|
||
|
||
// 4. 组装完整的请求体(appid 需要前端自己赋值)
|
||
val requestBody = UploadInspectionFileV2Request().apply {
|
||
this.appid = "test-appid" // 测试用的 appid,实际使用时前端会赋值
|
||
this.taskId = request.taskId
|
||
this.encrypted = encrypted
|
||
}
|
||
|
||
// 5. 返回加密数据和密钥派生信息
|
||
val response = GenerateEncryptedResponse(
|
||
encrypted = encrypted,
|
||
requestBody = requestBody,
|
||
plaintext = plaintext,
|
||
keyDerivationInfo = KeyDerivationInfo(
|
||
ikm = ikm,
|
||
salt = salt,
|
||
info = info,
|
||
keyLength = keyLength,
|
||
keyHex = aesKey.joinToString("") { "%02x".format(it) }
|
||
)
|
||
)
|
||
|
||
R.success(response, "加密数据生成成功")
|
||
} catch (e: Exception) {
|
||
R.fail("生成加密数据失败: ${e.message}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 快速生成测试数据(使用默认值)
|
||
*
|
||
* @return 加密后的数据和完整的请求体
|
||
*/
|
||
@ApiOperation(value = "快速生成测试数据", notes = "使用默认值快速生成加密数据,用于快速测试")
|
||
@GetMapping("/generateTestData")
|
||
@WebLog(value = "'快速生成测试数据:'", request = false)
|
||
fun generateTestData(): R<GenerateEncryptedResponse> {
|
||
return try {
|
||
// 使用默认测试数据
|
||
val request = GenerateEncryptedRequest(
|
||
licence = "TEST-LICENCE-001",
|
||
fingerprint = "TEST-FINGERPRINT-001",
|
||
taskId = "TASK-20260115-4875",
|
||
enterpriseId = 1173040813421105152L,
|
||
inspectionId = 702286470691215417L,
|
||
summary = "测试摘要信息"
|
||
)
|
||
|
||
generateEncrypted(request).data?.let {
|
||
R.success(it, "测试数据生成成功")
|
||
} ?: R.fail("生成测试数据失败")
|
||
} catch (e: Exception) {
|
||
R.fail("生成测试数据失败: ${e.message}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证加密数据(解密测试)
|
||
*
|
||
* 用于验证生成的加密数据是否能正确解密
|
||
*
|
||
* @param encrypted 加密后的 Base64 字符串
|
||
* @param licence 授权码
|
||
* @param fingerprint 硬件指纹
|
||
* @param taskId 任务ID
|
||
* @return 解密后的明文数据
|
||
*/
|
||
@ApiOperation(value = "验证加密数据", notes = "解密加密数据,验证加密是否正确")
|
||
@PostMapping("/verifyEncrypted")
|
||
@WebLog(value = "'验证加密数据:'", request = false)
|
||
fun verifyEncrypted(
|
||
@ApiParam(value = "加密后的 Base64 字符串", required = true)
|
||
@RequestParam encrypted: String,
|
||
|
||
@ApiParam(value = "授权码", required = true)
|
||
@RequestParam licence: String,
|
||
|
||
@ApiParam(value = "硬件指纹", required = true)
|
||
@RequestParam fingerprint: String,
|
||
|
||
@ApiParam(value = "任务ID", required = true)
|
||
@RequestParam taskId: String
|
||
): R<Map<String, Any>> {
|
||
return try {
|
||
// 1. 使用相同的密钥派生规则派生密钥
|
||
val ikm = "$licence$fingerprint"
|
||
val salt = taskId.toString()
|
||
val info = "inspection_report_encryption"
|
||
val aesKey = HkdfUtil.deriveKey(ikm, salt, info, 32)
|
||
|
||
// 2. 解密数据
|
||
val decrypted = AesGcmUtil.decrypt(encrypted, aesKey)
|
||
|
||
// 3. 解析 JSON
|
||
@Suppress("UNCHECKED_CAST")
|
||
val dataMap = objectMapper.readValue(decrypted, Map::class.java) as Map<String, Any>
|
||
|
||
R.success(dataMap, "解密成功")
|
||
} catch (e: Exception) {
|
||
R.fail("解密失败: ${e.message}")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成加密报告 ZIP 文件请求 DTO
|
||
*/
|
||
data class GenerateEncryptedZipRequest(
|
||
@ApiParam(value = "授权码", required = true)
|
||
val licence: String,
|
||
|
||
@ApiParam(value = "硬件指纹", required = true)
|
||
val fingerprint: String,
|
||
|
||
@ApiParam(value = "任务ID", required = true)
|
||
val taskId: String,
|
||
|
||
@ApiParam(value = "企业ID", required = true)
|
||
val enterpriseId: Long,
|
||
|
||
@ApiParam(value = "检查ID", required = true)
|
||
val inspectionId: Long,
|
||
|
||
@ApiParam(value = "摘要信息", required = true)
|
||
val summary: String,
|
||
|
||
@ApiParam(value = "资产信息 JSON", required = true)
|
||
val assetsJson: String,
|
||
|
||
@ApiParam(value = "漏洞信息 JSON", required = true)
|
||
val vulnerabilitiesJson: String,
|
||
|
||
@ApiParam(value = "弱密码信息 JSON", required = true)
|
||
val weakPasswordsJson: String,
|
||
|
||
@ApiParam(value = "漏洞评估报告 HTML", required = true)
|
||
val reportHtml: String,
|
||
|
||
@ApiParam(value = "PGP 私钥(可选,不提供则跳过 PGP 签名)", required = false)
|
||
val pgpPrivateKey: String? = null,
|
||
|
||
@ApiParam(value = "PGP 私钥密码(可选)", required = false)
|
||
val pgpPassphrase: String? = null
|
||
)
|
||
|
||
/**
|
||
* 生成加密报告 ZIP 文件
|
||
*
|
||
* 按照文档《工具箱端-报告加密与签名生成指南.md》生成加密报告 ZIP 文件
|
||
*
|
||
* @param request 生成请求
|
||
* @return ZIP 文件(二进制流)
|
||
*/
|
||
@ApiOperation(value = "生成加密报告 ZIP", notes = "生成带设备签名的加密报告 ZIP 文件,可被 uploadInspectionFileV2 接口解密")
|
||
@PostMapping("/generateEncryptedZip")
|
||
@WebLog(value = "'生成加密报告 ZIP:'", request = false)
|
||
fun generateEncryptedZip(
|
||
@RequestBody request: GenerateEncryptedZipRequest,
|
||
response: javax.servlet.http.HttpServletResponse
|
||
) {
|
||
try {
|
||
// 1. 准备文件内容
|
||
val assetsContent = request.assetsJson.toByteArray(Charsets.UTF_8)
|
||
val vulnerabilitiesContent = request.vulnerabilitiesJson.toByteArray(Charsets.UTF_8)
|
||
val weakPasswordsContent = request.weakPasswordsJson.toByteArray(Charsets.UTF_8)
|
||
val reportHtmlContent = request.reportHtml.toByteArray(Charsets.UTF_8)
|
||
|
||
// 2. 生成设备签名
|
||
// 2.1 密钥派生
|
||
val ikm = "${request.licence}${request.fingerprint}"
|
||
val salt = "AUTH_V3_SALT"
|
||
val info = "device_report_signature"
|
||
val derivedKey = HkdfUtil.deriveKey(ikm, salt, info, 32)
|
||
|
||
// 2.2 计算文件 SHA256
|
||
fun sha256Hex(content: ByteArray): String {
|
||
val digest = java.security.MessageDigest.getInstance("SHA-256")
|
||
return digest.digest(content).joinToString("") { "%02x".format(it) }
|
||
}
|
||
|
||
val assetsSha256 = sha256Hex(assetsContent)
|
||
val vulnerabilitiesSha256 = sha256Hex(vulnerabilitiesContent)
|
||
val weakPasswordsSha256 = sha256Hex(weakPasswordsContent)
|
||
val reportHtmlSha256 = sha256Hex(reportHtmlContent)
|
||
|
||
// 2.3 组装签名数据(严格顺序)
|
||
val signPayload = buildString {
|
||
append(request.taskId)
|
||
append(request.inspectionId)
|
||
append(assetsSha256)
|
||
append(vulnerabilitiesSha256)
|
||
append(weakPasswordsSha256)
|
||
append(reportHtmlSha256)
|
||
}
|
||
|
||
// 2.4 计算 HMAC-SHA256
|
||
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
|
||
val secretKey = javax.crypto.spec.SecretKeySpec(derivedKey, "HmacSHA256")
|
||
mac.init(secretKey)
|
||
val signatureBytes = mac.doFinal(signPayload.toByteArray(Charsets.UTF_8))
|
||
val deviceSignature = Base64.getEncoder().encodeToString(signatureBytes)
|
||
|
||
// 2.5 生成 summary.json
|
||
val summaryMap = mapOf(
|
||
"orgId" to request.enterpriseId,
|
||
"checkId" to request.inspectionId,
|
||
"taskId" to request.taskId,
|
||
"licence" to request.licence,
|
||
"fingerprint" to request.fingerprint,
|
||
"deviceSignature" to deviceSignature,
|
||
"summary" to request.summary
|
||
)
|
||
val summaryContent = objectMapper.writeValueAsString(summaryMap).toByteArray(Charsets.UTF_8)
|
||
|
||
// 3. 生成 manifest.json
|
||
val filesHashes = mapOf(
|
||
"summary.json" to sha256Hex(summaryContent),
|
||
"assets.json" to assetsSha256,
|
||
"vulnerabilities.json" to vulnerabilitiesSha256,
|
||
"weakPasswords.json" to weakPasswordsSha256,
|
||
"漏洞评估报告.html" to reportHtmlSha256
|
||
)
|
||
val manifest = mapOf("files" to filesHashes)
|
||
val manifestContent = objectMapper.writeValueAsString(manifest).toByteArray(Charsets.UTF_8)
|
||
|
||
// 4. 生成 signature.asc
|
||
val privateKey = request.pgpPrivateKey?.takeIf { it.isNotBlank() } ?: DEFAULT_PGP_PRIVATE_KEY
|
||
val passphrase = request.pgpPassphrase ?: DEFAULT_PGP_PASSPHRASE
|
||
|
||
val signatureAsc = try {
|
||
PgpSignatureUtil.generateDetachedSignature(
|
||
manifestContent,
|
||
privateKey,
|
||
passphrase
|
||
)
|
||
} catch (e: Exception) {
|
||
throw RuntimeException("生成 PGP 签名失败: ${e.message}", e)
|
||
}
|
||
|
||
// 5. 打包 ZIP 文件到内存
|
||
val baos = java.io.ByteArrayOutputStream()
|
||
java.util.zip.ZipOutputStream(baos).use { zipOut ->
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("summary.json"))
|
||
zipOut.write(summaryContent)
|
||
zipOut.closeEntry()
|
||
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("assets.json"))
|
||
zipOut.write(assetsContent)
|
||
zipOut.closeEntry()
|
||
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("vulnerabilities.json"))
|
||
zipOut.write(vulnerabilitiesContent)
|
||
zipOut.closeEntry()
|
||
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("weakPasswords.json"))
|
||
zipOut.write(weakPasswordsContent)
|
||
zipOut.closeEntry()
|
||
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("漏洞评估报告.html"))
|
||
zipOut.write(reportHtmlContent)
|
||
zipOut.closeEntry()
|
||
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("META-INF/manifest.json"))
|
||
zipOut.write(manifestContent)
|
||
zipOut.closeEntry()
|
||
|
||
zipOut.putNextEntry(java.util.zip.ZipEntry("META-INF/signature.asc"))
|
||
zipOut.write(signatureAsc)
|
||
zipOut.closeEntry()
|
||
}
|
||
|
||
val zipBytes = baos.toByteArray()
|
||
|
||
// 6. 设置响应头并输出
|
||
response.contentType = "application/octet-stream"
|
||
response.setHeader("Content-Disposition", "attachment; filename=\"report_${request.taskId}.zip\"")
|
||
response.setHeader("Content-Length", zipBytes.size.toString())
|
||
response.outputStream.write(zipBytes)
|
||
response.outputStream.flush()
|
||
} catch (e: Exception) {
|
||
response.reset()
|
||
response.contentType = "application/json; charset=UTF-8"
|
||
response.writer.write("{\"code\": 500, \"msg\": \"生成 ZIP 文件失败: ${e.message}\"}")
|
||
}
|
||
}
|
||
}
|
||
|