Loading...
private.me Docs
Get BallotSplit
PRIVATE.ME · Technical White Paper

BallotSplit: Electronic Voting Ballot Secrecy

Ballot secrecy via XorIDA threshold secret sharing across independent election authorities. No single authority can read individual votes. Threshold reconstruction requires K-of-N cooperation. HMAC-SHA256 integrity verification before counting. Information-theoretic security, no computational assumptions. Zero npm runtime dependencies.

v0.1.0 Xvote Product Information-Theoretic Gold Standard Bronze <1ms per ballot Dual ESM/CJS
Section 01

Executive Summary

Electronic voting systems face a fundamental security problem: ballots must be secret during voting but verifiable during counting. Compromising a single election authority breaks ballot secrecy completely.

BallotSplit solves this via XorIDA (threshold secret sharing over GF(2)). When a voter casts a ballot, castBallot() splits it into N shares and distributes one to each independent election authority. No single authority can read the vote. Tallying requires K-of-N authorities to cooperate — tallyElection() reconstructs all ballots from a threshold of shares, validates them with HMAC-SHA256, aggregates votes, and detects duplicate voting.

Information-theoretic guarantee: K-1 or fewer shares reveal mathematically zero information about the ballot contents. This security does not depend on computational assumptions — even infinite computing power cannot break K-1 shares. Quantum computers cannot break it either.

The implementation is production-ready: 180+ lines of TypeScript, 100% test coverage, full OWASP 2025 compliance, and integrated HMAC integrity verification before reconstruction.

Section 02

The Problem

Election authorities today are single points of failure for ballot secrecy.

One compromised authority = total ballot exposure. If a state election board, county clerk, or federal commission is hacked, coerced, or corrupt, all ballots for that jurisdiction are exposed. The attacker learns the complete vote of every voter. Voters cannot prove their vote was their choice. The integrity of the entire election is compromised.

Traditional approaches attempt to solve this with cryptography + procedural controls:

  • End-to-end verifiable voting (E2E-V): Ballots are encrypted, but voters must manually verify their receipt. Expensive per-voter; most voters skip verification. Does not protect against authority compromise.
  • Procedural controls (seals, observers, audit trails): Require human oversight and trust. Scalable only to small elections. Still vulnerable to coordinated insider threats.
  • Air-gapped systems: Reduce attack surface but do not eliminate ballot secrecy risk. A single insider can still access all ballots during counting.

None of these address the core problem: ballot secrecy depends on trusting one organization.

Approach Single Authority Breach Voter Effort Scaling
Centralized Authority All votes exposed Low Unlimited
E2E Verifiable (manual) All votes exposed High (verification) Unlimited
Procedural Controls Reduced but vulnerable High (observers, audits) Limited to small elections
BallotSplit (Threshold) K-1 authorities insufficient Zero Unlimited
Section 03

Solution Overview

Split each ballot across N independent authorities using XorIDA (Information Dispersal Algorithm over GF(2)). Require K authorities to cooperate for reconstruction. No K-1 subset of authorities can decrypt any ballot.

Three Operations

1. castBallot(ballot, config): Voter submits their selections. The system serializes the ballot to JSON, pads it to a prime-aligned block size, generates an HMAC-SHA256 key, splits the padded ballot via XorIDA into N shares, and assigns one share to each authority. Each share includes the ballot ID, authority ID, threshold parameters, the encrypted share data (base64), and the HMAC (base64). The result is returned as a CastResult with all shares and a SHA-256 ballot hash for audit.

2. tallyElection(allShares, config): After voting closes, election administrators from K-of-N authorities submit their shares. The system verifies each share's HMAC against the stored key. Once HMAC verification passes, shares are reconstructed via XorIDA, unpadded, and parsed as JSON. Voter IDs are checked for duplicates (preventing double voting). Votes are aggregated per contest and per choice. The result is a TallyResult with per-contest vote counts, total ballots, and timestamp.

3. Duplicate vote detection: During tallying, the system maintains a set of voter IDs seen. If a voter ID appears in more than one ballot, the tally operation returns DUPLICATE_VOTE and stops. This prevents both accidental and malicious double voting.

Quick start — cast and tally
import { castBallot, tallyElection } from '@private.me/ballotsplit';

const config = {
  electionId: 'ELECTION-2026',
  authorities: [
    { id: 'FED', name: 'Federal', jurisdiction: 'US' },
    { id: 'STATE', name: 'State', jurisdiction: 'US-CA' },
    { id: 'COUNTY', name: 'County', jurisdiction: 'US-CA-LA' },
  ],
  threshold: 2,
};

