/malware-check: Decrypt vote on 2nd-device w/ randomizers

This commit is contained in:
David Ernst
2025-12-27 03:10:02 -08:00
parent 1711c7b0a5
commit d2c2b84908
12 changed files with 145 additions and 268 deletions

View File

@@ -1,7 +1,7 @@
import { mapValues } from 'lodash-es'
import { NextApiRequest, NextApiResponse } from 'next'
import { pointToString, RP } from 'src/crypto/curve'
import decrypt from 'src/crypto/decrypt'
import { decrypt } from 'src/crypto/decrypt'
import { CipherStrings } from 'src/crypto/stringify-shuffle'
export default async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -0,0 +1,21 @@
import { firebase } from 'api/_services'
import { NextApiRequest, NextApiResponse } from 'next'
import { CipherStrings } from 'src/crypto/stringify-shuffle'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { auth_token, election_id } = req.query
if (typeof election_id !== 'string') return res.status(400).json({ error: 'Missing election_id' })
if (typeof auth_token !== 'string') return res.status(400).json({ error: 'Missing auth_token' })
const electionDoc = firebase.firestore().collection('elections').doc(election_id)
// Fetch stored vote from database
const votes = await electionDoc.collection('votes').where('auth', '==', auth_token).get()
if (votes.empty) return res.status(404).json({ error: 'Vote not found' })
const storedVote = votes.docs[0].data()
const storedEncryptedVote = storedVote.encrypted_vote as Record<string, CipherStrings>
return res.status(200).json({ encrypted_vote: storedEncryptedVote })
}

View File

@@ -1,14 +1,14 @@
import { firestore } from 'firebase-admin'
import { NextApiRequest, NextApiResponse } from 'next'
import { CipherStrings } from 'src/crypto/stringify-shuffle'
// import { firebase, pushover } from './_services'
import { firebase } from './_services'
import { generateEmailLoginCode } from './admin-login'
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })
const { auth_token, confirmed, election_id, issue_description, recalculated_encrypted_vote } = req.body
const { auth_token, confirmed, decrypted, election_id, issue_description } = req.body
if (typeof election_id !== 'string') return res.status(400).json({ error: 'Missing election_id' })
if (typeof auth_token !== 'string') return res.status(400).json({ error: 'Missing auth_token' })
@@ -19,51 +19,41 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
if (confirmed !== undefined) {
const checkDoc = electionDoc.collection('malware-checks').doc(auth_token)
const check = await checkDoc.get()
if (!check.exists) return res.status(404).json({ error: 'No malware check found' })
await checkDoc.update({
confirmed,
confirmed_at: firestore.FieldValue.serverTimestamp(),
issue_description: issue_description || null,
confirmations: firestore.FieldValue.arrayUnion({
confirmed,
confirmed_at: new Date(),
issue_description: issue_description || null,
otp: req.body.otp || null,
}),
})
return res.status(200).json({ success: true })
}
// Otherwise, this is the initial check request
if (!recalculated_encrypted_vote) return res.status(400).json({ error: 'Missing recalculated_encrypted_vote' })
// Fetch stored vote from database
const votes = await electionDoc.collection('votes').where('auth', '==', auth_token).get()
if (votes.empty) return res.status(404).json({ error: 'Vote not found', match: false })
const storedVote = votes.docs[0].data()
const storedEncryptedVote = storedVote.encrypted_vote as Record<string, CipherStrings>
// Compare recalculated encrypted vote with stored encrypted vote
const match = compareEncryptedVotes(storedEncryptedVote, recalculated_encrypted_vote)
// Get device info
const userAgent = req.headers['user-agent'] || 'Unknown'
const deviceInfo = parseUserAgent(userAgent)
// Generate a 6-digit OTP to match the device against later.
const otp = generateEmailLoginCode()
// Store device info in malware-checks subcollection
const checkData = {
auth: auth_token,
confirmed: null as boolean | null,
created_at: firestore.FieldValue.serverTimestamp(),
device_info: {
device_type: deviceInfo,
timestamp: firestore.FieldValue.serverTimestamp(),
user_agent: userAgent,
},
issue_description: null as null | string,
match,
recalculated_vote: recalculated_encrypted_vote,
}
await electionDoc.collection('malware-checks').doc(auth_token).set(checkData)
await electionDoc
.collection('malware-checks')
.doc(auth_token)
.set(
{
auth: auth_token,
decrypted: firestore.FieldValue.arrayUnion({
created_at: new Date(),
decrypted,
device_info: req.headers['user-agent'] || null,
otp,
}),
},
{ merge: true },
)
// Send Pushover notification to admin on mismatch
// if (!match)
@@ -72,53 +62,5 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
// `election: ${election_id}\nauth: ${auth_token}\ndevice: ${deviceInfo}\nVoter's recalculated vote does not match the stored vote.`,
// )
return res.status(200).json({
device_info: deviceInfo,
match,
})
}
function compareEncryptedVotes(
stored: Record<string, CipherStrings>,
recalculated: Record<string, CipherStrings>,
): boolean {
// Check if all keys match
const storedKeys = Object.keys(stored).sort()
const recalculatedKeys = Object.keys(recalculated).sort()
if (storedKeys.length !== recalculatedKeys.length) return false
// Compare each cipher
for (const key of storedKeys) {
if (!recalculated[key]) return false
const storedCipher = stored[key]
const recalculatedCipher = recalculated[key]
// Compare encrypted and lock values
if (storedCipher.encrypted !== recalculatedCipher.encrypted) return false
if (storedCipher.lock !== recalculatedCipher.lock) return false
}
return true
}
function parseUserAgent(userAgent: string): string {
// Simple user agent parsing for device type
const ua = userAgent.toLowerCase()
if (ua.includes('iphone')) return 'iPhone'
if (ua.includes('ipad')) return 'iPad'
if (ua.includes('android')) return 'Android'
if (ua.includes('windows')) return 'Windows'
if (ua.includes('mac')) return 'Mac'
if (ua.includes('linux')) return 'Linux'
// Extract browser info
if (ua.includes('safari') && !ua.includes('chrome')) return 'Safari'
if (ua.includes('chrome')) return 'Chrome'
if (ua.includes('firefox')) return 'Firefox'
if (ua.includes('edge')) return 'Edge'
return 'Unknown Device'
return res.status(200).json({ otp })
}

