/malware-check: Encode data as | delimited, instead of json

This commit is contained in:
David Ernst
2025-12-26 23:27:29 -08:00
parent 0f5f964eb5
commit 854399ffa7
2 changed files with 44 additions and 26 deletions

View File

@@ -17,8 +17,9 @@ type CheckResult = {
}
type VoteData = {
s?: Record<string, { p: string; r: string }> // short for s=selections, p=plaintext, r=randomizer
v?: string // short for v=verification_number
randomizers: Record<string, string> // questionId -> randomizer (base64url)
selections: Record<string, string> // questionId -> plaintext
verificationNumber: string
}
export default function MalwareCheckPage() {
@@ -43,7 +44,30 @@ export default function MalwareCheckPage() {
try {
const decoded = decodeBase64URL(hash)
const data: VoteData = JSON.parse(decoded)
// Parse compact format: tracking|questionId|plaintext|randomizer|questionId|plaintext|randomizer|...
const parts = decoded.split('|')
if (parts.length < 1 || (parts.length - 1) % 3 !== 0) {
throw new Error('Invalid data format')
}
const verificationNumber = parts[0]
const selections: Record<string, string> = {}
const randomizers: Record<string, string> = {}
// Process in groups of 3: questionId, plaintext, randomizer
for (let i = 1; i < parts.length; i += 3) {
const questionId = parts[i]
const plaintext = parts[i + 1]
const randomizer = parts[i + 2]
selections[questionId] = plaintext
randomizers[questionId] = randomizer
}
const data: VoteData = {
randomizers,
selections,
verificationNumber,
}
setVoteData(data)
// Move private data into document memory and clear URL
@@ -80,18 +104,16 @@ export default function MalwareCheckPage() {
const public_key = RP.fromHex(electionInfo.threshold_public_key)
const recalculated: Record<string, CipherStrings> = {}
// Un-shorten the vote data
const verificationNumber = voteData.v
const selections = voteData.s
const { randomizers, selections, verificationNumber } = voteData
if (!verificationNumber || !selections)
if (!verificationNumber || !selections || !randomizers)
return setCheckResult({ error: 'Invalid vote data format', match: false })
// Recalculate encryption for each selection
for (const [questionId, selection] of Object.entries(selections)) {
const plaintext = selection.p
for (const questionId of Object.keys(selections)) {
const plaintext = selections[questionId]
// Convert base64url encoded randomizer back to bigint
const randomizerBigInt = base64URLToBigint(selection.r)
const randomizerBigInt = base64URLToBigint(randomizers[questionId])
// Encode: stringToPoint(`${tracking}:${plaintext}`)
const encoded = stringToPoint(`${verificationNumber}:${plaintext}`)
@@ -229,10 +251,9 @@ export default function MalwareCheckPage() {
</thead>
<tbody>
{columns.map((columnId) => {
const selection = voteData.s?.[columnId]
if (!selection) return null
const plaintext = voteData.selections?.[columnId]
if (!plaintext) return null
const plaintext = selection.p
const displayValue = unTruncateSelection(plaintext, electionInfo.ballot_design || [], columnId)
// Find question title from ballot design

View File

@@ -57,21 +57,18 @@ export const MalwareCheck = ({
const qrUrl = useMemo(() => {
if (!state.tracking || !state.plaintext || !state.randomizer) return ''
// Build vote data object with shortened keys for compact encoding
// v = verification_number, s = selections, p = plaintext, r = randomizer (base64url encoded)
const voteData: Record<string, unknown> = {
s: Object.keys(state.plaintext).reduce((acc, questionId) => {
acc[questionId] = {
p: state.plaintext[questionId],
r: bigintToBase64URL(BigInt(state.randomizer[questionId])),
}
return acc
}, {} as Record<string, { p: string; r: string }>),
v: state.tracking.replace(/-/g, ''),
// Build compact format: tracking|questionId|plaintext|randomizer|questionId|plaintext|randomizer|...
// Using | as delimiter since it's unlikely to appear in plaintext or question IDs
const parts: string[] = [state.tracking.replace(/-/g, '')]
for (const questionId of Object.keys(state.plaintext)) {
parts.push(questionId)
parts.push(state.plaintext[questionId])
parts.push(bigintToBase64URL(BigInt(state.randomizer[questionId])))
}
// Encode vote data as JSON and base64url encode for hash
const encodedData = encodeBase64URL(JSON.stringify(voteData))
// Encode compact format as base64url
const encodedData = encodeBase64URL(parts.join('|'))
// Build URL: siv.org/malware-check/$election_id/$auth_token/#url_encoded_vote_data
const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'https:' : 'http:'