refactor(server): crypto 流程改用验证后的 licenceId

This commit is contained in:
2026-03-19 16:16:53 +08:00
parent 403eec3e12
commit 0f344b5847
3 changed files with 20 additions and 12 deletions

View File

@@ -17,7 +17,7 @@ const handler = new OpenAPIHandler(router, {
title: name, title: name,
version, version,
description: 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', docsPath: '/docs',

View File

@@ -8,7 +8,7 @@ export const encryptDeviceInfo = oc
operationId: 'encryptDeviceInfo', operationId: 'encryptDeviceInfo',
summary: '生成设备授权二维码密文', summary: '生成设备授权二维码密文',
description: 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'], tags: ['Crypto'],
}) })
.input(z.object({}).describe('空请求体。平台公钥由本地配置自动读取')) .input(z.object({}).describe('空请求体。平台公钥由本地配置自动读取'))
@@ -34,7 +34,7 @@ export const decryptTask = oc
operationId: 'decryptTask', operationId: 'decryptTask',
summary: '解密任务二维码数据', summary: '解密任务二维码数据',
description: 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'], tags: ['Crypto'],
}) })
.input( .input(
@@ -74,7 +74,7 @@ export const encryptSummary = oc
operationId: 'encryptSummary', operationId: 'encryptSummary',
summary: '加密摘要信息', summary: '加密摘要信息',
description: 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'], tags: ['Crypto'],
}) })
.input( .input(
@@ -116,7 +116,7 @@ export const signAndPackReport = oc
operationId: 'signAndPackReport', operationId: 'signAndPackReport',
summary: '签名并打包检查报告', summary: '签名并打包检查报告',
description: 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'], tags: ['Report'],
spec: (current) => { spec: (current) => {
const multipartContent = const multipartContent =

View File

@@ -18,6 +18,7 @@ import {
stringify as losslessStringify, stringify as losslessStringify,
} from 'lossless-json' } from 'lossless-json'
import { z } from 'zod' import { z } from 'zod'
import { isLicenceExpired } from '@/server/licence'
import { extractSafeZipFiles, ZipValidationError } from '@/server/safe-zip' import { extractSafeZipFiles, ZipValidationError } from '@/server/safe-zip'
import { getUxConfig } from '@/server/ux-config' import { getUxConfig } from '@/server/ux-config'
import { db } from '../middlewares' import { db } from '../middlewares'
@@ -45,12 +46,19 @@ const summaryPayloadSchema = z
const requireIdentity = async (dbInstance: Parameters<typeof getUxConfig>[0]) => { const requireIdentity = async (dbInstance: Parameters<typeof getUxConfig>[0]) => {
const config = await getUxConfig(dbInstance) const config = await getUxConfig(dbInstance)
if (!config || !config.licence) { if (!config || !config.licenceId || !config.licenceExpireTime) {
throw new ORPCError('PRECONDITION_FAILED', { throw new ORPCError('PRECONDITION_FAILED', {
message: 'Local identity is not initialized. Call config.get and then config.setLicence first.', 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 }) => { 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({ const deviceInfoJson = JSON.stringify({
licence: config.licence, licence: config.licenceId,
fingerprint: config.fingerprint, 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 }) => { export const decryptTask = os.crypto.decryptTask.use(db).handler(async ({ context, input }) => {
const config = await requireIdentity(context.db) 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) const decrypted = aesGcmDecrypt(input.encryptedData, key)
return { decrypted } 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 }) => { export const encryptSummary = os.crypto.encryptSummary.use(db).handler(async ({ context, input }) => {
const config = await requireIdentity(context.db) 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 aesKey = hkdfSha256(ikm, input.salt, 'inspection_report_encryption')
const encrypted = aesGcmEncrypt(input.plaintext, aesKey) const encrypted = aesGcmEncrypt(input.plaintext, aesKey)
return { encrypted } return { encrypted }
@@ -152,7 +160,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy
// Compute device signature // Compute device signature
// signPayload = taskId + inspectionId + assetsSha256 + vulnerabilitiesSha256 + weakPasswordsSha256 + reportHtmlSha256 // signPayload = taskId + inspectionId + assetsSha256 + vulnerabilitiesSha256 + weakPasswordsSha256 + reportHtmlSha256
// (plain concatenation, no separators, fixed order — matching Kotlin reference) // (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 signingKey = hkdfSha256(ikm, 'AUTH_V3_SALT', 'device_report_signature')
const signPayload = `${summaryPayload.taskId}${checkId}${assetsSha256}${vulnerabilitiesSha256}${weakPasswordsSha256}${reportHtmlSha256}` 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)), orgId: toLosslessNumber(String(orgId)),
checkId: toLosslessNumber(checkId), checkId: toLosslessNumber(checkId),
taskId: summaryPayload.taskId, taskId: summaryPayload.taskId,
licence: config.licence, licence: config.licenceId,
fingerprint: config.fingerprint, fingerprint: config.fingerprint,
deviceSignature, deviceSignature,
summary: summaryPayload.summary ?? '', summary: summaryPayload.summary ?? '',