diff --git a/docs/摘要+zip/EncryptionTestController.kt b/docs/摘要+zip/EncryptionTestController.kt new file mode 100644 index 0000000..4714000 --- /dev/null +++ b/docs/摘要+zip/EncryptionTestController.kt @@ -0,0 +1,432 @@ +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}\"}") + } + } +} +