refactor(crypto): use Zod safeParse for summary.json validation instead of manual checks
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user