View File

@@ -1,7 +1,7 @@
import { RP } from './curve'
import { Cipher } from './shuffle'
import { Cipher, Public_Key } from './shuffle'
export default function decrypt(secret_key: bigint, cipher: Cipher): RP {
export function decrypt(secret_key: bigint, cipher: Cipher): RP {
const { encrypted, lock } = cipher
const shared_secret = lock.multiply(secret_key)
@@ -9,3 +9,22 @@ export default function decrypt(secret_key: bigint, cipher: Cipher): RP {
return message
}
/**
* Decrypt a cipher using the randomizer and public key.
* This allows decryption without the secret key, useful for malware checks.
*
* Encryption: encrypted = message + (public_key * randomizer)
* Decryption: message = encrypted - (public_key * randomizer)
*/
export function decryptWithRandomizer(public_key: Public_Key, randomizer: bigint, cipher: Cipher): RP {
const { encrypted } = cipher
// Calculate shared_secret = public_key * randomizer (same as in encryption)
const shared_secret = public_key.multiply(randomizer)
// Decrypt: message = encrypted - shared_secret
const message = encrypted.subtract(shared_secret)
return message
}

View File

@@ -1,7 +1,7 @@
import { G, RP } from './curve'
import { Cipher, Public_Key } from './shuffle'
export default function encrypt(public_key: Public_Key, randomizer: bigint, message: RP): Cipher {
export function encrypt(public_key: Public_Key, randomizer: bigint, message: RP): Cipher {
// Calculate our encrypted message
const shared_secret = public_key.multiply(randomizer)
const encrypted = message.add(shared_secret)

View File

@@ -3,8 +3,8 @@ import { expect, test } from 'bun:test'
import { isEqual, range } from 'lodash'
import { pointToString, random_bigint, stringToPoint } from '../curve'
import decrypt from '../decrypt'
import encrypt from '../encrypt'
import { decrypt } from '../decrypt'
import { encrypt } from '../encrypt'
import { generate_key_pair } from '../generate-key-pair'
import { rename_to_c1_and_2, shuffleWithProof } from '../shuffle'
import { verify_shuffle_proof } from '../shuffle-proof'

View File

@@ -2,8 +2,8 @@ import { expect, test } from 'bun:test'
import { mapValues, noop } from 'lodash'
import { CURVE, G, random_bigint, RP, stringToPoint } from '../curve'
import decrypt from '../decrypt'
import encrypt from '../encrypt'
import { decrypt } from '../decrypt'
import { encrypt } from '../encrypt'
import {
combine_partials,
compute_keyshare,

View File

@@ -1,8 +1,8 @@
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { api } from 'src/api-helper'
import { RP, stringToPoint } from 'src/crypto/curve'
import encrypt from 'src/crypto/encrypt'
import { pointToString, RP } from 'src/crypto/curve'
import { decryptWithRandomizer } from 'src/crypto/decrypt'
import { CipherStrings } from 'src/crypto/stringify-shuffle'
import { Head } from 'src/Head'
import { unTruncateSelection } from 'src/status/un-truncate-selection'
@@ -10,18 +10,15 @@ import { useElectionInfo } from 'src/status/use-election-info'
import { TailwindPreflight } from 'src/TailwindPreflight'
import { generateColumnNames } from 'src/vote/generateColumnNames'
import { base64URLToBigint } from 'src/vote/submitted/base64url'
import { decodePlaintext } from 'src/vote/submitted/encodePlaintext'
type CheckResult = {
device_info?: string
error?: string
match: boolean
otp?: string
}
type VoteData = {
decryptedPlaintext: Record<string, string> // questionId: plaintext
randomizers: Record<string, string> // questionId -> randomizer (base64url)
selections: Record<string, string> // questionId -> plaintext
verificationNumber: string
}
export function MalwareCheckPage() {
@@ -44,108 +41,102 @@ function PageContent() {
const [showIssueForm, setShowIssueForm] = useState(false)
const { ballot_design, threshold_public_key } = useElectionInfo()
// Extract vote data from URL hash on page load
// Extract randomizers from URL hash on page load
useEffect(() => {
if (typeof window === 'undefined' || !election_id || !auth_token) return
const hash = window.location.hash.slice(1) // Remove #
if (!hash) return setCheckResult({ error: 'Missing vote data in URL', match: false })
if (!hash) return setCheckResult({ error: 'Missing vote data in URL' })
try {
// Parse compact format directly (no base64url decoding needed): tracking|questionId|plaintext|randomizer|questionId|plaintext|randomizer|...
// Parse compact format: questionId|randomizer|questionId|randomizer|...
const parts = hash.split('|')
if (parts.length < 1 || (parts.length - 1) % 3 !== 0) throw new Error('Invalid data format')
if (parts.length < 2 || parts.length % 2 !== 0) throw new Error('Invalid data format')
// Decode verification number from base64url
const verificationNumberBigInt = base64URLToBigint(parts[0])
// Convert back to decimal string format, with dashes every 4 digits
const verificationNumberString = verificationNumberBigInt.toString().padStart(12, '0')
const verificationNumber = `${verificationNumberString.slice(0, 4)}-${verificationNumberString.slice(
4,
8,
)}-${verificationNumberString.slice(8, 12)}`
const selections: Record<string, string> = {}
const randomizers: Record<string, string> = {}
// Process in groups of 3: questionId, encodedPlaintext, randomizer
// Note: We'll decode plaintext later when we have ballot_design
for (let i = 1; i < parts.length; i += 3) {
// Process in pairs: questionId, randomizer
for (let i = 0; i < parts.length; i += 2) {
const questionId = parts[i]
const encodedPlaintext = parts[i + 1]
const randomizer = parts[i + 2]
selections[questionId] = encodedPlaintext
const randomizer = parts[i + 1]
randomizers[questionId] = randomizer
}
const data: VoteData = {
randomizers,
selections,
verificationNumber,
}
const data: VoteData = { decryptedPlaintext: {}, randomizers }
setVoteData(data)
// Move private data into document memory and clear URL
window.history.replaceState(null, '', window.location.pathname)
} catch (_error) {
setCheckResult({ error: 'Failed to parse vote data', match: false })
setCheckResult({ error: 'Failed to parse vote data' })
console.error('Failed to parse vote data', _error)
}
}, [election_id, auth_token])
// Recalculate encrypted vote and send to server
// Fetch encrypted vote, try to decrypt with randomizers, and send success/fail to server
useEffect(() => {
if (!voteData || !threshold_public_key || checkResult) return
;(async () => {
if (!ballot_design) return setCheckResult({ error: 'Missing ballot design', match: false })
try {
if (!threshold_public_key) return setCheckResult({ error: 'Missing election public key', match: false })
if (!threshold_public_key) return setCheckResult({ error: 'Missing election public key' })
const public_key = RP.fromHex(threshold_public_key)
const recalculated: Record<string, CipherStrings> = {}
const { randomizers } = voteData
const { randomizers, selections, verificationNumber } = voteData
if (!randomizers || Object.keys(randomizers).length === 0)
return setCheckResult({ error: 'Invalid vote data format' })
if (!verificationNumber || !selections || !randomizers)
return setCheckResult({ error: 'Invalid vote data format', match: false })
// Fetch encrypted vote from server
const encryptedVoteResponse = await api(`/election/${election_id}/encrypted-vote?auth_token=${auth_token}`)
if (!encryptedVoteResponse.ok) return setCheckResult({ error: 'Failed to fetch encrypted vote' })
// Recalculate encryption for each selection
for (const questionId of Object.keys(selections)) {
// Decode plaintext from compact format (i{index}, b, w:{text}, or as-is)
const encodedPlaintext = selections[questionId]
const plaintext = decodePlaintext(encodedPlaintext, questionId, ballot_design)
const { encrypted_vote } = (await encryptedVoteResponse.json()) as {
encrypted_vote: Record<string, CipherStrings>
}
// Decrypt each cipher using the randomizer
const decryptedPlaintext: Record<string, string> = {}
for (const questionId of Object.keys(randomizers)) {
const cipher = encrypted_vote[questionId]
if (!cipher) return setCheckResult({ error: `Missing encrypted vote for question: ${questionId}` })
// Convert base64url encoded randomizer back to bigint
const randomizerBigInt = base64URLToBigint(randomizers[questionId])
// Encode: stringToPoint(`${tracking}:${plaintext}`)
const encoded = stringToPoint(`${verificationNumber}:${plaintext}`)
// Decrypt using randomizer and public key
const decryptedPoint = decryptWithRandomizer(public_key, randomizerBigInt, {
encrypted: RP.fromHex(cipher.encrypted),
lock: RP.fromHex(cipher.lock),
})
// Encrypt: encrypt(public_key, randomizer, encoded)
const cipher = encrypt(public_key, randomizerBigInt, encoded)
// Extract plaintext from point (format: "verification_number:plaintext")
const decodedString = pointToString(decryptedPoint)
const colonIndex = decodedString.indexOf(':')
if (colonIndex === -1) return setCheckResult({ error: `Invalid decoded format for question: ${questionId}` })
// Convert to CipherStrings format
recalculated[questionId] = {
encrypted: String(cipher.encrypted),
lock: String(cipher.lock),
}
const plaintext = decodedString.slice(colonIndex + 1)
decryptedPlaintext[questionId] = plaintext
}
// Send to API endpoint
// Update voteData with decrypted plaintext
setVoteData({ ...voteData, decryptedPlaintext })
// Let server know we were able to decrypt the vote
const response = await api('/malware-check', {
auth_token,
decrypted: true,
election_id,
recalculated_encrypted_vote: recalculated,
})
const result: CheckResult = await response.json()
setCheckResult(result)
} catch (error) {
setCheckResult({ error: 'Failed to recalculate or verify vote', match: false })
console.error('Failed to recalculate or verify vote', error)
setCheckResult({ error: 'Failed to decrypt or verify vote' })
console.error('Failed to decrypt or verify vote', error)
}
})()
}, [voteData, ballot_design, threshold_public_key, election_id, auth_token, checkResult])
}, [voteData, threshold_public_key, election_id, auth_token, checkResult])
const columns = useMemo(() => generateColumnNames({ ballot_design }).columns, [ballot_design])
@@ -153,7 +144,7 @@ function PageContent() {
if (!confirmed) return setShowIssueForm(true)
try {
const response = await api('/api/malware-check', {
const response = await api('/malware-check', {
auth_token,
confirmed: true,
election_id,
@@ -168,7 +159,7 @@ function PageContent() {
const handleSubmitIssue = async () => {
try {
const response = await api('/api/malware-check', {
const response = await api('/malware-check', {
auth_token,
confirmed: false,
election_id,
@@ -192,6 +183,14 @@ function PageContent() {
</div>
)
if (!voteData.decryptedPlaintext || Object.keys(voteData.decryptedPlaintext).length === 0) {
return (
<div className="p-8 text-center">
<p>Decrypting vote...</p>
</div>
)
}
if (checkResult?.error)
return (
<div className="p-8">
@@ -215,7 +214,7 @@ function PageContent() {
</div>
)
if (!checkResult.match)
if (!checkResult.otp)
return (
<div className="p-8">
<h2 className="mb-4 font-bold text-red-600"> Vote Mismatch Detected</h2>
@@ -244,11 +243,9 @@ function PageContent() {
</thead>
<tbody>
{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() {
</div>
)}
{checkResult.device_info && <p className="mt-4 text-sm text-gray-600">Device: {checkResult.device_info}</p>}
<p className="mt-4 text-xs text-gray-500">
<strong>Note:</strong> 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

View File

@@ -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()

View File

@@ -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}`

View File

@@ -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}`
}

View File

@@ -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'