From 4b893c9ee1e35eb013fc9653ba50dc566715945f Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:33:19 +0000 Subject: [PATCH 01/33] Add implementation plans for Helios Booth Lit Redesign Four-phase implementation plan for rebuilding the voting booth with Lit web components, TypeScript, and Vite (booth2026). Co-Authored-By: Claude Opus 4.6 --- .../phase_01.md | 1075 ++++++++++++ .../phase_02.md | 733 ++++++++ .../phase_03.md | 1519 +++++++++++++++++ .../phase_04.md | 900 ++++++++++ 4 files changed, 4227 insertions(+) create mode 100644 docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md create mode 100644 docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_02.md create mode 100644 docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_03.md create mode 100644 docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_04.md diff --git a/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md new file mode 100644 index 000000000..f32f94d61 --- /dev/null +++ b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md @@ -0,0 +1,1075 @@ +# Helios Booth Lit Redesign - Phase 1: Setup & Core Shell + +> **For Claude:** REQUIRED SUB-SKILL: Use ed3d-plan-and-execute:executing-an-implementation-plan to implement this plan task-by-task. + +**Goal:** Create the project scaffolding and basic navigation for the new Lit-based voting booth + +**Architecture:** Vite + TypeScript + Lit 3.x web components. Single-bundle output for offline operation. Props-drilling state management with booth-app as the central state holder. + +**Tech Stack:** Lit 3.3.x, TypeScript 5.x, Vite 6.x + +**Scope:** 4 phases from original design (this is phase 1 of 4) + +**Codebase verified:** 2026-01-18 + +--- + +## Task 1: Create Project Directory Structure + +**Files:** +- Create: `heliosbooth2026/` directory +- Create: `heliosbooth2026/package.json` +- Create: `heliosbooth2026/tsconfig.json` +- Create: `heliosbooth2026/vite.config.ts` +- Create: `heliosbooth2026/index.html` + +**Step 1: Create the directory and package.json** + +Create directory `heliosbooth2026/` at project root. + +Create `heliosbooth2026/package.json`: +```json +{ + "name": "heliosbooth2026", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "lit": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} +``` + +**Step 2: Create tsconfig.json** + +Create `heliosbooth2026/tsconfig.json`: +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "skipLibCheck": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +**Step 3: Create vite.config.ts** + +Create `heliosbooth2026/vite.config.ts`: +```typescript +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: '/booth2026/', + build: { + outDir: 'dist', + cssCodeSplit: false, + rollupOptions: { + output: { + manualChunks: () => 'booth', + entryFileNames: 'booth.js', + assetFileNames: 'booth.[ext]' + } + } + }, + server: { + port: 5173 + } +}); +``` + +**Step 4: Create index.html entry point** + +Create `heliosbooth2026/index.html`: +```html + + + + + + Helios Voting Booth + + + + + + + +``` + +**Step 5: Verify installation** + +Run: +```bash +cd heliosbooth2026 && npm install +``` +Expected: Dependencies installed without errors + +Run: +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build fails (no src files yet) - this is expected at this step + +**Step 6: Commit** + +```bash +git add heliosbooth2026/package.json heliosbooth2026/tsconfig.json heliosbooth2026/vite.config.ts heliosbooth2026/index.html +git commit -m "feat(booth2026): initialize Vite + TypeScript + Lit project structure" +``` + +--- + +## Task 2: Copy and Organize Crypto Libraries + +**Files:** +- Create: `heliosbooth2026/lib/jscrypto/` directory (copied from `heliosbooth/js/jscrypto/`) +- Create: `heliosbooth2026/lib/underscore-min.js` (copied from `heliosbooth/js/underscore-min.js`) +- Create: `heliosbooth2026/src/crypto/types.ts` + +**Step 1: Copy jscrypto directory** + +```bash +cp -r heliosbooth/js/jscrypto heliosbooth2026/lib/ +cp heliosbooth/js/underscore-min.js heliosbooth2026/lib/ +``` + +Expected: Files copied successfully + +**Step 2: Create TypeScript type declarations for crypto globals** + +Create `heliosbooth2026/src/crypto/types.ts`: +```typescript +/** + * TypeScript type declarations for the Helios jscrypto library globals. + * These are loaded via script tags from lib/jscrypto/ and available globally. + */ + +// BigInt from lib/jscrypto/bigint.js (wraps sjcl BigInteger) +export interface BigIntType { + ZERO: BigIntInstance; + ONE: BigIntInstance; + TWO: BigIntInstance; + fromInt(value: number): BigIntInstance; + fromJSONObject(obj: string): BigIntInstance; + setup(callback: () => void, errorCallback?: () => void): void; +} + +export interface BigIntInstance { + add(other: BigIntInstance): BigIntInstance; + subtract(other: BigIntInstance): BigIntInstance; + multiply(other: BigIntInstance): BigIntInstance; + mod(modulus: BigIntInstance): BigIntInstance; + modPow(exponent: BigIntInstance, modulus: BigIntInstance): BigIntInstance; + modInverse(modulus: BigIntInstance): BigIntInstance; + equals(other: BigIntInstance): boolean; + toJSONObject(): string; + toString(): string; +} + +// ElGamal from lib/jscrypto/elgamal.js +export interface ElGamalParams { + p: BigIntInstance; + q: BigIntInstance; + g: BigIntInstance; +} + +export interface ElGamalPublicKey { + p: BigIntInstance; + q: BigIntInstance; + g: BigIntInstance; + y: BigIntInstance; + toJSONObject(): ElGamalPublicKeyJSON; +} + +export interface ElGamalPublicKeyJSON { + p: string; + q: string; + g: string; + y: string; +} + +export interface ElGamalType { + Params: { + fromJSONObject(obj: ElGamalParams): ElGamalParams; + }; + PublicKey: { + fromJSONObject(obj: ElGamalPublicKeyJSON): ElGamalPublicKey; + }; +} + +// Question structure from election JSON +export interface Question { + question: string; + short_name: string; + answers: string[]; + answer_urls?: (string | null)[]; + min: number; + max: number; + randomize_answer_order?: boolean; +} + +// Election from lib/jscrypto/helios.js +export interface Election { + uuid: string; + name: string; + short_name: string; + description: string; + questions: Question[]; + public_key: ElGamalPublicKey; + cast_url: string; + frozen_at: string; + openreg: boolean; + voters_hash: string | null; + use_voter_aliases: boolean; + voting_starts_at: string | null; + voting_ends_at: string | null; + election_hash: string; + hash: string; + BOGUS_P?: boolean; + question_answer_orderings?: number[][]; +} + +export interface EncryptedAnswer { + toJSONObject(includeRandomness?: boolean): EncryptedAnswerJSON; +} + +export interface EncryptedAnswerJSON { + choices: unknown[]; + individual_proofs: unknown[]; + overall_proof: unknown; + randomness?: unknown[]; + answer?: number[]; +} + +export interface EncryptedVote { + toJSONObject(): EncryptedVoteJSON; + get_hash(): string; +} + +export interface EncryptedVoteJSON { + answers: EncryptedAnswerJSON[]; + election_hash: string; + election_uuid: string; +} + +export interface HELIOSType { + Election: { + fromJSONString(raw: string): Election; + fromJSONObject(obj: unknown): Election; + }; + EncryptedAnswer: { + new(question: Question, answer: number[], publicKey: ElGamalPublicKey): EncryptedAnswer; + fromJSONObject(obj: EncryptedAnswerJSON, election: Election): EncryptedAnswer; + }; + EncryptedVote: { + fromEncryptedAnswers(election: Election, answers: EncryptedAnswer[]): EncryptedVote; + }; + get_bogus_public_key(): ElGamalPublicKey; +} + +export interface RandomType { + getRandomInteger(max: BigIntInstance): BigIntInstance; +} + +export interface UTILSType { + array_remove_value(arr: T[], val: T): T[]; + PROGRESS: new () => { + n_ticks: number; + current_tick: number; + addTicks(n: number): void; + tick(): void; + progress(): number; + }; + object_sort_keys(obj: T): T; +} + +// Election metadata from server +export interface ElectionMetadata { + help_email: string; + randomize_answer_order?: boolean; + use_advanced_audit_features?: boolean; +} + +// Declare globals that will be available after loading jscrypto scripts +declare global { + const BigInt: BigIntType; + const ElGamal: ElGamalType; + const HELIOS: HELIOSType; + const Random: RandomType; + const UTILS: UTILSType; + const USE_SJCL: boolean; + const sjcl: { + random: { + startCollectors(): void; + addEntropy(data: string): void; + }; + }; + function b64_sha256(data: string): string; +} +``` + +**Step 3: Verify files exist** + +Run: +```bash +ls -la heliosbooth2026/lib/jscrypto/ +``` +Expected: Lists all crypto library files (bigint.js, elgamal.js, helios.js, etc.) + +**Step 4: Commit** + +```bash +git add heliosbooth2026/lib/ heliosbooth2026/src/crypto/ +git commit -m "feat(booth2026): copy jscrypto library and add TypeScript declarations" +``` + +--- + +## Task 3: Create Base CSS Styles + +**Files:** +- Create: `heliosbooth2026/src/styles/booth.css` + +**Step 1: Create the styles directory and base CSS** + +Create `heliosbooth2026/src/styles/booth.css`: +```css +/* Helios Booth 2026 - Base Styles */ + +:root { + --color-primary: #1a73e8; + --color-primary-dark: #1557b0; + --color-background: #fff; + --color-surface: #f5f5f5; + --color-border: #ddd; + --color-text: #333; + --color-text-secondary: #666; + --color-success: #28a745; + --color-warning: #ffc107; + --color-error: #dc3545; + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.25rem; + --font-size-xl: 1.5rem; + --border-radius: 4px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-family); + font-size: var(--font-size-md); + color: var(--color-text); + background-color: var(--color-background); + line-height: 1.5; +} + +/* Utility classes */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Button styles */ +button, +.button { + display: inline-block; + padding: var(--spacing-sm) var(--spacing-md); + font-family: inherit; + font-size: var(--font-size-md); + font-weight: 500; + text-align: center; + text-decoration: none; + color: #fff; + background-color: var(--color-primary); + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: background-color 0.2s ease; +} + +button:hover, +.button:hover { + background-color: var(--color-primary-dark); +} + +button:disabled, +.button:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +button:focus, +.button:focus { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Secondary button style */ +button.secondary, +.button.secondary { + background-color: transparent; + color: var(--color-primary); + border: 1px solid var(--color-primary); +} + +button.secondary:hover, +.button.secondary:hover { + background-color: var(--color-surface); +} +``` + +**Step 2: Verify file created** + +Run: +```bash +cat heliosbooth2026/src/styles/booth.css | head -20 +``` +Expected: Shows beginning of CSS file + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/styles/booth.css +git commit -m "feat(booth2026): add base CSS styles" +``` + +--- + +## Task 4: Create Main Entry Point and Booth App Shell + +**Files:** +- Create: `heliosbooth2026/src/main.ts` +- Create: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Create the main entry point** + +Create `heliosbooth2026/src/main.ts`: +```typescript +/** + * Main entry point for the Helios Voting Booth 2026. + * Loads crypto libraries and initializes the booth app. + */ + +import './booth-app.js'; + +// The booth-app component will handle initialization after crypto libs load +console.log('Helios Booth 2026 loaded'); +``` + +**Step 2: Create the booth-app component shell** + +Create `heliosbooth2026/src/booth-app.ts`: +```typescript +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import type { Election, ElectionMetadata, EncryptedAnswer } from './crypto/types.js'; + +/** + * Screen states for the voting booth flow. + */ +type BoothScreen = 'loading' | 'election' | 'question' | 'review' | 'submit' | 'audit'; + +/** + * Main booth application component. + * Holds all voter state and manages screen navigation. + */ +@customElement('booth-app') +export class BoothApp extends LitElement { + static styles = css` + :host { + display: block; + max-width: 800px; + margin: 0 auto; + padding: var(--spacing-md, 16px); + } + + .banner { + background-color: var(--color-surface, #f5f5f5); + padding: var(--spacing-md, 16px); + text-align: center; + margin-bottom: var(--spacing-lg, 24px); + border-bottom: 1px solid var(--color-border, #ddd); + } + + .banner h1 { + margin: 0; + font-size: var(--font-size-xl, 1.5rem); + } + + .banner .exit-link { + float: right; + font-size: var(--font-size-sm, 0.875rem); + } + + .banner .exit-link a { + color: var(--color-text-secondary, #666); + text-decoration: none; + } + + .banner .exit-link a:hover { + text-decoration: underline; + } + + .content { + min-height: 400px; + } + + .loading { + text-align: center; + padding: var(--spacing-xl, 32px); + } + + .error { + background-color: #fee; + border: 1px solid var(--color-error, #dc3545); + color: var(--color-error, #dc3545); + padding: var(--spacing-md, 16px); + border-radius: var(--border-radius, 4px); + margin-bottom: var(--spacing-md, 16px); + } + + .progress-bar { + display: flex; + justify-content: center; + gap: var(--spacing-md, 16px); + margin-bottom: var(--spacing-lg, 24px); + padding: var(--spacing-sm, 8px); + background-color: var(--color-surface, #f5f5f5); + border-radius: var(--border-radius, 4px); + } + + .progress-step { + padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px); + border-radius: var(--border-radius, 4px); + font-size: var(--font-size-sm, 0.875rem); + } + + .progress-step.active { + background-color: var(--color-primary, #1a73e8); + color: #fff; + } + + .progress-step.completed { + color: var(--color-success, #28a745); + } + `; + + // Application state + @state() private currentScreen: BoothScreen = 'loading'; + @state() private election: Election | null = null; + @state() private electionMetadata: ElectionMetadata | null = null; + @state() private electionUrl: string = ''; + @state() private error: string | null = null; + + // Voting state + @state() private currentQuestionIndex: number = 0; + @state() private answers: number[][] = []; + @state() private allQuestionsSeen: boolean = false; + + // Encryption state + @state() private encryptedAnswers: (EncryptedAnswer | null)[] = []; + @state() private encryptedBallotHash: string = ''; + @state() private encryptedVoteJson: string = ''; + + // Crypto readiness + @state() private cryptoReady: boolean = false; + + connectedCallback(): void { + super.connectedCallback(); + this.initializeBooth(); + } + + /** + * Initialize the booth by loading crypto libraries and election data. + */ + private async initializeBooth(): Promise { + try { + // Get election URL from query params + const params = new URLSearchParams(window.location.search); + const electionUrl = params.get('election_url'); + + if (!electionUrl) { + this.error = 'No election URL provided. Please access this page from an election link.'; + this.currentScreen = 'election'; + return; + } + + this.electionUrl = electionUrl; + + // Wait for BigInt crypto to be ready + await this.waitForCrypto(); + this.cryptoReady = true; + + // Load election data + await this.loadElection(electionUrl); + + this.currentScreen = 'election'; + } catch (err) { + this.error = `Failed to initialize booth: ${err instanceof Error ? err.message : String(err)}`; + this.currentScreen = 'election'; + } + } + + /** + * Wait for the BigInt crypto library to be ready. + */ + private waitForCrypto(): Promise { + return new Promise((resolve, reject) => { + if (typeof BigInt !== 'undefined' && BigInt.setup) { + BigInt.setup(resolve, reject); + } else { + // Crypto libs not loaded via script tags yet - for now just resolve + // In production, crypto libs are loaded via script tags in index.html + console.warn('BigInt not available - crypto operations will fail'); + resolve(); + } + }); + } + + /** + * Load election data from the server. + */ + private async loadElection(electionUrl: string): Promise { + // Fetch election JSON + const electionResponse = await fetch(electionUrl); + if (!electionResponse.ok) { + throw new Error(`Failed to fetch election: ${electionResponse.status}`); + } + const rawJson = await electionResponse.text(); + + // Fetch election metadata + const metaResponse = await fetch(`${electionUrl}/meta`); + if (metaResponse.ok) { + this.electionMetadata = await metaResponse.json(); + } + + // Parse election using HELIOS library + if (typeof HELIOS !== 'undefined') { + this.election = HELIOS.Election.fromJSONString(rawJson); + this.election.hash = b64_sha256(rawJson); + this.election.election_hash = this.election.hash; + + // Initialize answer tracking + this.answers = this.election.questions.map(() => []); + this.encryptedAnswers = this.election.questions.map(() => null); + + // Set up answer ordering (for randomization if configured) + this.setupAnswerOrderings(); + + // Update document title + document.title = `Helios Voting Booth - ${this.election.name}`; + } else { + throw new Error('HELIOS crypto library not loaded'); + } + } + + /** + * Set up answer orderings for each question (supports randomization). + */ + private setupAnswerOrderings(): void { + if (!this.election) return; + + this.election.question_answer_orderings = this.election.questions.map((question, _i) => { + const ordering = question.answers.map((_, j) => j); + + // Shuffle if randomization is enabled at election or question level + if ( + (this.electionMetadata?.randomize_answer_order) || + question.randomize_answer_order + ) { + this.shuffleArray(ordering); + } + + return ordering; + }); + } + + /** + * Fisher-Yates shuffle algorithm. + */ + private shuffleArray(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + /** + * Handle exit button click. + */ + private handleExit(): void { + if (this.currentScreen !== 'election' && this.currentScreen !== 'loading') { + const confirmed = confirm( + 'Are you sure you want to exit the booth and lose all information about your current ballot?' + ); + if (!confirmed) return; + } + + if (this.election?.cast_url) { + window.location.href = this.election.cast_url; + } + } + + /** + * Navigate to a specific screen. + */ + navigateTo(screen: BoothScreen): void { + this.currentScreen = screen; + } + + /** + * Start voting - go to first question. + */ + startVoting(): void { + this.currentQuestionIndex = 0; + this.currentScreen = 'question'; + } + + /** + * Get the current progress step number (1-4). + */ + private getProgressStep(): number { + switch (this.currentScreen) { + case 'question': return 1; + case 'review': return 2; + case 'submit': return 3; + case 'audit': return 4; + default: return 0; + } + } + + render() { + return html` + + + ${this.currentScreen !== 'loading' && this.currentScreen !== 'election' ? html` + + ` : ''} + + ${this.error ? html` + + ` : ''} + +
+ ${this.renderCurrentScreen()} +
+ `; + } + + private renderCurrentScreen() { + switch (this.currentScreen) { + case 'loading': + return html` +
+

Loading election...

+
+ `; + + case 'election': + return this.renderElectionScreen(); + + case 'question': + return html`

Question screen - to be implemented in Phase 2

`; + + case 'review': + return html`

Review screen - to be implemented in Phase 3

`; + + case 'submit': + return html`

Submit screen - to be implemented in Phase 3

`; + + case 'audit': + return html`

Audit screen - to be implemented in Phase 3

`; + + default: + return html`

Unknown screen

`; + } + } + + /** + * Render the election info screen (start screen). + */ + private renderElectionScreen() { + if (!this.election) { + return html` +
+

Loading election information...

+
+ `; + } + + return html` +
+

${this.election.name}

+ + ${this.election.description ? html` +
+

${this.election.description}

+
+ ` : ''} + +
+

To vote, follow these steps:

+
    +
  1. Select your preferred options.
  2. +
  3. Review your choices, which are then encrypted.
  4. +
  5. Submit your encrypted ballot and authenticate to verify your eligibility.
  6. +
+
+ +
+ +
+ + ${this.electionMetadata?.help_email ? html` +

+ You can + + email for help + . +

+ ` : ''} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'booth-app': BoothApp; + } +} +``` + +**Step 3: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors (or only warnings about unused variables which is expected at this stage) + +**Step 4: Commit** + +```bash +git add heliosbooth2026/src/main.ts heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): add main entry point and booth-app shell component" +``` + +--- + +## Task 5: Update index.html with Crypto Script Loading + +**Files:** +- Modify: `heliosbooth2026/index.html` + +**Step 1: Update index.html to load crypto scripts before app** + +Replace `heliosbooth2026/index.html` with: +```html + + + + + + Helios Voting Booth + + + + + + + + + + + + + + + + + + + + +``` + +**Step 2: Verify the app runs in development** + +Run: +```bash +cd heliosbooth2026 && npm run dev +``` +Expected: Vite dev server starts. Visit http://localhost:5173/booth2026/ and see the booth app loading (will show "No election URL provided" error which is expected) + +**Step 3: Commit** + +```bash +git add heliosbooth2026/index.html +git commit -m "feat(booth2026): add crypto library script loading to index.html" +``` + +--- + +## Task 6: Add Django URL Route for booth2026 + +**Files:** +- Modify: `urls.py` (project root) + +**Step 1: Add URL route for booth2026** + +In `urls.py`, add a new re_path after line 12 (the existing booth route): + +Find this line: +```python + re_path(r'booth/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth'}), +``` + +Add after it: +```python + re_path(r'booth2026/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth2026'}), +``` + +**Step 2: Verify the route works** + +Run: +```bash +uv run python manage.py runserver +``` + +In a separate terminal: +```bash +curl -I http://localhost:8000/booth2026/ +``` +Expected: HTTP 200 response (or 304 if cached) + +**Step 3: Commit** + +```bash +git add urls.py +git commit -m "feat(booth2026): add Django URL route for new booth" +``` + +--- + +## Task 7: Verify Complete Phase 1 Setup + +**Files:** None (verification only) + +**Step 1: Run the full test suite** + +Run: +```bash +uv run python manage.py test -v 2 +``` +Expected: All existing tests pass (booth2026 is independent, shouldn't break anything) + +**Step 2: Verify development workflow** + +Run: +```bash +cd heliosbooth2026 && npm run dev +``` +Expected: Vite dev server starts successfully + +In another terminal: +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build completes, `dist/` directory created with bundled files + +**Step 3: Verify crypto types** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No TypeScript errors + +**Step 4: Final commit for phase 1** + +```bash +git add -A +git status +``` +If any uncommitted files, commit them: +```bash +git commit -m "feat(booth2026): complete Phase 1 setup and core shell" +``` + +--- + +## Phase 1 Completion Checklist + +- [ ] `heliosbooth2026/` directory created with Vite + TypeScript + Lit configuration +- [ ] `lib/jscrypto/` contains copied crypto libraries +- [ ] `src/crypto/types.ts` provides TypeScript declarations for crypto globals +- [ ] `src/booth-app.ts` implements main component with screen switching +- [ ] `src/screens/election-screen.ts` implemented inline in booth-app (loads election, shows info, start button) +- [ ] Base CSS structure in place +- [ ] Django URL route serves `/booth2026/` +- [ ] Can load an election from URL parameter +- [ ] Can display election info +- [ ] Can click "Start" to switch screens (navigates to question screen placeholder) +- [ ] All existing tests pass diff --git a/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_02.md b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_02.md new file mode 100644 index 000000000..1b178115e --- /dev/null +++ b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_02.md @@ -0,0 +1,733 @@ +# Helios Booth Lit Redesign - Phase 2: Voting Flow + +> **For Claude:** REQUIRED SUB-SKILL: Use ed3d-plan-and-execute:executing-an-implementation-plan to implement this plan task-by-task. + +**Goal:** Implement complete question navigation and answer selection + +**Architecture:** Question screen component receives election data and current question index via props from booth-app. User selections are tracked in booth-app state and passed down. Events bubble up for navigation and answer changes. + +**Tech Stack:** Lit 3.3.x, TypeScript 5.x + +**Scope:** 4 phases from original design (this is phase 2 of 4) + +**Codebase verified:** 2026-01-18 + +**Dependencies:** Phase 1 must be complete (booth-app, election loading, crypto types) + +--- + +## Task 1: Create Question Screen Component + +**Files:** +- Create: `heliosbooth2026/src/screens/question-screen.ts` + +**Step 1: Create the question screen component** + +Create `heliosbooth2026/src/screens/question-screen.ts`: +```typescript +import { LitElement, html, css } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import type { Question } from '../crypto/types.js'; + +/** + * Events emitted by the question screen. + */ +export interface AnswerChangeEvent { + questionIndex: number; + answerIndex: number; + selected: boolean; +} + +export interface NavigationEvent { + direction: 'previous' | 'next' | 'review'; +} + +/** + * Question screen component for displaying and selecting answers. + */ +@customElement('question-screen') +export class QuestionScreen extends LitElement { + static styles = css` + :host { + display: block; + } + + .question-header { + margin-bottom: var(--spacing-md, 16px); + } + + .question-text { + font-size: var(--font-size-lg, 1.25rem); + font-weight: bold; + white-space: pre-line; + margin-bottom: var(--spacing-sm, 8px); + } + + .question-meta { + font-size: var(--font-size-sm, 0.875rem); + color: var(--color-text-secondary, #666); + } + + .answers-list { + list-style: none; + padding: 0; + margin: var(--spacing-md, 16px) 0; + } + + .answer-item { + padding: var(--spacing-sm, 8px) var(--spacing-md, 16px); + margin-bottom: var(--spacing-xs, 4px); + border: 1px solid var(--color-border, #ddd); + border-radius: var(--border-radius, 4px); + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; + } + + .answer-item:hover { + background-color: var(--color-surface, #f5f5f5); + } + + .answer-item.selected { + background-color: #e3f2fd; + border-color: var(--color-primary, #1a73e8); + } + + .answer-item.disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .answer-item.disabled:hover { + background-color: transparent; + } + + .answer-checkbox { + margin-right: var(--spacing-sm, 8px); + } + + .answer-label { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm, 8px); + } + + .answer-text { + flex: 1; + } + + .answer-link { + font-size: var(--font-size-sm, 0.875rem); + white-space: nowrap; + } + + .warning-box { + color: var(--color-success, #28a745); + text-align: center; + font-size: var(--font-size-sm, 0.875rem); + padding: var(--spacing-sm, 8px); + min-height: 50px; + } + + .navigation { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: var(--spacing-lg, 24px); + padding-top: var(--spacing-md, 16px); + border-top: 1px solid var(--color-border, #ddd); + } + + .nav-left { + display: flex; + gap: var(--spacing-sm, 8px); + } + + .nav-right { + display: flex; + gap: var(--spacing-sm, 8px); + } + `; + + // Props from parent + @property({ type: Object }) question: Question | null = null; + @property({ type: Number }) questionIndex: number = 0; + @property({ type: Number }) totalQuestions: number = 0; + @property({ type: Array }) selectedAnswers: number[] = []; + @property({ type: Array }) answerOrdering: number[] = []; + @property({ type: Boolean }) showReviewButton: boolean = false; + + // Local state + @state() private maxReached: boolean = false; + + /** + * Check if maximum selections reached. + */ + private updateMaxReached(): void { + if (!this.question) return; + this.maxReached = this.question.max !== null && + this.selectedAnswers.length >= this.question.max; + } + + updated(changedProperties: Map): void { + if (changedProperties.has('selectedAnswers') || changedProperties.has('question')) { + this.updateMaxReached(); + } + } + + /** + * Handle answer checkbox click. + */ + private handleAnswerClick(answerIndex: number): void { + const isSelected = this.selectedAnswers.includes(answerIndex); + + // If trying to select but max reached, ignore + if (!isSelected && this.maxReached) { + return; + } + + this.dispatchEvent(new CustomEvent('answer-change', { + detail: { + questionIndex: this.questionIndex, + answerIndex, + selected: !isSelected + }, + bubbles: true, + composed: true + })); + } + + /** + * Handle keyboard interaction on answer items. + */ + private handleAnswerKeydown(event: KeyboardEvent, answerIndex: number): void { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.handleAnswerClick(answerIndex); + } + } + + /** + * Handle navigation button clicks. + */ + private handleNavigation(direction: 'previous' | 'next' | 'review'): void { + this.dispatchEvent(new CustomEvent('navigate', { + detail: { direction }, + bubbles: true, + composed: true + })); + } + + /** + * Build the selection constraint text (e.g., "vote for 1 to 3"). + */ + private getConstraintText(): string { + if (!this.question) return ''; + + const { min, max } = this.question; + + if (min && min > 0) { + if (max) { + return `vote for ${min} to ${max}`; + } + return `vote for at least ${min}`; + } + + if (max) { + if (max > 1) { + return `vote for up to ${max}`; + } + return `vote for ${max}`; + } + + return 'vote for as many as you approve of'; + } + + /** + * Get warning message based on selection state. + */ + private getWarningMessage(): string { + if (!this.question) return ''; + + if (this.maxReached && this.question.max && this.question.max > 1) { + return 'Maximum number of options selected. To change your selection, please de-select a current selection first.'; + } + + if (!this.maxReached && this.question.max && this.selectedAnswers.length < this.question.max) { + return `You may select up to ${this.question.max} choices total.`; + } + + return ''; + } + + render() { + if (!this.question) { + return html`

Loading question...

`; + } + + const ordering = this.answerOrdering.length > 0 + ? this.answerOrdering + : this.question.answers.map((_, i) => i); + + const isFirstQuestion = this.questionIndex === 0; + const isLastQuestion = this.questionIndex === this.totalQuestions - 1; + + return html` +
e.preventDefault()} aria-label="Question ${this.questionIndex + 1} of ${this.totalQuestions}"> +
+
${this.question.question}
+
+ #${this.questionIndex + 1} of ${this.totalQuestions} — + ${this.getConstraintText()} +
+
+ +
    + ${ordering.map(answerIndex => { + const isSelected = this.selectedAnswers.includes(answerIndex); + const isDisabled = !isSelected && this.maxReached; + const answerText = this.question!.answers[answerIndex]; + const answerUrl = this.question!.answer_urls?.[answerIndex]; + + return html` + + `; + })} +
+ +
+ ${this.getWarningMessage()} +
+ + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'question-screen': QuestionScreen; + } +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/screens/question-screen.ts +git commit -m "feat(booth2026): add question-screen component with answer selection" +``` + +--- + +## Task 2: Integrate Question Screen into Booth App + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add import for question-screen** + +At the top of `heliosbooth2026/src/booth-app.ts`, after the existing imports, add: + +```typescript +import './screens/question-screen.js'; +import type { AnswerChangeEvent, NavigationEvent } from './screens/question-screen.js'; +``` + +**Step 2: Add event handlers for answer changes and navigation** + +In the `BoothApp` class, add these methods after the `startVoting()` method: + +```typescript + /** + * Handle answer selection changes from question screen. + */ + private handleAnswerChange(event: CustomEvent): void { + const { questionIndex, answerIndex, selected } = event.detail; + + // Create a new answers array (immutable update) + const newAnswers = [...this.answers]; + const questionAnswers = [...(newAnswers[questionIndex] || [])]; + + if (selected) { + // Add answer if not already present + if (!questionAnswers.includes(answerIndex)) { + questionAnswers.push(answerIndex); + } + } else { + // Remove answer + const idx = questionAnswers.indexOf(answerIndex); + if (idx !== -1) { + questionAnswers.splice(idx, 1); + } + } + + newAnswers[questionIndex] = questionAnswers; + this.answers = newAnswers; + + // Mark this question's encryption as dirty (will be used in Phase 3) + // For now, just track that answers changed + } + + /** + * Validate current question has minimum required selections. + */ + private validateCurrentQuestion(): boolean { + if (!this.election) return false; + + const question = this.election.questions[this.currentQuestionIndex]; + const answers = this.answers[this.currentQuestionIndex] || []; + + if (answers.length < question.min) { + alert(`You need to select at least ${question.min} answer(s).`); + return false; + } + + return true; + } + + /** + * Handle navigation events from question screen. + */ + private handleNavigation(event: CustomEvent): void { + const { direction } = event.detail; + + // Validate before navigating away + if (!this.validateCurrentQuestion()) { + return; + } + + switch (direction) { + case 'previous': + if (this.currentQuestionIndex > 0) { + this.currentQuestionIndex--; + } + break; + + case 'next': + if (this.election && this.currentQuestionIndex < this.election.questions.length - 1) { + // Mark that we've reached the last question when we get there + if (this.currentQuestionIndex === this.election.questions.length - 2) { + this.allQuestionsSeen = true; + } + this.currentQuestionIndex++; + } + break; + + case 'review': + this.currentScreen = 'review'; + break; + } + } + + /** + * Go to a specific question (used for editing from review screen). + */ + goToQuestion(index: number): void { + if (this.election && index >= 0 && index < this.election.questions.length) { + this.currentQuestionIndex = index; + this.currentScreen = 'question'; + } + } +``` + +**Step 3: Update the renderCurrentScreen method** + +In `booth-app.ts`, find the `renderCurrentScreen()` method and update the `'question'` case: + +Replace: +```typescript + case 'question': + return html`

Question screen - to be implemented in Phase 2

`; +``` + +With: +```typescript + case 'question': + return this.renderQuestionScreen(); +``` + +**Step 4: Add the renderQuestionScreen method** + +Add this method to the `BoothApp` class: + +```typescript + /** + * Render the question screen. + */ + private renderQuestionScreen() { + if (!this.election) { + return html`

Loading...

`; + } + + const question = this.election.questions[this.currentQuestionIndex]; + const ordering = this.election.question_answer_orderings?.[this.currentQuestionIndex] + ?? question.answers.map((_, i) => i); + + // Show review button once user has seen the last question + // or if they're on the last question + const showReview = this.allQuestionsSeen || + this.currentQuestionIndex === this.election.questions.length - 1; + + return html` + + `; + } +``` + +**Step 5: Update the startVoting method** + +In `booth-app.ts`, update the `startVoting()` method to mark the first question as seen if there's only one question: + +```typescript + /** + * Start voting - go to first question. + */ + startVoting(): void { + this.currentQuestionIndex = 0; + this.currentScreen = 'question'; + + // If only one question, show review button immediately + if (this.election && this.election.questions.length === 1) { + this.allQuestionsSeen = true; + } + } +``` + +**Step 6: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 7: Commit** + +```bash +git add heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): integrate question-screen with answer tracking and navigation" +``` + +--- + +## Task 3: Add onbeforeunload Warning for In-Progress Ballots + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add beforeunload handler** + +In `booth-app.ts`, add a `disconnectedCallback` method and update `connectedCallback`: + +Find the `connectedCallback` method and update it to add the beforeunload listener: + +```typescript + private boundBeforeUnload = this.handleBeforeUnload.bind(this); + + connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('beforeunload', this.boundBeforeUnload); + this.initializeBooth(); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('beforeunload', this.boundBeforeUnload); + } + + /** + * Handle beforeunload event - warn user about losing ballot data. + */ + private handleBeforeUnload(event: BeforeUnloadEvent): string | undefined { + // Only warn if user has started voting (is on question or later screens) + if (this.currentScreen === 'question' || + this.currentScreen === 'review' || + this.currentScreen === 'audit') { + const message = 'If you leave this page with an in-progress ballot, your ballot will be lost.'; + event.preventDefault(); + event.returnValue = message; + return message; + } + return undefined; + } +``` + +Note: You'll need to add the `boundBeforeUnload` property declaration near the other state declarations. + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): add beforeunload warning for in-progress ballots" +``` + +--- + +## Task 4: Test Question Navigation Flow + +**Files:** None (verification only) + +**Step 1: Start development server** + +Run: +```bash +cd heliosbooth2026 && npm run dev +``` + +**Step 2: Test with a sample election URL** + +To test, you'll need a running Helios server with an election. Start the Django server in another terminal: + +```bash +uv run python manage.py runserver +``` + +Then access the booth with an election URL parameter. If you have a test election UUID, visit: +``` +http://localhost:5173/booth2026/?election_url=http://localhost:8000/helios/elections/ +``` + +**Step 3: Verify these behaviors manually:** + +1. Election info screen shows election name, description, and Start button +2. Clicking Start navigates to question 1 +3. Can select/deselect answers +4. Selection limit enforced (can't select more than max) +5. Previous/Next buttons work +6. Proceed button appears after viewing last question +7. Progress bar updates correctly +8. Browser warns when trying to leave mid-vote + +**Step 4: Run build to ensure production build works** + +Run: +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build completes successfully + +**Step 5: Run Django tests to ensure no regressions** + +Run: +```bash +uv run python manage.py test -v 2 +``` +Expected: All tests pass + +**Step 6: Commit any final adjustments** + +```bash +git add -A +git status +``` +If any uncommitted changes: +```bash +git commit -m "feat(booth2026): complete Phase 2 voting flow implementation" +``` + +--- + +## Phase 2 Completion Checklist + +- [ ] `src/screens/question-screen.ts` created with full answer selection UI +- [ ] Question display shows question text and selection constraints +- [ ] Answer checkboxes work with selection tracking +- [ ] Answer randomization respects election/question settings +- [ ] Min/max answer constraints enforced +- [ ] Warning messages displayed appropriately +- [ ] Previous/Next navigation works +- [ ] Progress indicator shows "Question N of M" +- [ ] Proceed button appears after seeing all questions +- [ ] Browser warns before leaving mid-vote +- [ ] TypeScript compiles without errors +- [ ] Build succeeds +- [ ] All existing Django tests pass diff --git a/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_03.md b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_03.md new file mode 100644 index 000000000..437050f15 --- /dev/null +++ b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_03.md @@ -0,0 +1,1519 @@ +# Helios Booth Lit Redesign - Phase 3: Crypto & Submission + +> **For Claude:** REQUIRED SUB-SKILL: Use ed3d-plan-and-execute:executing-an-implementation-plan to implement this plan task-by-task. + +**Goal:** Implement full voting flow including encryption and ballot submission + +**Architecture:** Web Worker handles encryption in background thread. Booth-app orchestrates worker communication and tracks progress. Review screen shows encrypted ballot summary. Submit screen handles form POST to server. + +**Tech Stack:** Lit 3.3.x, TypeScript 5.x, Web Workers + +**Scope:** 4 phases from original design (this is phase 3 of 4) + +**Codebase verified:** 2026-01-18 + +**Dependencies:** Phase 2 must be complete (question navigation, answer selection) + +--- + +## Task 1: Create Encryption Worker + +**Files:** +- Create: `heliosbooth2026/workers/encryption-worker.js` + +**Step 1: Create the worker file** + +Create `heliosbooth2026/workers/encryption-worker.js`: +```javascript +/** + * Web Worker for encrypting ballots in background thread. + * Adapted from heliosbooth/boothworker-single.js + * + * Message types: + * - setup: Initialize with election JSON + * - encrypt: Encrypt an answer for a specific question + * + * Response types: + * - log: Logging message + * - result: Encrypted answer result + */ + +// Import crypto libraries - paths relative to worker location +importScripts( + '../lib/underscore-min.js', + '../lib/jscrypto/jsbn.js', + '../lib/jscrypto/jsbn2.js', + '../lib/jscrypto/sjcl.js', + '../lib/jscrypto/class.js', + '../lib/jscrypto/bigint.js', + '../lib/jscrypto/random.js', + '../lib/jscrypto/elgamal.js', + '../lib/jscrypto/sha1.js', + '../lib/jscrypto/sha2.js', + '../lib/jscrypto/helios.js' +); + +// Console shim - sends logs back to main thread +var console = { + log: function(msg) { + self.postMessage({ type: 'log', msg: msg }); + } +}; + +// Election object - set during setup +var ELECTION = null; + +/** + * Handle setup message - parse and store election. + */ +function do_setup(message) { + console.log('Setting up encryption worker'); + ELECTION = HELIOS.Election.fromJSONString(message.election); + console.log('Election loaded: ' + ELECTION.name); +} + +/** + * Handle encrypt message - encrypt answer for a question. + */ +function do_encrypt(message) { + console.log('Encrypting answer for question ' + message.q_num); + + var encrypted_answer = new HELIOS.EncryptedAnswer( + ELECTION.questions[message.q_num], + message.answer, + ELECTION.public_key + ); + + console.log('Done encrypting question ' + message.q_num); + + // Send result back to main thread + self.postMessage({ + type: 'result', + q_num: message.q_num, + encrypted_answer: encrypted_answer.toJSONObject(true), + id: message.id + }); +} + +/** + * Message handler - dispatch to appropriate function. + */ +self.onmessage = function(event) { + if (event.data.type === 'setup') { + do_setup(event.data); + } else if (event.data.type === 'encrypt') { + do_encrypt(event.data); + } +}; +``` + +**Step 2: Verify worker file is valid JavaScript** + +Run: +```bash +node --check heliosbooth2026/workers/encryption-worker.js 2>&1 || echo "Note: importScripts is Web Worker API, not available in Node - syntax is OK" +``` +Expected: May show importScripts error (that's OK - it's Web Worker API) + +**Step 3: Commit** + +```bash +git add heliosbooth2026/workers/encryption-worker.js +git commit -m "feat(booth2026): add encryption Web Worker" +``` + +--- + +## Task 2: Add Worker Types to Crypto Types + +**Files:** +- Modify: `heliosbooth2026/src/crypto/types.ts` + +**Step 1: Add worker message types** + +At the end of `heliosbooth2026/src/crypto/types.ts`, add: + +```typescript +// Worker message types +export interface WorkerSetupMessage { + type: 'setup'; + election: string; +} + +export interface WorkerEncryptMessage { + type: 'encrypt'; + q_num: number; + answer: number[]; + id: number; +} + +export type WorkerInMessage = WorkerSetupMessage | WorkerEncryptMessage; + +export interface WorkerLogMessage { + type: 'log'; + msg: string; +} + +export interface WorkerResultMessage { + type: 'result'; + q_num: number; + encrypted_answer: EncryptedAnswerJSON; + id: number; +} + +export type WorkerOutMessage = WorkerLogMessage | WorkerResultMessage; + +// BALLOT helper (from helios.js) +export interface BALLOTType { + pretty_choices(election: Election, ballot: { answers: number[][] }): string[][]; +} + +declare global { + const BALLOT: BALLOTType; +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/crypto/types.ts +git commit -m "feat(booth2026): add worker message types to crypto declarations" +``` + +--- + +## Task 3: Create Review Screen Component + +**Files:** +- Create: `heliosbooth2026/src/screens/review-screen.ts` + +**Step 1: Create the review screen component** + +Create `heliosbooth2026/src/screens/review-screen.ts`: +```typescript +import { LitElement, html, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { Election, ElectionMetadata, Question } from '../crypto/types.js'; + +/** + * Events emitted by the review screen. + */ +export interface ReviewNavigationEvent { + action: 'change-question' | 'cast' | 'audit'; + questionIndex?: number; +} + +/** + * Review screen component - shows ballot summary, hash, and submission options. + */ +@customElement('review-screen') +export class ReviewScreen extends LitElement { + static styles = css` + :host { + display: block; + } + + h2 { + margin-top: 0; + } + + .ballot-summary { + background-color: var(--color-surface, #f5f5f5); + border: 1px solid var(--color-border, #ddd); + border-radius: var(--border-radius, 4px); + padding: var(--spacing-md, 16px); + margin-bottom: var(--spacing-lg, 24px); + } + + .question-summary { + margin-bottom: var(--spacing-md, 16px); + } + + .question-summary:last-child { + margin-bottom: 0; + } + + .question-label { + font-weight: 500; + margin-bottom: var(--spacing-xs, 4px); + } + + .choice { + margin-left: var(--spacing-md, 16px); + padding: var(--spacing-xs, 4px) 0; + } + + .choice::before { + content: '\\2713 '; + color: var(--color-success, #28a745); + } + + .no-choice { + margin-left: var(--spacing-md, 16px); + font-style: italic; + color: var(--color-text-secondary, #666); + } + + .no-choice::before { + content: '\\2610 '; + } + + .selection-info { + font-size: var(--font-size-sm, 0.875rem); + color: var(--color-text-secondary, #666); + margin-left: var(--spacing-md, 16px); + } + + .change-link { + font-size: var(--font-size-sm, 0.875rem); + margin-left: var(--spacing-sm, 8px); + } + + .ballot-tracker { + margin: var(--spacing-lg, 24px) 0; + } + + .tracker-hash { + font-family: monospace; + font-size: var(--font-size-lg, 1.25rem); + word-break: break-all; + background-color: var(--color-surface, #f5f5f5); + padding: var(--spacing-sm, 8px); + border-radius: var(--border-radius, 4px); + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--spacing-md, 16px); + } + + .primary-action { + display: flex; + align-items: center; + gap: var(--spacing-sm, 8px); + } + + .loading-indicator { + display: inline-block; + width: 20px; + height: 20px; + } + + .audit-section { + background-color: lightyellow; + border: 1px solid var(--color-border, #ddd); + padding: var(--spacing-md, 16px); + border-radius: var(--border-radius, 4px); + margin-top: var(--spacing-lg, 24px); + max-width: 400px; + } + + .audit-section h4 { + margin: 0 0 var(--spacing-sm, 8px) 0; + cursor: pointer; + } + + .audit-section h4:hover { + text-decoration: underline; + } + + .audit-content { + font-size: var(--font-size-sm, 0.875rem); + } + + .audit-content p { + margin: var(--spacing-sm, 8px) 0; + } + `; + + @property({ type: Object }) election: Election | null = null; + @property({ type: Object }) electionMetadata: ElectionMetadata | null = null; + @property({ type: Array }) questions: Question[] = []; + @property({ type: Array }) choices: string[][] = []; + @property({ type: String }) ballotHash: string = ''; + @property({ type: String }) encryptedVoteJson: string = ''; + @property({ type: Boolean }) isLoading: boolean = false; + @property({ type: Boolean }) showAuditSection: boolean = false; + + private auditExpanded: boolean = false; + + /** + * Handle change question link click. + */ + private handleChangeQuestion(index: number, event: Event): void { + event.preventDefault(); + this.dispatchEvent(new CustomEvent('review-navigate', { + detail: { action: 'change-question', questionIndex: index }, + bubbles: true, + composed: true + })); + } + + /** + * Handle cast ballot button click. + */ + private handleCast(): void { + this.dispatchEvent(new CustomEvent('review-navigate', { + detail: { action: 'cast' }, + bubbles: true, + composed: true + })); + } + + /** + * Handle audit ballot button click. + */ + private handleAudit(): void { + this.dispatchEvent(new CustomEvent('review-navigate', { + detail: { action: 'audit' }, + bubbles: true, + composed: true + })); + } + + /** + * Toggle audit section visibility. + */ + private toggleAuditSection(): void { + this.auditExpanded = !this.auditExpanded; + this.requestUpdate(); + } + + render() { + return html` +

Review your Ballot

+ +
+ ${this.questions.map((question, index) => html` +
+
+ Question #${index + 1}: ${question.short_name} + this.handleChangeQuestion(index, e)}> + [change] + +
+ + ${this.choices[index]?.length === 0 ? html` +
No choice selected
+ ` : this.choices[index]?.map(choice => html` +
${choice}
+ `)} + + ${this.choices[index]?.length < question.max ? html` +
+ [${this.choices[index]?.length || 0} selections out of possible ${question.min}-${question.max}] +
+ ` : ''} +
+ `)} +
+ +
+

Your ballot tracker is:

+
+ ${this.ballotHash || 'Calculating...'} +
+
+ +
+
+ + ${this.isLoading ? html` + + ` : ''} +
+
+ + + + + ${this.showAuditSection ? html` +
+

+ Spoil & Audit + [optional] +

+ ${this.auditExpanded ? html` +
+

+ If you choose, you can spoil this ballot and reveal how your choices + were encrypted. This is an optional auditing process. +

+

+ You will then be guided to re-encrypt your choices for final casting. +

+ +
+ ` : ''} +
+ ` : ''} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'review-screen': ReviewScreen; + } +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/screens/review-screen.ts +git commit -m "feat(booth2026): add review-screen component with ballot summary" +``` + +--- + +## Task 4: Create Submit Screen Component + +**Files:** +- Create: `heliosbooth2026/src/screens/submit-screen.ts` + +**Step 1: Create the submit screen component** + +Create `heliosbooth2026/src/screens/submit-screen.ts`: +```typescript +import { LitElement, html, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { Election } from '../crypto/types.js'; + +/** + * Submit screen component - final confirmation before ballot submission. + */ +@customElement('submit-screen') +export class SubmitScreen extends LitElement { + static styles = css` + :host { + display: block; + } + + h2 { + margin-top: 0; + } + + .info-section { + margin-bottom: var(--spacing-lg, 24px); + } + + .info-section p { + margin: var(--spacing-sm, 8px) 0; + } + + .tracker-section { + background-color: var(--color-surface, #f5f5f5); + padding: var(--spacing-md, 16px); + border-radius: var(--border-radius, 4px); + margin-bottom: var(--spacing-lg, 24px); + } + + .tracker-hash { + font-family: monospace; + font-size: 1.1rem; + word-break: break-all; + } + + .cast-url { + font-family: monospace; + font-size: 1.1rem; + word-break: break-all; + background-color: var(--color-surface, #f5f5f5); + padding: var(--spacing-sm, 8px); + border-radius: var(--border-radius, 4px); + } + + .submit-form { + margin-top: var(--spacing-lg, 24px); + } + `; + + @property({ type: Object }) election: Election | null = null; + @property({ type: String }) ballotHash: string = ''; + @property({ type: String }) encryptedVoteJson: string = ''; + + /** + * Handle form submission - allow beforeunload to pass. + */ + private handleSubmit(): void { + // Dispatch event to let booth-app know we're submitting + this.dispatchEvent(new CustomEvent('ballot-submit', { + bubbles: true, + composed: true + })); + } + + render() { + if (!this.election) { + return html`

Loading...

`; + } + + return html` +

Submit Your Encrypted Ballot

+ +
+

+ All information, other than your encrypted ballot, + has been removed from memory. +

+
+ +
+

As a reminder, your ballot tracking number is:

+

${this.ballotHash}

+
+ +
+

According to the election definition file, this ballot will be submitted to:

+

${this.election.cast_url}

+

where you will log in to validate your eligibility to vote.

+
+ +
+ + + + + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'submit-screen': SubmitScreen; + } +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/screens/submit-screen.ts +git commit -m "feat(booth2026): add submit-screen component" +``` + +--- + +## Task 5: Create Audit Screen Component + +**Files:** +- Create: `heliosbooth2026/src/screens/audit-screen.ts` + +**Step 1: Create the audit screen component** + +Create `heliosbooth2026/src/screens/audit-screen.ts`: +```typescript +import { LitElement, html, css } from 'lit'; +import { customElement, property, query } from 'lit/decorators.js'; + +/** + * Events emitted by the audit screen. + */ +export interface AuditNavigationEvent { + action: 'back-to-voting' | 'post-audit'; +} + +/** + * Audit screen component - displays audit trail for spoiled ballots. + */ +@customElement('audit-screen') +export class AuditScreen extends LitElement { + static styles = css` + :host { + display: block; + } + + h2 { + margin-top: 0; + } + + .warning { + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: var(--border-radius, 4px); + padding: var(--spacing-md, 16px); + margin-bottom: var(--spacing-lg, 24px); + } + + .warning strong { + text-decoration: underline; + } + + .explanation { + margin-bottom: var(--spacing-lg, 24px); + } + + .explanation p { + margin: var(--spacing-sm, 8px) 0; + } + + .audit-trail-section { + margin-bottom: var(--spacing-lg, 24px); + } + + .audit-textarea { + width: 100%; + min-height: 200px; + font-family: monospace; + font-size: var(--font-size-sm, 0.875rem); + padding: var(--spacing-sm, 8px); + border: 1px solid var(--color-border, #ddd); + border-radius: var(--border-radius, 4px); + resize: vertical; + } + + .instructions { + margin: var(--spacing-md, 16px) 0; + font-size: var(--font-size-sm, 0.875rem); + } + + .actions { + display: flex; + gap: var(--spacing-md, 16px); + flex-wrap: wrap; + align-items: center; + } + + .post-note { + margin-top: var(--spacing-md, 16px); + font-size: var(--font-size-sm, 0.875rem); + color: var(--color-text-secondary, #666); + } + + .post-note strong { + color: var(--color-text, #333); + } + `; + + @property({ type: String }) auditTrail: string = ''; + @property({ type: String }) electionUrl: string = ''; + @property({ type: Boolean }) postingAudit: boolean = false; + + @query('#audit_trail') private auditTextarea!: HTMLTextAreaElement; + + /** + * Select all text in the audit trail textarea. + */ + private selectAuditTrail(event: Event): void { + event.preventDefault(); + if (this.auditTextarea) { + this.auditTextarea.select(); + } + } + + /** + * Handle back to voting button click. + */ + private handleBackToVoting(): void { + this.dispatchEvent(new CustomEvent('audit-navigate', { + detail: { action: 'back-to-voting' }, + bubbles: true, + composed: true + })); + } + + /** + * Handle post audited ballot button click. + */ + private handlePostAudit(): void { + this.dispatchEvent(new CustomEvent('audit-navigate', { + detail: { action: 'post-audit' }, + bubbles: true, + composed: true + })); + } + + render() { + const verifierUrl = this.electionUrl + ? `single-ballot-verify.html?election_url=${encodeURIComponent(this.electionUrl)}` + : '#'; + + return html` +

Your audited ballot

+ + + +
+

+ Why? Helios prevents you from auditing and casting the + same ballot to provide you with some protection against coercion. +

+ +

+ Now what? + Select your ballot audit info, + copy it to your clipboard, then use the + + ballot verifier + + to verify it. +

+

+ Once you're satisfied, click the "back to voting" button to re-encrypt + and cast your ballot. +

+
+ +
+ +
+ +
+ Before going back to voting, you can post this audited ballot to the + Helios tracking center so that others might double-check the verification + of this ballot. +
+ +
+ + Even if you post your audited ballot, you must go back to voting and + choose "cast" if you want your vote to count. + +
+ +
+ + + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'audit-screen': AuditScreen; + } +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/screens/audit-screen.ts +git commit -m "feat(booth2026): add audit-screen component for ballot spoiling" +``` + +--- + +## Task 6: Create Encrypting Screen Component + +**Files:** +- Create: `heliosbooth2026/src/screens/encrypting-screen.ts` + +**Step 1: Create the encrypting screen component** + +Create `heliosbooth2026/src/screens/encrypting-screen.ts`: +```typescript +import { LitElement, html, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +/** + * Encrypting screen component - shows progress during ballot encryption. + */ +@customElement('encrypting-screen') +export class EncryptingScreen extends LitElement { + static styles = css` + :host { + display: block; + text-align: center; + padding: var(--spacing-xl, 32px); + } + + h2 { + margin-bottom: var(--spacing-lg, 24px); + } + + .spinner { + margin: var(--spacing-lg, 24px) 0; + } + + .spinner img { + width: 64px; + height: 64px; + } + + .progress { + font-size: var(--font-size-lg, 1.25rem); + margin-top: var(--spacing-md, 16px); + } + + .note { + color: var(--color-text-secondary, #666); + margin-top: var(--spacing-lg, 24px); + } + `; + + @property({ type: Number }) percentDone: number = 0; + + render() { + return html` +

Helios is now encrypting your ballot

+ + + +
+ ${this.percentDone}% complete +
+ +

+ This may take up to two minutes. +

+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'encrypting-screen': EncryptingScreen; + } +} +``` + +**Step 2: Copy the encrypting.gif from old booth** + +```bash +cp heliosbooth/encrypting.gif heliosbooth2026/ +cp heliosbooth/loading.gif heliosbooth2026/ +``` + +**Step 3: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 4: Commit** + +```bash +git add heliosbooth2026/src/screens/encrypting-screen.ts heliosbooth2026/encrypting.gif heliosbooth2026/loading.gif +git commit -m "feat(booth2026): add encrypting-screen with progress display" +``` + +--- + +## Task 7: Integrate Crypto Screens into Booth App + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add screen imports** + +At the top of `heliosbooth2026/src/booth-app.ts`, after the existing screen import, add: + +```typescript +import './screens/review-screen.js'; +import './screens/submit-screen.js'; +import './screens/audit-screen.js'; +import './screens/encrypting-screen.js'; +import type { ReviewNavigationEvent } from './screens/review-screen.js'; +import type { AuditNavigationEvent } from './screens/audit-screen.js'; +import type { WorkerOutMessage, EncryptedAnswerJSON } from './crypto/types.js'; +``` + +**Step 2: Update the BoothScreen type** + +Find the `BoothScreen` type definition and update it to include 'encrypting': + +```typescript +type BoothScreen = 'loading' | 'election' | 'question' | 'encrypting' | 'review' | 'submit' | 'audit'; +``` + +**Step 3: Add encryption-related state properties** + +In the BoothApp class, add these new state properties after the existing ones: + +```typescript + // Encryption state + @state() private worker: Worker | null = null; + @state() private encryptionProgress: number = 0; + @state() private answerTimestamps: number[] = []; + @state() private dirty: boolean[] = []; + @state() private encryptedBallot: unknown = null; // Full encrypted vote object + @state() private auditTrail: string = ''; + @state() private rawElectionJson: string = ''; + @state() private postingAudit: boolean = false; +``` + +**Step 4: Add worker initialization method** + +Add these methods to the BoothApp class: + +```typescript + /** + * Initialize the encryption worker. + */ + private initializeWorker(): void { + if (this.worker || !this.rawElectionJson) return; + + this.worker = new Worker('/workers/encryption-worker.js'); + + this.worker.onmessage = (event: MessageEvent) => { + if (event.data.type === 'log') { + console.log('[Worker]', event.data.msg); + } else if (event.data.type === 'result') { + this.handleEncryptionResult(event.data.q_num, event.data.encrypted_answer, event.data.id); + } + }; + + // Send election to worker + this.worker.postMessage({ + type: 'setup', + election: this.rawElectionJson + }); + + // Initialize dirty tracking + if (this.election) { + this.dirty = this.election.questions.map(() => true); + this.answerTimestamps = this.election.questions.map(() => 0); + } + } + + /** + * Handle encryption result from worker. + */ + private handleEncryptionResult(qNum: number, encryptedAnswer: EncryptedAnswerJSON, id: number): void { + // Check timestamp to avoid race conditions + if (id !== this.answerTimestamps[qNum]) { + console.log('Ignoring stale encryption result for question', qNum); + return; + } + + // Store encrypted answer + if (typeof HELIOS !== 'undefined' && this.election) { + const ea = HELIOS.EncryptedAnswer.fromJSONObject(encryptedAnswer, this.election); + this.encryptedAnswers = [...this.encryptedAnswers]; + this.encryptedAnswers[qNum] = ea; + } + + // Update progress + const done = this.encryptedAnswers.filter(a => a !== null).length; + this.encryptionProgress = Math.round((done / this.encryptedAnswers.length) * 100); + + // Check if all done + if (done === this.encryptedAnswers.length) { + this.finalizeEncryption(); + } + } + + /** + * Launch async encryption for a specific question. + */ + private launchAsyncEncryption(questionNum: number): void { + if (!this.worker) return; + + const timestamp = Date.now(); + this.answerTimestamps[questionNum] = timestamp; + this.encryptedAnswers[questionNum] = null; + this.dirty[questionNum] = false; + + this.worker.postMessage({ + type: 'encrypt', + q_num: questionNum, + answer: this.answers[questionNum] || [], + id: timestamp + }); + } + + /** + * Start encryption process - seal the ballot. + */ + private sealBallot(): void { + this.currentScreen = 'encrypting'; + this.encryptionProgress = 0; + + // Launch encryption for all dirty questions + this.dirty.forEach((isDirty, qNum) => { + if (isDirty || this.encryptedAnswers[qNum] === null) { + this.launchAsyncEncryption(qNum); + } + }); + + // If nothing to encrypt (all cached), finalize immediately + const allDone = this.encryptedAnswers.every(a => a !== null); + if (allDone) { + this.finalizeEncryption(); + } + } + + /** + * Finalize encryption after all answers are encrypted. + */ + private finalizeEncryption(): void { + if (!this.election || typeof HELIOS === 'undefined') return; + + // Create the full encrypted ballot from individual answers + this.encryptedBallot = HELIOS.EncryptedVote.fromEncryptedAnswers( + this.election, + this.encryptedAnswers as EncryptedAnswer[] + ); + + // Serialize and hash + const ballotObj = (this.encryptedBallot as EncryptedVote).toJSONObject(); + this.encryptedVoteJson = JSON.stringify(ballotObj); + this.encryptedBallotHash = b64_sha256(this.encryptedVoteJson); + + // Navigate to review + this.currentScreen = 'review'; + } + + /** + * Get pretty choices for display. + */ + private getPrettyChoices(): string[][] { + if (!this.election || typeof BALLOT === 'undefined') { + return []; + } + return BALLOT.pretty_choices(this.election, { answers: this.answers }); + } +``` + +**Step 5: Update the loadElection method** + +In the `loadElection` method, store the raw JSON and initialize the worker. Find where it fetches the election and add: + +After the line: +```typescript + const rawJson = await electionResponse.text(); +``` + +Add: +```typescript + this.rawElectionJson = rawJson; +``` + +And at the end of `loadElection`, after `document.title` is set, add: + +```typescript + // Initialize encryption worker + this.initializeWorker(); +``` + +**Step 6: Add handler for validateCurrentQuestion to mark dirty** + +Update the `handleAnswerChange` method to mark the question as dirty: + +At the end of `handleAnswerChange`, add: + +```typescript + // Mark this question's encryption as dirty + if (this.dirty.length > questionIndex) { + this.dirty = [...this.dirty]; + this.dirty[questionIndex] = true; + } +``` + +**Step 7: Update handleNavigation to trigger encryption on review** + +In the `handleNavigation` method, update the `'review'` case: + +```typescript + case 'review': + this.sealBallot(); + break; +``` + +**Step 8: Add handlers for review and audit navigation** + +Add these methods: + +```typescript + /** + * Handle navigation events from review screen. + */ + private handleReviewNavigation(event: CustomEvent): void { + const { action, questionIndex } = event.detail; + + switch (action) { + case 'change-question': + if (typeof questionIndex === 'number') { + this.goToQuestion(questionIndex); + } + break; + + case 'cast': + this.prepareForCast(); + break; + + case 'audit': + this.auditBallot(); + break; + } + } + + /** + * Prepare for casting - clear plaintexts and go to submit screen. + */ + private prepareForCast(): void { + // Clear plaintexts from answers (security measure) + this.answers = this.answers.map(() => []); + + // Clear plaintexts from encrypted ballot + if (this.encryptedBallot && typeof (this.encryptedBallot as EncryptedVote).clearPlaintexts === 'function') { + (this.encryptedBallot as EncryptedVote).clearPlaintexts(); + } + + // Clear audit trail + this.auditTrail = ''; + + this.currentScreen = 'submit'; + } + + /** + * Audit the ballot - show audit trail. + */ + private auditBallot(): void { + if (!this.encryptedBallot) return; + + // Get audit trail (includes plaintexts and randomness) + const auditObj = (this.encryptedBallot as EncryptedVote).toJSONObject(true); + this.auditTrail = JSON.stringify(auditObj, null, 2); + + this.currentScreen = 'audit'; + } + + /** + * Handle navigation events from audit screen. + */ + private handleAuditNavigation(event: CustomEvent): void { + const { action } = event.detail; + + switch (action) { + case 'back-to-voting': + this.resetAndReencrypt(); + break; + + case 'post-audit': + this.postAuditedBallot(); + break; + } + } + + /** + * Reset encryption and go back to re-encrypt. + */ + private resetAndReencrypt(): void { + // Mark all answers as dirty to force re-encryption + this.dirty = this.dirty.map(() => true); + this.encryptedAnswers = this.encryptedAnswers.map(() => null); + this.encryptedBallot = null; + this.encryptedBallotHash = ''; + this.encryptedVoteJson = ''; + this.auditTrail = ''; + + // Go back to seal ballot (re-encrypt) + this.sealBallot(); + } + + /** + * Post audited ballot to tracking center. + */ + private async postAuditedBallot(): Promise { + if (!this.electionUrl || !this.auditTrail) return; + + this.postingAudit = true; + + try { + const response = await fetch(`${this.electionUrl}/post-audited-ballot`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `audited_ballot=${encodeURIComponent(this.auditTrail)}` + }); + + if (response.ok) { + alert('This audited ballot has been posted.\nRemember, this vote will only be used for auditing and will not be tallied.\nClick "back to voting" and cast a new ballot to make sure your vote counts.'); + } else { + alert('Failed to post audited ballot. Please try again.'); + } + } catch (err) { + alert('Failed to post audited ballot: ' + (err instanceof Error ? err.message : String(err))); + } finally { + this.postingAudit = false; + } + } + + /** + * Handle ballot submission. + */ + private handleBallotSubmit(): void { + // Allow the page to unload + // The beforeunload handler checks currentScreen + this.currentScreen = 'loading'; // Temporarily set to allow navigation + } +``` + +**Step 9: Add required import for EncryptedVote** + +In the types import, make sure to include `EncryptedVote`: + +```typescript +import type { Election, ElectionMetadata, EncryptedAnswer, EncryptedVote } from './crypto/types.js'; +``` + +**Step 10: Update renderCurrentScreen method** + +Update the `renderCurrentScreen` method with the new cases: + +```typescript + private renderCurrentScreen() { + switch (this.currentScreen) { + case 'loading': + return html` +
+

Loading election...

+
+ `; + + case 'election': + return this.renderElectionScreen(); + + case 'question': + return this.renderQuestionScreen(); + + case 'encrypting': + return html` + + `; + + case 'review': + return html` + + `; + + case 'submit': + return html` + + `; + + case 'audit': + return html` + + `; + + default: + return html`

Unknown screen

`; + } + } +``` + +**Step 11: Update the getProgressStep method** + +```typescript + private getProgressStep(): number { + switch (this.currentScreen) { + case 'question': return 1; + case 'encrypting': + case 'review': return 2; + case 'submit': return 3; + case 'audit': return 4; + default: return 0; + } + } +``` + +**Step 12: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors (or only minor type adjustments needed) + +**Step 13: Commit** + +```bash +git add heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): integrate crypto screens and encryption workflow" +``` + +--- + +## Task 8: Verify Complete Phase 3 Implementation + +**Files:** None (verification only) + +**Step 1: Run TypeScript check** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 2: Run the build** + +Run: +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build completes successfully + +**Step 3: Run Django tests** + +Run: +```bash +uv run python manage.py test -v 2 +``` +Expected: All tests pass + +**Step 4: Manual testing** + +Start both servers: +```bash +# Terminal 1 +cd heliosbooth2026 && npm run dev + +# Terminal 2 +uv run python manage.py runserver +``` + +Test with a real election: +1. Create a test election in Helios or use an existing one +2. Access `http://localhost:5173/booth2026/?election_url=http://localhost:8000/helios/elections/` +3. Verify complete flow: election info → questions → encryption → review → submit/audit + +**Step 5: Final commit** + +```bash +git add -A +git status +``` +If any uncommitted changes: +```bash +git commit -m "feat(booth2026): complete Phase 3 crypto and submission implementation" +``` + +--- + +## Phase 3 Completion Checklist + +- [ ] `workers/encryption-worker.js` created and adapted from boothworker-single.js +- [ ] Worker message types added to crypto/types.ts +- [ ] `review-screen.ts` shows encryption progress, ballot summary, seal/audit/submit buttons +- [ ] `submit-screen.ts` displays ballot hash, provides cast form +- [ ] `audit-screen.ts` shows audit trail JSON, back-to-voting flow +- [ ] `encrypting-screen.ts` shows encryption progress +- [ ] Encryption orchestration in booth-app with worker communication +- [ ] Progress tracking during encryption +- [ ] Can encrypt ballot and see progress percentage +- [ ] Can view ballot hash/tracker +- [ ] Can submit via form POST +- [ ] Can audit and re-vote (spoil ballot, re-encrypt) +- [ ] Can post audited ballot to tracking center +- [ ] TypeScript compiles without errors +- [ ] Build succeeds +- [ ] All Django tests pass diff --git a/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_04.md b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_04.md new file mode 100644 index 000000000..61084a055 --- /dev/null +++ b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_04.md @@ -0,0 +1,900 @@ +# Helios Booth Lit Redesign - Phase 4: Polish & Deployment + +> **For Claude:** REQUIRED SUB-SKILL: Use ed3d-plan-and-execute:executing-an-implementation-plan to implement this plan task-by-task. + +**Goal:** Production-ready booth with accessibility, error handling, and deployment configuration + +**Architecture:** Enhance existing components with ARIA labels, keyboard navigation, error states, and loading indicators. Configure Vite for production builds served at `/booth2026/`. + +**Tech Stack:** Lit 3.3.x, TypeScript 5.x, Vite 6.x + +**Scope:** 4 phases from original design (this is phase 4 of 4) + +**Codebase verified:** 2026-01-18 + +**Dependencies:** Phase 3 must be complete (full voting flow working) + +--- + +## Task 1: Enhance Accessibility in Booth App + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add skip link and improve landmark structure** + +In the `render()` method of `booth-app.ts`, update to include skip links and proper ARIA landmarks: + +Find the opening of the render() return and update: + +```typescript + render() { + return html` + + + + + ${this.currentScreen !== 'loading' && this.currentScreen !== 'election' ? html` + + ` : ''} + + ${this.error ? html` + + ` : ''} + +
+ ${this.renderCurrentScreen()} +
+ `; + } +``` + +**Step 2: Add skip-link styles** + +Add to the static styles in booth-app.ts: + +```css + .skip-link { + position: absolute; + top: -40px; + left: 0; + background: var(--color-primary, #1a73e8); + color: white; + padding: var(--spacing-sm, 8px) var(--spacing-md, 16px); + z-index: 100; + text-decoration: none; + } + + .skip-link:focus { + top: 0; + } +``` + +**Step 3: Add focus management for screen transitions** + +Add a method to manage focus when screens change: + +```typescript + /** + * Focus the main content area when screen changes. + */ + private focusMainContent(): void { + // Use requestAnimationFrame to ensure DOM has updated + requestAnimationFrame(() => { + const main = this.shadowRoot?.querySelector('#main-content') as HTMLElement; + if (main) { + main.focus(); + } + }); + } +``` + +Update the `navigateTo` method to call this: + +```typescript + navigateTo(screen: BoothScreen): void { + this.currentScreen = screen; + this.focusMainContent(); + } +``` + +Also update `sealBallot`, `prepareForCast`, `auditBallot`, and other screen transition methods to call `this.focusMainContent()` at the end. + +**Step 4: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 5: Commit** + +```bash +git add heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): enhance accessibility with skip links and focus management" +``` + +--- + +## Task 2: Add Error Handling and Loading States + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add loading state property** + +Add to the state properties: + +```typescript + @state() private isInitializing: boolean = true; +``` + +**Step 2: Create error screen renderer** + +Add a method to render error states: + +```typescript + /** + * Render error screen with recovery options. + */ + private renderErrorScreen() { + return html` + + `; + } +``` + +**Step 3: Add error screen styles** + +Add to static styles: + +```css + .error-screen { + text-align: center; + padding: var(--spacing-xl, 32px); + } + + .error-message { + background-color: #fee; + border: 1px solid var(--color-error, #dc3545); + color: var(--color-error, #dc3545); + padding: var(--spacing-md, 16px); + border-radius: var(--border-radius, 4px); + margin: var(--spacing-lg, 24px) 0; + } + + .error-actions { + display: flex; + gap: var(--spacing-md, 16px); + justify-content: center; + } +``` + +**Step 4: Improve initializeBooth error handling** + +Update the `initializeBooth` method to handle various error cases: + +```typescript + private async initializeBooth(): Promise { + this.isInitializing = true; + this.error = null; + + try { + // Get election URL from query params + const params = new URLSearchParams(window.location.search); + const electionUrl = params.get('election_url'); + + if (!electionUrl) { + this.error = 'No election URL provided. Please access this page from an election link.'; + this.currentScreen = 'election'; + this.isInitializing = false; + return; + } + + this.electionUrl = electionUrl; + + // Wait for BigInt crypto to be ready with timeout + await this.waitForCryptoWithTimeout(10000); + this.cryptoReady = true; + + // Load election data + await this.loadElection(electionUrl); + + this.currentScreen = 'election'; + } catch (err) { + console.error('Booth initialization failed:', err); + + if (err instanceof Error) { + if (err.message.includes('fetch')) { + this.error = 'Unable to connect to the election server. Please check your internet connection and try again.'; + } else if (err.message.includes('crypto') || err.message.includes('BigInt')) { + this.error = 'Failed to initialize cryptographic libraries. Please try using a different browser.'; + } else { + this.error = `Failed to initialize booth: ${err.message}`; + } + } else { + this.error = 'An unexpected error occurred. Please reload the page.'; + } + + this.currentScreen = 'election'; + } finally { + this.isInitializing = false; + } + } + + /** + * Wait for crypto with a timeout. + */ + private waitForCryptoWithTimeout(timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error('Crypto library initialization timed out')); + }, timeoutMs); + + this.waitForCrypto() + .then(() => { + clearTimeout(timeoutId); + resolve(); + }) + .catch((err) => { + clearTimeout(timeoutId); + reject(err); + }); + }); + } +``` + +**Step 5: Update renderCurrentScreen to handle errors** + +Update the loading case to show initialization progress: + +```typescript + case 'loading': + return html` +
+

${this.isInitializing ? 'Initializing voting booth...' : 'Loading...'}

+

This may take a few seconds

+
+ `; +``` + +Also update the election case to show errors properly: + +```typescript + case 'election': + if (this.error && !this.election) { + return this.renderErrorScreen(); + } + return this.renderElectionScreen(); +``` + +**Step 6: Commit** + +```bash +git add heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): improve error handling and loading states" +``` + +--- + +## Task 3: Add Responsive CSS and Visual Polish + +**Files:** +- Modify: `heliosbooth2026/src/styles/booth.css` + +**Step 1: Update the base CSS with responsive styles** + +Replace the content of `heliosbooth2026/src/styles/booth.css`: + +```css +/* Helios Booth 2026 - Base Styles */ + +:root { + --color-primary: #1a73e8; + --color-primary-dark: #1557b0; + --color-background: #fff; + --color-surface: #f5f5f5; + --color-border: #ddd; + --color-text: #333; + --color-text-secondary: #666; + --color-success: #28a745; + --color-warning: #ffc107; + --color-error: #dc3545; + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.25rem; + --font-size-xl: 1.5rem; + --border-radius: 4px; + --max-width: 800px; +} + +* { + box-sizing: border-box; +} + +html { + font-size: 16px; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-family); + font-size: var(--font-size-md); + color: var(--color-text); + background-color: var(--color-background); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Focus visible for keyboard navigation */ +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Remove default focus for mouse users */ +:focus:not(:focus-visible) { + outline: none; +} + +/* Utility classes */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Button styles */ +button, +.button { + display: inline-block; + padding: var(--spacing-sm) var(--spacing-md); + font-family: inherit; + font-size: var(--font-size-md); + font-weight: 500; + text-align: center; + text-decoration: none; + color: #fff; + background-color: var(--color-primary); + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: background-color 0.2s ease, transform 0.1s ease; + min-height: 44px; /* Touch target size */ + min-width: 44px; +} + +button:hover, +.button:hover { + background-color: var(--color-primary-dark); +} + +button:active, +.button:active { + transform: scale(0.98); +} + +button:disabled, +.button:disabled { + background-color: #ccc; + cursor: not-allowed; + transform: none; +} + +button:focus-visible, +.button:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Secondary button style */ +button.secondary, +.button.secondary { + background-color: transparent; + color: var(--color-primary); + border: 1px solid var(--color-primary); +} + +button.secondary:hover, +.button.secondary:hover { + background-color: var(--color-surface); +} + +button.secondary:disabled, +.button.secondary:disabled { + color: #999; + border-color: #ccc; + background-color: transparent; +} + +/* Link styles */ +a { + color: var(--color-primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Form elements */ +input[type="checkbox"] { + width: 20px; + height: 20px; + cursor: pointer; +} + +textarea { + font-family: monospace; + font-size: var(--font-size-sm); +} + +/* Responsive breakpoints */ +@media (max-width: 600px) { + :root { + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 24px; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + } + + button, + .button { + width: 100%; + justify-content: center; + } +} + +/* Print styles */ +@media print { + body { + background: white; + color: black; + } + + button, + .button { + display: none; + } +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --color-border: #000; + --color-text-secondary: #000; + } + + button, + .button { + border: 2px solid currentColor; + } +} +``` + +**Step 2: Commit** + +```bash +git add heliosbooth2026/src/styles/booth.css +git commit -m "feat(booth2026): add responsive CSS and visual polish" +``` + +--- + +## Task 4: Configure Vite for Production Build + +**Files:** +- Modify: `heliosbooth2026/vite.config.ts` +- Modify: `heliosbooth2026/index.html` + +**Step 1: Update vite.config.ts for production** + +Update `heliosbooth2026/vite.config.ts`: + +```typescript +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + root: '.', + base: '/booth2026/', + build: { + outDir: 'dist', + // Ensure single bundle for offline operation + cssCodeSplit: false, + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html') + }, + output: { + // Force everything into single bundle + manualChunks: () => 'booth', + entryFileNames: 'assets/booth.[hash].js', + chunkFileNames: 'assets/booth.[hash].js', + assetFileNames: 'assets/booth.[hash].[ext]' + } + }, + // Generate sourcemaps for debugging + sourcemap: true, + // Minify for production + minify: 'terser', + terserOptions: { + compress: { + drop_console: false, // Keep console for debugging crypto issues + drop_debugger: true + } + } + }, + server: { + port: 5173, + // Proxy API requests to Django during development + proxy: { + '/helios': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + }, + // Copy static assets + publicDir: 'public' +}); +``` + +**Step 2: Update index.html for production paths** + +Update `heliosbooth2026/index.html`: + +```html + + + + + + + + Helios Voting Booth + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**Step 3: Create public directory for static assets** + +```bash +mkdir -p heliosbooth2026/public +mv heliosbooth2026/encrypting.gif heliosbooth2026/public/ +mv heliosbooth2026/loading.gif heliosbooth2026/public/ +``` + +**Step 4: Update worker path for production** + +In `booth-app.ts`, update the worker initialization to use the correct path: + +```typescript + this.worker = new Worker(new URL('/booth2026/workers/encryption-worker.js', import.meta.url)); +``` + +**Step 5: Verify build works** + +Run: +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build completes with dist/ containing bundled assets + +**Step 6: Commit** + +```bash +git add heliosbooth2026/vite.config.ts heliosbooth2026/index.html heliosbooth2026/public/ +git commit -m "feat(booth2026): configure Vite for production build" +``` + +--- + +## Task 5: Update Django URL Configuration for Production + +**Files:** +- Modify: `urls.py` (project root) + +**Step 1: Update URL route to serve both dev and built assets** + +The existing route should work for both development (serving source files) and production (serving built dist files). Update `urls.py`: + +Find the booth2026 route and update the comment: + +```python + # New Lit-based booth (serves source files in dev, built files in production) + # In production, this should point to heliosbooth2026/dist instead + re_path(r'booth2026/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth2026'}), +``` + +For production deployment, this would be changed to: +```python + re_path(r'booth2026/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth2026/dist'}), +``` + +**Step 2: Verify routing works** + +Run: +```bash +uv run python manage.py runserver +``` + +Visit http://localhost:8000/booth2026/ to verify the page loads. + +**Step 3: Commit** + +```bash +git add urls.py +git commit -m "docs(booth2026): add production deployment comment to URL config" +``` + +--- + +## Task 6: Verify Offline Operation + +**Files:** None (verification only) + +**Step 1: Build production assets** + +```bash +cd heliosbooth2026 && npm run build +``` + +**Step 2: Test offline scenario** + +1. Start Django server: + ```bash + uv run python manage.py runserver + ``` + +2. Load the booth with an election URL in the browser + +3. Open browser DevTools → Network tab + +4. Click "Start" to begin voting + +5. After the election loads, go to DevTools → Network → set to "Offline" + +6. Complete the voting process: + - Answer all questions + - Review ballot + - Verify encryption works (should complete without network) + +7. Go back online before clicking "Submit" (submission requires network) + +**Step 3: Document offline verification** + +The booth should work completely offline between "Start" and "Submit". All these should work offline: +- Question navigation +- Answer selection +- Ballot encryption +- Review screen display +- Audit trail generation (if spoiled) + +**Step 4: Commit any fixes** + +If any issues found, fix and commit: +```bash +git add -A +git commit -m "fix(booth2026): ensure offline operation during voting" +``` + +--- + +## Task 7: Run Full Test Suite and Final Verification + +**Files:** None (verification only) + +**Step 1: Run Django tests** + +```bash +uv run python manage.py test -v 2 +``` +Expected: All tests pass + +**Step 2: TypeScript verification** + +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Production build** + +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build succeeds + +**Step 4: Manual end-to-end test** + +Test the complete flow with a real election: + +1. Create a test election (or use existing one) +2. Navigate to booth2026 with election URL +3. Verify: + - [ ] Election info displays correctly + - [ ] Start button works + - [ ] Questions display with proper formatting + - [ ] Answer selection works (including limits) + - [ ] Navigation (Previous/Next) works + - [ ] Progress indicator updates + - [ ] Encryption completes with progress display + - [ ] Review screen shows correct choices + - [ ] Ballot hash displays + - [ ] Submit form works (redirects to login) + - [ ] Audit feature works (if enabled) + - [ ] Keyboard navigation works throughout + - [ ] Screen reader announces changes (test with VoiceOver/NVDA) + +**Step 5: Final commit** + +```bash +git add -A +git status +``` +If any uncommitted changes: +```bash +git commit -m "feat(booth2026): complete Phase 4 polish and deployment" +``` + +--- + +## Phase 4 Completion Checklist + +**Accessibility:** +- [ ] Skip link to main content +- [ ] Proper heading hierarchy +- [ ] ARIA landmarks (banner, main, navigation) +- [ ] ARIA labels on interactive elements +- [ ] Focus management on screen transitions +- [ ] Keyboard navigation throughout +- [ ] High contrast mode support +- [ ] Reduced motion support + +**Error Handling:** +- [ ] Graceful handling of network errors +- [ ] Crypto initialization timeout handling +- [ ] User-friendly error messages +- [ ] Recovery options (reload, return to election) + +**Visual Polish:** +- [ ] Responsive layout (mobile-friendly) +- [ ] Consistent spacing and typography +- [ ] Touch-friendly button sizes (44px minimum) +- [ ] Loading states with feedback +- [ ] Print styles + +**Deployment:** +- [ ] Vite configured for production build +- [ ] Single bundle output (no code splitting) +- [ ] Sourcemaps generated +- [ ] Static assets properly handled +- [ ] Django URL route configured + +**Offline Operation:** +- [ ] All assets bundled for offline use +- [ ] Encryption works without network +- [ ] No network calls between Start and Submit +- [ ] Verified by testing with DevTools offline mode + +**Final Verification:** +- [ ] All Django tests pass +- [ ] TypeScript compiles without errors +- [ ] Production build succeeds +- [ ] End-to-end manual testing complete +- [ ] Booth accessible at `/booth2026/` From 4812fe50be3524494fb316b62b9731c22c19a2af Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:38:46 +0000 Subject: [PATCH 02/33] feat(booth2026): initialize Vite + TypeScript + Lit project structure --- heliosbooth2026/index.html | 13 +++++++++++++ heliosbooth2026/package.json | 18 ++++++++++++++++++ heliosbooth2026/tsconfig.json | 25 +++++++++++++++++++++++++ heliosbooth2026/vite.config.ts | 20 ++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 heliosbooth2026/index.html create mode 100644 heliosbooth2026/package.json create mode 100644 heliosbooth2026/tsconfig.json create mode 100644 heliosbooth2026/vite.config.ts diff --git a/heliosbooth2026/index.html b/heliosbooth2026/index.html new file mode 100644 index 000000000..a7961601d --- /dev/null +++ b/heliosbooth2026/index.html @@ -0,0 +1,13 @@ + + + + + + Helios Voting Booth + + + + + + + diff --git a/heliosbooth2026/package.json b/heliosbooth2026/package.json new file mode 100644 index 000000000..92287331c --- /dev/null +++ b/heliosbooth2026/package.json @@ -0,0 +1,18 @@ +{ + "name": "heliosbooth2026", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "lit": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/heliosbooth2026/tsconfig.json b/heliosbooth2026/tsconfig.json new file mode 100644 index 000000000..113e4911f --- /dev/null +++ b/heliosbooth2026/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "skipLibCheck": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/heliosbooth2026/vite.config.ts b/heliosbooth2026/vite.config.ts new file mode 100644 index 000000000..5e4b6086e --- /dev/null +++ b/heliosbooth2026/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: '/booth2026/', + build: { + outDir: 'dist', + cssCodeSplit: false, + rollupOptions: { + output: { + manualChunks: () => 'booth', + entryFileNames: 'booth.js', + assetFileNames: 'booth.[ext]' + } + } + }, + server: { + port: 5173 + } +}); From 051a43d3bc75f9428fc139d5ac4fb48e5e4c7118 Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:39:48 +0000 Subject: [PATCH 03/33] feat(booth2026): copy jscrypto library and add TypeScript declarations Copy crypto libraries from the legacy booth to the new Lit-based booth2026: - Copy jscrypto directory with all crypto libraries (jsbn, sjcl, bigint, elgamal, helios, sha1, sha2) - Copy underscore-min.js utility library - Add TypeScript type declarations for crypto globals (BigInt, ElGamal, HELIOS, etc.) - Types enable type-safe crypto operations in the voting booth Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/lib/jscrypto/README | 5 + heliosbooth2026/lib/jscrypto/bigint.class | Bin 0 -> 797 bytes heliosbooth2026/lib/jscrypto/bigint.dummy.js | 85 +++ heliosbooth2026/lib/jscrypto/bigint.java | 25 + heliosbooth2026/lib/jscrypto/bigint.js | 207 ++++++ heliosbooth2026/lib/jscrypto/class.js | 65 ++ heliosbooth2026/lib/jscrypto/elgamal.js | 538 +++++++++++++++ heliosbooth2026/lib/jscrypto/helios.js | 635 ++++++++++++++++++ heliosbooth2026/lib/jscrypto/jsbn.js | 560 ++++++++++++++++ heliosbooth2026/lib/jscrypto/jsbn2.js | 648 +++++++++++++++++++ heliosbooth2026/lib/jscrypto/random.js | 34 + heliosbooth2026/lib/jscrypto/sha1.js | 202 ++++++ heliosbooth2026/lib/jscrypto/sha2.js | 144 +++++ heliosbooth2026/lib/jscrypto/sjcl.js | 47 ++ heliosbooth2026/lib/underscore-min.js | 26 + heliosbooth2026/src/crypto/types.ts | 167 +++++ 16 files changed, 3388 insertions(+) create mode 100644 heliosbooth2026/lib/jscrypto/README create mode 100644 heliosbooth2026/lib/jscrypto/bigint.class create mode 100644 heliosbooth2026/lib/jscrypto/bigint.dummy.js create mode 100644 heliosbooth2026/lib/jscrypto/bigint.java create mode 100644 heliosbooth2026/lib/jscrypto/bigint.js create mode 100644 heliosbooth2026/lib/jscrypto/class.js create mode 100644 heliosbooth2026/lib/jscrypto/elgamal.js create mode 100644 heliosbooth2026/lib/jscrypto/helios.js create mode 100644 heliosbooth2026/lib/jscrypto/jsbn.js create mode 100644 heliosbooth2026/lib/jscrypto/jsbn2.js create mode 100644 heliosbooth2026/lib/jscrypto/random.js create mode 100644 heliosbooth2026/lib/jscrypto/sha1.js create mode 100644 heliosbooth2026/lib/jscrypto/sha2.js create mode 100644 heliosbooth2026/lib/jscrypto/sjcl.js create mode 100644 heliosbooth2026/lib/underscore-min.js create mode 100644 heliosbooth2026/src/crypto/types.ts diff --git a/heliosbooth2026/lib/jscrypto/README b/heliosbooth2026/lib/jscrypto/README new file mode 100644 index 000000000..745f867fa --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/README @@ -0,0 +1,5 @@ + +JavaScript crypto. + +IMPORTANT: this library REQUIRES that a variable JSCRYPTO_HOME be set by an HTML file, indicating +the complete path to the current directory diff --git a/heliosbooth2026/lib/jscrypto/bigint.class b/heliosbooth2026/lib/jscrypto/bigint.class new file mode 100644 index 0000000000000000000000000000000000000000..f8e435bd63fd7fded0b46a12ac80fa843aa0d101 GIT binary patch literal 797 zcmb7?T}uK{5Qg95cdKPtTKU!2jb^oUZ3Gnrf<$BpUaorBPUsrjTJ*2#Y9XN?(2t5{ zc7+xyR7j13yT!`M*+-QP!j zp+VL+xV6<#&bk~`8vn7;TBEVulh=x;knAc`SKz{KI1U9^6pgAtwW!Yod zr!(+f1u`CPJUtDhqhEN7KyL2X=&W*c$3cnB`5m^ghXmaUexeY(ebN+tm1q?lJ#-;H z=!FiCH0A{pq$^01GP+CGiP6Lap^l62dw|v`yNb0by-!yyOoTQ;q*E)phz9u;A~RT@ m#}|%V#DaW=$PHwt`~fu*A^#-%7mCL&;=zI(p*ccA*!%@9I'; + // var applet_html = ' No Java Support. '; + $("#applet_div").html(applet_html); + } + + return use_applet; + }; + + // Set up the pointer to the applet if necessary, and some + // basic Big Ints that everyone needs (0, 1, 2, and 42) + BigInt._setup = function() { + if (BigInt.use_applet) { + BigInt.APPLET = document.applets["bigint"]; + } + + try { + BigInt.ZERO = new BigInt("0",10); + BigInt.ONE = new BigInt("1",10); + BigInt.TWO = new BigInt("2",10); + BigInt.FORTY_TWO = new BigInt("42",10); + + BigInt.ready_p = true; + } catch (e) { + // not ready + // count how many times we've tried + if (this.num_invocations == null) + this.num_invocations = 0; + + this.num_invocations += 1; + + if (this.num_invocations > 5) { + // try SJCL + if (!USE_SJCL) { + USE_SJCL = true; + this.num_invocations = 1; + BigInt.use_applet = false; + } else { + + if (BigInt.setup_interval) + window.clearInterval(BigInt.setup_interval); + + if (BigInt.setup_fail) { + BigInt.setup_fail(); + } else { + alert('bigint failed!'); + } + } + } + return; + } + + if (BigInt.setup_interval) + window.clearInterval(BigInt.setup_interval); + + if (BigInt.setup_callback) + BigInt.setup_callback(); + }; + + BigInt.setup = function(callback, fail_callback) { + if (callback) + BigInt.setup_callback = callback; + + if (fail_callback) + BigInt.setup_fail = fail_callback; + + BigInt.setup_interval = window.setInterval("BigInt._setup()", 1000); + } +} + +BigInt.fromJSONObject = function(s) { + return new BigInt(s, 10); +}; + +BigInt.fromInt = function(i) { + return BigInt.fromJSONObject("" + i); +}; + +BigInt.use_applet = false; diff --git a/heliosbooth2026/lib/jscrypto/class.js b/heliosbooth2026/lib/jscrypto/class.js new file mode 100644 index 000000000..c1b33a002 --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/class.js @@ -0,0 +1,65 @@ + +/* + * John Resig's Class Inheritance + */ + +// Inspired by base2 and Prototype +(function(){ + var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + + // The base Class implementation (does nothing) + this.Class = function(){}; + + // Create a new Class that inherits from this class + Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for (var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && fnTest.test(prop[name]) ? + (function(name, fn){ + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if ( !initializing && this.init ) + this.init.apply(this, arguments); + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); diff --git a/heliosbooth2026/lib/jscrypto/elgamal.js b/heliosbooth2026/lib/jscrypto/elgamal.js new file mode 100644 index 000000000..bfc2a6a4d --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/elgamal.js @@ -0,0 +1,538 @@ + +// +// inspired by George Danezis, rewritten by Ben Adida. +// + +ElGamal = {}; + +ElGamal.Params = Class.extend({ + init: function(p, q, g) { + this.p = p; + this.q = q; + this.g = g; + }, + + generate: function() { + // get the value x + var x = Random.getRandomInteger(this.q); + var y = this.g.modPow(x, this.p); + var pk = new ElGamal.PublicKey(this.p, this.q, this.g, y); + var sk = new ElGamal.SecretKey(x, pk); + return sk; + }, + + toJSONObject: function() { + return {g: this.g.toJSONObject(), p: this.p.toJSONObject(), q: this.q.toJSONObject()}; + } +}); + +ElGamal.Params.fromJSONObject = function(d) { + var params = new ElGamal.Params(); + params.p = BigInt.fromJSONObject(d.p); + params.q = BigInt.fromJSONObject(d.q); + params.g = BigInt.fromJSONObject(d.g); + return params; +}; + +ElGamal.PublicKey = Class.extend({ + init : function(p,q,g,y) { + this.p = p; + this.q = q; + this.g = g; + this.y = y; + }, + + toJSONObject: function() { + return {g : this.g.toJSONObject(), p : this.p.toJSONObject(), q : this.q.toJSONObject(), y : this.y.toJSONObject()}; + }, + + verifyKnowledgeOfSecretKey: function(proof, challenge_generator) { + // if challenge_generator is present, we have to check that the challenge was properly generated. + if (challenge_generator != null) { + if (!proof.challenge.equals(challenge_generator(proof.commitment))) { + return false; + } + } + + // verify that g^response = s * y^challenge + var check = this.g.modPow(proof.response, this.p).equals(this.y.modPow(proof.challenge, this.p).multiply(proof.commitment).mod(this.p)); + + return check; + }, + + // check if the decryption factor is correct for this public key, given the proof + verifyDecryptionFactor: function(ciphertext, decryption_factor, decryption_proof, challenge_generator) { + return decryption_proof.verify(this.g, ciphertext.alpha, this.y, decryption_factor, this.p, this.q, challenge_generator); + }, + + multiply: function(other) { + // base condition + if (other == 0 || other == 1) { + return this; + } + + // check params + if (!this.p.equals(other.p)) + throw "mismatched params"; + if (!this.g.equals(other.g)) + throw "mismatched params"; + + var new_pk = new ElGamal.PublicKey(this.p, this.q, this.g, this.y.multiply(other.y).mod(this.p)); + return new_pk; + }, + + equals: function(other) { + return (this.p.equals(other.p) && this.q.equals(other.q) && this.g.equals(other.g) && this.y.equals(other.y)); + } + +}); + +ElGamal.PublicKey.fromJSONObject = function(d) { + var pk = new ElGamal.PublicKey(); + pk.p = BigInt.fromJSONObject(d.p); + pk.q = BigInt.fromJSONObject(d.q); + pk.g = BigInt.fromJSONObject(d.g); + pk.y = BigInt.fromJSONObject(d.y); + return pk; +}; + +ElGamal.SecretKey = Class.extend({ + init: function(x, pk) { + this.x = x; + this.pk = pk; + }, + + toJSONObject: function() { + return {public_key: this.pk.toJSONObject(), x: this.x.toJSONObject()}; + }, + + // a decryption factor is *not yet* mod-inverted, because it needs to be part of the proof. + decryptionFactor: function(ciphertext) { + var decryption_factor = ciphertext.alpha.modPow(this.x, this.pk.p); + return decryption_factor; + }, + + decrypt: function(ciphertext, decryption_factor) { + if (!decryption_factor) + decryption_factor = this.decryptionFactor(ciphertext); + + // use the ciphertext's built-in decryption given a list of decryption factors. + return ciphertext.decrypt([decryption_factor]); + }, + + decryptAndProve: function(ciphertext, challenge_generator) { + var dec_factor_and_proof = this.decryptionFactorAndProof(ciphertext, challenge_generator); + + // decrypt, but using the already computed decryption factor + var plaintext = this.decrypt(ciphertext, dec_factor_and_proof.decryption_factor); + + return { + 'plaintext': plaintext, + 'proof': dec_factor_and_proof.decryption_proof + }; + }, + + decryptionFactorAndProof: function(ciphertext, challenge_generator) { + var decryption_factor = this.decryptionFactor(ciphertext); + + // the DH tuple we need to prove, given the secret key x, is: + // g, alpha, y, beta/m + var proof = ElGamal.Proof.generate(this.pk.g, ciphertext.alpha, this.x, this.pk.p, this.pk.q, challenge_generator); + + return { + 'decryption_factor' : decryption_factor, + 'decryption_proof' : proof + } + }, + + // generate a proof of knowledge of the secret exponent x + proveKnowledge: function(challenge_generator) { + // generate random w + var w = Random.getRandomInteger(this.pk.q); + + // compute s = g^w for random w. + var s = this.pk.g.modPow(w, this.pk.p); + + // get challenge + var challenge = challenge_generator(s); + + // compute response = w + x * challenge + var response = w.add(this.x.multiply(challenge)).mod(this.pk.q); + + return new ElGamal.DLogProof(s, challenge, response); + } +}); + +ElGamal.SecretKey.fromJSONObject = function(d) { + var sk = new ElGamal.SecretKey(); + sk.pk = ElGamal.PublicKey.fromJSONObject(d.public_key); + sk.x = BigInt.fromJSONObject(d.x); + return sk; +} + +ElGamal.Ciphertext = Class.extend({ + init: function(alpha, beta, pk) { + this.alpha = alpha; + this.beta = beta; + this.pk = pk; + }, + + toString: function() { + return this.alpha.toString() + ',' + this.beta.toString(); + }, + + toJSONObject: function() { + return {alpha: this.alpha.toJSONObject(), beta: this.beta.toJSONObject()} + }, + + multiply: function(other) { + // special case if other is 1 to enable easy aggregate ops + if (other == 1) + return this; + + // homomorphic multiply + return new ElGamal.Ciphertext(this.alpha.multiply(other.alpha).mod(this.pk.p), + this.beta.multiply(other.beta).mod(this.pk.p), + this.pk); + }, + + // a decryption method by decryption factors + decrypt: function(list_of_dec_factors) { + var running_decryption = this.beta; + var self = this; + _(list_of_dec_factors).each(function(dec_factor) { + running_decryption = dec_factor.modInverse(self.pk.p).multiply(running_decryption).mod(self.pk.p); + }); + + return new ElGamal.Plaintext(running_decryption, this.pk, false); + }, + + generateProof: function(plaintext, randomness, challenge_generator) { + // DH tuple to prove is + // g, y, alpha, beta/m + // with dlog randomness + var proof = ElGamal.Proof.generate(this.pk.g, this.pk.y, randomness, this.pk.p, this.pk.q, challenge_generator); + + return proof; + }, + + simulateProof: function(plaintext, challenge) { + // compute beta/plaintext, the completion of the DH tuple + var beta_over_plaintext = this.beta.multiply(plaintext.m.modInverse(this.pk.p)).mod(this.pk.p); + + // the DH tuple we are simulating here is + // g, y, alpha, beta/m + return ElGamal.Proof.simulate(this.pk.g, this.pk.y, this.alpha, beta_over_plaintext, this.pk.p, this.pk.q, challenge); + }, + + verifyProof: function(plaintext, proof, challenge_generator) { + // DH tuple to verify is + // g, y, alpha, beta/m + var beta_over_m = this.beta.multiply(plaintext.m.modInverse(this.pk.p)).mod(this.pk.p); + + return proof.verify(this.pk.g, this.pk.y, this.alpha, beta_over_m, this.pk.p, this.pk.q, challenge_generator); + }, + + verifyDecryptionProof: function(plaintext, proof, challenge_generator) { + // DH tuple to verify is + // g, alpha, y, beta/m + // since the proven dlog is the secret key x, y=g^x. + var beta_over_m = this.beta.multiply(plaintext.m.modInverse(this.pk.p)).mod(this.pk.p); + + return proof.verify(this.pk.g, this.alpha, this.pk.y, beta_over_m, this.pk.p, this.pk.q, challenge_generator); + }, + + generateDisjunctiveProof: function(list_of_plaintexts, real_index, randomness, challenge_generator) { + // go through all plaintexts and simulate the ones that must be simulated. + // note how the interface is as such so that the result does not reveal which is the real proof. + var self = this; + + var proofs = _(list_of_plaintexts).map(function(plaintext, p_num) { + if (p_num == real_index) { + // no real proof yet + return {}; + } else { + // simulate! + return self.simulateProof(plaintext); + } + }); + + // do the real proof + var real_proof = this.generateProof(list_of_plaintexts[real_index], randomness, function(commitment) { + // now we generate the challenge for the real proof by first determining + // the challenge for the whole disjunctive proof. + + // set up the partial real proof so we're ready to get the hash; + proofs[real_index] = {'commitment' : commitment}; + + // get the commitments in a list and generate the whole disjunctive challenge + var commitments = _(proofs).map(function(proof) { + return proof.commitment; + }); + + var disjunctive_challenge = challenge_generator(commitments); + + // now we must subtract all of the other challenges from this challenge. + var real_challenge = disjunctive_challenge; + _(proofs).each(function(proof, proof_num) { + if (proof_num != real_index) + real_challenge = real_challenge.add(proof.challenge.negate()); + }); + + // make sure we mod q, the exponent modulus + return real_challenge.mod(self.pk.q); + }); + + // set the real proof + proofs[real_index] = real_proof; + return new ElGamal.DisjunctiveProof(proofs); + }, + + verifyDisjunctiveProof: function(list_of_plaintexts, disj_proof, challenge_generator) { + var result = true; + var proofs = disj_proof.proofs; + + // for loop because we want to bail out of the inner loop + // if we fail one of the verifications. + for (var i=0; i < list_of_plaintexts.length; i++) { + if (!this.verifyProof(list_of_plaintexts[i], proofs[i])) + return false; + } + + // check the overall challenge + + // first the one expected from the proofs + var commitments = _(proofs).map(function(proof) {return proof.commitment;}); + var expected_challenge = challenge_generator(commitments); + + // then the one that is the sum of the previous one. + var sum = new BigInt("0", 10); var self = this; + _(proofs).each(function(proof) {sum = sum.add(proof.challenge).mod(self.pk.q);}); + + return expected_challenge.equals(sum); + }, + + equals: function(other) { + return (this.alpha.equals(other.alpha) && this.beta.equals(other.beta)); + } +}); + +ElGamal.Ciphertext.fromJSONObject = function(d, pk) { + return new ElGamal.Ciphertext(BigInt.fromJSONObject(d.alpha), BigInt.fromJSONObject(d.beta), pk); +}; + +// we need the public key to figure out how to encode m +ElGamal.Plaintext = Class.extend({ + init: function(m, pk, encode_m) { + if (m == null) { + alert('oy null m'); + return; + } + + this.pk = pk; + + if (encode_m) { + // need to encode the message given that p = 2q+1 + var y = m.add(BigInt.ONE); + var test = y.modPow(pk.q, pk.p); + if (test.equals(BigInt.ONE)) { + this.m = y; + } else { + this.m = y.negate().mod(pk.p); + } + } else { + this.m = m; + } + }, + + getPlaintext: function() { + var y; + + // if m < q + if (this.m.compareTo(this.pk.q) < 0) { + y = this.m; + } else { + y = this.m.negate().mod(this.pk.p); + } + + return y.subtract(BigInt.ONE); + }, + + getM: function() { + return this.m; + } + +}); + +// +// Proof abstraction +// + +ElGamal.Proof = Class.extend({ + init: function(A, B, challenge, response) { + this.commitment = {}; + this.commitment.A = A; + this.commitment.B = B; + this.challenge = challenge; + this.response = response; + }, + + toJSONObject: function() { + return { + challenge : this.challenge.toJSONObject(), + commitment : {A: this.commitment.A.toJSONObject(), B: this.commitment.B.toJSONObject()}, + response : this.response.toJSONObject() + } + }, + + // verify a DH tuple proof + verify: function(little_g, little_h, big_g, big_h, p, q, challenge_generator) { + // check that little_g^response = A * big_g^challenge + var first_check = little_g.modPow(this.response, p).equals(big_g.modPow(this.challenge, p).multiply(this.commitment.A).mod(p)); + + // check that little_h^response = B * big_h^challenge + var second_check = little_h.modPow(this.response, p).equals(big_h.modPow(this.challenge, p).multiply(this.commitment.B).mod(p)); + + var third_check = true; + + if (challenge_generator) { + third_check = this.challenge.equals(challenge_generator(this.commitment)); + } + + return (first_check && second_check && third_check); + } +}); + +ElGamal.Proof.fromJSONObject = function(d) { + return new ElGamal.Proof( + BigInt.fromJSONObject(d.commitment.A), + BigInt.fromJSONObject(d.commitment.B), + BigInt.fromJSONObject(d.challenge), + BigInt.fromJSONObject(d.response)); +}; + +// a generic way to prove that four values are a DH tuple. +// a DH tuple is g,h,G,H where G = g^x and H=h^x +// challenge generator takes a commitment, whose subvalues are A and B +// all modulo p, with group order q, which we provide just in case. +// as it turns out, G and H are not necessary to generate this proof, given that they're implied by x. +ElGamal.Proof.generate = function(little_g, little_h, x, p, q, challenge_generator) { + // generate random w + var w = Random.getRandomInteger(q); + + // create a proof instance + var proof = new ElGamal.Proof(); + + // compute A=little_g^w, B=little_h^w + proof.commitment.A = little_g.modPow(w, p); + proof.commitment.B = little_h.modPow(w, p); + + // Get the challenge from the callback that generates it + proof.challenge = challenge_generator(proof.commitment); + + // Compute response = w + x * challenge + proof.response = w.add(x.multiply(proof.challenge)).mod(q); + + return proof; +}; + +// simulate a a DH-tuple proof, with a potentially assigned challenge (but can be null) +ElGamal.Proof.simulate = function(little_g, little_h, big_g, big_h, p, q, challenge) { + // generate a random challenge if not provided + if (challenge == null) { + challenge = Random.getRandomInteger(q); + } + + // random response, does not even need to depend on the challenge + var response = Random.getRandomInteger(q); + + // now we compute A and B + // A = little_g ^ w, and at verification time, g^response = G^challenge * A, so A = (G^challenge)^-1 * g^response + var A = big_g.modPow(challenge, p).modInverse(p).multiply(little_g.modPow(response, p)).mod(p); + + // B = little_h ^ w, and at verification time, h^response = H^challenge * B, so B = (H^challenge)^-1 * h^response + var B = big_h.modPow(challenge, p).modInverse(p).multiply(little_h.modPow(response, p)).mod(p); + + return new ElGamal.Proof(A, B, challenge, response); +}; + +ElGamal.DisjunctiveProof = Class.extend({ + init: function(list_of_proofs) { + this.proofs = list_of_proofs; + }, + + toJSONObject: function() { + return _(this.proofs).map(function(proof) { + return proof.toJSONObject(); + }); + } +}); + +ElGamal.DisjunctiveProof.fromJSONObject = function(d) { + if (d==null) + return null; + + return new ElGamal.DisjunctiveProof( + _(d).map(function(p) { + return ElGamal.Proof.fromJSONObject(p); + }) + ); +}; + +ElGamal.encrypt = function(pk, plaintext, r) { + if (plaintext.getM().equals(BigInt.ZERO)) + throw "Can't encrypt 0 with El Gamal" + + if (!r) + r = Random.getRandomInteger(pk.q); + + var alpha = pk.g.modPow(r, pk.p); + var beta = (pk.y.modPow(r, pk.p)).multiply(plaintext.m).mod(pk.p); + + return new ElGamal.Ciphertext(alpha, beta, pk); +}; + +// +// DLog Proof +// +ElGamal.DLogProof = Class.extend({ + init: function(commitment, challenge, response) { + this.commitment = commitment; + this.challenge = challenge; + this.response = response; + }, + + toJSONObject: function() { + return {'challenge' : this.challenge.toJSONObject(), 'commitment': this.commitment.toJSONObject(), 'response': this.response.toJSONObject()}; + } +}); + +ElGamal.DLogProof.fromJSONObject = function(d) { + return new ElGamal.DLogProof(BigInt.fromJSONObject(d.commitment || d.s), BigInt.fromJSONObject(d.challenge), BigInt.fromJSONObject(d.response)); +}; + +// a challenge generator based on a list of commitments of +// proofs of knowledge of plaintext. Just appends A and B with commas. +ElGamal.disjunctive_challenge_generator = function(commitments) { + var strings_to_hash = []; + + // go through all proofs and append the commitments + _(commitments).each(function(commitment) { + // toJSONObject instead of toString because of IE weirdness. + strings_to_hash[strings_to_hash.length] = commitment.A.toJSONObject(); + strings_to_hash[strings_to_hash.length] = commitment.B.toJSONObject(); + }); + + // console.log(strings_to_hash); + // STRINGS = strings_to_hash; + return new BigInt(hex_sha1(strings_to_hash.join(",")), 16); +}; + +// a challenge generator for Fiat-Shamir +ElGamal.fiatshamir_challenge_generator = function(commitment) { + return ElGamal.disjunctive_challenge_generator([commitment]); +}; + +ElGamal.fiatshamir_dlog_challenge_generator = function(commitment) { + return new BigInt(hex_sha1(commitment.toJSONObject()), 16); +}; \ No newline at end of file diff --git a/heliosbooth2026/lib/jscrypto/helios.js b/heliosbooth2026/lib/jscrypto/helios.js new file mode 100644 index 000000000..93c0ed48c --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/helios.js @@ -0,0 +1,635 @@ + +// +// Helios Protocols +// +// ben@adida.net +// +// FIXME: needs a healthy refactor/cleanup based on Class.extend() +// + +// extend jquery to do object keys +// from http://snipplr.com/view.php?codeview&id=10430 +/* +$.extend({ + keys: function(obj){ + var a = []; + $.each(obj, function(k){ a.push(k) }); + return a.sort(); + } +}); +*/ + +var UTILS = {}; + +UTILS.array_remove_value = function(arr, val) { + var new_arr = []; + _(arr).each(function(v, i) { + if (v != val) { + new_arr.push(v); + } + }); + + return new_arr; +}; + +UTILS.select_element_content = function(element) { + var range; + if (window.getSelection) { // FF, Safari, Opera + var sel = window.getSelection(); + range = document.createRange(); + range.selectNodeContents(element); + sel.removeAllRanges(); + sel.addRange(range); + } else { + document.selection.empty(); + range = document.body.createTextRange(); + range.moveToElementText(el); + range.select(); + } +}; + +// a progress tracker +UTILS.PROGRESS = Class.extend({ + init: function() { + this.n_ticks = 0.0; + this.current_tick = 0.0; + }, + + addTicks: function(n_ticks) { + this.n_ticks += n_ticks; + }, + + tick: function() { + this.current_tick += 1.0; + }, + + progress: function() { + return Math.round((this.current_tick / this.n_ticks) * 100); + } +}); + +// produce the same object but with keys sorted +UTILS.object_sort_keys = function(obj) { + var new_obj = {}; + _(_.keys(obj)).each(function(k) { + new_obj[k] = obj[k]; + }); + return new_obj; +}; + +// +// Helios Stuff +// + +HELIOS = {}; + +// a bogus default public key to allow for ballot previewing, nothing more +// this public key should not be used ever, that's why the secret key is +// not given. +HELIOS.get_bogus_public_key = function() { + return ElGamal.PublicKey.fromJSONObject(JSON.parse('{"g": "14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533", "p": "16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071", "q": "61329566248342901292543872769978950870633559608669337131139375508370458778917", "y": "8049609819434159960341080485505898805169812475728892670296439571117039276506298996734003515763387841154083296559889658342770776712289026341097211553854451556820509582109412351633111518323196286638684857563764318086496248973278960517204721786711381246407429787246857335714789053255852788270719245108665072516217144567856965465184127683058484847896371648547639041764249621310049114411288049569523544645318180042074181845024934696975226908854019646138985505600641910417380245960080668869656287919893859172484656506039729440079008919716011166605004711585860172862472422362509002423715947870815838511146670204726187094944"}')); +}; + +// election +HELIOS.Election = Class.extend({ + init: function() { + }, + + toJSONObject: function() { + var json_obj = {uuid : this.uuid, + description : this.description, short_name : this.short_name, name : this.name, + public_key: this.public_key.toJSONObject(), questions : this.questions, + cast_url: this.cast_url, frozen_at: this.frozen_at, + openreg: this.openreg, voters_hash: this.voters_hash, + use_voter_aliases: this.use_voter_aliases, + voting_starts_at: this.voting_starts_at, + voting_ends_at: this.voting_ends_at}; + + return UTILS.object_sort_keys(json_obj); + }, + + get_hash: function() { + if (this.election_hash) + return this.election_hash; + + // otherwise + return b64_sha256(this.toJSON()); + }, + + toJSON: function() { + // FIXME: only way around the backslash thing for now.... how ugly + //return jQuery.toJSON(this.toJSONObject()).replace(/\//g,"\\/"); + return JSON.stringify(this.toJSONObject()); + } +}); + +HELIOS.Election.fromJSONString = function(raw_json) { + var json_object = JSON.parse(raw_json); + + // let's hash the raw_json + var election = HELIOS.Election.fromJSONObject(json_object); + election.election_hash = b64_sha256(raw_json); + + return election; +}; + +HELIOS.Election.fromJSONObject = function(d) { + var el = new HELIOS.Election(); + _.extend(el, d); + + // empty questions + if (!el.questions) + el.questions = []; + + if (el.public_key) { + el.public_key = ElGamal.PublicKey.fromJSONObject(el.public_key); + } else { + // a placeholder that will allow hashing; + el.public_key = HELIOS.get_bogus_public_key(); + el.BOGUS_P = true; + } + + return el; +}; + +HELIOS.Election.setup = function(election) { + return ELECTION.fromJSONObject(election); +}; + + +// ballot handling +BALLOT = {}; + +BALLOT.pretty_choices = function(election, ballot) { + var questions = election.questions; + var answers = ballot.answers; + + // process the answers + var choices = _(questions).map(function(q, q_num) { + return _(answers[q_num]).map(function(ans) { + return questions[q_num].answers[ans]; + }); + }); + + return choices; +}; + + +// open up a new window and do something with it. +UTILS.open_window_with_content = function(content, mime_type) { + if (!mime_type) + mime_type = "text/plain"; + if (BigInt.is_ie) { + w = window.open(""); + w.document.open(mime_type); + w.document.write(content); + w.document.close(); + } else { + w = window.open("data:" + mime_type + "," + encodeURIComponent(content)); + } +}; + +// generate an array of the first few plaintexts +UTILS.generate_plaintexts = function(pk, min, max) { + var last_plaintext = BigInt.ONE; + + // an array of plaintexts + var plaintexts = []; + + if (min == null) + min = 0; + + // questions with more than one possible answer, add to the array. + for (var i=0; i<=max; i++) { + if (i >= min) + plaintexts.push(new ElGamal.Plaintext(last_plaintext, pk, false)); + last_plaintext = last_plaintext.multiply(pk.g).mod(pk.p); + } + + return plaintexts; +} + + +// +// crypto +// + + +HELIOS.EncryptedAnswer = Class.extend({ + init: function(question, answer, pk, progress) { + // if nothing in the constructor + if (question == null) + return; + + // store answer + // CHANGE 2008-08-06: answer is now an *array* of answers, not just a single integer + this.answer = answer; + + // do the encryption + var enc_result = this.doEncryption(question, answer, pk, null, progress); + + this.choices = enc_result.choices; + this.randomness = enc_result.randomness; + this.individual_proofs = enc_result.individual_proofs; + this.overall_proof = enc_result.overall_proof; + }, + + doEncryption: function(question, answer, pk, randomness, progress) { + var choices = []; + var individual_proofs = []; + var overall_proof = null; + + // possible plaintexts [question.min .. , question.max] + var plaintexts = null; + if (question.max != null) { + plaintexts = UTILS.generate_plaintexts(pk, question.min, question.max); + } + + var zero_one_plaintexts = UTILS.generate_plaintexts(pk, 0, 1); + + // keep track of whether we need to generate new randomness + var generate_new_randomness = false; + if (!randomness) { + randomness = []; + generate_new_randomness = true; + } + + // keep track of number of options selected. + var num_selected_answers = 0; + + // go through each possible answer and encrypt either a g^0 or a g^1. + for (var i=0; i= 0) { + var v = x*this.arr[i++]+w.arr[j]+c; + c = Math.floor(v/0x4000000); + w.arr[j++] = v&0x3ffffff; + } + return c; +} +// am2 avoids a big mult-and-extract completely. +// Max digit bits should be <= 30 because we do bitwise ops +// on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) +function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this.arr[i]&0x7fff; + var h = this.arr[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w.arr[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w.arr[j++] = l&0x3fffffff; + } + return c; +} +// Alternately, set max digit bits to 28 since some +// browsers slow down when dealing with 32-bit numbers. +function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this.arr[i]&0x3fff; + var h = this.arr[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w.arr[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w.arr[j++] = l&0xfffffff; + } + return c; +} +if(j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; +} +else if(j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; +} +else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; +} + +BigInteger.prototype.DB = dbits; +BigInteger.prototype.DM = ((1<= 0; --i) r.arr[i] = this.arr[i]; + r.t = this.t; + r.s = this.s; +} + +// (protected) set from integer value x, -DV <= x < DV +function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this.arr[0] = x; + else if(x < -1) this.arr[0] = x+DV; + else this.t = 0; +} + +// return bigint initialized to value +function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + +// (protected) set from string and radix +function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this.arr[this.t++] = x; + else if(sh+k > this.DB) { + this.arr[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this.arr[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this.arr[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this.arr[this.t-1] == c) --this.t; +} + +// (public) return string representation in given radix +function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this.arr[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this.arr[i]&((1<>(p+=this.DB-k); + } + else { + d = (this.arr[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; +} + +// (public) -this +function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + +// (public) |this| +function bnAbs() { return (this.s<0)?this.negate():this; } + +// (public) return + if this > a, - if this < a, 0 if equal +function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return r; + while(--i >= 0) if((r=this.arr[i]-a.arr[i]) != 0) return r; + return 0; +} + +// returns bit length of the integer x +function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; +} + +// (public) return the number of bits in "this" +function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this.arr[this.t-1]^(this.s&this.DM)); +} + +// (protected) r = this << n*DB +function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r.arr[i+n] = this.arr[i]; + for(i = n-1; i >= 0; --i) r.arr[i] = 0; + r.t = this.t+n; + r.s = this.s; +} + +// (protected) r = this >> n*DB +function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r.arr[i-n] = this.arr[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; +} + +// (protected) r = this << n +function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r.arr[i+ds+1] = (this.arr[i]>>cbs)|c; + c = (this.arr[i]&bm)<= 0; --i) r.arr[i] = 0; + r.arr[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); +} + +// (protected) r = this >> n +function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r.arr[i-ds-1] |= (this.arr[i]&bm)<>bs; + } + if(bs > 0) r.arr[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this.arr[i]; + r.arr[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a.arr[i]; + r.arr[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r.arr[i++] = this.DV+c; + else if(c > 0) r.arr[i++] = c; + r.t = i; + r.clamp(); +} + +// (protected) r = this * a, r != this,a (HAC 14.12) +// "this" should be the larger one if appropriate. +function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r.arr[i] = 0; + for(i = 0; i < y.t; ++i) r.arr[i+x.t] = x.am(0,y.arr[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); +} + +// (protected) r = this^2, r != this (HAC 14.16) +function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r.arr[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x.arr[i],r,2*i,0,1); + if((r.arr[i+x.t]+=x.am(i+1,2*x.arr[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r.arr[i+x.t] -= x.DV; + r.arr[i+x.t+1] = 1; + } + } + if(r.t > 0) r.arr[r.t-1] += x.am(i,x.arr[i],r,2*i,0,1); + r.s = 0; + r.clamp(); +} + +// (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) +// r != q, this != m. q or r may be null. +function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm.arr[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y.arr[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y.arr[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r.arr[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y.arr[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r.arr[--i]==y0)?this.DM:Math.floor(r.arr[i]*d1+(r.arr[i-1]+e)*d2); + if((r.arr[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r.arr[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); +} + +// (public) this mod a +function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; +} + +// Modular reduction using "classic" algorithm +function Classic(m) { this.m = m; } +function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; +} +function cRevert(x) { return x; } +function cReduce(x) { x.divRemTo(this.m,null,x); } +function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } +function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +Classic.prototype.convert = cConvert; +Classic.prototype.revert = cRevert; +Classic.prototype.reduce = cReduce; +Classic.prototype.mulTo = cMulTo; +Classic.prototype.sqrTo = cSqrTo; + +// (protected) return "-1/this % 2^DB"; useful for Mont. reduction +// justification: +// xy == 1 (mod m) +// xy = 1+km +// xy(2-xy) = (1+km)(1-km) +// x.arr[y(2-xy)] = 1-k^2m^2 +// x.arr[y(2-xy)] == 1 (mod m^2) +// if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 +// should reduce x and y(2-xy) by m^2 at each step to keep size bounded. +// JS multiply "overflows" differently from C/C++, so care is needed here. +function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this.arr[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; +} + +// Montgomery reduction +function Montgomery(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp&0x7fff; + this.mph = this.mp>>15; + this.um = (1<<(m.DB-15))-1; + this.mt2 = 2*m.t; +} + +// xR mod m +function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; +} + +// x/R mod m +function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; +} + +// x = x/R mod m (HAC 14.32) +function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x.arr[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x.arr[i]*mp mod DV + var j = x.arr[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x.arr[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x.arr[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x.arr[j] >= x.DV) { x.arr[j] -= x.DV; x.arr[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = "x^2/R mod m"; x != r +function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = "xy/R mod m"; x,y != r +function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Montgomery.prototype.convert = montConvert; +Montgomery.prototype.revert = montRevert; +Montgomery.prototype.reduce = montReduce; +Montgomery.prototype.mulTo = montMulTo; +Montgomery.prototype.sqrTo = montSqrTo; + +// (protected) true iff this is even +function bnpIsEven() { return ((this.t>0)?(this.arr[0]&1):this.s) == 0; } + +// (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) +function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); +} + +// (public) this^e % m, 0 <= e < 2^32 +function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); +} + +// protected +BigInteger.prototype.copyTo = bnpCopyTo; +BigInteger.prototype.fromInt = bnpFromInt; +BigInteger.prototype.fromString = bnpFromString; +BigInteger.prototype.clamp = bnpClamp; +BigInteger.prototype.dlShiftTo = bnpDLShiftTo; +BigInteger.prototype.drShiftTo = bnpDRShiftTo; +BigInteger.prototype.lShiftTo = bnpLShiftTo; +BigInteger.prototype.rShiftTo = bnpRShiftTo; +BigInteger.prototype.subTo = bnpSubTo; +BigInteger.prototype.multiplyTo = bnpMultiplyTo; +BigInteger.prototype.squareTo = bnpSquareTo; +BigInteger.prototype.divRemTo = bnpDivRemTo; +BigInteger.prototype.invDigit = bnpInvDigit; +BigInteger.prototype.isEven = bnpIsEven; +BigInteger.prototype.exp = bnpExp; + +// public +BigInteger.prototype.toString = bnToString; +BigInteger.prototype.negate = bnNegate; +BigInteger.prototype.abs = bnAbs; +BigInteger.prototype.compareTo = bnCompareTo; +BigInteger.prototype.bitLength = bnBitLength; +BigInteger.prototype.mod = bnMod; +BigInteger.prototype.modPowInt = bnModPowInt; + +// "constants" +BigInteger.ZERO = nbv(0); +BigInteger.ONE = nbv(1); diff --git a/heliosbooth2026/lib/jscrypto/jsbn2.js b/heliosbooth2026/lib/jscrypto/jsbn2.js new file mode 100644 index 000000000..bdbaf1864 --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/jsbn2.js @@ -0,0 +1,648 @@ +// Copyright (c) 2005-2009 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Extended JavaScript BN functions, required for RSA private ops. + +// Version 1.1: new BigInteger("0", 10) returns "proper" zero + +// (public) +function bnClone() { var r = nbi(); this.copyTo(r); return r; } + +// (public) return value as integer +function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this.arr[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this.arr[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this.arr[1]&((1<<(32-this.DB))-1))<>24; } + +// (public) return value as short (assumes DB>=16) +function bnShortValue() { return (this.t==0)?this.s:(this.arr[0]<<16)>>16; } + +// (protected) return x s.t. r^x < DV +function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + +// (public) 0 if this == 0, 1 if this > 0 +function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this.arr[0] <= 0)) return 0; + else return 1; +} + +// (protected) convert to radix string +function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; +} + +// (protected) convert from radix string +function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); +} + +// (protected) alternate constructor +function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = new Array(), t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x.arr[0] &= ((1< 0) { + if(p < this.DB && (d = this.arr[i]>>p) != (this.s&this.DM)>>p) + r.arr[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this.arr[i]&((1<>(p+=this.DB-8); + } + else { + d = (this.arr[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r.arr[k++] = d; + } + } + return r; +} + +function bnEquals(a) { return(this.compareTo(a)==0); } +function bnMin(a) { return(this.compareTo(a)<0)?this:a; } +function bnMax(a) { return(this.compareTo(a)>0)?this:a; } + +// (protected) r = this op a (bitwise) +function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r.arr[i] = op(this.arr[i],a.arr[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r.arr[i] = op(this.arr[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r.arr[i] = op(f,a.arr[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); +} + +// (public) this & a +function op_and(x,y) { return x&y; } +function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + +// (public) this | a +function op_or(x,y) { return x|y; } +function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + +// (public) this ^ a +function op_xor(x,y) { return x^y; } +function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + +// (public) this & ~a +function op_andnot(x,y) { return x&~y; } +function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + +// (public) ~this +function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r.arr[i] = this.DM&~this.arr[i]; + r.t = this.t; + r.s = ~this.s; + return r; +} + +// (public) this << n +function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; +} + +// (public) this >> n +function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; +} + +// return index of lowest 1-bit in x, x < 2^31 +function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; +} + +// (public) returns index of lowest 1-bit (or -1 if none) +function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this.arr[i] != 0) return i*this.DB+lbit(this.arr[i]); + if(this.s < 0) return this.t*this.DB; + return -1; +} + +// return number of 1 bits in x +function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; +} + +// (public) return number of set bits +function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this.arr[i]^x); + return r; +} + +// (public) true iff nth bit is set +function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this.arr[j]&(1<<(n%this.DB)))!=0); +} + +// (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this.arr[i]; + r.arr[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a.arr[i]; + r.arr[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r.arr[i++] = c; + else if(c < -1) r.arr[i++] = this.DV+c; + r.t = i; + r.clamp(); +} + +// (public) this + a +function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + +// (public) this - a +function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + +// (public) this * a +function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + +// (public) this / a +function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + +// (public) this % a +function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + +// (public) [this/a,this%a] +function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); +} + +// (protected) this *= n, this >= 0, 1 < n < DV +function bnpDMultiply(n) { + this.arr[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); +} + +// (protected) this += n << w words, this >= 0 +function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this.arr[this.t++] = 0; + this.arr[w] += n; + while(this.arr[w] >= this.DV) { + this.arr[w] -= this.DV; + if(++w >= this.t) this.arr[this.t++] = 0; + ++this.arr[w]; + } +} + +// A "null" reducer +function NullExp() {} +function nNop(x) { return x; } +function nMulTo(x,y,r) { x.multiplyTo(y,r); } +function nSqrTo(x,r) { x.squareTo(r); } + +NullExp.prototype.convert = nNop; +NullExp.prototype.revert = nNop; +NullExp.prototype.mulTo = nMulTo; +NullExp.prototype.sqrTo = nSqrTo; + +// (public) this^e +function bnPow(e) { return this.exp(e,new NullExp()); } + +// (protected) r = lower n words of "this * a", a.t <= n +// "this" should be the larger one if appropriate. +function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r.arr[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r.arr[i+this.t] = this.am(0,a.arr[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a.arr[i],r,i,0,n-i); + r.clamp(); +} + +// (protected) r = "this * a" without lower n words, n > 0 +// "this" should be the larger one if appropriate. +function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r.arr[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r.arr[this.t+i-n] = this.am(n-i,a.arr[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); +} + +// Barrett modular reduction +function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; +} + +function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } +} + +function barrettRevert(x) { return x; } + +// x = x mod m (HAC 14.42) +function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = x^2 mod m; x != r +function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = x*y mod m; x,y != r +function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Barrett.prototype.convert = barrettConvert; +Barrett.prototype.revert = barrettRevert; +Barrett.prototype.reduce = barrettReduce; +Barrett.prototype.mulTo = barrettMulTo; +Barrett.prototype.sqrTo = barrettSqrTo; + +// (public) this^e % m (HAC 14.85) +function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = new Array(), n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e.arr[j])-1; + while(j >= 0) { + if(i >= k1) w = (e.arr[j]>>(i-k1))&km; + else { + w = (e.arr[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e.arr[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e.arr[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; +} + +// (protected) this % n, n < 2^26 +function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this.arr[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this.arr[i])%n; + return r; +} + +// (public) 1/this % m (HAC 14.61) +function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; +} + +var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509]; +var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + +// (public) test primality with certainty >= 1-.5^t +function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x.arr[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x.arr[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); +} + +// (protected) true if probably prime (HAC 4.24, Miller-Rabin) +function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + a.fromInt(lowprimes[i]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; +} + +// protected +BigInteger.prototype.chunkSize = bnpChunkSize; +BigInteger.prototype.toRadix = bnpToRadix; +BigInteger.prototype.fromRadix = bnpFromRadix; +BigInteger.prototype.fromNumber = bnpFromNumber; +BigInteger.prototype.bitwiseTo = bnpBitwiseTo; +BigInteger.prototype.changeBit = bnpChangeBit; +BigInteger.prototype.addTo = bnpAddTo; +BigInteger.prototype.dMultiply = bnpDMultiply; +BigInteger.prototype.dAddOffset = bnpDAddOffset; +BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; +BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; +BigInteger.prototype.modInt = bnpModInt; +BigInteger.prototype.millerRabin = bnpMillerRabin; + +// public +BigInteger.prototype.clone = bnClone; +BigInteger.prototype.intValue = bnIntValue; +BigInteger.prototype.byteValue = bnByteValue; +BigInteger.prototype.shortValue = bnShortValue; +BigInteger.prototype.signum = bnSigNum; +BigInteger.prototype.toByteArray = bnToByteArray; +BigInteger.prototype.equals = bnEquals; +BigInteger.prototype.min = bnMin; +BigInteger.prototype.max = bnMax; +BigInteger.prototype.and = bnAnd; +BigInteger.prototype.or = bnOr; +BigInteger.prototype.xor = bnXor; +BigInteger.prototype.andNot = bnAndNot; +BigInteger.prototype.not = bnNot; +BigInteger.prototype.shiftLeft = bnShiftLeft; +BigInteger.prototype.shiftRight = bnShiftRight; +BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; +BigInteger.prototype.bitCount = bnBitCount; +BigInteger.prototype.testBit = bnTestBit; +BigInteger.prototype.setBit = bnSetBit; +BigInteger.prototype.clearBit = bnClearBit; +BigInteger.prototype.flipBit = bnFlipBit; +BigInteger.prototype.add = bnAdd; +BigInteger.prototype.subtract = bnSubtract; +BigInteger.prototype.multiply = bnMultiply; +BigInteger.prototype.divide = bnDivide; +BigInteger.prototype.remainder = bnRemainder; +BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; +BigInteger.prototype.modPow = bnModPow; +BigInteger.prototype.modInverse = bnModInverse; +BigInteger.prototype.pow = bnPow; +BigInteger.prototype.gcd = bnGCD; +BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + +// BigInteger interfaces not implemented in jsbn: + +// BigInteger(int signum, byte[] magnitude) +// double doubleValue() +// float floatValue() +// int hashCode() +// long longValue() +// static BigInteger valueOf(long val) diff --git a/heliosbooth2026/lib/jscrypto/random.js b/heliosbooth2026/lib/jscrypto/random.js new file mode 100644 index 000000000..8e363a6d0 --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/random.js @@ -0,0 +1,34 @@ + +/* + * Random Number generation, now uses the glue to Java + */ + +Random = {}; + +Random.GENERATOR = null; + +Random.setupGenerator = function() { + // no longer needed +/* if (Random.GENERATOR == null && !USE_SJCL) { + if (BigInt.use_applet) { + var foo = BigInt.APPLET.newSecureRandom(); + Random.GENERATOR = BigInt.APPLET.newSecureRandom(); + } else { + // we do it twice because of some weird bug; + var foo = new java.security.SecureRandom(); + Random.GENERATOR = new java.security.SecureRandom(); + } + } + */ +}; + +Random.getRandomInteger = function(max) { + var bit_length = max.bitLength(); + Random.setupGenerator(); + var random; + random = sjcl.random.randomWords(Math.ceil(bit_length / 32) + 2, 6); + // we get a bit array instead of a BigInteger in this case + var rand_bi = new BigInt(sjcl.codec.hex.fromBits(random), 16); + return rand_bi.mod(max); +}; + diff --git a/heliosbooth2026/lib/jscrypto/sha1.js b/heliosbooth2026/lib/jscrypto/sha1.js new file mode 100644 index 000000000..ca6c8523a --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/sha1.js @@ -0,0 +1,202 @@ +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS PUB 180-1 + * Version 2.1a Copyright Paul Johnston 2000 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} +function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} +function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} +function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} +function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} +function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha1_vm_test() +{ + return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; +} + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function core_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + var olde = e; + + for(var j = 0; j < 80; j++) + { + if(j < 16) w[j] = x[i + j]; + else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return Array(a, b, c, d, e); + +} + +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if(t < 20) return (b & c) | ((~b) & d); + if(t < 40) return b ^ c ^ d; + if(t < 60) return (b & c) | (b & d) | (c & d); + return b ^ c ^ d; +} + +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} + +/* + * Calculate the HMAC-SHA1 of a key and some data + */ +function core_hmac_sha1(key, data) +{ + var bkey = str2binb(key); + if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); + return core_sha1(opad.concat(hash), 512 + 160); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ +function str2binb(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); + return bin; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); + return str; +} + +/* + * Convert an array of big-endian words to a hex string. + */ +function binb2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of big-endian words to a base-64 string + */ +function binb2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} \ No newline at end of file diff --git a/heliosbooth2026/lib/jscrypto/sha2.js b/heliosbooth2026/lib/jscrypto/sha2.js new file mode 100644 index 000000000..3199a8c4e --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/sha2.js @@ -0,0 +1,144 @@ +/* A JavaScript implementation of the Secure Hash Standard + * Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/ + * Distributed under the BSD License + * Some bits taken from Paul Johnston's SHA-1 implementation + */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ +var hexcase = 0;/* hex output format. 0 - lowercase; 1 - uppercase */ + +function safe_add (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +function S (X, n) {return ( X >>> n ) | (X << (32 - n));} + +function R (X, n) {return ( X >>> n );} + +function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));} + +function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));} + +function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));} + +function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));} + +function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));} + +function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));} + +function Sigma0512(x) {return (S(x, 28) ^ S(x, 34) ^ S(x, 39));} + +function Sigma1512(x) {return (S(x, 14) ^ S(x, 18) ^ S(x, 41));} + +function Gamma0512(x) {return (S(x, 1) ^ S(x, 8) ^ R(x, 7));} + +function Gamma1512(x) {return (S(x, 19) ^ S(x, 61) ^ R(x, 6));} + +function core_sha256 (m, l) { + var K = new Array(0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2); + var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19); + var W = new Array(64); + var a, b, c, d, e, f, g, h, i, j; + var T1, T2; + + /* append padding */ + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >> 9) << 4) + 15] = l; + + for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32); + return bin; +} + +function binb2str (bin) { + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask); + return str; +} + +function binb2hex (binarray) { + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; +} + +function binb2b64 (binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} + +function hex_sha256(s){return binb2hex(core_sha256(str2binb(s),s.length * chrsz));} +function b64_sha256(s){return binb2b64(core_sha256(str2binb(s),s.length * chrsz));} +function str_sha256(s){return binb2str(core_sha256(str2binb(s),s.length * chrsz));} diff --git a/heliosbooth2026/lib/jscrypto/sjcl.js b/heliosbooth2026/lib/jscrypto/sjcl.js new file mode 100644 index 000000000..3a1c15b87 --- /dev/null +++ b/heliosbooth2026/lib/jscrypto/sjcl.js @@ -0,0 +1,47 @@ +"use strict";function q(a){throw a;}var t=void 0,u=!1;var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}}; +"undefined"!=typeof module&&module.exports&&(module.exports=sjcl); +sjcl.cipher.aes=function(a){this.j[0][0][0]||this.D();var b,c,d,e,f=this.j[0][4],g=this.j[1];b=a.length;var h=1;4!==b&&(6!==b&&8!==b)&&q(new sjcl.exception.invalid("invalid aes key size"));this.a=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c& +255]]}; +sjcl.cipher.aes.prototype={encrypt:function(a){return y(this,a,0)},decrypt:function(a){return y(this,a,1)},j:[[[],[],[],[],[]],[[],[],[],[],[]]],D:function(){var a=this.j[0],b=this.j[1],c=a[4],d=b[4],e,f,g,h=[],l=[],k,n,m,p;for(e=0;0x100>e;e++)l[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=k||1,g=l[g]||1){m=g^g<<1^g<<2^g<<3^g<<4;m=m>>8^m&255^99;c[f]=m;d[m]=f;n=h[e=h[k=h[f]]];p=0x1010101*n^0x10001*e^0x101*k^0x1010100*f;n=0x101*h[m]^0x1010100*m;for(e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8}for(e= +0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}}; +function y(a,b,c){4!==b.length&&q(new sjcl.exception.invalid("invalid aes block size"));var d=a.a[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,l,k,n=d.length/4-2,m,p=4,s=[0,0,0,0];h=a.j[c];a=h[0];var r=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m>>24]^r[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],l=a[f>>>24]^r[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],k=a[g>>>24]^r[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^r[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=l,g=k;for(m=0;4> +m;m++)s[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return s} +sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.O(a.slice(b/32),32-(b&31)).slice(1);return c===t?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return u;var c=0,d;for(d=0;d>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32>>24),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.I,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;dh&&q(new sjcl.exception.invalid("this isn't base64!")),26>>e),g=h<<32-e):(e+=6,g^=h<<32-e);e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.a[0]||this.D();a?(this.q=a.q.slice(0),this.m=a.m.slice(0),this.g=a.g):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.q=this.M.slice(0);this.m=[];this.g=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.m=sjcl.bitArray.concat(this.m,a);b=this.g;a=this.g=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)z(this,c.splice(0,16));return this},finalize:function(){var a,b=this.m,c=this.q,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.g/ +4294967296));for(b.push(this.g|0);b.length;)z(this,b.splice(0,16));this.reset();return c},M:[],a:[],D:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}var b=0,c=2,d;a:for(;64>b;c++){for(d=2;d*d<=c;d++)if(0===c%d)continue a;8>b&&(this.M[b]=a(Math.pow(c,0.5)));this.a[b]=a(Math.pow(c,1/3));b++}}}; +function z(a,b){var c,d,e,f=b.slice(0),g=a.q,h=a.a,l=g[0],k=g[1],n=g[2],m=g[3],p=g[4],s=g[5],r=g[6],v=g[7];for(c=0;64>c;c++)16>c?d=f[c]:(d=f[c+1&15],e=f[c+14&15],d=f[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+f[c&15]+f[c+9&15]|0),d=d+v+(p>>>6^p>>>11^p>>>25^p<<26^p<<21^p<<7)+(r^p&(s^r))+h[c],v=r,r=s,s=p,p=m+d|0,m=n,n=k,k=l,l=d+(k&n^m&(k^n))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;g[0]=g[0]+l|0;g[1]=g[1]+k|0;g[2]=g[2]+n|0;g[3]=g[3]+m|0;g[4]=g[4]+p|0;g[5]=g[5]+s|0;g[6]= +g[6]+r|0;g[7]=g[7]+v|0} +sjcl.mode.ccm={name:"ccm",encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,l=h.bitLength(c)/8,k=h.bitLength(g)/8;e=e||64;d=d||[];7>l&&q(new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"));for(f=2;4>f&&k>>>8*f;f++);f<15-l&&(f=15-l);c=h.clamp(c,8*(15-f));b=sjcl.mode.ccm.K(a,b,c,d,e,f);g=sjcl.mode.ccm.n(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),l=f.clamp(b,h-e),k=f.bitSlice(b, +h-e),h=(h-e)/8;7>g&&q(new sjcl.exception.invalid("ccm: iv must be at least 7 bytes"));for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));l=sjcl.mode.ccm.n(a,l,c,k,e,b);a=sjcl.mode.ccm.K(a,l.data,c,d,e,b);f.equal(l.tag,a)||q(new sjcl.exception.corrupt("ccm: tag doesn't match"));return l.data},K:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,l=h.k;e/=8;(e%2||4>e||16=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c]));g=h.concat(g,d);for(d=0;de.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c);return a.encrypt(f(d(f(h, +d(h))),g))},G:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}}; +sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.n(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.n(u,a,f,d,c,e);g.equal(a.tag,b)||q(new sjcl.exception.corrupt("gcm: tag doesn't match"));return a.data},U:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.k;e=[0,0,0,0];f=b.slice(0); +for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},f:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;de&&(a=b.hash(a));for(d=0;dd||0>c)&&q(sjcl.exception.invalid("invalid params to pbkdf2"));"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,l,k=[],n=sjcl.bitArray;for(l=1;32*k.length<(d||1);l++){e=f=a.encrypt(n.concat(b,[l]));for(g=1;gg;g++)e.push(0x100000000*Math.random()|0);for(g=0;g=1<this.i&&(this.i=f);this.F++; +this.a=sjcl.hash.sha256.hash(this.a.concat(e));this.A=new sjcl.cipher.aes(this.a);for(d=0;4>d&&!(this.e[d]=this.e[d]+1|0,this.e[d]);d++);}for(d=0;d>>=1;this.b[g].update([d,this.C++,2,b,f,a.length].concat(a))}break;case "string":b===t&&(b=a.length);this.b[g].update([d,this.C++,3,b,f,a.length]);this.b[g].update(a); +break;default:l=1}l&&q(new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string"));this.h[g]+=b;this.c+=b;h===this.l&&(this.isReady()!==this.l&&C("seeded",Math.max(this.i,this.c)),C("progress",this.getProgress()))},isReady:function(a){a=this.H[a!==t?a:this.B];return this.i&&this.i>=a?this.h[0]>this.P&&(new Date).valueOf()>this.N?this.w|this.u:this.u:this.c>=a?this.w|this.l:this.l},getProgress:function(a){a=this.H[a?a:this.B];return this.i>=a?1:this.c>a?1:this.c/ +a},startCollectors:function(){this.p||(window.addEventListener?(window.addEventListener("load",this.r,u),window.addEventListener("mousemove",this.s,u)):document.attachEvent?(document.attachEvent("onload",this.r),document.attachEvent("onmousemove",this.s)):q(new sjcl.exception.bug("can't attach event")),this.p=!0)},stopCollectors:function(){this.p&&(window.removeEventListener?(window.removeEventListener("load",this.r,u),window.removeEventListener("mousemove",this.s,u)):window.detachEvent&&(window.detachEvent("onload", +this.r),window.detachEvent("onmousemove",this.s)),this.p=u)},addEventListener:function(a,b){this.z[a][this.S++]=b},removeEventListener:function(a,b){var c,d,e=this.z[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;cb&&!(a.e[b]=a.e[b]+1|0,a.e[b]);b++);return a.A.encrypt(a.e)}sjcl.random=new sjcl.prng(6);try{var D=new Uint32Array(32);crypto.getRandomValues(D);sjcl.random.addEntropy(D,1024,"crypto['getRandomValues']")}catch(E){} +sjcl.json={defaults:{v:1,iter:1E3,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},encrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.d({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.d(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length|| +4=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a, +c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;bd?1:0}),"value")};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.zip=function(){for(var a=f.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c), +e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=F||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a, +b.identity)};b.functions=b.methods=function(a){return b.filter(b.keys(a),function(c){return b.isFunction(a[c])}).sort()};b.extend=function(a){h(f.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){h(f.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,c){if(a===c)return!0;var d=typeof a;if(d!= +typeof c)return!1;if(a==c)return!0;if(!a&&c||a&&!c)return!1;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual)return a.isEqual(c);if(b.isDate(a)&&b.isDate(c))return a.getTime()===c.getTime();if(b.isNaN(a)&&b.isNaN(c))return!1;if(b.isRegExp(a)&&b.isRegExp(c))return a.source===c.source&&a.global===c.global&&a.ignoreCase===c.ignoreCase&&a.multiline===c.multiline;if(d!=="object")return!1;if(a.length&&a.length!==c.length)return!1;d=b.keys(a);var e=b.keys(c);if(d.length!=e.length)return!1; +for(var f in a)if(!(f in c)||!b.isEqual(a[f],c[f]))return!1;return!0};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return!1;return!0};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=n||function(a){return E.call(a)==="[object Array]"};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)}; +b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===!0||a===!1};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===!1))};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){p._=C;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e= +0;e/g,interpolate:/<%=([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate|| +null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return c?d(c):d};var j=function(a){this._wrapped=a};b.prototype=j.prototype;var r=function(a,c){return c?b(a).chain():a},H=function(a,c){j.prototype[a]=function(){var a=f.call(arguments);D.call(a,this._wrapped);return r(c.apply(b,a),this._chain)}};b.mixin(b);h(["pop","push","reverse","shift","sort", +"splice","unshift"],function(a){var b=i[a];j.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});h(["concat","join","slice"],function(a){var b=i[a];j.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});j.prototype.chain=function(){this._chain=!0;return this};j.prototype.value=function(){return this._wrapped}})(); diff --git a/heliosbooth2026/src/crypto/types.ts b/heliosbooth2026/src/crypto/types.ts new file mode 100644 index 000000000..f8af66236 --- /dev/null +++ b/heliosbooth2026/src/crypto/types.ts @@ -0,0 +1,167 @@ +/** + * TypeScript type declarations for the Helios jscrypto library globals. + * These are loaded via script tags from lib/jscrypto/ and available globally. + */ + +// BigInt from lib/jscrypto/bigint.js (wraps sjcl BigInteger) +export interface BigIntType { + ZERO: BigIntInstance; + ONE: BigIntInstance; + TWO: BigIntInstance; + fromInt(value: number): BigIntInstance; + fromJSONObject(obj: string): BigIntInstance; + setup(callback: () => void, errorCallback?: () => void): void; +} + +export interface BigIntInstance { + add(other: BigIntInstance): BigIntInstance; + subtract(other: BigIntInstance): BigIntInstance; + multiply(other: BigIntInstance): BigIntInstance; + mod(modulus: BigIntInstance): BigIntInstance; + modPow(exponent: BigIntInstance, modulus: BigIntInstance): BigIntInstance; + modInverse(modulus: BigIntInstance): BigIntInstance; + equals(other: BigIntInstance): boolean; + toJSONObject(): string; + toString(): string; +} + +// ElGamal from lib/jscrypto/elgamal.js +export interface ElGamalParams { + p: BigIntInstance; + q: BigIntInstance; + g: BigIntInstance; +} + +export interface ElGamalPublicKey { + p: BigIntInstance; + q: BigIntInstance; + g: BigIntInstance; + y: BigIntInstance; + toJSONObject(): ElGamalPublicKeyJSON; +} + +export interface ElGamalPublicKeyJSON { + p: string; + q: string; + g: string; + y: string; +} + +export interface ElGamalType { + Params: { + fromJSONObject(obj: ElGamalParams): ElGamalParams; + }; + PublicKey: { + fromJSONObject(obj: ElGamalPublicKeyJSON): ElGamalPublicKey; + }; +} + +// Question structure from election JSON +export interface Question { + question: string; + short_name: string; + answers: string[]; + answer_urls?: (string | null)[]; + min: number; + max: number; + randomize_answer_order?: boolean; +} + +// Election from lib/jscrypto/helios.js +export interface Election { + uuid: string; + name: string; + short_name: string; + description: string; + questions: Question[]; + public_key: ElGamalPublicKey; + cast_url: string; + frozen_at: string; + openreg: boolean; + voters_hash: string | null; + use_voter_aliases: boolean; + voting_starts_at: string | null; + voting_ends_at: string | null; + election_hash: string; + hash: string; + BOGUS_P?: boolean; + question_answer_orderings?: number[][]; +} + +export interface EncryptedAnswer { + toJSONObject(includeRandomness?: boolean): EncryptedAnswerJSON; +} + +export interface EncryptedAnswerJSON { + choices: unknown[]; + individual_proofs: unknown[]; + overall_proof: unknown; + randomness?: unknown[]; + answer?: number[]; +} + +export interface EncryptedVote { + toJSONObject(): EncryptedVoteJSON; + get_hash(): string; +} + +export interface EncryptedVoteJSON { + answers: EncryptedAnswerJSON[]; + election_hash: string; + election_uuid: string; +} + +export interface HELIOSType { + Election: { + fromJSONString(raw: string): Election; + fromJSONObject(obj: unknown): Election; + }; + EncryptedAnswer: { + new(question: Question, answer: number[], publicKey: ElGamalPublicKey): EncryptedAnswer; + fromJSONObject(obj: EncryptedAnswerJSON, election: Election): EncryptedAnswer; + }; + EncryptedVote: { + fromEncryptedAnswers(election: Election, answers: EncryptedAnswer[]): EncryptedVote; + }; + get_bogus_public_key(): ElGamalPublicKey; +} + +export interface RandomType { + getRandomInteger(max: BigIntInstance): BigIntInstance; +} + +export interface UTILSType { + array_remove_value(arr: T[], val: T): T[]; + PROGRESS: new () => { + n_ticks: number; + current_tick: number; + addTicks(n: number): void; + tick(): void; + progress(): number; + }; + object_sort_keys(obj: T): T; +} + +// Election metadata from server +export interface ElectionMetadata { + help_email: string; + randomize_answer_order?: boolean; + use_advanced_audit_features?: boolean; +} + +// Declare globals that will be available after loading jscrypto scripts +declare global { + const BigInt: BigIntType; + const ElGamal: ElGamalType; + const HELIOS: HELIOSType; + const Random: RandomType; + const UTILS: UTILSType; + const USE_SJCL: boolean; + const sjcl: { + random: { + startCollectors(): void; + addEntropy(data: string): void; + }; + }; + function b64_sha256(data: string): string; +} From e319da69326855c1c21668ae05a243b64878676e Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:40:33 +0000 Subject: [PATCH 04/33] feat(booth2026): add base CSS styles --- heliosbooth2026/src/styles/booth.css | 100 +++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 heliosbooth2026/src/styles/booth.css diff --git a/heliosbooth2026/src/styles/booth.css b/heliosbooth2026/src/styles/booth.css new file mode 100644 index 000000000..926f67373 --- /dev/null +++ b/heliosbooth2026/src/styles/booth.css @@ -0,0 +1,100 @@ +/* Helios Booth 2026 - Base Styles */ + +:root { + --color-primary: #1a73e8; + --color-primary-dark: #1557b0; + --color-background: #fff; + --color-surface: #f5f5f5; + --color-border: #ddd; + --color-text: #333; + --color-text-secondary: #666; + --color-success: #28a745; + --color-warning: #ffc107; + --color-error: #dc3545; + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.25rem; + --font-size-xl: 1.5rem; + --border-radius: 4px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-family); + font-size: var(--font-size-md); + color: var(--color-text); + background-color: var(--color-background); + line-height: 1.5; +} + +/* Utility classes */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Button styles */ +button, +.button { + display: inline-block; + padding: var(--spacing-sm) var(--spacing-md); + font-family: inherit; + font-size: var(--font-size-md); + font-weight: 500; + text-align: center; + text-decoration: none; + color: #fff; + background-color: var(--color-primary); + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: background-color 0.2s ease; +} + +button:hover, +.button:hover { + background-color: var(--color-primary-dark); +} + +button:disabled, +.button:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +button:focus, +.button:focus { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Secondary button style */ +button.secondary, +.button.secondary { + background-color: transparent; + color: var(--color-primary); + border: 1px solid var(--color-primary); +} + +button.secondary:hover, +.button.secondary:hover { + background-color: var(--color-surface); +} From 77ac96f516b41244a24f10aba62b76b9ea735f7a Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:43:06 +0000 Subject: [PATCH 05/33] feat(booth2026): add main entry point and booth-app shell component - Create src/main.ts as the entry point that imports booth-app component - Create src/booth-app.ts with BoothApp LitElement component that: - Manages voting booth screen state (loading, election, question, review, submit, audit) - Holds voter state including election data, answers, and encrypted ballot - Implements election loading from server with metadata support - Provides screen navigation and exit functionality - Renders election info screen with start button - Includes placeholder screens for Phase 2-3 work - Update crypto/types.ts to remove global declarations (use window access instead) - Adjust tsconfig.json to disable unused locals/params checking for Phase 1 shell This provides the main application shell for the voting booth with state management ready for Phase 2 (voting flow) and Phase 3 (submission/audit). Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/src/booth-app.ts | 400 ++++++++++++++++++++++++++++ heliosbooth2026/src/crypto/types.ts | 19 +- heliosbooth2026/src/main.ts | 9 + heliosbooth2026/tsconfig.json | 4 +- 4 files changed, 414 insertions(+), 18 deletions(-) create mode 100644 heliosbooth2026/src/booth-app.ts create mode 100644 heliosbooth2026/src/main.ts diff --git a/heliosbooth2026/src/booth-app.ts b/heliosbooth2026/src/booth-app.ts new file mode 100644 index 000000000..003502c9e --- /dev/null +++ b/heliosbooth2026/src/booth-app.ts @@ -0,0 +1,400 @@ +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import type { Election, ElectionMetadata, EncryptedAnswer, BigIntType } from './crypto/types.js'; + +/** + * Screen states for the voting booth flow. + */ +type BoothScreen = 'loading' | 'election' | 'question' | 'review' | 'submit' | 'audit'; + +/** + * Main booth application component. + * Holds all voter state and manages screen navigation. + */ +@customElement('booth-app') +export class BoothApp extends LitElement { + static styles = css` + :host { + display: block; + max-width: 800px; + margin: 0 auto; + padding: var(--spacing-md, 16px); + } + + .banner { + background-color: var(--color-surface, #f5f5f5); + padding: var(--spacing-md, 16px); + text-align: center; + margin-bottom: var(--spacing-lg, 24px); + border-bottom: 1px solid var(--color-border, #ddd); + } + + .banner h1 { + margin: 0; + font-size: var(--font-size-xl, 1.5rem); + } + + .banner .exit-link { + float: right; + font-size: var(--font-size-sm, 0.875rem); + } + + .banner .exit-link a { + color: var(--color-text-secondary, #666); + text-decoration: none; + } + + .banner .exit-link a:hover { + text-decoration: underline; + } + + .content { + min-height: 400px; + } + + .loading { + text-align: center; + padding: var(--spacing-xl, 32px); + } + + .error { + background-color: #fee; + border: 1px solid var(--color-error, #dc3545); + color: var(--color-error, #dc3545); + padding: var(--spacing-md, 16px); + border-radius: var(--border-radius, 4px); + margin-bottom: var(--spacing-md, 16px); + } + + .progress-bar { + display: flex; + justify-content: center; + gap: var(--spacing-md, 16px); + margin-bottom: var(--spacing-lg, 24px); + padding: var(--spacing-sm, 8px); + background-color: var(--color-surface, #f5f5f5); + border-radius: var(--border-radius, 4px); + } + + .progress-step { + padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px); + border-radius: var(--border-radius, 4px); + font-size: var(--font-size-sm, 0.875rem); + } + + .progress-step.active { + background-color: var(--color-primary, #1a73e8); + color: #fff; + } + + .progress-step.completed { + color: var(--color-success, #28a745); + } + `; + + // Application state + @state() private currentScreen: BoothScreen = 'loading'; + @state() private election: Election | null = null; + @state() private electionMetadata: ElectionMetadata | null = null; + @state() private electionUrl: string = ''; + @state() private error: string | null = null; + + // Voting state + @state() private currentQuestionIndex: number = 0; + @state() private answers: number[][] = []; + @state() private allQuestionsSeen: boolean = false; + + // Encryption state + @state() private encryptedAnswers: (EncryptedAnswer | null)[] = []; + @state() private encryptedBallotHash: string = ''; + @state() private encryptedVoteJson: string = ''; + + // Crypto readiness + @state() private cryptoReady: boolean = false; + + connectedCallback(): void { + super.connectedCallback(); + this.initializeBooth(); + } + + /** + * Initialize the booth by loading crypto libraries and election data. + */ + private async initializeBooth(): Promise { + try { + // Get election URL from query params + const params = new URLSearchParams(window.location.search); + const electionUrl = params.get('election_url'); + + if (!electionUrl) { + this.error = 'No election URL provided. Please access this page from an election link.'; + this.currentScreen = 'election'; + return; + } + + this.electionUrl = electionUrl; + + // Wait for BigInt crypto to be ready + await this.waitForCrypto(); + this.cryptoReady = true; + + // Load election data + await this.loadElection(electionUrl); + + this.currentScreen = 'election'; + } catch (err) { + this.error = `Failed to initialize booth: ${err instanceof Error ? err.message : String(err)}`; + this.currentScreen = 'election'; + } + } + + /** + * Wait for the BigInt crypto library to be ready. + */ + private waitForCrypto(): Promise { + return new Promise((resolve, reject) => { + const cryptoBigInt = (typeof window !== 'undefined' ? (window as any).BigInt : undefined) as BigIntType; + if (cryptoBigInt && typeof cryptoBigInt.setup === 'function') { + cryptoBigInt.setup(resolve, reject); + } else { + // Crypto libs not loaded via script tags yet - for now just resolve + // In production, crypto libs are loaded via script tags in index.html + console.warn('BigInt not available - crypto operations will fail'); + resolve(); + } + }); + } + + /** + * Load election data from the server. + */ + private async loadElection(electionUrl: string): Promise { + // Fetch election JSON + const electionResponse = await fetch(electionUrl); + if (!electionResponse.ok) { + throw new Error(`Failed to fetch election: ${electionResponse.status}`); + } + const rawJson = await electionResponse.text(); + + // Fetch election metadata + const metaResponse = await fetch(`${electionUrl}/meta`); + if (metaResponse.ok) { + this.electionMetadata = await metaResponse.json(); + } + + // Parse election using HELIOS library + const helios = (typeof window !== 'undefined' ? (window as any).HELIOS : undefined); + const b64_sha256_fn = (typeof window !== 'undefined' ? (window as any).b64_sha256 : undefined); + + if (helios && typeof helios.Election === 'object' && typeof helios.Election.fromJSONString === 'function') { + const parsedElection = helios.Election.fromJSONString(rawJson); + if (b64_sha256_fn && typeof b64_sha256_fn === 'function') { + parsedElection.hash = b64_sha256_fn(rawJson); + parsedElection.election_hash = parsedElection.hash; + } + + this.election = parsedElection; + + // Initialize answer tracking + this.answers = parsedElection.questions.map(() => []); + this.encryptedAnswers = parsedElection.questions.map(() => null); + + // Set up answer ordering (for randomization if configured) + this.setupAnswerOrderings(); + + // Update document title + document.title = `Helios Voting Booth - ${parsedElection.name}`; + } else { + throw new Error('HELIOS crypto library not loaded'); + } + } + + /** + * Set up answer orderings for each question (supports randomization). + */ + private setupAnswerOrderings(): void { + if (!this.election) return; + + this.election.question_answer_orderings = this.election.questions.map((question, _i) => { + const ordering = question.answers.map((_, j) => j); + + // Shuffle if randomization is enabled at election or question level + if ( + (this.electionMetadata?.randomize_answer_order) || + question.randomize_answer_order + ) { + this.shuffleArray(ordering); + } + + return ordering; + }); + } + + /** + * Fisher-Yates shuffle algorithm. + */ + private shuffleArray(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + /** + * Handle exit button click. + */ + private handleExit(): void { + if (this.currentScreen !== 'election' && this.currentScreen !== 'loading') { + const confirmed = confirm( + 'Are you sure you want to exit the booth and lose all information about your current ballot?' + ); + if (!confirmed) return; + } + + if (this.election?.cast_url) { + window.location.href = this.election.cast_url; + } + } + + /** + * Navigate to a specific screen. + */ + navigateTo(screen: BoothScreen): void { + this.currentScreen = screen; + } + + /** + * Start voting - go to first question. + */ + startVoting(): void { + this.currentQuestionIndex = 0; + this.currentScreen = 'question'; + } + + /** + * Get the current progress step number (1-4). + */ + private getProgressStep(): number { + switch (this.currentScreen) { + case 'question': return 1; + case 'review': return 2; + case 'submit': return 3; + case 'audit': return 4; + default: return 0; + } + } + + render() { + return html` + + + ${this.currentScreen !== 'loading' && this.currentScreen !== 'election' ? html` + + ` : ''} + + ${this.error ? html` + + ` : ''} + +
+ ${this.renderCurrentScreen()} +
+ `; + } + + private renderCurrentScreen() { + switch (this.currentScreen) { + case 'loading': + return html` +
+

Loading election...

+
+ `; + + case 'election': + return this.renderElectionScreen(); + + case 'question': + return html`

Question screen - to be implemented in Phase 2

`; + + case 'review': + return html`

Review screen - to be implemented in Phase 3

`; + + case 'submit': + return html`

Submit screen - to be implemented in Phase 3

`; + + case 'audit': + return html`

Audit screen - to be implemented in Phase 3

`; + + default: + return html`

Unknown screen

`; + } + } + + /** + * Render the election info screen (start screen). + */ + private renderElectionScreen() { + if (!this.election) { + return html` +
+

Loading election information...

+
+ `; + } + + return html` +
+

${this.election.name}

+ + ${this.election.description ? html` +
+

${this.election.description}

+
+ ` : ''} + +
+

To vote, follow these steps:

+
    +
  1. Select your preferred options.
  2. +
  3. Review your choices, which are then encrypted.
  4. +
  5. Submit your encrypted ballot and authenticate to verify your eligibility.
  6. +
+
+ +
+ +
+ + ${this.electionMetadata?.help_email ? html` +

+ You can + + email for help + . +

+ ` : ''} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'booth-app': BoothApp; + } +} diff --git a/heliosbooth2026/src/crypto/types.ts b/heliosbooth2026/src/crypto/types.ts index f8af66236..38c6428af 100644 --- a/heliosbooth2026/src/crypto/types.ts +++ b/heliosbooth2026/src/crypto/types.ts @@ -149,19 +149,6 @@ export interface ElectionMetadata { use_advanced_audit_features?: boolean; } -// Declare globals that will be available after loading jscrypto scripts -declare global { - const BigInt: BigIntType; - const ElGamal: ElGamalType; - const HELIOS: HELIOSType; - const Random: RandomType; - const UTILS: UTILSType; - const USE_SJCL: boolean; - const sjcl: { - random: { - startCollectors(): void; - addEntropy(data: string): void; - }; - }; - function b64_sha256(data: string): string; -} +// Note: Crypto globals are accessed via window context in booth-app.ts +// to avoid conflicts with TypeScript's built-in BigInt type. +// They are loaded via + + + + + + + + + + From 30d57266a22bb4add66ea0402cfc70568a497dfc Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:45:14 +0000 Subject: [PATCH 07/33] feat(booth2026): add Django URL route for new booth --- urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/urls.py b/urls.py index 06150f8b7..bc518984e 100644 --- a/urls.py +++ b/urls.py @@ -10,6 +10,7 @@ # SHOULD BE REPLACED BY APACHE STATIC PATH re_path(r'booth/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth'}), + re_path(r'booth2026/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth2026'}), re_path(r'verifier/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosverifier'}), re_path(r'static/auth/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/helios_auth/media'}), From 39ef4919779610381fa680974e72c7bd81756957 Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:51:02 +0000 Subject: [PATCH 08/33] chore(booth2026): add .gitignore and track package-lock.json Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/.gitignore | 2 + heliosbooth2026/package-lock.json | 1192 +++++++++++++++++++++++++++++ 2 files changed, 1194 insertions(+) create mode 100644 heliosbooth2026/.gitignore create mode 100644 heliosbooth2026/package-lock.json diff --git a/heliosbooth2026/.gitignore b/heliosbooth2026/.gitignore new file mode 100644 index 000000000..b94707787 --- /dev/null +++ b/heliosbooth2026/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/heliosbooth2026/package-lock.json b/heliosbooth2026/package-lock.json new file mode 100644 index 000000000..2414cc70e --- /dev/null +++ b/heliosbooth2026/package-lock.json @@ -0,0 +1,1192 @@ +{ + "name": "heliosbooth2026", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "heliosbooth2026", + "version": "1.0.0", + "dependencies": { + "lit": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz", + "integrity": "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} From cec180adbb2dc0aa048d5b266029c738c7e4ebe3 Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 15:57:10 +0000 Subject: [PATCH 09/33] fix(booth2026): address code review feedback from Phase 1 - Fix crypto script paths from root-absolute (/lib/jscrypto/) to relative (../lib/jscrypto/) to work correctly when served at /booth2026/ in production - Add typed declare global block in crypto/types.ts for non-conflicting globals (HELIOS, b64_sha256, ElGamal, Random, UTILS, sjcl) with proper type narrowing - Update booth-app.ts to use declared typed globals directly instead of (window as any) casting - Move inline styles to CSS classes (.start-button-container and .help-email) in booth-app.ts static styles - Export BoothScreen type for child component usage in later phases Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/index.html | 22 +++++++++++----------- heliosbooth2026/src/booth-app.ts | 27 +++++++++++++++++---------- heliosbooth2026/src/crypto/types.ts | 18 +++++++++++++++--- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/heliosbooth2026/index.html b/heliosbooth2026/index.html index fdef110df..2221a4fc9 100644 --- a/heliosbooth2026/index.html +++ b/heliosbooth2026/index.html @@ -7,17 +7,17 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/heliosbooth2026/src/booth-app.ts b/heliosbooth2026/src/booth-app.ts index 003502c9e..b56f21e0e 100644 --- a/heliosbooth2026/src/booth-app.ts +++ b/heliosbooth2026/src/booth-app.ts @@ -5,7 +5,7 @@ import type { Election, ElectionMetadata, EncryptedAnswer, BigIntType } from './ /** * Screen states for the voting booth flow. */ -type BoothScreen = 'loading' | 'election' | 'question' | 'review' | 'submit' | 'audit'; +export type BoothScreen = 'loading' | 'election' | 'question' | 'review' | 'submit' | 'audit'; /** * Main booth application component. @@ -90,6 +90,15 @@ export class BoothApp extends LitElement { .progress-step.completed { color: var(--color-success, #28a745); } + + .start-button-container { + text-align: center; + margin-top: var(--spacing-lg, 24px); + } + + .help-email { + margin-top: var(--spacing-lg, 24px); + } `; // Application state @@ -153,6 +162,7 @@ export class BoothApp extends LitElement { */ private waitForCrypto(): Promise { return new Promise((resolve, reject) => { + // Note: BigInt is accessed via (window as any) to avoid conflicts with TypeScript's built-in BigInt const cryptoBigInt = (typeof window !== 'undefined' ? (window as any).BigInt : undefined) as BigIntType; if (cryptoBigInt && typeof cryptoBigInt.setup === 'function') { cryptoBigInt.setup(resolve, reject); @@ -183,13 +193,10 @@ export class BoothApp extends LitElement { } // Parse election using HELIOS library - const helios = (typeof window !== 'undefined' ? (window as any).HELIOS : undefined); - const b64_sha256_fn = (typeof window !== 'undefined' ? (window as any).b64_sha256 : undefined); - - if (helios && typeof helios.Election === 'object' && typeof helios.Election.fromJSONString === 'function') { - const parsedElection = helios.Election.fromJSONString(rawJson); - if (b64_sha256_fn && typeof b64_sha256_fn === 'function') { - parsedElection.hash = b64_sha256_fn(rawJson); + if (typeof HELIOS !== 'undefined' && HELIOS.Election && typeof HELIOS.Election.fromJSONString === 'function') { + const parsedElection = HELIOS.Election.fromJSONString(rawJson); + if (typeof b64_sha256 === 'function') { + parsedElection.hash = b64_sha256(rawJson); parsedElection.election_hash = parsedElection.hash; } @@ -373,14 +380,14 @@ export class BoothApp extends LitElement { -
+
${this.electionMetadata?.help_email ? html` -

+

You can diff --git a/heliosbooth2026/src/crypto/types.ts b/heliosbooth2026/src/crypto/types.ts index 38c6428af..53ae61cb3 100644 --- a/heliosbooth2026/src/crypto/types.ts +++ b/heliosbooth2026/src/crypto/types.ts @@ -149,6 +149,18 @@ export interface ElectionMetadata { use_advanced_audit_features?: boolean; } -// Note: Crypto globals are accessed via window context in booth-app.ts -// to avoid conflicts with TypeScript's built-in BigInt type. -// They are loaded via @@ -18,9 +25,24 @@ + + + +

+ - + + diff --git a/heliosbooth2026/package-lock.json b/heliosbooth2026/package-lock.json index 2414cc70e..935a163f5 100644 --- a/heliosbooth2026/package-lock.json +++ b/heliosbooth2026/package-lock.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "terser": "^5.46.0", "typescript": "^5.7.0", "vite": "^6.0.0" } @@ -458,6 +459,56 @@ "node": ">=18" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", @@ -846,6 +897,33 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1065,6 +1143,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1075,6 +1163,36 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/heliosbooth2026/package.json b/heliosbooth2026/package.json index 92287331c..ec624af69 100644 --- a/heliosbooth2026/package.json +++ b/heliosbooth2026/package.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "terser": "^5.46.0", "typescript": "^5.7.0", "vite": "^6.0.0" } diff --git a/heliosbooth2026/public/encrypting.gif b/heliosbooth2026/public/encrypting.gif new file mode 100644 index 0000000000000000000000000000000000000000..58bfafe5aed2567c9b6c9ceff8b6b6fde53a0af9 GIT binary patch literal 2892 zcmd7U{aX^&0taximZ0{sRf_0sRvHLuD^n4&)zei$t(=x1Xg4cG5LZs8yxH4Q5wxwc z35u(H1u-`@6-0YcO99trrt;=mc~$VOo0|!ucHN$9mwWF&aL@Cc^TT=0bDr~gzUM6% z8-(z;8UdNW9!M873E z5R+e;3UQ@;nQScDZTqN^)mKq0CikySNZ4Qcjh|gD=TZ44P-8&FXS(Mt9TCl%+S>V% z>^9lRXFaV~2g&E0V)rO7q@Iq&{-nJy?q^!a7_{yV!dK=rSMj=$Fui?_9tJW2N&I+W zZgjU{5X+Z3LNGQhNOFM&Po6iSIBU137jNw8%tot^77jn+be5+W>~`{w zk$w}F$K4|6eRsAox`yh%X88^`y&NCRKhbsVEJF9JX<}l5?z;|FmHm;cuMrByjCED8 zF}zcH6%Q|^Vd(z+!5nw830&lE6Gihgi9jetk>;UXuVJ*ni4d%~IjF-w(Pl(U zE1404rYiTvtRSo`5%-r77n@y5H=k8d3<^RD_0kPmvZJHVld_!&#;YlYlGH2d-^6yb zE!?<=uNJ(iG`oC#P&cFAZ0FDiOC9U5YTR4fXEVFg`*_gqnl<9bgL|D0??))6QLo#Y zNVCKv{PSIdZpJJU2AS{%ngP~J=2Q;;NPasI$afcE<!xjeeUPyCDPdS=nQixVrAbITCEGKiyt>hA$bcFjeYt zSbs19IbW8p$(aW#;F^$mN)f8KP>zQ?7EdT-aT9t`HJgd`s45O}s_9YVNpu|(mb%?f!q#xNsYU%=Yt8njhm{)Dqk&^q#@AvCkOR47H(u_h?OzYFk+g177Y3cke zaZ|t7=c5aSLlxWZBX$^^?z;FmUQWNVC0G9IkNn}}v`F`j|8Sz54$G-m`-Ff0piYhY z#jF2hlY^$C=;t(Azt=hLgV*OCfBhU61)Vzv&MJITG1mo_6k?EIEWP_>w>5~T?)D+m ztwVfq{6<3vu&l*%M!`XPA%(+<=Q~6$zT1;W4?rkg_)R~U-(0RZadY0068EIMPa6wIgx{J? zdyA>io-&)(ZYU$4G%-}M%V1G(=uwuZ*pFY8{wU@D_Pk4|R^YW2pQa!fUg!c3hI=O5QE^0x* zO)mZmI2JV&+R~bNH}>hGXGxgnnYQs?Kp)RfD3M1AD_Ec<)?ybgzirIi4K_!CSIhM! z{QkOibCf#n!$uYYT77G$$@Z>`_o=7)hi>H1@<#)@4?^8f9#?%4b?@h@XUa9H4~T}F znuf}{$9K@?&pR4MeIVgo;?bF<-Ai2=1;P5%x2seIuqH`Aa(0!xtSU%uudjj$`Em>? z(SeSX{)QPCkr>p`YbkUb<`m>x1&6^z!KG*vI*4m4KsbktLzwbhX^pN`;gM~}Y$c%= zxKn6XPlAW&F2UAwT1Sv4dpKSsCcKpFqCAF6+SbKP2qeL9wcXQ(I z4XyoaTKpwc(bEgFUChN#eP7K5b!Ru{sH)to9~!+Aff+&`JIXQG0Ty9xu#eU|Y79L8 z!gdl?RHV@O5;QwfwxI1|C!ICL zd8~?(sk~}qdULc5hXHqmj|2meU<8tA)W}1M z_|;;C>`G{YS&g7p+fKrXV!Kdn>m;if3F*;csR68gLxlH{*;<_yQ zzD)mct+AypK)L0)RTckL{}sPe>zi%ixF@dOMuaD`FRqam;7i6QY@W4?d>r*fMxNK) z{bM-jV?vxeS(=442Gb3ElOqV# zLPyd$?$rb%tzMA}Q52!X;YDEdJSD_dc23EYakWY_NGBqSClT7oP4DkdJ4;5{|1ka! N|BvWD{=0vce**RaQgHwP literal 0 HcmV?d00001 diff --git a/heliosbooth2026/public/loading.gif b/heliosbooth2026/public/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..3c2f7c058836808f7f1f5f9a4bdf37d4e5f9284a GIT binary patch literal 3208 zcmc(ic~Dc=9>*`aOO|`};l05I0veErPzymNWmO?SfPgFlg5pAq3huH91c9)G1Y`|i z4JZjDhG<1Z5!{L(f?6D`8``H2sCB`;j(xVT^L)Wphk5_LnZEPqnRDmP=X1Wl@66!` z`n$Ttvj7(G763kc_y7RFrAwCz3JSWqx*8f9xLj^@boA)x=);E(&z?OyXU-f5f{bVW zSl0ix;3aK}PuV15r6r~$u;RDIr*GdCFLF%Wxp^00{VN2}j0dehpey_$SMt2W{1!QK zKojHE!m014ehNU3O@{&#81Ry?oX#6DZ$$v0J3e>A35z_WBvJ<_#BKo;WU| zlhe}qUa=5u3mwW&3lJ7s?M1x36dY=FTw|QvgGz$IR&C=!53NBJpfA=OKGM`_RmbT% znZm9NNG{r+6zds~vIJC01Jq2Sfd~xI=Y0{MfaQy zn2ZzlCpfx2_e$RKF6Y3;lDC^Ctng6>y!>|IX5edIqlS+PO-?8+ z`B&W3L?HdjNFjrNI!Jn^_xX`vLz8IS;`MxK?2dHilQLyLW(Kk1FgksCojERsJ!?iEkw+`1cDYq6akXxle%?Jr<{{=0nz`Kk-S^@n0J8?VXMIkDd80qP5Zm)#`}B9q`aYD-x25 zc@QMAn3TmSh+$G`MJqYrrZlSzXqzXwkxe}q+R{=~MXl6{TMe0tZ;lxDwHaEwS~Tn) z%Z4-bbN=m#CC+_Hj=V@B(_K9qdqPDt^t)b6FaB0hLKPppyu1i6y5o8OFfai$3|@Hf z;}f9$JoCBho5!)9?In}=Wi7?^t?W>oEX>UIsE7wEM6JuV|urBCNX|~_fosA>efw^cee6+8#zdilg;yU=9%o2Tr8vKU(UXB z3kWh_IQ#Dlz2mDX28*Vsv~^2N0@-2rA5dndqT#a_FD7Mja*;&mNGTuQl4hBa#RlbU zyNJdn0&7;IFttot;-xlVx#2#Rt0hHS8Yc?$hTuI$Ax^85FTg>Ou?^asn^v zc4TamL;dN)1SL|wK5J+}IUv2WZek)s&{URu5`W(KbZO#xJ-h7I%bmY@-Nh&FUD-3b zWYh3hA$_f%(+^E&|9Jfl`pIECdq1scZFL2~(DjE!P`xQick6HdB~DW0RW%CJs%Egc z5*vQ&0+H<+8=2yLP{*8J|AcQU5HKERhC^Yc8+NlT`wE?W{KMilM$MR*u`F^Vg|y0P zH$vvm4^8ofIt;5X%DqHWn*2F7FBENb*Qjev#6oN7p$rX0Wr+o zs`8{oPY+ryQp?#Sq!&YSG)vgY_Gs^!%G7))-)}L!8*2e#qa^10fs}hSj~-QC@-4P~ z6qFe9!gDNk%%gbp7$K<>c~-GPNqH$TKYQ-6`*N1g%+J>kPgn4EssJL|j0Ip5#AY?s zRM6Erzwp(Dilg}V_^V)%qWGU*#U9ns-X-MKYl| zwFePZV^uR!FKtm8+&~Gt)DlKfaDSp(XD8Bx>sdSsxd$cN6#7_!m=A>Xob*j5%IRbb zL+IeOburN9EZZ>Z9V|2W!Ll&m3Wh3Gp-TYt&PcD{jknNG3RUzoTSoVzE3-^Q04Zo> zo;@!8+wSODeZ97yngE&Z;n_3~QezZYX6lH()hmh|!W>Kvk9*v*4a;;;uE^_s5$88j z@v}80$2lr=(S2WP{rV(s;4ea&y7i}<7XxY=T&X^_9@OJUZ0sn8#??REOF5?yT1o`- zcy532%O{1)9c9x=V!U)kdGqd6mgst zjK)D-dV{YE!y_F;(H;WUcZBDP7GSpl>Q%HuunND8;a5kUr6+R98O-cNL&bM=ik$%oZJ^bN~{`Ou$DNS@CB>aXDEiy1~>dAVzrxJXf|%q~{3 zV+sT$5OlN3ch~51Ia#f2Dy#?LDRKz$p>(uvXKchk3lKrb!5U$BE`ni$=yiZPfK&CDbpRi{y#a8x>Lvn-cH8Z2YFcxCWPvAg{g4_(vBgWOcI!oCDiIr*tgFD z0>S>ZbG=}lo*<*B9x-NM2+WPPzk!bHFPppF5E{UBX{72*x15C{|HfBzB=y)?!u4((=0EgFLA_ z6`T@*qVPu%h`}%=g4~IcPci+B9@-2D7oZGStf5opdO-$lH-c!vJHV>+`Sv#v^E=-M zy2;5mj{xJ#ck$qxWMVRMnc%^tr=x`E2j(mK&uiab@cCNZ3*; z{}ciWc1dFPu?S2#l*O}QL#Hy~RyUEaitnx6%8J5aG?N#&&2ooOFi*BoP^rKruGE6e zcty2q{Z3UiqprS6E6a4e(ctyDh^*`q;E_{?+fE^2WEl1@`Khci${^T>BfB-uBvB zWRm+Rso1^=^H?Vo|byTTbgxVWRzkrjj8ud(@m}8ax_s zY?YdiajB#$UkG9tIz0b*bBDr_s}UX3GqXvExGLdpADx_i0Helios is now encrypting your ballot
diff --git a/heliosbooth2026/src/screens/review-screen.ts b/heliosbooth2026/src/screens/review-screen.ts index c44f8a679..033536ad4 100644 --- a/heliosbooth2026/src/screens/review-screen.ts +++ b/heliosbooth2026/src/screens/review-screen.ts @@ -238,7 +238,7 @@ export class ReviewScreen extends LitElement { ${this.isLoading ? html` ` : ''}
diff --git a/heliosbooth2026/vite.config.ts b/heliosbooth2026/vite.config.ts index 5e4b6086e..0c6b544fe 100644 --- a/heliosbooth2026/vite.config.ts +++ b/heliosbooth2026/vite.config.ts @@ -1,20 +1,46 @@ import { defineConfig } from 'vite'; +import { resolve } from 'path'; export default defineConfig({ root: '.', base: '/booth2026/', build: { outDir: 'dist', + // Ensure single bundle for offline operation cssCodeSplit: false, rollupOptions: { + input: { + main: resolve(__dirname, 'index.html') + }, output: { + // Force everything into single bundle manualChunks: () => 'booth', - entryFileNames: 'booth.js', - assetFileNames: 'booth.[ext]' + entryFileNames: 'assets/booth.[hash].js', + chunkFileNames: 'assets/booth.[hash].js', + assetFileNames: 'assets/booth.[hash].[ext]' + } + }, + // Generate sourcemaps for debugging + sourcemap: true, + // Minify for production + minify: 'terser', + terserOptions: { + compress: { + drop_console: false, // Keep console for debugging crypto issues + drop_debugger: true } } }, server: { - port: 5173 - } + port: 5173, + // Proxy API requests to Django during development + proxy: { + '/helios': { + target: 'http://localhost:8000', + changeOrigin: true + } + } + }, + // Copy static assets + publicDir: 'public' }); From c5e8f30b5f0c4bbffb393eb008dd20bd8cecc9d9 Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 16:38:31 +0000 Subject: [PATCH 26/33] docs(booth2026): add production deployment comment to URL config Add documentation comment to the booth2026 URL route explaining how to switch from serving source files in development to serving built dist files in production deployment. --- urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/urls.py b/urls.py index bc518984e..55cd5ebb7 100644 --- a/urls.py +++ b/urls.py @@ -10,6 +10,8 @@ # SHOULD BE REPLACED BY APACHE STATIC PATH re_path(r'booth/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth'}), + # New Lit-based booth (serves source files in dev, built files in production) + # In production, this should point to heliosbooth2026/dist instead re_path(r'booth2026/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosbooth2026'}), re_path(r'verifier/(?P.*)$', serve, {'document_root' : settings.ROOT_PATH + '/heliosverifier'}), From 952cee01be08abe26d91f489245abcf928815bc0 Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 16:39:54 +0000 Subject: [PATCH 27/33] feat(booth2026): move GIF assets to public directory for production build Move encrypting.gif and loading.gif from root heliosbooth2026/ to public/ directory so they are properly included in the production build output. Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/encrypting.gif | Bin 2892 -> 0 bytes heliosbooth2026/loading.gif | Bin 3208 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 heliosbooth2026/encrypting.gif delete mode 100644 heliosbooth2026/loading.gif diff --git a/heliosbooth2026/encrypting.gif b/heliosbooth2026/encrypting.gif deleted file mode 100644 index 58bfafe5aed2567c9b6c9ceff8b6b6fde53a0af9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2892 zcmd7U{aX^&0taximZ0{sRf_0sRvHLuD^n4&)zei$t(=x1Xg4cG5LZs8yxH4Q5wxwc z35u(H1u-`@6-0YcO99trrt;=mc~$VOo0|!ucHN$9mwWF&aL@Cc^TT=0bDr~gzUM6% z8-(z;8UdNW9!M873E z5R+e;3UQ@;nQScDZTqN^)mKq0CikySNZ4Qcjh|gD=TZ44P-8&FXS(Mt9TCl%+S>V% z>^9lRXFaV~2g&E0V)rO7q@Iq&{-nJy?q^!a7_{yV!dK=rSMj=$Fui?_9tJW2N&I+W zZgjU{5X+Z3LNGQhNOFM&Po6iSIBU137jNw8%tot^77jn+be5+W>~`{w zk$w}F$K4|6eRsAox`yh%X88^`y&NCRKhbsVEJF9JX<}l5?z;|FmHm;cuMrByjCED8 zF}zcH6%Q|^Vd(z+!5nw830&lE6Gihgi9jetk>;UXuVJ*ni4d%~IjF-w(Pl(U zE1404rYiTvtRSo`5%-r77n@y5H=k8d3<^RD_0kPmvZJHVld_!&#;YlYlGH2d-^6yb zE!?<=uNJ(iG`oC#P&cFAZ0FDiOC9U5YTR4fXEVFg`*_gqnl<9bgL|D0??))6QLo#Y zNVCKv{PSIdZpJJU2AS{%ngP~J=2Q;;NPasI$afcE<!xjeeUPyCDPdS=nQixVrAbITCEGKiyt>hA$bcFjeYt zSbs19IbW8p$(aW#;F^$mN)f8KP>zQ?7EdT-aT9t`HJgd`s45O}s_9YVNpu|(mb%?f!q#xNsYU%=Yt8njhm{)Dqk&^q#@AvCkOR47H(u_h?OzYFk+g177Y3cke zaZ|t7=c5aSLlxWZBX$^^?z;FmUQWNVC0G9IkNn}}v`F`j|8Sz54$G-m`-Ff0piYhY z#jF2hlY^$C=;t(Azt=hLgV*OCfBhU61)Vzv&MJITG1mo_6k?EIEWP_>w>5~T?)D+m ztwVfq{6<3vu&l*%M!`XPA%(+<=Q~6$zT1;W4?rkg_)R~U-(0RZadY0068EIMPa6wIgx{J? zdyA>io-&)(ZYU$4G%-}M%V1G(=uwuZ*pFY8{wU@D_Pk4|R^YW2pQa!fUg!c3hI=O5QE^0x* zO)mZmI2JV&+R~bNH}>hGXGxgnnYQs?Kp)RfD3M1AD_Ec<)?ybgzirIi4K_!CSIhM! z{QkOibCf#n!$uYYT77G$$@Z>`_o=7)hi>H1@<#)@4?^8f9#?%4b?@h@XUa9H4~T}F znuf}{$9K@?&pR4MeIVgo;?bF<-Ai2=1;P5%x2seIuqH`Aa(0!xtSU%uudjj$`Em>? z(SeSX{)QPCkr>p`YbkUb<`m>x1&6^z!KG*vI*4m4KsbktLzwbhX^pN`;gM~}Y$c%= zxKn6XPlAW&F2UAwT1Sv4dpKSsCcKpFqCAF6+SbKP2qeL9wcXQ(I z4XyoaTKpwc(bEgFUChN#eP7K5b!Ru{sH)to9~!+Aff+&`JIXQG0Ty9xu#eU|Y79L8 z!gdl?RHV@O5;QwfwxI1|C!ICL zd8~?(sk~}qdULc5hXHqmj|2meU<8tA)W}1M z_|;;C>`G{YS&g7p+fKrXV!Kdn>m;if3F*;csR68gLxlH{*;<_yQ zzD)mct+AypK)L0)RTckL{}sPe>zi%ixF@dOMuaD`FRqam;7i6QY@W4?d>r*fMxNK) z{bM-jV?vxeS(=442Gb3ElOqV# zLPyd$?$rb%tzMA}Q52!X;YDEdJSD_dc23EYakWY_NGBqSClT7oP4DkdJ4;5{|1ka! N|BvWD{=0vce**RaQgHwP diff --git a/heliosbooth2026/loading.gif b/heliosbooth2026/loading.gif deleted file mode 100644 index 3c2f7c058836808f7f1f5f9a4bdf37d4e5f9284a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3208 zcmc(ic~Dc=9>*`aOO|`};l05I0veErPzymNWmO?SfPgFlg5pAq3huH91c9)G1Y`|i z4JZjDhG<1Z5!{L(f?6D`8``H2sCB`;j(xVT^L)Wphk5_LnZEPqnRDmP=X1Wl@66!` z`n$Ttvj7(G763kc_y7RFrAwCz3JSWqx*8f9xLj^@boA)x=);E(&z?OyXU-f5f{bVW zSl0ix;3aK}PuV15r6r~$u;RDIr*GdCFLF%Wxp^00{VN2}j0dehpey_$SMt2W{1!QK zKojHE!m014ehNU3O@{&#81Ry?oX#6DZ$$v0J3e>A35z_WBvJ<_#BKo;WU| zlhe}qUa=5u3mwW&3lJ7s?M1x36dY=FTw|QvgGz$IR&C=!53NBJpfA=OKGM`_RmbT% znZm9NNG{r+6zds~vIJC01Jq2Sfd~xI=Y0{MfaQy zn2ZzlCpfx2_e$RKF6Y3;lDC^Ctng6>y!>|IX5edIqlS+PO-?8+ z`B&W3L?HdjNFjrNI!Jn^_xX`vLz8IS;`MxK?2dHilQLyLW(Kk1FgksCojERsJ!?iEkw+`1cDYq6akXxle%?Jr<{{=0nz`Kk-S^@n0J8?VXMIkDd80qP5Zm)#`}B9q`aYD-x25 zc@QMAn3TmSh+$G`MJqYrrZlSzXqzXwkxe}q+R{=~MXl6{TMe0tZ;lxDwHaEwS~Tn) z%Z4-bbN=m#CC+_Hj=V@B(_K9qdqPDt^t)b6FaB0hLKPppyu1i6y5o8OFfai$3|@Hf z;}f9$JoCBho5!)9?In}=Wi7?^t?W>oEX>UIsE7wEM6JuV|urBCNX|~_fosA>efw^cee6+8#zdilg;yU=9%o2Tr8vKU(UXB z3kWh_IQ#Dlz2mDX28*Vsv~^2N0@-2rA5dndqT#a_FD7Mja*;&mNGTuQl4hBa#RlbU zyNJdn0&7;IFttot;-xlVx#2#Rt0hHS8Yc?$hTuI$Ax^85FTg>Ou?^asn^v zc4TamL;dN)1SL|wK5J+}IUv2WZek)s&{URu5`W(KbZO#xJ-h7I%bmY@-Nh&FUD-3b zWYh3hA$_f%(+^E&|9Jfl`pIECdq1scZFL2~(DjE!P`xQick6HdB~DW0RW%CJs%Egc z5*vQ&0+H<+8=2yLP{*8J|AcQU5HKERhC^Yc8+NlT`wE?W{KMilM$MR*u`F^Vg|y0P zH$vvm4^8ofIt;5X%DqHWn*2F7FBENb*Qjev#6oN7p$rX0Wr+o zs`8{oPY+ryQp?#Sq!&YSG)vgY_Gs^!%G7))-)}L!8*2e#qa^10fs}hSj~-QC@-4P~ z6qFe9!gDNk%%gbp7$K<>c~-GPNqH$TKYQ-6`*N1g%+J>kPgn4EssJL|j0Ip5#AY?s zRM6Erzwp(Dilg}V_^V)%qWGU*#U9ns-X-MKYl| zwFePZV^uR!FKtm8+&~Gt)DlKfaDSp(XD8Bx>sdSsxd$cN6#7_!m=A>Xob*j5%IRbb zL+IeOburN9EZZ>Z9V|2W!Ll&m3Wh3Gp-TYt&PcD{jknNG3RUzoTSoVzE3-^Q04Zo> zo;@!8+wSODeZ97yngE&Z;n_3~QezZYX6lH()hmh|!W>Kvk9*v*4a;;;uE^_s5$88j z@v}80$2lr=(S2WP{rV(s;4ea&y7i}<7XxY=T&X^_9@OJUZ0sn8#??REOF5?yT1o`- zcy532%O{1)9c9x=V!U)kdGqd6mgst zjK)D-dV{YE!y_F;(H;WUcZBDP7GSpl>Q%HuunND8;a5kUr6+R98O-cNL&bM=ik$%oZJ^bN~{`Ou$DNS@CB>aXDEiy1~>dAVzrxJXf|%q~{3 zV+sT$5OlN3ch~51Ia#f2Dy#?LDRKz$p>(uvXKchk3lKrb!5U$BE`ni$=yiZPfK&CDbpRi{y#a8x>Lvn-cH8Z2YFcxCWPvAg{g4_(vBgWOcI!oCDiIr*tgFD z0>S>ZbG=}lo*<*B9x-NM2+WPPzk!bHFPppF5E{UBX{72*x15C{|HfBzB=y)?!u4((=0EgFLA_ z6`T@*qVPu%h`}%=g4~IcPci+B9@-2D7oZGStf5opdO-$lH-c!vJHV>+`Sv#v^E=-M zy2;5mj{xJ#ck$qxWMVRMnc%^tr=x`E2j(mK&uiab@cCNZ3*; z{}ciWc1dFPu?S2#l*O}QL#Hy~RyUEaitnx6%8J5aG?N#&&2ooOFi*BoP^rKruGE6e zcty2q{Z3UiqprS6E6a4e(ctyDh^*`q;E_{?+fE^2WEl1@`Khci${^T>BfB-uBvB zWRm+Rso1^=^H?Vo|byTTbgxVWRzkrjj8ud(@m}8ax_s zY?YdiajB#$UkG9tIz0b*bBDr_s}UX3GqXvExGLdpADx_i0 Date: Sun, 8 Feb 2026 16:45:07 +0000 Subject: [PATCH 28/33] fix(booth2026): address all code review issues Critical (C1): Fix aria-current attribute binding on progress steps - Changed from string interpolation (which Lit ignores) to proper conditional attribute binding using aria-current=${condition ? 'step' : nothing} - Imported 'nothing' from 'lit' for proper undefined handling Important (I1): Add focusMainContent() to startVoting() and goToQuestion() - Both screen transition methods now call focusMainContent() like other transitions (sealBallot, prepareForCast, auditBallot) for consistent focus management and accessibility Important (I2): Remove redundant role="main" from main element - The
element has implicit role="main", explicit attribute was redundant and violates HTML best practices Minor (M1): Consolidate duplicate .error and .error-message styles - Both classes had identical background, border, color, padding, and border-radius CSS. Now consolidated with shared rule and context- specific margin handling for .error-screen .error-message Minor (M2): Remove useless preload link from index.html - The before the actual stylesheet provided zero benefit and cluttered the HTML All issues verified: - TypeScript compiles without errors (npx tsc --noEmit) - Production build succeeds (npm run build) - No regressions introduced Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/index.html | 3 --- heliosbooth2026/src/booth-app.ts | 27 ++++++++++++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/heliosbooth2026/index.html b/heliosbooth2026/index.html index 5dc7f0a8e..e3fa9c208 100644 --- a/heliosbooth2026/index.html +++ b/heliosbooth2026/index.html @@ -7,9 +7,6 @@ Helios Voting Booth - - - diff --git a/heliosbooth2026/src/booth-app.ts b/heliosbooth2026/src/booth-app.ts index 0a4bcd069..6d724f973 100644 --- a/heliosbooth2026/src/booth-app.ts +++ b/heliosbooth2026/src/booth-app.ts @@ -1,4 +1,4 @@ -import { LitElement, html, css } from 'lit'; +import { LitElement, html, css, nothing } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import type { Election, ElectionMetadata, EncryptedAnswer, EncryptedVote, BigIntType } from './crypto/types.js'; import './screens/question-screen.js'; @@ -87,12 +87,16 @@ export class BoothApp extends LitElement { color: var(--color-text-secondary, #666); } - .error { + .error, + .error-message { background-color: #fee; border: 1px solid var(--color-error, #dc3545); color: var(--color-error, #dc3545); padding: var(--spacing-md, 16px); border-radius: var(--border-radius, 4px); + } + + .error { margin-bottom: var(--spacing-md, 16px); } @@ -135,12 +139,7 @@ export class BoothApp extends LitElement { padding: var(--spacing-xl, 32px); } - .error-message { - background-color: #fee; - border: 1px solid var(--color-error, #dc3545); - color: var(--color-error, #dc3545); - padding: var(--spacing-md, 16px); - border-radius: var(--border-radius, 4px); + .error-screen .error-message { margin: var(--spacing-lg, 24px) 0; } @@ -585,6 +584,7 @@ export class BoothApp extends LitElement { startVoting(): void { this.currentQuestionIndex = 0; this.currentScreen = 'question'; + this.focusMainContent(); // If only one question, show review button immediately if (this.election && this.election.questions.length === 1) { @@ -683,6 +683,7 @@ export class BoothApp extends LitElement { if (this.election && index >= 0 && index < this.election.questions.length) { this.currentQuestionIndex = index; this.currentScreen = 'question'; + this.focusMainContent(); } } @@ -883,19 +884,19 @@ export class BoothApp extends LitElement { ${this.currentScreen !== 'loading' && this.currentScreen !== 'election' ? html` @@ -905,7 +906,7 @@ export class BoothApp extends LitElement { ` : ''} -
+
${this.renderCurrentScreen()}
`; From d4ca87932e8cd1653eeb62775f3ebbf0837c7699 Mon Sep 17 00:00:00 2001 From: Ben Adida Date: Sun, 8 Feb 2026 16:56:36 +0000 Subject: [PATCH 29/33] fix: address code review feedback for Helios Booth 2026 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1: Fix crypto script paths in index.html - change ../lib/ to lib/ to resolve correctly when served from /booth2026/ - I1: Re-enable noUnusedLocals and noUnusedParameters in tsconfig.json and fix unused cryptoReady state - I2: Clarify production deployment comment in urls.py - serve full heliosbooth2026/ directory (not just dist/) because crypto libs in lib/ are needed - I3: Fix choices[index]?.length < question.max comparison - use nullish coalescing (?? 0) to avoid undefined comparison - M1: Change encryptedBallot type from unknown to EncryptedVote | null and remove unnecessary type casts - M2: Merge duplicate import statements from crypto/types into single import All issues fixed: ✓ TypeScript compilation passes with strict unused checking ✓ npm run build succeeds ✓ Django test suite passes (198/198 tests) Co-Authored-By: Claude Opus 4.6 --- heliosbooth2026/index.html | 22 ++++++++++---------- heliosbooth2026/src/booth-app.ts | 13 ++++-------- heliosbooth2026/src/screens/review-screen.ts | 2 +- heliosbooth2026/tsconfig.json | 4 ++-- urls.py | 3 ++- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/heliosbooth2026/index.html b/heliosbooth2026/index.html index e3fa9c208..25f51bbf6 100644 --- a/heliosbooth2026/index.html +++ b/heliosbooth2026/index.html @@ -11,17 +11,17 @@ - - - - - - - - - - - + + + + + + + + + + +