refactor(crypto): use Zod safeParse for summary.json validation instead of manual checks

This commit is contained in:
2026-03-06 16:39:38 +08:00
parent 4d64cfb93d
commit 3d27f8ccfa

View File

@@ -11,6 +11,7 @@ import {
import { ORPCError } from '@orpc/server' import { ORPCError } from '@orpc/server'
import type { JSZipObject } from 'jszip' import type { JSZipObject } from 'jszip'
import JSZip from 'jszip' import JSZip from 'jszip'
import { z } from 'zod'
import { getUxConfig } from '@/server/ux-config' import { getUxConfig } from '@/server/ux-config'
import { db } from '../middlewares' import { db } from '../middlewares'
import { os } from '../server' import { os } from '../server'
@@ -20,6 +21,14 @@ interface ZipFileItem {
bytes: Uint8Array bytes: Uint8Array
} }
const summaryPayloadSchema = z
.object({
taskId: z.string().min(1, 'summary.json must contain a non-empty taskId'),
checkId: z.string().optional(),
inspectionId: z.string().optional(),
})
.passthrough()
const MAX_RAW_ZIP_BYTES = 50 * 1024 * 1024 const MAX_RAW_ZIP_BYTES = 50 * 1024 * 1024
const MAX_SINGLE_FILE_BYTES = 20 * 1024 * 1024 const MAX_SINGLE_FILE_BYTES = 20 * 1024 * 1024
const MAX_TOTAL_UNCOMPRESSED_BYTES = 60 * 1024 * 1024 const MAX_TOTAL_UNCOMPRESSED_BYTES = 60 * 1024 * 1024
@@ -165,7 +174,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy
const zipFiles = await listSafeZipFiles(rawZip) const zipFiles = await listSafeZipFiles(rawZip)
// Extract and parse summary.json from the ZIP // Extract and validate summary.json from the ZIP
const summaryFile = zipFiles.find((f) => f.name === 'summary.json') const summaryFile = zipFiles.find((f) => f.name === 'summary.json')
if (!summaryFile) { if (!summaryFile) {
throw new ORPCError('BAD_REQUEST', { throw new ORPCError('BAD_REQUEST', {
@@ -173,24 +182,25 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy
}) })
} }
let summaryPayload: Record<string, unknown> let rawJson: unknown
try { try {
summaryPayload = JSON.parse(Buffer.from(summaryFile.bytes).toString('utf-8')) rawJson = JSON.parse(Buffer.from(summaryFile.bytes).toString('utf-8'))
} catch { } catch {
throw new ORPCError('BAD_REQUEST', { throw new ORPCError('BAD_REQUEST', {
message: 'summary.json in the ZIP is not valid JSON', message: 'summary.json in the ZIP is not valid JSON',
}) })
} }
// Derive signingContext from summary.json fields const parsed = summaryPayloadSchema.safeParse(rawJson)
const taskId = String(summaryPayload.taskId ?? '') if (!parsed.success) {
const checkId = String(summaryPayload.checkId ?? summaryPayload.inspectionId ?? '')
if (!taskId) {
throw new ORPCError('BAD_REQUEST', { throw new ORPCError('BAD_REQUEST', {
message: 'summary.json must contain a taskId field', message: `Invalid summary.json: ${z.prettifyError(parsed.error)}`,
}) })
} }
const signingContext = `${taskId}${checkId}`
const summaryPayload = parsed.data
const checkId = summaryPayload.checkId ?? summaryPayload.inspectionId ?? ''
const signingContext = `${summaryPayload.taskId}${checkId}`
// Compute device signature // Compute device signature
const ikm = config.licence + config.fingerprint const ikm = config.licence + config.fingerprint