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 { 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 { 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> { 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 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}\"}") } } }