// Voter casts ballot
const ballot = {
  ballotId: 'BALLOT-001',
  voterId: 'VOTER-42',
  electionId: 'ELECTION-2026',
  selections: { president: 'candidate-a', governor: 'candidate-b' },
  timestamp: new Date().toISOString(),
};

const cast = await castBallot(ballot, config);
if (!cast.ok) throw new Error(cast.error);

// Distribute shares to authorities...

// After voting closes, tally from K-of-N shares
const tally = await tallyElection([cast.value.shares], config);
if (!tally.ok) throw new Error(tally.error);
console.log(tally.value.results);
// { president: { 'candidate-a': 1 }, governor: { 'candidate-b': 1 } }
Section 04

Real-World Use Cases

Five scenarios where BallotSplit brings ballot secrecy guarantees to different electoral contexts.

🗳️
Government
Federal Elections

Split ballots across Federal Election Commission, state boards, and county clerks. K=2 of 3 threshold. Any 2 authorities can tally; no single agency compromise exposes votes.

castBallot() + tallyElection()
🏛️
Local Government
Municipal Elections

County clerk + city council + registry. 2-of-3 reconstruction. Procedurally forces cooperation: clerk alone cannot produce results; must ask city + registry or registry + clerk.

3-of-3 authorities, K=2
🏢
Corporate
Shareholder Voting

Split across independent proxy services and the company. Requires both company and external validator to tally. Prevents unilateral manipulation by issuer.

2-of-2 threshold = maximum security
🌍
International
Cross-Border Elections

Ballots split across election commissions in different countries. 2-of-3 or 3-of-5 threshold. Language barriers and jurisdictional boundaries add procedural separation.

Multi-authority + geographic dispersion
📊
Governance
Board Resolutions

Votes on resolutions split across board directors + independent auditor. Prevents insider manipulation. Transparent to stakeholders who receive the count.

N=5, K=3 threshold
⚖️
Legal
Jury Verdicts

Verdict ballots split across jury foreperson, court clerk, and independent notary. Prevents coercion: no single party can extract the verdict before official recording.

Split across 3 independent parties
Section 05

Technical Architecture

Four independent modules. Ballot casting, HMAC integrity, XorIDA reconstruction, and duplicate detection.

Threshold Splitting
XorIDA over GF(2)
N authorities, K reconstruction threshold
<1ms per ballot, sub-millisecond
K-1 shares reveal zero information
Quantum-safe, information-theoretic
Integrity Verification
HMAC-SHA256
Fresh HMAC key per ballot
Verification BEFORE reconstruction
Failed HMAC = ballot rejected
Prevents tampered share acceptance
Audit & Verification
SHA-256 Hash
Each cast ballot hashed (SHA-256)
Auditors can verify ballot without access
Prevents retroactive ballot substitution
Part of CastResult returned to voter

Data Flow

Voter Selections + Metadata Serialize JSON + Pad HMAC-SHA256 Key + Sig XorIDA Split N shares Auth A Auth B Auth C Vote → Serialize → Protect → Split → Distribute to Authorities
Section 06

Complete Vote Pipeline

End-to-end: ballot submission through official election result.

Ballot Casting

1. Voter submits ballot with contest-choice selections (e.g., { president: 'candidate-a', governor: 'candidate-b' }).
2. castBallot(ballot, config) validates config (≥2 authorities, threshold ≤ N).
3. Ballot JSON serialized and padded to blockSize = nextOddPrime(N) - 1.
4. HMAC key generated via generateHMAC(paddedBallot); HMAC signature computed. Key stored with shares (allows verification by recipients).
5. Padded ballot split via XorIDA into N shares, each ≤ padded size.
6. Each share assigned to an authority with metadata: ballotId, electionId, authorityId, index (0 to N-1), total (N), threshold (K), data (base64 with share header), hmac (base64 key.sig concatenation), originalSize.
7. SHA-256 hash of original ballot computed for audit.
8. CastResult returned: ballotId, all N shares, ballotHash.

Deterministic Results
CastResult includes a ballotHash (SHA-256 of original ballot before padding). Voters receive this hash as a receipt. During audit, anyone can verify the hash matches the official result. This prevents retroactive ballot substitution after casting.

Vote Tallying

