From 0f344b584777252b0c22147704ac22e3890efb50 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Thu, 19 Mar 2026 16:16:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor(server):=20crypto=20=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E9=AA=8C=E8=AF=81=E5=90=8E=E7=9A=84=20licenc?= =?UTF-8?q?eId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/src/routes/api/$.ts | 2 +- .../server/api/contracts/crypto.contract.ts | 8 +++---- .../src/server/api/routers/crypto.router.ts | 22 +++++++++++++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/server/src/routes/api/$.ts b/apps/server/src/routes/api/$.ts index fda1675..12fdc73 100644 --- a/apps/server/src/routes/api/$.ts +++ b/apps/server/src/routes/api/$.ts @@ -17,7 +17,7 @@ const handler = new OpenAPIHandler(router, { title: name, version, description: - 'UX 授权服务 OpenAPI 文档。该服务用于工具箱侧本地身份初始化与密码学能力调用,覆盖设备授权密文生成、任务二维码解密、摘要信息加密、报告签名打包等流程。\n\n推荐调用顺序:\n1) 写入 licence 与 OpenPGP 私钥;\n2) 读取本机身份状态进行前置校验;\n3) 执行加密/解密与签名接口。\n\n说明:除文件下载接口外,返回体均为 JSON;字段示例已提供,便于联调和 Mock。', + 'UX 授权服务 OpenAPI 文档。该服务用于工具箱侧本地身份初始化与密码学能力调用,覆盖设备授权密文生成、任务二维码解密、摘要信息加密、报告签名打包等流程。\n\n推荐调用顺序:\n1) 写入平台公钥;\n2) 写入已签名 licence JSON;\n3) 写入 OpenPGP 私钥;\n4) 读取本机身份状态进行前置校验;\n5) 执行加密/解密与签名接口。\n\n说明:除文件下载接口外,返回体均为 JSON;字段示例已提供,便于联调和 Mock。', }, }, docsPath: '/docs', diff --git a/apps/server/src/server/api/contracts/crypto.contract.ts b/apps/server/src/server/api/contracts/crypto.contract.ts index f3b0a11..d976df1 100644 --- a/apps/server/src/server/api/contracts/crypto.contract.ts +++ b/apps/server/src/server/api/contracts/crypto.contract.ts @@ -8,7 +8,7 @@ export const encryptDeviceInfo = oc operationId: 'encryptDeviceInfo', summary: '生成设备授权二维码密文', description: - '生成设备授权流程所需的二维码密文。\n\n处理流程:\n- 读取本机 licence、fingerprint 与本地持久化的平台公钥;\n- 组装为授权载荷 JSON;\n- 使用平台公钥执行 RSA-OAEP(SHA-256) 加密;\n- 返回 Base64 密文供前端生成二维码。\n\n适用场景:设备授权申请、重新授权。\n\n前置条件:需先调用 config.setPlatformPublicKey 写入平台公钥。', + '生成设备授权流程所需的二维码密文。\n\n处理流程:\n- 读取本机已验证的 licenceId、fingerprint 与本地持久化的平台公钥;\n- 组装为授权载荷 JSON;\n- 使用平台公钥执行 RSA-OAEP(SHA-256) 加密;\n- 返回 Base64 密文供前端生成二维码。\n\n适用场景:设备授权申请、重新授权。\n\n前置条件:需先调用 config.setPlatformPublicKey 写入平台公钥,并通过 config.setLicence 安装已签名 licence。', tags: ['Crypto'], }) .input(z.object({}).describe('空请求体。平台公钥由本地配置自动读取')) @@ -34,7 +34,7 @@ export const decryptTask = oc operationId: 'decryptTask', summary: '解密任务二维码数据', description: - '解密 App 下发的任务二维码密文。\n\n处理流程:\n- 基于本机 licence + fingerprint 派生 AES-256-GCM 密钥;\n- 对二维码中的 Base64 密文进行解密;\n- 返回任务明文 JSON 字符串。\n\n适用场景:扫码接收任务后解析任务详情。', + '解密 App 下发的任务二维码密文。\n\n处理流程:\n- 基于本机已验证的 licenceId + fingerprint 派生 AES-256-GCM 密钥;\n- 对二维码中的 Base64 密文进行解密;\n- 返回任务明文 JSON 字符串。\n\n适用场景:扫码接收任务后解析任务详情。', tags: ['Crypto'], }) .input( @@ -74,7 +74,7 @@ export const encryptSummary = oc operationId: 'encryptSummary', summary: '加密摘要信息', description: - '加密检查摘要信息并产出二维码密文。\n\n处理流程:\n- 使用 licence + fingerprint 结合 taskId(salt) 通过 HKDF-SHA256 派生密钥;\n- 使用 AES-256-GCM 加密摘要明文;\n- 返回 Base64 密文用于摘要二维码生成。\n\n适用场景:任务执行后提交摘要信息。', + '加密检查摘要信息并产出二维码密文。\n\n处理流程:\n- 使用已验证的 licenceId + fingerprint 结合 taskId(salt) 通过 HKDF-SHA256 派生密钥;\n- 使用 AES-256-GCM 加密摘要明文;\n- 返回 Base64 密文用于摘要二维码生成。\n\n适用场景:任务执行后提交摘要信息。', tags: ['Crypto'], }) .input( @@ -116,7 +116,7 @@ export const signAndPackReport = oc operationId: 'signAndPackReport', summary: '签名并打包检查报告', description: - '对原始报告执行设备签名与 OpenPGP 签名并重新打包。\n\n处理流程:\n- 解析上传 ZIP 并提取 summary.json;\n- 用 licence/fingerprint 计算 deviceSignature(HKDF + HMAC-SHA256) 并回写 summary.json;\n- 生成 META-INF/manifest.json;\n- 使用本地 OpenPGP 私钥生成 detached signature(`META-INF/signature.asc`);\n- 返回签名后 ZIP。\n\n适用场景:检查结果归档、可追溯签名分发。', + '对原始报告执行设备签名与 OpenPGP 签名并重新打包。\n\n处理流程:\n- 解析上传 ZIP 并提取 summary.json;\n- 用已验证的 licenceId/fingerprint 计算 deviceSignature(HKDF + HMAC-SHA256) 并回写 summary.json;\n- 生成 META-INF/manifest.json;\n- 使用本地 OpenPGP 私钥生成 detached signature(`META-INF/signature.asc`);\n- 返回签名后 ZIP。\n\n适用场景:检查结果归档、可追溯签名分发。', tags: ['Report'], spec: (current) => { const multipartContent = diff --git a/apps/server/src/server/api/routers/crypto.router.ts b/apps/server/src/server/api/routers/crypto.router.ts index ab082d0..06a39d4 100644 --- a/apps/server/src/server/api/routers/crypto.router.ts +++ b/apps/server/src/server/api/routers/crypto.router.ts @@ -18,6 +18,7 @@ import { stringify as losslessStringify, } from 'lossless-json' import { z } from 'zod' +import { isLicenceExpired } from '@/server/licence' import { extractSafeZipFiles, ZipValidationError } from '@/server/safe-zip' import { getUxConfig } from '@/server/ux-config' import { db } from '../middlewares' @@ -45,12 +46,19 @@ const summaryPayloadSchema = z const requireIdentity = async (dbInstance: Parameters[0]) => { const config = await getUxConfig(dbInstance) - if (!config || !config.licence) { + if (!config || !config.licenceId || !config.licenceExpireTime) { throw new ORPCError('PRECONDITION_FAILED', { message: 'Local identity is not initialized. Call config.get and then config.setLicence first.', }) } - return config as typeof config & { licence: string } + + if (isLicenceExpired(config.licenceExpireTime)) { + throw new ORPCError('PRECONDITION_FAILED', { + message: 'Local licence has expired. Install a new signed licence before calling crypto APIs.', + }) + } + + return config as typeof config & { licenceId: string; licenceExpireTime: string } } export const encryptDeviceInfo = os.crypto.encryptDeviceInfo.use(db).handler(async ({ context }) => { @@ -63,7 +71,7 @@ export const encryptDeviceInfo = os.crypto.encryptDeviceInfo.use(db).handler(asy } const deviceInfoJson = JSON.stringify({ - licence: config.licence, + licence: config.licenceId, fingerprint: config.fingerprint, }) @@ -74,7 +82,7 @@ export const encryptDeviceInfo = os.crypto.encryptDeviceInfo.use(db).handler(asy export const decryptTask = os.crypto.decryptTask.use(db).handler(async ({ context, input }) => { const config = await requireIdentity(context.db) - const key = sha256(config.licence + config.fingerprint) + const key = sha256(config.licenceId + config.fingerprint) const decrypted = aesGcmDecrypt(input.encryptedData, key) return { decrypted } }) @@ -82,7 +90,7 @@ export const decryptTask = os.crypto.decryptTask.use(db).handler(async ({ contex export const encryptSummary = os.crypto.encryptSummary.use(db).handler(async ({ context, input }) => { const config = await requireIdentity(context.db) - const ikm = config.licence + config.fingerprint + const ikm = config.licenceId + config.fingerprint const aesKey = hkdfSha256(ikm, input.salt, 'inspection_report_encryption') const encrypted = aesGcmEncrypt(input.plaintext, aesKey) return { encrypted } @@ -152,7 +160,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy // Compute device signature // signPayload = taskId + inspectionId + assetsSha256 + vulnerabilitiesSha256 + weakPasswordsSha256 + reportHtmlSha256 // (plain concatenation, no separators, fixed order — matching Kotlin reference) - const ikm = config.licence + config.fingerprint + const ikm = config.licenceId + config.fingerprint const signingKey = hkdfSha256(ikm, 'AUTH_V3_SALT', 'device_report_signature') const signPayload = `${summaryPayload.taskId}${checkId}${assetsSha256}${vulnerabilitiesSha256}${weakPasswordsSha256}${reportHtmlSha256}` @@ -163,7 +171,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy orgId: toLosslessNumber(String(orgId)), checkId: toLosslessNumber(checkId), taskId: summaryPayload.taskId, - licence: config.licence, + licence: config.licenceId, fingerprint: config.fingerprint, deviceSignature, summary: summaryPayload.summary ?? '',