Skip to content

Conversation

@dsernst
Copy link
Member

@dsernst dsernst commented Dec 27, 2025

1. New expandable link ("Test for Malware") to initiate a 2nd-Device Malware Check

Submitted Screen:

Screenshot 2026-01-05 at 10 34 20 PM

Addresses #16

@vercel
Copy link

vercel bot commented Dec 27, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
siv Ready Ready Preview Jan 6, 2026 6:50am

@dsernst
Copy link
Member Author

dsernst commented Dec 27, 2025

Implement Malware Check Feature

This plan implements the malware detection system that enables voters to verify their encrypted vote was submitted correctly using a second device.

Overview

The system works by:

  1. Generating a QR code with vote data (verification #, plaintext selections, randomizers) encoded in the URL hash
  2. Scanning the QR code on a second device which recalculates the encrypted vote
  3. Comparing the recalculated vote with the stored vote on the server
  4. Alerting the voter if there's a mismatch (indicating potential malware)

Implementation Tasks

1. Update MalwareCheck Component

File: src/vote/submitted/MalwareCheck.tsx

  • Generate QR code URL: siv.org/malware-check/$election_id/$auth_token/#url_encoded_vote_data
  • Encode vote data in URL hash: {verification number, selections: [question_id]: { plaintext, randomizers } }
  • Pass election_id, auth, and state as props from SubmittedScreen
  • Use URL encoding for the hash portion (data only in URI hash, not sent to servers)

2. Create Malware Check Page

File: [pages/malware-check/[election_id]/[auth_token].tsx](pages/malware-check/[election_id]/[auth_token].tsx) (new file)

  • Extract vote data from URL hash on page load
  • Move private data into document memory and clear URL using window.history.replace()
  • Recalculate encrypted vote from plaintext, randomizers, and verification #
  • Send recalculated encrypted vote to server API endpoint
  • Display confirmation UI if match, alert if mismatch
  • Show vote selections table for voter confirmation
  • Handle "Yes" confirmation: save to database, update original vote page
  • Handle "No" confirmation: collect issue description, trigger remediation flow
  • Prompt for Anti-Malware Code (SMS verification) if available

3. Create API Endpoint

File: pages/api/malware-check.ts (new file)

  • Accept: election_id, auth_token, recalculated_encrypted_vote
  • Fetch stored vote from database using auth and election_id
  • Compare recalculated encrypted vote with stored encrypted vote
  • Store device info (timestamp, user agent) in malware-checks subcollection
  • Return match/mismatch result
  • Send Pushover notification to admin on mismatch
  • Store device type info for election-wide trend analysis

4. Update SubmittedScreen

File: src/vote/submitted/SubmittedScreen.tsx

  • Pass election_id, auth, and state props to MalwareCheck component
  • Display verification status from 2nd device (e.g., "Verified with 1 separate device - iPhone iOS 15.3, Safari 11.3")
  • Poll or listen for updates to malware check status

5. Helper Functions

Recalculate Encryption (in malware-check page):

  • Use stringToPoint(\${tracking}:${plaintext}`)` to encode
  • Use encrypt(public_key, randomizer, encoded_point) to encrypt
  • Convert result to CipherStrings format

Display Selections (in malware-check page):

  • Use generateColumnNames to get column names from ballot design
  • Use unTruncateSelection to display full option names
  • Show table similar to UnlockedVote component

Data Flow

flowchart TD
    A[Voter Submits Vote] --> B[MalwareCheck Component]
    B --> C[Generate QR Code with Vote Data]
    C --> D[Voter Scans QR on 2nd Device]
    D --> E[Malware Check Page Loads]
    E --> F[Extract Data from URL Hash]
    F --> G[Recalculate Encrypted Vote]
    G --> H[Send to API Endpoint]
    H --> I{Match?}
    I -->|Yes| J[Show Confirmation UI]
    I -->|No| K[Alert Voter + Notify Admin]
    J --> L[Voter Confirms Selections]
    L --> M[Save to Database]
    M --> N[Update Original Vote Page]
Loading

Database Schema

Collection: elections/{election_id}/malware-checks/{check_id}

  • auth: string
  • created_at: timestamp
  • device_info: { user_agent, timestamp }
  • match: boolean
  • recalculated_vote: encrypted vote object
  • confirmed: boolean (if voter confirmed)
  • issue_description: string (if voter said no)

Security Considerations

  • Vote data only in URL hash (not sent to server in request)
  • Clear URL hash immediately after extraction
  • Validate auth token and election_id

@dsernst
Copy link
Member Author

dsernst commented Dec 27, 2025

Notes while getting the URL data more compact:

After shorter keys:

/election/1766806654863/malware-check/7313490016
#%7B%22s%22%3A%7B%22vote%22%3A%7B%22p%22%3A%22Chocolate%22%2C%22r%22%3A%224706147882646245142293479430738401094611198236207016203689764984241613788275%22%7D%7D%2C%22v%22%3A%227510-3389-4744%22%7D

After base64 encoding:

#eyJzIjp7InZvdGUiOnsicCI6IkNob2NvbGF0ZSIsInIiOiI0NzA2MTQ3ODgyNjQ2MjQ1MTQyMjkzNDc5NDMwNzM4NDAxMDk0NjExMTk4MjM2MjA3MDE2MjAzNjg5NzY0OTg0MjQxNjEzNzg4Mjc1In19LCJ2IjoiNzUxMC0zMzg5LTQ3NDQifQ

After base64 encoding the randomizers directly:

#eyJzIjp7InZvdGUiOnsicCI6IkNob2NvbGF0ZSIsInIiOiJDbWVWMXJrN1FMRmhWRXh4ZW90VWNMOWFMX1ZCakV4SkRZcFNneEV0VkhNIn19LCJ2IjoiNzUxMDMzODk0NzQ0In0

{"s":{"vote":{"p":"Chocolate","r":"CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM"}},"v":"751033894744"}

751033894744,vote:"Chocolate",CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM

After delimiter based encoding, instead of json:

#NzUxMDMzODk0NzQ0fHZvdGV8Q2hvY29sYXRlfENtZVYxcms3UUxGaFZFeHhlb3RVY0w5YUxfVkJqRXhKRFlwU2d4RXRWSE0

751033894744|vote|Chocolate|CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM

After straight string instead of base64:

/election/1766806654863/malware-check/7313490016
#751033894744|vote|Chocolate|CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM

After removing 'election/' from path:

/malware-check/1766806654863/7313490016
#751033894744|vote|Chocolate|CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM

After base64 encoding VerifNum directly:

/malware-check/1766806654863/7313490016
#rt0bx1g|vote|Chocolate|CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM

After encoding selections from ballot_design:

/malware-check/1766806654863/7313490016
#rt0bx1g|vote|i0|CmeV1rk7QLFhVExxeotUcL9aL_VBjExJDYpSgxEtVHM

@dsernst dsernst marked this pull request as ready for review January 6, 2026 06:29
@dsernst
Copy link
Member Author

dsernst commented Jan 6, 2026

2nd-device check

Nice-to-haves, but shouldn't block shipping:

  • Show feedback to the user on the verification page when they confirmed w/ another device.

    Confirmed with:

    1. Safari 18.3 on iOS 18.3
    2. Firefox 123.4 on Windows 11.1
      ...
  • Some friction still around the QR codes not being re-usable (which is intentional to protect vote privacy when using borrowed devices). But smoother would be auto-generated a new QR whenever the first one is used, instead of having the voter hit an error page that tells them to go back and refresh the original page.

    • Added admin ping whenever they hit this, so we can see when it starts happening at all, instead of prematurely optimizing for it.
  • Add name (or email?) on the verification page, to help protect against auth recycling attacks

    • "You are verifying vote for ALICE SMITH (auth token 13245abcde)"
    • Added the auth token to the top of the page.
      • But they may not check them
      • Or 1st-device malware could even modify what it looks like in their original invitation email.
    • Need to protect Privacy before showing emails— need to make sure only the original voter can get to it, not anyone who knows their (now public) auth token can use that to find out other voters' email addresses.
    • Some elections have display names that we could use to, but not all.
    • Some elections have internal only "emails" just to represent QR-distributed voter tokens, wouldn't want to show those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants