From 83f1e21805fce317574f2b0d61abd4ee66506ec0 Mon Sep 17 00:00:00 2001
From: David Ernst setIsOpen(!isOpen)}
+ >
+ [+] Test for Malware
+
+ {isOpen && (
+ Scan to Begin
+
Loading...
+{checkResult.error}
+Verifying your vote...
+Your response has been recorded. You can now close this window.
++ The vote recalculated on this device does not match the vote stored on the server. This may indicate malware + on your original voting device. +
++ Please contact the election administrator immediately. +
+The SIV admin has been notified about this issue.
+| Question | +Selection | +
|---|---|
| {question?.title || columnId}: | +{plaintext === 'BLANK' ? '' : displayValue} | +
Do these selections match what you intended to vote for?
+Device: {checkResult.device_info}
} + ++ Note: You may be prompted to confirm via SMS to ensure that a live person is performing this + 2nd device check, rather than malware from the original device. If you were given an Anti-Malware Code, please + have it ready. +
++ As an additional layer of security, you can use multiple devices to verify your vote was submitted as + intended. +
+ {checkStatus && checkStatus.verified_count > 0 && ( ++ Verified with {checkStatus.verified_count} separate device{checkStatus.verified_count > 1 ? 's' : ''} + {checkStatus.checks + .filter((c) => c.confirmed === true) + .slice(0, 1) + .map((c) => ` - ${c.device_info}`) + .join(', ')} +
+ )}+ + {url} + +
+
-
+
+ Or share this link:
+
{url}
⚠️ This QR contains your private vote selections.
Verified with {checkStatus.verified_count} separate device{checkStatus.verified_count > 1 ? 's' : ''}
diff --git a/src/vote/submitted/MalwareCheckQRCode.tsx b/src/vote/submitted/MalwareCheckQRCode.tsx
index c01663e6..8d699fae 100644
--- a/src/vote/submitted/MalwareCheckQRCode.tsx
+++ b/src/vote/submitted/MalwareCheckQRCode.tsx
@@ -105,6 +105,9 @@ export const MalwareCheckQRCode = ({ className, url }: { className?: string; url
⚠️ Contains your private vote selections Loading... Loading {!voteData ? 'vote data' : !ballot_design ? 'ballot design' : 'public key'}... Loading {!voteData ? 'vote data' : !ballot_design ? 'ballot design' : 'public key'}... {checkResult.error} Verifying your vote... Your response has been recorded. You can now close this window.
- The vote recalculated on this device does not match the vote stored on the server. This may indicate malware
- on your original voting device.
-
- Please contact the election administrator immediately.
- The SIV admin has been notified about this issue. Do these selections match what you intended to vote for? Device: {checkResult.device_info}
- Note: You may be prompted to confirm via SMS to ensure that a live person is performing this
- 2nd device check, rather than malware from the original device. If you were given an Anti-Malware Code, please
- have it ready.
- Loading {!voteData ? 'vote data' : !ballot_design ? 'ballot design' : 'public key'}... {checkResult.error} Verifying your vote... Your response has been recorded. You can now close this window.
+ The vote recalculated on this device does not match the vote stored on the server. This may indicate malware
+ on your original voting device.
+
+ Please contact the election administrator immediately.
+ The SIV admin has been notified about this issue. Do these selections match what you intended to vote for? Device: {checkResult.device_info}
+ Note: You may be prompted to confirm via SMS to ensure that a live person is performing this
+ 2nd device check, rather than malware from the original device. If you were given an Anti-Malware Code, please
+ have it ready.
+
- The vote recalculated on this device does not match the vote stored on the server. This may indicate malware
- on your original voting device.
+ The vote recalculated on this device does not match the original vote submitted to the server. This may
+ indicate malware on your original voting device.
- Please contact the election administrator immediately.
+ Please contact your election administrator.
The SIV admin has been notified about this issue. The SIV admin has also been notified about this issue. Decrypting vote... Device: {checkResult.device_info}
Note: You may be prompted to confirm via SMS to ensure that a live person is performing this
2nd device check, rather than malware from the original device. If you were given an Anti-Malware Code, please
diff --git a/src/protocol/VoteContext.tsx b/src/protocol/VoteContext.tsx
index d611478a..73e6e45d 100644
--- a/src/protocol/VoteContext.tsx
+++ b/src/protocol/VoteContext.tsx
@@ -3,7 +3,7 @@ import { createContext, useContext, useMemo, useReducer } from 'react'
import { random_bigint, RP, stringToPoint } from 'src/crypto/curve'
import { generateTrackingNum } from 'src/vote/tracking-num'
-import encrypt from '../crypto/encrypt'
+import { encrypt } from '../crypto/encrypt'
import { public_key, voters } from './election-parameters'
const rand = () => RP.BASE.multiplyUnsafe(random_bigint()).toHex()
diff --git a/src/vote/submitted/MalwareCheck.tsx b/src/vote/submitted/MalwareCheck.tsx
index ce74d40b..c918015c 100644
--- a/src/vote/submitted/MalwareCheck.tsx
+++ b/src/vote/submitted/MalwareCheck.tsx
@@ -3,7 +3,6 @@ import { useMemo, useState } from 'react'
import { State } from '../vote-state'
import { bigintToBase64URL } from './base64url'
-import { encodePlaintext } from './encodePlaintext'
import { MalwareCheckQRCode } from './MalwareCheckQRCode'
type CheckStatus = {
@@ -56,27 +55,22 @@ export const MalwareCheck = ({
// }, [isOpen, election_id, auth])
const qrUrl = useMemo(() => {
- if (!state.tracking || !state.plaintext || !state.randomizer || !state.ballot_design) return ''
+ if (!state.randomizer) return ''
- // Build compact format: tracking|questionId|plaintext|randomizer|questionId|plaintext|randomizer|...
- // Using | as delimiter since it's unlikely to appear in plaintext or question IDs
+ // Build compact format: questionId|randomizer|questionId|randomizer|...
+ // Using | as delimiter since it's unlikely to appear in question IDs
// All characters are URL-safe, so no need for base64url encoding
- // Verification number encoded as base64url for compactness
- const trackingNum = BigInt(state.tracking.replace(/-/g, ''))
- const parts: string[] = [bigintToBase64URL(trackingNum)]
+ const parts: string[] = []
- for (const questionId of Object.keys(state.plaintext)) {
+ for (const questionId of Object.keys(state.randomizer)) {
parts.push(questionId)
- // Encode plaintext compactly (index for options, w: prefix for write-ins, b for BLANK)
- const encodedPlaintext = encodePlaintext(state.plaintext[questionId], questionId, state.ballot_design)
- parts.push(encodedPlaintext)
parts.push(bigintToBase64URL(BigInt(state.randomizer[questionId])))
}
// Use compact format directly (no base64url encoding needed - all chars are URL-safe)
const encodedData = parts.join('|')
- // Build URL: siv.org/malware-check/$election_id/$auth_token/#url_encoded_vote_data
+ // Build URL: siv.org/malware-check/$election_id/$auth_token/#randomizers
const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'https:' : 'http:'
const host = typeof window !== 'undefined' ? window.location.host.replace('localhost', LOCALHOST_IP) : 'siv.org'
return `${protocol}//${host}/malware-check/${election_id}/${auth}#${encodedData}`
diff --git a/src/vote/submitted/encodePlaintext.ts b/src/vote/submitted/encodePlaintext.ts
deleted file mode 100644
index a8bacb59..00000000
--- a/src/vote/submitted/encodePlaintext.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { multi_vote_regex } from '../../status/tally-votes'
-import { Item } from '../storeElectionInfo'
-
-/** Decode encoded plaintext back to original selection */
-export function decodePlaintext(encoded: string, questionId: string, ballotDesign: Item[]): string {
- if (encoded === 'b') return 'BLANK'
-
- // If it's a write-in, remove "w:" prefix
- if (encoded.startsWith('w:')) return encoded.slice(2)
-
- if (!ballotDesign) return 'MissingBallotDesign'
-
- // If it's an indexed option, consult ballot design
- if (encoded.startsWith('i')) {
- const index = Number.parseInt(encoded.slice(1), 10)
- if (isNaN(index)) return encoded // Invalid format, return as-is
-
- // Find the question
- const multiSuffix = questionId.match(multi_vote_regex)
- let baseQuestionId =
- multiSuffix && !ballotDesign.find((item) => item.id === questionId)
- ? questionId.slice(0, -multiSuffix[0].length)
- : questionId
-
- // For score/budget, the column format is "questionId_optionName", extract base questionId
- let question = ballotDesign.find((item) => item.id === baseQuestionId)
- if (!question) {
- // Try to find by matching prefix (for score/budget voting: "vote_Chocolate" -> "vote")
- question = ballotDesign.find((item) => {
- if (!item.id) return false
- return baseQuestionId.startsWith(item.id + '_')
- })
- if (question) baseQuestionId = question.id || baseQuestionId
- }
-
- if (!question || index < 0 || index >= question.options.length) return encoded
-
- const option = question.options[index]
-
- // Return the value if available, otherwise the name
- return option.value || option.name
- }
-
- // Otherwise, it's a score or special value, keep as-is
- return encoded
-}
-
-/**
- * Encode plaintext selection compactly:
- * - If matches an option: encode as "i{index}" (0-based)
- * - If "BLANK": encode as "b"
- * - Otherwise: encode as "w:{plaintext}" for write-ins, or keep as-is for scores/special values
- */
-export function encodePlaintext(plaintext: string, questionId: string, ballotDesign: Item[]): string {
- if (plaintext === 'BLANK') return 'b'
-
- if (!ballotDesign) return `w:${plaintext}`
-
- // Find the question in ballot design
- // Handle multi-vote format (e.g., "president_1" -> "president")
- // Handle score/budget format (e.g., "vote_Chocolate" -> "vote")
- const multiSuffix = questionId.match(multi_vote_regex)
- let baseQuestionId =
- multiSuffix && !ballotDesign.find((item) => item.id === questionId)
- ? questionId.slice(0, -multiSuffix[0].length)
- : questionId
-
- // For score/budget, the column format is "questionId_optionName", extract base questionId
- let question = ballotDesign.find((item) => item.id === baseQuestionId)
- if (!question && (baseQuestionId.includes('_') || baseQuestionId.includes('-'))) {
- // Try to find by matching prefix (for score/budget voting)
- question = ballotDesign.find((item) => {
- if (!item.id) return false
- return baseQuestionId.startsWith(item.id + '_') || baseQuestionId.startsWith(item.id + '-')
- })
- if (question) baseQuestionId = question.id || baseQuestionId
- }
-
- if (!question) return `w:${plaintext}`
-
- // For score/budget voting, numeric strings are valid and should be kept as-is
- if ((question.type === 'score' || question.type === 'budget') && /^\d+$/.test(plaintext)) return plaintext
-
- // Check if plaintext matches any option (check both value and name)
- // Use original order (not shuffled)
- const optionIndex = question.options.findIndex((option) => {
- // Check if plaintext matches the option value or name
- return (option.value || option.name) === plaintext
- })
- if (optionIndex >= 0) return `i${optionIndex}`
-
- // Otherwise, treat as write-in
- return `w:${plaintext}`
-}
diff --git a/src/vote/vote-state.ts b/src/vote/vote-state.ts
index 4a9b27df..fa8b472f 100644
--- a/src/vote/vote-state.ts
+++ b/src/vote/vote-state.ts
@@ -2,7 +2,7 @@ import { mapValues, merge } from 'lodash-es'
import { random_bigint, RP, stringToPoint } from 'src/crypto/curve'
import { CipherStrings } from 'src/crypto/stringify-shuffle'
-import encrypt from '../crypto/encrypt'
+import { encrypt } from '../crypto/encrypt'
import { Item } from './storeElectionInfo'
import { generateTrackingNum } from './tracking-num'
import { useLocalStorageReducer } from './useLocalStorage'
From 475a2896c91b8d55ecd485adebebb5fbf3992a9b Mon Sep 17 00:00:00 2001
From: David Ernst
- The vote recalculated on this device does not match the original vote submitted to the server. This may
- indicate malware on your original voting device.
+
+ This device is unable to decrypt your vote.
- Please contact your election administrator.
- The SIV admin has also been notified about this issue. Please contact your election administrator. The SIV admin has also been notified about this issue. Do these selections match what you intended to vote for?
+ {Object.keys(columns).length === 1 ? 'Does this selection' : 'Do these selections'} match how you intended
+ to vote?
+
- Note: You may be prompted to confirm via SMS to ensure that a live person is performing this
- 2nd device check, rather than malware from the original device. If you were given an Anti-Malware Code, please
- have it ready.
- ⚠️ QR code not loaded...
- As an additional layer of security, you can use multiple devices to verify your vote was submitted as
- intended.
- ⚠️ This QR contains your private vote selections.
- Verified with {checkStatus.verified_count} separate device{checkStatus.verified_count > 1 ? 's' : ''}
- {checkStatus.checks
- .filter((c) => c.confirmed === true)
- .slice(0, 1)
- .map((c) => ` - ${c.device_info}`)
- .join(', ')}
+
+ As an additional layer of security, you can use multiple devices to verify your vote was submitted
+ as intended.
⚠️ This QR contains your private vote selections.
+ Verified with {checkStatus.verified_count} separate device{checkStatus.verified_count > 1 ? 's' : ''}
+ {checkStatus.checks
+ .filter((c) => c.confirmed === true)
+ .slice(0, 1)
+ .map((c) => ` - ${c.device_info}`)
+ .join(', ')}
+
- Or share this link:
+ Or open this link on your 2nd device:
{url}
From 2c6aad2156fd560ab58537552c0d79468f540a89 Mon Sep 17 00:00:00 2001
From: David Ernst ⚠️ {qrError} ⚠️ QR code not loaded... Decrypting vote... This 2nd Device Malware Check was already used. Restart from the original voting device. Why? Each malware-check can only be used once.
+ This protects against someone viewing the browser history to learn private vote data, when a borrowed device
+ is used.
+ {checkResult.error} {checkResult.error} This 2nd Device Malware Check was already used. Restart from the original voting device. Refresh the original device's Submission screen to generate a new QR code.
+ setIsOpen(!isOpen)}>
+ {isOpen ? '[-]' : '[+]'} Verify your vote wasn't changed by malware
+
+ ⚠️ {qrError}
- setIsOpen(!isOpen)}>
- {isOpen ? '[-]' : '[+]'} Verify your vote wasn't changed by malware
+
+ setIsOpen(!isOpen)}>
+ {isOpen ? '[-]' : '[+]'} Verify your vote {"wasn't"} changed by malware
You're using the same device & browser used to submit your vote.
+ This device was already used for a previous malware check.
+ ID: {alreadySeenDevice}
+ For better cross-device security verification, consider using a different device.
+ beta feature
As an additional layer of security, you can use multiple devices to verify your vote was submitted
as intended.
+
beta feature
As an additional layer of security, you can use multiple devices to verify your vote was submitted
as intended.
⚠️ This QR contains your private vote selections.
+ How: Before each check, refresh the page to
+ generate a new QR code.
+
+ What happens: Scanning or copy/pasting the link
+ rebuilds your encrypted vote on a different device and asks the server if it matches the original
+ submission.
+
+ ⚠️ This QR contains your private vote selections.{' '}
+
+ Learn more →
+
+
Verified with {checkStatus.verified_count} separate device{checkStatus.verified_count > 1 ? 's' : ''}
From 2dee0486785bcd267d9794141e72b939b95dd28e Mon Sep 17 00:00:00 2001
From: "Ariana I."
- beta feature
As an additional layer of security, you can use multiple devices to verify your vote was submitted
as intended.
- How: Before each check, refresh the page to
- generate a new QR code.
-
What happens: Scanning or copy/pasting the link
rebuilds your encrypted vote on a different device and asks the server if it matches the original
From b51fe069c08ebb75b69f810b717fc63fed4d207e Mon Sep 17 00:00:00 2001
From: David Ernst
+ {electionInfoError instanceof Error ? electionInfoError.message : String(electionInfoError)}
+ Election ID: {election_id}
- What happens: Scanning or copy/pasting the link
- rebuilds your encrypted vote on a different device and asks the server if it matches the original
- submission.
-
- ⚠️ This QR contains your private vote selections.{' '}
-
- Learn more →
-
+
+ What happens: This rebuilds your encrypted vote
+ submission, then lets you confirm the selections are as you intended.
⚠️ This QR contains your private vote selections.
Verified with {checkStatus.verified_count} separate device{checkStatus.verified_count > 1 ? 's' : ''}
From a33b5a9c1246510a09149654497ce74cd07c949b Mon Sep 17 00:00:00 2001
From: David Ernst For better cross-device security verification, consider using a different device. For cross-device security verification, consider using a separate device. This 2nd Device Malware Check was already used. Restart from the original voting device. Refresh the original device's Submission screen to generate a new QR code.
+ Refresh the original device's "Vote Submitted" screen to generate a new QR code.
+ Your response has been recorded. You can now close this window. You can now close this window. {checkResult.error} {error} Loading {!voteData ? 'vote data' : !ballot_design ? 'ballot design' : 'public key'}... Decrypting vote... Verifying your vote...
- This device is unable to decrypt your vote. Please contact your election administrator. The SIV admin has also been notified about this issue. You're using the same device & browser used to submit your vote.
- This device was already used for a previous malware check.
- ID: {alreadySeenDevice}
- For cross-device security verification, consider using a separate device. You're using the same device & browser used to submit your vote.
+ This device was already used for a previous malware check.
+ ID: {alreadySeenDevice}
+ For cross-device security verification, consider using a separate device.
- {Object.keys(columns).length === 1 ? 'Does this selection' : 'Do these selections'} match how you intended
- to vote?
-
+ {Object.keys(columns).length === 1 ? 'Does this selection' : 'Do these selections'} match how you intended
+ to vote?
+ ⚠️ {qrError} ⚠️ QR code not loaded...
From 009a2442af3b1e04a154dbc6908c2b22022f00a4 Mon Sep 17 00:00:00 2001
From: David Ernst
- What happens: This rebuilds your encrypted vote
+ How it works: This rebuilds your encrypted vote
submission, then lets you confirm the selections are as you intended.
Scan to Begin
+ {/* Warning */}
+ Error
- Thank you
- ⚠️ Vote Mismatch Detected
- Confirm your selections:
-
-
-
-
-
- {showIssueForm ? (
-
-
-
-
- {columns.map((columnId) => {
- const encodedPlaintext = voteData.selections?.[columnId]
- if (!encodedPlaintext) return null
-
- // Decode plaintext from compact format
- const plaintext = decodePlaintext(encodedPlaintext, columnId, ballot_design || [])
- const displayValue = unTruncateSelection(plaintext, ballot_design || [], columnId)
-
- // Find question title from ballot design
- const question = ballot_design.find((item) => {
- if (item.id === columnId) return true
- // Handle multi-vote columns like "president_1", "president_2"
- const baseId = columnId.replace(/_\d+$/, '')
- return item.id === baseId
- })
-
- return (
- Question
- Selection
-
-
- )
- })}
-
- {question?.title || columnId}:
- {plaintext === 'BLANK' ? '' : displayValue}
- Error
+ Thank you
+ ⚠️ Vote Mismatch Detected
+ Confirm your selections:
+
+
+
+
+
+ {showIssueForm ? (
+
+
+
+
+ {columns.map((columnId) => {
+ const encodedPlaintext = voteData.selections?.[columnId]
+ if (!encodedPlaintext) return null
+
+ // Decode plaintext from compact format
+ const plaintext = decodePlaintext(encodedPlaintext, columnId, ballot_design || [])
+ const displayValue = unTruncateSelection(plaintext, ballot_design || [], columnId)
+
+ // Find question title from ballot design
+ const question = ballot_design.find((item) => {
+ if (item.id === columnId) return true
+ // Handle multi-vote columns like "president_1", "president_2"
+ const baseId = columnId.replace(/_\d+$/, '')
+ return item.id === baseId
+ })
+
+ return (
+ Question
+ Selection
+
+
+ )
+ })}
+
+ {question?.title || columnId}:
+ {plaintext === 'BLANK' ? '' : displayValue}
+ ⚠️ Vote Mismatch Detected
⚠️ Vote Mismatch Detected
@@ -244,11 +243,9 @@ function PageContent() {
{columns.map((columnId) => {
- const encodedPlaintext = voteData.selections?.[columnId]
- if (!encodedPlaintext) return null
+ const plaintext = voteData.decryptedPlaintext?.[columnId]
+ if (!plaintext) return null
- // Decode plaintext from compact format
- const plaintext = decodePlaintext(encodedPlaintext, columnId, ballot_design || [])
const displayValue = unTruncateSelection(plaintext, ballot_design || [], columnId)
// Find question title from ballot design
@@ -305,8 +302,6 @@ function PageContent() {
)}
- {checkResult.device_info && ⚠️ Vote Mismatch Detected
-
+ This may indicate malware on your original voting device.
-
)
@@ -286,29 +284,26 @@ function PageContent() {
) : (
{question?.title || columnId}:
+ {question?.title || columnId}
{plaintext === 'BLANK' ? '' : displayValue}
Scan to Begin
- {qrUrl ? Scan to Begin
+
+
+
+ Scan to Begin
@@ -113,7 +110,7 @@ export const MalwareCheckQRCode = ({ className, url }: { className?: string; url
{/* Text link */}
Scan to Begin
-
+
Sorry
+ Error
+ Error
- setIsOpen(!isOpen)}
- >
- [+] Test for Malware
-
+ ⚠️ Same Device Warning
+
+ Confirm your selections:
From 3612a1460963f17d2eda63363b44dbbdd11276e7 Mon Sep 17 00:00:00 2001
From: "Ariana I."
Error Loading Election
+ Confirm your selections:
+
+ Confirm your selections
+
+ FOR AUTH TOKEN: {auth_token}
+
From d6cbef2cf352a0050788e5f6a2256ed5c25b1843 Mon Sep 17 00:00:00 2001
From: David Ernst
- {showIssueForm ? (
- Confirm your selections
From 02bfec48044508e9f17c21c44ceba3e82b9161d9 Mon Sep 17 00:00:00 2001
From: David Ernst Multi-Device Malware Check
Confirm your selections
From febff43d02be29ebac3b2b27c9662329123b0dac Mon Sep 17 00:00:00 2001
From: David Ernst
Thank you
- Sorry
@@ -205,15 +196,14 @@ function PageContent() {
Error
- ⚠️ Vote Mismatch Detected
-
- This may indicate malware on your original voting device.
- Multi-Device Malware Check
Confirm your selections
diff --git a/src/malware-check/DeviceWarning.tsx b/src/malware-check/SameDeviceWarning.tsx
similarity index 89%
rename from src/malware-check/DeviceWarning.tsx
rename to src/malware-check/SameDeviceWarning.tsx
index 0fb3e464..24bd100a 100644
--- a/src/malware-check/DeviceWarning.tsx
+++ b/src/malware-check/SameDeviceWarning.tsx
@@ -1,33 +1,5 @@
import { useState } from 'react'
-export function DeviceWarning({ alreadySeenDevice }: { alreadySeenDevice: null | string }) {
- const [dismissed, setDismissed] = useState(false)
-
- if (!alreadySeenDevice || dismissed) return null
-
- return (
- ⚠️ Same Device Warning
-
- ⚠️ Same Device Warning
+
+ Scan to Begin
-
+