1. After voting closes, administrators from K-of-N authorities submit their ballot shares (one per ballot) to tallyElection().
2. For each ballot set (one per voter):

  • Config validation: authority count matches total, threshold ≤ count.
  • HMAC verification (CRITICAL): For each share, recompute HMAC-SHA256 with the stored key. If any share fails, return HMAC_FAILED. Abort tally.
  • XorIDA reconstruction: combine K shares to recover padded ballot bytes.
  • Unpadding: remove PKCS7 padding, recover original size.
  • JSON parsing: deserialize to Ballot object.
  • Voter ID uniqueness check: if any voterId seen before, return DUPLICATE_VOTE.

3. Aggregate votes: for each ballot, iterate contests and increment per-choice counters.
4. Return TallyResult: electionId, results (nested Record<contest, Record<choice, count>>), totalBallots, timestamp.

No Plaintext in Logs
All ballot reconstructions happen in-memory. No plaintext ballot is ever written to logs, audit trails, or persistent storage. Only the aggregated results (per-choice vote counts) are recorded. This maintains ballot secrecy even after counting.
Section 07

Security Properties

Eight cryptographic and procedural guarantees.

Property Mechanism Guarantee
Ballot Secrecy XorIDA K-of-N threshold sharing K-1 authorities cannot read any ballot
Information-Theoretic GF(2) binary field operations K-1 shares reveal zero information regardless of computing power
Integrity HMAC-SHA256 verification before reconstruction Tampered shares rejected, prevents ballot manipulation
Duplicate Detection Voter ID uniqueness check during tally Multiple votes by same voter detected and rejected
Audit Trail SHA-256 ballot hash in CastResult Voters can verify their ballot was cast as intended
Share Confidentiality TLS in transit, filesystem permissions at rest Shares are base64 but require application-level encryption for full security
Quantum Safety XorIDA mathematical structure Information-theoretic security, not quantum-resistant but post-quantum-proof
Randomness Quality crypto.getRandomValues() Only cryptographic RNG used, never Math.random()

Attack Mitigation

K-1 Authority Compromise: If fewer than K authorities are breached, their shares alone cannot reconstruct any ballot. Information-theoretic security mathematically guarantees this.

All-Authority Collusion (K authorities): If all K required authorities cooperate, they can reconstruct votes. This is by design — K-of-N threshold is a policy choice. Increasing N or lowering K redistributes the collusion risk. With N=3, K=2, a single authority cannot act alone; requires another cooperator.

Share Tampering: If an authority modifies a share before reconstruction, HMAC verification fails before reconstruction is attempted. The ballot is rejected and the tally reports HMAC_FAILED.

Insider at Single Authority: An insider with access to one share learns nothing about the ballot. With access to K-1 shares, still learns nothing. Only with K shares (across at least 2 authorities) can the ballot be reconstructed.

SHARE TRANSPORT SECURITY
This package provides application-level ballot splitting and reconstruction. Share transport between voter systems and election authorities is the deployer's responsibility. Use TLS 1.3 for all share transmission in transit. At rest, store shares with filesystem permissions (Unix mode 0600) or encrypted storage. Shares are base64 but not encrypted — encryption is a deployment decision, not built-in.
Section 08

Benchmarks & Performance

Measured on Node.js 20 LTS, Apple M2 Pro.

<1ms
castBallot per ballot
<1ms
tallyElection per ballot
~100µs
HMAC verification
~50µs
XorIDA split/reconstruct
30+
Test cases passing
Operation Time Notes
castBallot (single 1KB ballot) <1ms Serialization, padding, HMAC, XorIDA split
XorIDA split (1KB, 3-of-3) ~50µs Pure GF(2) operations, highly optimized
HMAC-SHA256 key generation ~50µs Web Crypto API native
tallyElection (100 ballots, 2-of-3) ~50ms Includes HMAC verification + XorIDA reconstruction per ballot
1000 ballots tally ~500ms Linear scaling, no slowdown with ballot count

Scaling: BallotSplit is I/O-bound in production — the bottleneck is network latency for share delivery and storage I/O, not cryptography. A single election authority can process 1M ballots in under 20 minutes (sub-second per ballot). Multi-authority coordination adds procedural latency (waiting for K shares), not cryptographic latency.

Section 09

Limitations

This package is a cryptographic ballot protection layer. It does not solve every aspect of election security.

Out of Scope

  • Voter Authentication: This package does not verify voter identity. Deployers must use existing identity systems (voter registration databases, credential verification) before ballot submission.
  • Voter Coercion: A voter cannot prove to someone else which way they voted (a good property), but this also means they cannot prove they voted freely. Coercion / vote buying prevention requires procedural controls outside this system.
  • Network Security: Share transport requires TLS. This package provides application-level encryption but does not handle network infrastructure security.
  • Share Storage Security: Once authorities store their shares, filesystem and database security are the deployer's responsibility. Recommend encrypted storage with key escrow.
  • All K Authorities Compromise: If all K required authorities are compromised (hacked, coerced, or corrupt), ballots can be reconstructed and exposed. This is a policy risk, not a cryptographic weakness.

Known Trade-Offs

Reconstruction Requires K Shares: In a 2-of-3 scheme, if only 2 authorities can provide shares (third is down, offline, or refusing to cooperate), results must be tallied from 2 shares. Deployers must choose K conservatively to allow for authority downtime.

No End-to-End Verification: Unlike some E2E-V systems, voters do not perform a verification step after casting. Trust in the system is placed in the threshold architecture, not voter action. This trades voter verification overhead for procedural simplicity.

Ballot Size Padding: Ballots are padded to prime-aligned block sizes, which increases share storage by up to ~(N)% per authority. For typical ballots (<10KB), this is negligible (<50 bytes overhead per ballot per authority).

Section 10

Integration Guide

Typical integration patterns for election administrators and voting system architects.

Pattern 1: Vote Server Integration

Express.js vote submission endpoint
import express from 'express';
import { castBallot } from '@private.me/ballotsplit';

const config = {
  electionId: 'PRES-2026',
  authorities: [...],
  threshold: 2,
};

app.post('/vote/submit', async (req, res) => {
  const ballot = req.body.ballot;
  const cast = await castBallot(ballot, config);

  if (!cast.ok) {
    res.status(400).json({ error: cast.error });
    return;
  }

  // Distribute shares to authorities via separate secure channels
  for (const share of cast.value.shares) {
    await sendToAuthority(share);
  }

  // Return ballot hash receipt to voter
  res.json({
    ballotId: cast.value.ballotId,
    ballotHash: cast.value.ballotHash,
  });
});

Pattern 2: Tally Administration

End-of-election tally script
import { tallyElection } from '@private.me/ballotsplit';

// Collect shares from K authorities
const sharesPerBallot = await Promise.all([
  getSharesFromFED(),
  getSharesFromSTATE(),
  // Note: only 2 of 3 required, COUNTY shares not needed
]);

const allShares = zipByBallotId(sharesPerBallot);

const tally = await tallyElection(allShares, config);

if (!tally.ok) {
  console.error('Tally failed:', tally.error);
  process.exit(1);
}

// Official results
console.log('Election Results:', tally.value.results);
console.log('Total Ballots:', tally.value.totalBallots);

Pattern 3: Error Handling

Comprehensive error handling
import { toBallotSplitError } from '@private.me/ballotsplit';

const cast = await castBallot(ballot, config);

if (!cast.ok) {
  const err = toBallotSplitError(cast.error);

  switch (err.code) {
    case 'INVALID_CONFIG':
      console.error('Bad config:', err.message, err.subCode);
      break;
    case 'SPLIT_FAILED':
      console.error('Crypto error during split', err.message);
      break;
    default:
      console.error('Unknown error:', err.code, ' at ', err.docUrl);
  }
}
Section 11

Error Codes

All operations return Result<T, BallotError>. Errors are machine-readable string codes.

Code Sub-Code When
INVALID_CONFIG THRESHOLD_MISMATCH Fewer than 2 authorities, or K > N, or K < 2
INVALID_CONFIG ELECTION_MISMATCH Ballot electionId does not match config electionId
SPLIT_FAILED XorIDA split error (rare, indicates data corruption)
HMAC_FAILED HMAC verification failed during reconstruction (share tampered or key mismatch)
RECONSTRUCT_FAILED UNPAD PKCS7 unpadding failed (shares corrupted)
INSUFFICIENT_SHARES Fewer than K shares provided to tallyElection
DUPLICATE_VOTE Same voter ID appears in multiple ballots
Error Classes
Use toBallotSplitError(code) to convert string codes to typed error classes (BallotSplitError, BallotConfigError, BallotIntegrityError, BallotReconstructError) for catch handlers. Each includes a docUrl property linking to detailed documentation.
Section 12

API Surface

castBallot(ballot, config): Promise<Result<CastResult, BallotError>>

Split a ballot across authorities via XorIDA. Returns CastResult with all N shares and ballot hash, or error code.

tallyElection(allShares, config): Promise<Result<TallyResult, BallotError>>

Reconstruct ballots from K-of-N shares and aggregate votes. Validates HMAC, detects duplicate votes. Returns TallyResult with per-contest results, or error code.

Type Definitions

interface Ballot

{ ballotId, electionId, voterId, selections: Record<contest, choice>, timestamp } — Voter's completed ballot.

interface ElectionAuthority

{ id, name, jurisdiction } — Independent authority holding a ballot share.

interface ElectionConfig

{ authorities, threshold, electionId } — Threshold-split election configuration.

interface BallotShare

{ ballotId, electionId, authorityId, index, total, threshold, data (base64), hmac, originalSize } — Single share assigned to one authority.

interface CastResult

{ ballotId, shares[], ballotHash (SHA-256 hex) } — Result of casting a ballot.

interface TallyResult

{ electionId, results: Record<contest, Record<choice, count>>, totalBallots, talliedAt } — Aggregated election results.

Section 13

Codebase Statistics

Production-ready implementation with comprehensive test coverage.

180+
Lines of TypeScript
30+
Test cases
100%
Line coverage
0
npm dependencies
Module Lines Responsibility
ballot-caster.ts ~100 Ballot serialization, padding, HMAC, XorIDA split
ballot-tallier.ts ~80 HMAC verification, XorIDA reconstruction, vote aggregation, duplicate detection
types.ts ~60 Ballot, ElectionAuthority, ElectionConfig, BallotShare, CastResult, TallyResult interfaces
errors.ts ~90 Error class hierarchy, error message mapping, type guards
index.ts ~15 Public API barrel export

Test Structure: Abuse tests (double voting, invalid configs), integrity tests (HMAC, XorIDA round-trip), error tests (all error codes covered). All tests use mock election configs with 2–3 authorities and various thresholds. Known-answer test vectors validate XorIDA consistency.

Appendix

Glossary

Authority (Election Authority): An independent organization responsible for holding and protecting a ballot share. Examples: Federal Election Commission, state election board, county clerk.

Ballot: A voter's completed submission with selections for each contest (e.g., { president: 'candidate-a', governor: 'candidate-b' }).

Ballot Hash: SHA-256 digest of the original ballot JSON. Used for audit verification — voters receive the hash as a receipt.

BallotShare: One cryptographic share of a split ballot, assigned to one authority. Contains metadata (authority ID, threshold params), share data (base64), HMAC signature, and original size.

Casting: The process of splitting a voter's ballot via XorIDA and assigning shares to authorities.

Duplicate Vote Detection: During tallying, the system checks if any voter ID appears more than once. If so, tally fails with DUPLICATE_VOTE error.

Election Config: Configuration specifying authorities, threshold (K), and election ID. Passed to both castBallot and tallyElection.

GF(2) (Galois Field of 2): Binary field used by XorIDA. Operations are XOR-based, highly efficient.

HMAC (Hash-based Message Authentication Code): Cryptographic signature using SHA-256. Used to verify ballot share integrity before reconstruction.

Information-Theoretic Security: Security based on mathematical structure, not computational assumptions. Cannot be broken by any amount of computing power.

Reconstruction: The process of combining K-of-N shares to recover the original ballot.

Share: One piece of a split ballot. K-of-N shares are necessary and sufficient to reconstruct the ballot. K-1 shares reveal zero information.

Tallying: The process of collecting K-of-N shares from authorities, reconstructing all ballots, validating them, and aggregating votes.

Threshold (K): Minimum number of shares required to reconstruct a ballot. Set by election configuration (e.g., K=2 of N=3 means any 2 of 3 authorities can tally).

XorIDA: Information Dispersal Algorithm over GF(2). Splits data into N shares; any K shares reconstruct the data; K-1 shares reveal zero information.

Deployment Options

📦

SDK Integration

Embed directly in your application. Runs in your codebase with full programmatic control.

  • npm install @private.me/ballotsplit
  • TypeScript/JavaScript SDK
  • Full source access
  • Enterprise support available
Get Started →
🏢

On-Premise Upon Request

Enterprise CLI for compliance, air-gap, or data residency requirements.

  • Complete data sovereignty
  • Air-gap capable deployment
  • Custom SLA + dedicated support
  • Professional services included
Request Quote →

Enterprise On-Premise Deployment

While ballotSplit is primarily delivered as SaaS or SDK, we build dedicated on-premise infrastructure for customers with:

  • Regulatory mandates — HIPAA, SOX, FedRAMP, CMMC requiring self-hosted processing
  • Air-gapped environments — SCIF, classified networks, offline operations
  • Data residency requirements — EU GDPR, China data laws, government mandates
  • Custom integration needs — Embed in proprietary platforms, specialized workflows

Includes: Enterprise CLI, Docker/Kubernetes orchestration, RBAC, audit logging, and dedicated support.

Contact sales for assessment and pricing →