diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..1c6935bb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.13'] + postgres-version: ['16'] + + services: + postgres: + image: postgres:${{ matrix.postgres-version }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: helios + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + PGHOST: localhost + PGUSER: postgres + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libldap2-dev libsasl2-dev + + - name: Install Python dependencies + run: | + uv sync + uv pip freeze + + - name: Run tests + run: uv run python -Wall manage.py test -v 2 --settings=settings_ci diff --git a/.gitignore b/.gitignore index 681947f7f..430d84e58 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ deploy-latest.sh .DS_Store *~ media/* -venv +.venv +venv* celerybeat-* env.sh -.cache \ No newline at end of file +.cache +.idea/ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index be482e4e4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -sudo: false -language: python -python: - - "2.7" -# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: - - pip install setuptools==24.3.1 - - pip install -r requirements.txt -# command to run tests, e.g. python setup.py test -script: "python manage.py test" -addons: - postgresql: "9.3" -before_script: - - psql -c 'create database helios;' -U postgres diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..5e6c02db0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,158 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with the Helios Election System codebase. + +## Project Overview + +Helios is an end-to-end verifiable voting system that provides secure, transparent online elections with cryptographic verification. It supports multiple authentication systems (Google, Facebook, GitHub, LDAP, CAS, password, etc.) and uses homomorphic encryption for privacy-preserving vote tallying. + +## Critical Instructions for Claude when preparing a PR + +- always run the tests. Install everything and run the tests. Every time. + +## Technology Stack + +- **Python**: 3.13 +- **Framework**: Django 5.2 +- **Database**: PostgreSQL 9.5+ +- **Task Queue**: Celery with RabbitMQ +- **Crypto**: pycryptodome +- **Package Manager**: uv + +## Common Commands + +```bash +# Install dependencies +uv sync + +# Run development server +uv run python manage.py runserver + +# Run all tests +uv run python manage.py test -v 2 + +# Run tests for a specific app +uv run python manage.py test helios -v 2 +uv run python manage.py test helios_auth -v 2 + +# Run a specific test class +uv run python manage.py test helios.tests.ElectionModelTests -v 2 + +# Database migrations +uv run python manage.py makemigrations +uv run python manage.py migrate + +# Reset database (drops and recreates) +./reset.sh + +# Start Celery worker (for background tasks) +uv run celery --app helios worker --events --beat --concurrency 1 +``` + +## Project Structure + +- `helios/` - Core election system (models, views, crypto, forms) +- `helios_auth/` - Authentication system with multiple backends +- `server_ui/` - Admin web interface +- `heliosbooth/` - JavaScript voting booth interface +- `heliosverifier/` - JavaScript ballot verification interface + +## Code Style Conventions + +### Naming + +- **Boolean fields**: Use `_p` suffix (e.g., `private_p`, `frozen_p`, `admin_p`, `featured_p`) +- **Datetime fields**: Use `_at` suffix (e.g., `created_at`, `frozen_at`, `voting_ends_at`) +- **Functions/methods**: snake_case +- **Classes**: PascalCase + +### Indentation + +- Use 2-space indentation throughout Python files + +### Imports + +```python +# Standard library +import copy, csv, datetime, uuid + +# Third-party +from django.db import models, transaction +import bleach + +# Local +from helios import datatypes, utils +from helios_auth.jsonfield import JSONField +``` + +## Key Patterns + +### View Decorators + +Use existing security decorators for views: + +```python +from helios.security import election_view, election_admin, trustee_check + +@election_view(frozen=True) +def my_view(request, election): + pass + +@election_admin() +def admin_view(request, election): + pass +``` + +### Model Base Class + +All domain models inherit from `HeliosModel`: + +```python +class MyModel(HeliosModel): + class Meta: + app_label = 'helios' +``` + +### JSON Responses + +```python +from helios.views import render_json +return render_json({'key': 'value'}) +``` + +### Template Rendering + +```python +from helios.views import render_template +return render_template(request, 'template_name', {'context': 'vars'}) +``` + +### Database Queries + +- Use `@transaction.atomic` for operations that need atomicity +- Prefer `select_related()` for foreign key joins +- Use `get_or_create()` pattern for safe creation + +## Security Considerations + +- Always use `check_csrf(request)` for POST handlers +- Use `bleach.clean()` for user-provided HTML (see `description_bleached` pattern) +- Never store plaintext passwords; use the auth system's hashing +- Check permissions with `user_can_admin_election()` and similar helpers + +## Configuration + +Settings use environment variables with defaults: + +```python +from settings import get_from_env +MY_SETTING = get_from_env('MY_SETTING', 'default_value') +``` + +Key environment variables: `DEBUG`, `SECRET_KEY`, `DATABASE_URL`, `CELERY_BROKER_URL`, `AUTH_ENABLED_AUTH_SYSTEMS` + +## Testing + +- Tests use Django's TestCase with django-webtest +- Fixtures are in `helios/fixtures/` +- Test classes: `ElectionModelTests`, `VoterModelTests`, `ElectionBlackboxTests`, etc. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 61ac19608..a0c88cf51 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -7,3 +7,6 @@ Significant contributors: - Olivier de Marneffe - Emily Stark, Mike Hamburg, Tom Wu, and Dan Boneh for SJCL and integration of javascript crypto. - Nicholas Chang-Fong and Aleksander Essex for security reports and fixes. +- Shirley Chaves +- Marco Ciotola +- Lucas Araujo diff --git a/INSTALL.md b/INSTALL.md index fdb0ae10f..282956c91 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,50 +1,69 @@ -* install PostgreSQL 8.3+ +# Helios Server Installation -* make sure you have virtualenv installed: -http://www.virtualenv.org/en/latest/ +## Prerequisites -* download helios-server +* Install PostgreSQL 12+ -* cd into the helios-server directory +* Install RabbitMQ + This is needed for Celery to work, which does background processing such as + the processing of uploaded list-of-voter CSV files. -* create a virtualenv: +* Download helios-server + +* `cd` into the helios-server directory + +## Python Setup + +* Install Python 3.13 including dev packages ``` -virtualenv venv +sudo apt install python3 python3-dev ``` -* activate virtual environment +* Install uv (modern Python package manager) ``` -source venv/bin/activate -```` +curl -LsSf https://astral.sh/uv/install.sh | sh +``` -* install requirements +* You'll also need Postgres dev libraries. For example on Ubuntu: ``` -pip install -r requirements.txt +sudo apt install libpq-dev ``` -* reset database +* Install dependencies (uv creates a virtual environment automatically) + +``` +uv sync +``` + +## Database Setup + +* Reset database ``` ./reset.sh ``` -* start server +## Running the Server + +* Start server ``` -python manage.py runserver +uv run python manage.py runserver ``` -* to get Google Auth working: +## Google Auth Configuration + +To get Google Auth working: -** go to https://console.developers.google.com +* Go to https://console.developers.google.com -** create an application +* Create an application -** set up oauth2 credentials as a web application, with your origin, e.g. https://myhelios.example.com, and your auth callback, which, based on our example, is https://myhelios.example.com/auth/after/ +* Set up OAuth2 credentials as a web application, with your origin, e.g. `https://myhelios.example.com`, and your auth callback, which based on our example is `https://myhelios.example.com/auth/after/` -** still in the developer console, enable the Google+ API. +* In the developer console, enable the Google People API -** set the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET configuration variables accordingly. \ No newline at end of file +* Set the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` configuration variables accordingly diff --git a/Procfile b/Procfile index 63d8d0cf6..04ed6cbca 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ web: gunicorn wsgi:application -b 0.0.0.0:$PORT -w 8 -worker: python manage.py celeryd -E -B --beat --concurrency=1 \ No newline at end of file +worker: celery --app helios worker --events --beat --concurrency 1 \ No newline at end of file diff --git a/README.md b/README.md index 5c90a9c20..9a5519bfc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ Helios is an end-to-end verifiable voting system. -![Travis Build Status](https://travis-ci.org/benadida/helios-server.svg?branch=master) +[![Travis Build Status](https://travis-ci.org/benadida/helios-server.svg?branch=master)](https://travis-ci.org/benadida/helios-server) [![Stories in Ready](https://badge.waffle.io/benadida/helios-server.png?label=ready&title=Ready)](https://waffle.io/benadida/helios-server) diff --git a/docs/design-plans/2026-01-18-helios-booth-lit-redesign.md b/docs/design-plans/2026-01-18-helios-booth-lit-redesign.md new file mode 100644 index 000000000..ba7db1759 --- /dev/null +++ b/docs/design-plans/2026-01-18-helios-booth-lit-redesign.md @@ -0,0 +1,153 @@ +# Helios Booth Lit Redesign + +## Summary + +The Helios Booth Lit Redesign builds a modern replacement for the existing jQuery-based voting booth using Lit web components, TypeScript, and Vite. The new booth runs independently at `/booth2026/` without requiring server changes, preserving the election system's offline security model and byte-for-byte identical cryptographic operations. + +The implementation follows a props-drilling state management pattern where the main `booth-app` component holds all voter state and distributes data to screen components (election, question, review, submit, and audit screens) through reactive properties, with screens communicating state changes via events. By copying and adapting the existing crypto libraries (`jscrypto/`), reusing the Web Worker encryption pattern, and maintaining identical ballot formatting, the new booth achieves code clarity and maintainability while ensuring full compatibility with existing election servers and verification tools. + +## Definition of Done + +**Deliverables:** +- A design plan for a new Lit-based voting booth at `/booth2026/` +- Runs alongside existing booth (separate URL, no server changes needed) +- Preserves all cryptographic operations unchanged +- Preserves offline security model (no network during voting) +- Basic accessibility from the start (semantic HTML, ARIA labels, keyboard navigation) +- i18n architecture noted but implementation deferred to future iteration +- 4 high-level implementation phases + +**Success criteria:** +A simpler, clearer version of the existing detailed plan that's actionable across multiple PRs. + +## Glossary + +- **Lit 3.x**: A lightweight web components library for building fast, efficient UI with template literals and reactive properties. +- **Web Components**: Browser-native reusable UI elements defined as custom HTML tags with encapsulated DOM and styling. +- **Vite**: A modern frontend build tool that uses native ES modules during development and creates optimized production bundles. +- **Props drilling**: A state management pattern where parent components pass data down to children via properties, and children communicate back via events. +- **Web Worker**: A browser API enabling background JavaScript execution in a separate thread, used here for non-blocking ballot encryption. +- **jscrypto**: Helios's JavaScript cryptographic library implementing ElGamal encryption, BigInt arithmetic, and ballot serialization. +- **ElGamal encryption**: The public-key cryptographic scheme used by Helios for encrypting individual votes. +- **@lit/localize**: Lit's internationalization library using the `msg()` function for translation (planned for future iteration). +- **ARIA labels**: Accessibility attributes describing UI elements for screen readers and assistive technologies. + +## Architecture + +A new Lit-based voting booth at `heliosbooth2026/` running independently alongside the existing jQuery booth at `heliosbooth/`. + +**Technology stack:** +- Lit 3.x for web components +- TypeScript for type safety +- Vite for bundling (single bundle, no code splitting for offline operation) + +**State management:** Props drilling pattern. `booth-app` holds all state as reactive properties and passes data to screen components via props. Screens emit events for state changes. No global state or context API. + +**Component structure:** + +``` +booth-app (main container, holds all state) +├── election-screen (election info, start button) +├── question-screen (question display, answer selection, navigation) +├── review-screen (encryption progress, ballot review) +├── submit-screen (ballot hash, cast form) +└── audit-screen (audit trail display) +``` + +**Data flow:** +1. `booth-app` loads election JSON on mount +2. User navigates through questions, `booth-app` tracks answers +3. On review, Web Worker encrypts ballot, posts progress back +4. On submit, form POSTs encrypted ballot to server's cast URL + +**Crypto integration:** Copy `heliosbooth/js/jscrypto/` into `heliosbooth2026/lib/jscrypto/`. Create TypeScript type declarations for the global objects (`HELIOS`, `ElGamal`, `BigInt`). Adapt `boothworker-single.js` for the new structure. Encrypted ballot JSON must be byte-for-byte identical to old booth output. + +**Critical constraint:** Zero network calls between "Start" and "Submit". All assets bundled, all encryption client-side. + +## Existing Patterns + +Investigation found the existing booth at `heliosbooth/` uses: +- Single HTML entry point (`vote.html`) loading a compressed JS bundle +- `BOOTH` object as monolithic state container +- jQuery jTemplates for rendering +- Panel-based UI with show/hide +- Web Worker for async encryption + +**Patterns this design follows:** +- Same crypto layer (unchanged `jscrypto/` directory) +- Same Web Worker encryption approach +- Same API endpoints and ballot format +- Same panel-based screen switching concept + +**Patterns this design changes:** +- jQuery → Lit web components +- Monolithic BOOTH object → typed state in booth-app +- jQuery templates → Lit template literals +- Implicit globals → explicit TypeScript types + +The new booth produces identical encrypted ballots, ensuring server compatibility. + +## Implementation Phases + +### Phase 1: Setup & Core Shell +**Goal:** Project scaffolding and basic navigation working + +**Components:** +- `heliosbooth2026/` directory with Vite + TypeScript + Lit configuration +- `lib/jscrypto/` — copied crypto libraries from old booth +- `src/crypto/types.ts` — TypeScript declarations for crypto globals +- `src/booth-app.ts` — main component with screen switching +- `src/screens/election-screen.ts` — loads election, shows info, start button +- Basic CSS structure + +**Dependencies:** None (first phase) + +**Done when:** Can load an election from URL parameter, display election info, click "Start" to switch screens + +### Phase 2: Voting Flow +**Goal:** Complete question navigation and answer selection + +**Components:** +- `src/screens/question-screen.ts` — question display, answer checkboxes, validation +- Navigation logic in `booth-app` — previous/next/review +- Answer state tracking — selections per question +- Progress indicator — "Question N of M" +- Answer randomization — shuffle if election specifies + +**Dependencies:** Phase 1 (booth-app, election loading) + +**Done when:** Can navigate through all questions, select answers respecting min/max constraints, see progress, proceed to review + +### Phase 3: Crypto & Submission +**Goal:** Full voting flow including encryption and submission + +**Components:** +- `workers/encryption-worker.js` — adapted from `boothworker-single.js` +- `src/screens/review-screen.ts` — encryption progress, ballot summary, seal/audit/submit buttons +- `src/screens/submit-screen.ts` — ballot hash display, cast form +- `src/screens/audit-screen.ts` — audit trail JSON, back-to-voting flow +- Encryption orchestration in `booth-app` — worker communication, progress tracking + +**Dependencies:** Phase 2 (answers collected) + +**Done when:** Can encrypt ballot, see progress, view ballot hash, submit via form POST, or audit and re-vote + +### Phase 4: Polish & Deployment +**Goal:** Production-ready booth at `/booth2026/` + +**Components:** +- Accessibility — semantic HTML, ARIA labels, keyboard navigation +- Error handling — loading states, error messages +- CSS polish — responsive layout, visual consistency +- Offline verification — test zero network during voting +- Deployment configuration — serve from `/booth2026/` URL + +**Dependencies:** Phase 3 (complete flow working) + +**Done when:** Booth passes accessibility basics, handles errors gracefully, works offline during voting, deployed and accessible at `/booth2026/` + +## Additional Considerations + +**i18n architecture:** Design supports future i18n via `@lit/localize`. All user-facing strings would use `msg()` function. Implementation deferred — first iteration is English only. + +**Offline verification:** Each phase should verify no unexpected network calls. Phase 4 includes explicit offline testing (disconnect network after load, complete vote). diff --git a/extract-passwords-for-email.py b/extract-passwords-for-email.py index d72705792..dc9ac9564 100644 --- a/extract-passwords-for-email.py +++ b/extract-passwords-for-email.py @@ -5,12 +5,16 @@ # python extract-passwords-for-email.py # -from django.core.management import setup_environ -import settings, sys, csv +import sys -setup_environ(settings) +import csv +import django +import os -from helios.models import * +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") +django.setup() + +from helios.models import Election election_uuid = sys.argv[1] email = sys.argv[2] diff --git a/helios/__init__.py b/helios/__init__.py index 06f05140b..21f21bc59 100644 --- a/helios/__init__.py +++ b/helios/__init__.py @@ -1,7 +1,9 @@ - from django.conf import settings -from django.core.urlresolvers import reverse -from helios.views import election_shortcut +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery_app import app as celery_app + +__all__ = ('celery_app', 'TEMPLATE_BASE', 'ADMIN_ONLY', 'VOTERS_UPLOAD', 'VOTERS_EMAIL',) TEMPLATE_BASE = settings.HELIOS_TEMPLATE_BASE or "helios/templates/base.html" diff --git a/helios/apps.py b/helios/apps.py new file mode 100644 index 000000000..900974328 --- /dev/null +++ b/helios/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class HeliosConfig(AppConfig): + name = 'helios' + verbose_name = "Helios" diff --git a/helios/celery_app.py b/helios/celery_app.py new file mode 100644 index 000000000..89f0ecb54 --- /dev/null +++ b/helios/celery_app.py @@ -0,0 +1,21 @@ +import os + +# set the default Django settings module for the 'celery' program. +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') + +app = Celery() + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() + +@app.task(bind=True) +def debug_task(self): + print('Request: {0!r}'.format(self.request)) diff --git a/helios/crypto/algs.py b/helios/crypto/algs.py index acac4f44b..b7439d9bd 100644 --- a/helios/crypto/algs.py +++ b/helios/crypto/algs.py @@ -7,157 +7,61 @@ ben@adida.net """ -import math, hashlib, logging -import randpool, number +import logging -import numtheory +from Crypto.Hash import SHA1 +from Crypto.Util import number -# some utilities -class Utils: - RAND = randpool.RandomPool() - - @classmethod - def random_seed(cls, data): - cls.RAND.add_event(data) - - @classmethod - def random_mpz(cls, n_bits): - low = 2**(n_bits-1) - high = low * 2 - - # increment and find a prime - # return randrange(low, high) - - return number.getRandomNumber(n_bits, cls.RAND.get_bytes) - - @classmethod - def random_mpz_lt(cls, max): - # return randrange(0, max) - n_bits = int(math.floor(math.log(max, 2))) - return (number.getRandomNumber(n_bits, cls.RAND.get_bytes) % max) - - @classmethod - def random_prime(cls, n_bits): - return number.getPrime(n_bits, cls.RAND.get_bytes) - - @classmethod - def is_prime(cls, mpz): - #return numtheory.miller_rabin(mpz) - return number.isPrime(mpz) - - @classmethod - def xgcd(cls, a, b): - """ - Euclid's Extended GCD algorithm - """ - mod = a%b - - if mod == 0: - return 0,1 - else: - x,y = cls.xgcd(b, mod) - return y, x-(y*(a/b)) - - @classmethod - def inverse(cls, mpz, mod): - # return cls.xgcd(mpz,mod)[0] - return number.inverse(mpz, mod) - - @classmethod - def random_safe_prime(cls, n_bits): - p = None - q = None - - while True: - p = cls.random_prime(n_bits) - q = (p-1)/2 - if cls.is_prime(q): - return p - - @classmethod - def random_special_prime(cls, q_n_bits, p_n_bits): - p = None - q = None - - z_n_bits = p_n_bits - q_n_bits - - q = cls.random_prime(q_n_bits) - - while True: - z = cls.random_mpz(z_n_bits) - p = q*z + 1 - if cls.is_prime(p): - return p, q, z +from helios.crypto.utils import random +from helios.utils import to_json class ElGamal: def __init__(self): - self.p = None - self.q = None - self.g = None - - @classmethod - def generate(cls, n_bits): - """ - generate an El-Gamal environment. Returns an instance - of ElGamal(), with prime p, group size q, and generator g - """ - - EG = ElGamal() - - # find a prime p such that (p-1)/2 is prime q - EG.p = Utils.random_safe_prime(n_bits) - - # q is the order of the group - # FIXME: not always p-1/2 - EG.q = (EG.p-1)/2 - - # find g that generates the q-order subgroup - while True: - EG.g = Utils.random_mpz_lt(EG.p) - if pow(EG.g, EG.q, EG.p) == 1: - break - - return EG + self.p = None + self.q = None + self.g = None def generate_keypair(self): - """ - generates a keypair in the setting - """ + """ + generates a keypair in the setting + """ - keypair = EGKeyPair() - keypair.generate(self.p, self.q, self.g) + keypair = EGKeyPair() + keypair.generate(self.p, self.q, self.g) - return keypair + return keypair def toJSONDict(self): - return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)} + return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)} @classmethod def fromJSONDict(cls, d): - eg = cls() - eg.p = int(d['p']) - eg.q = int(d['q']) - eg.g = int(d['g']) - return eg + eg = cls() + eg.p = int(d['p']) + eg.q = int(d['q']) + eg.g = int(d['g']) + return eg + class EGKeyPair: def __init__(self): - self.pk = EGPublicKey() - self.sk = EGSecretKey() + self.pk = EGPublicKey() + self.sk = EGSecretKey() def generate(self, p, q, g): - """ - Generate an ElGamal keypair - """ - self.pk.g = g - self.pk.p = p - self.pk.q = q + """ + Generate an ElGamal keypair + """ + self.pk.g = g + self.pk.p = p + self.pk.q = q + + self.sk.x = random.mpz_lt(q) + self.pk.y = pow(g, self.sk.x, p) - self.sk.x = Utils.random_mpz_lt(q) - self.pk.y = pow(g, self.sk.x, p) + self.sk.pk = self.pk - self.sk.pk = self.pk class EGPublicKey: def __init__(self): @@ -166,7 +70,7 @@ def __init__(self): self.g = None self.q = None - def encrypt_with_r(self, plaintext, r, encode_message= False): + def encrypt_with_r(self, plaintext, r, encode_message=False): """ expecting plaintext.m to be a big integer """ @@ -175,13 +79,13 @@ def encrypt_with_r(self, plaintext, r, encode_message= False): # make sure m is in the right subgroup if encode_message: - y = plaintext.m + 1 - if pow(y, self.q, self.p) == 1: - m = y - else: - m = -y % self.p + y = plaintext.m + 1 + if pow(y, self.q, self.p) == 1: + m = y + else: + m = -y % self.p else: - m = plaintext.m + m = plaintext.m ciphertext.alpha = pow(self.g, r, self.p) ciphertext.beta = (m * pow(self.y, r, self.p)) % self.p @@ -192,7 +96,7 @@ def encrypt_return_r(self, plaintext): """ Encrypt a plaintext and return the randomness just generated and used. """ - r = Utils.random_mpz_lt(self.q) + r = random.mpz_lt(self.q) ciphertext = self.encrypt_with_r(plaintext, r) return [ciphertext, r] @@ -207,70 +111,69 @@ def to_dict(self): """ Serialize to dictionary. """ - return {'y' : str(self.y), 'p' : str(self.p), 'g' : str(self.g) , 'q' : str(self.q)} + return {'y': str(self.y), 'p': str(self.p), 'g': str(self.g), 'q': str(self.q)} toJSONDict = to_dict # quick hack FIXME def toJSON(self): - import utils - return utils.to_json(self.toJSONDict()) + return to_json(self.toJSONDict()) - def __mul__(self,other): - if other == 0 or other == 1: - return self + def __mul__(self, other): + if other == 0 or other == 1: + return self - # check p and q - if self.p != other.p or self.q != other.q or self.g != other.g: - raise Exception("incompatible public keys") + # check p and q + if self.p != other.p or self.q != other.q or self.g != other.g: + raise Exception("incompatible public keys") - result = EGPublicKey() - result.p = self.p - result.q = self.q - result.g = self.g - result.y = (self.y * other.y) % result.p - return result + result = EGPublicKey() + result.p = self.p + result.q = self.q + result.g = self.g + result.y = (self.y * other.y) % result.p + return result - def verify_sk_proof(self, dlog_proof, challenge_generator = None): - """ - verify the proof of knowledge of the secret key - g^response = commitment * y^challenge - """ - left_side = pow(self.g, dlog_proof.response, self.p) - right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p + def verify_sk_proof(self, dlog_proof, challenge_generator=None): + """ + verify the proof of knowledge of the secret key + g^response = commitment * y^challenge + """ + left_side = pow(self.g, dlog_proof.response, self.p) + right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p - expected_challenge = challenge_generator(dlog_proof.commitment) % self.q + expected_challenge = challenge_generator(dlog_proof.commitment) % self.q - return ((left_side == right_side) and (dlog_proof.challenge == expected_challenge)) + return (left_side == right_side) and (dlog_proof.challenge == expected_challenge) def validate_pk_params(self): - # check primality of p - if not number.isPrime(self.p): - raise Exception("p is not prime.") + # check primality of p + if not number.isPrime(self.p): + raise Exception("p is not prime.") - # check length of p - if not (number.size(self.p) >= 2048): - raise Exception("p of insufficient length. Should be 2048 bits or greater.") + # check length of p + if not (number.size(self.p) >= 2048): + raise Exception("p of insufficient length. Should be 2048 bits or greater.") - # check primality of q - if not number.isPrime(self.q): - raise Exception("q is not prime.") + # check primality of q + if not number.isPrime(self.q): + raise Exception("q is not prime.") - # check length of q - if not (number.size(self.q) >= 256): - raise Exception("q of insufficient length. Should be 256 bits or greater.") + # check length of q + if not (number.size(self.q) >= 256): + raise Exception("q of insufficient length. Should be 256 bits or greater.") - if (pow(self.g,self.q,self.p)!=1): - raise Exception("g does not generate subgroup of order q.") + if pow(self.g, self.q, self.p) != 1: + raise Exception("g does not generate subgroup of order q.") - if not (1 < self.g < self.p-1): - raise Exception("g out of range.") + if not (1 < self.g < self.p - 1): + raise Exception("g out of range.") - if not (1 < self.y < self.p-1): - raise Exception("y out of range.") + if not (1 < self.y < self.p - 1): + raise Exception("y out of range.") - if (pow(self.y,self.q,self.p)!=1): - raise Exception("g does not generate proper group.") + if pow(self.y, self.q, self.p) != 1: + raise Exception("g does not generate proper group.") @classmethod def from_dict(cls, d): @@ -284,14 +187,15 @@ def from_dict(cls, d): pk.q = int(d['q']) try: - pk.validate_pk_params() + pk.validate_pk_params() except Exception as e: - raise + raise e return pk fromJSONDict = from_dict + class EGSecretKey: def __init__(self): self.x = None @@ -317,25 +221,25 @@ def decryption_factor_and_proof(self, ciphertext, challenge_generator=None): return dec_factor, proof - def decrypt(self, ciphertext, dec_factor = None, decode_m=False): + def decrypt(self, ciphertext, dec_factor=None, decode_m=False): """ Decrypt a ciphertext. Optional parameter decides whether to encode the message into the proper subgroup. """ if not dec_factor: dec_factor = self.decryption_factor(ciphertext) - m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p + m = (number.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p if decode_m: - # get m back from the q-order subgroup - if m < self.pk.q: - y = m - else: - y = -m % self.pk.p + # get m back from the q-order subgroup + if m < self.pk.q: + y = m + else: + y = -m % self.pk.p - return EGPlaintext(y-1, self.pk) + return EGPlaintext(y - 1, self.pk) else: - return EGPlaintext(m, self.pk) + return EGPlaintext(m, self.pk) def prove_decryption(self, ciphertext): """ @@ -350,66 +254,66 @@ def prove_decryption(self, ciphertext): and alpha^t = b * beta/m ^ c """ - m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p - beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p + m = (number.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p + beta_over_m = (ciphertext.beta * number.inverse(m, self.pk.p)) % self.pk.p # pick a random w - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) a = pow(self.pk.g, w, self.pk.p) b = pow(ciphertext.alpha, w, self.pk.p) - c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16) + c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(), 16) t = (w + self.x * c) % self.pk.q return m, { - 'commitment' : {'A' : str(a), 'B': str(b)}, - 'challenge' : str(c), - 'response' : str(t) - } + 'commitment': {'A': str(a), 'B': str(b)}, + 'challenge': str(c), + 'response': str(t) + } def to_dict(self): - return {'x' : str(self.x), 'public_key' : self.pk.to_dict()} + return {'x': str(self.x), 'public_key': self.pk.to_dict()} toJSONDict = to_dict def prove_sk(self, challenge_generator): - """ - Generate a PoK of the secret key - Prover generates w, a random integer modulo q, and computes commitment = g^w mod p. - Verifier provides challenge modulo q. - Prover computes response = w + x*challenge mod q, where x is the secret key. - """ - w = Utils.random_mpz_lt(self.pk.q) - commitment = pow(self.pk.g, w, self.pk.p) - challenge = challenge_generator(commitment) % self.pk.q - response = (w + (self.x * challenge)) % self.pk.q - - return DLogProof(commitment, challenge, response) + """ + Generate a PoK of the secret key + Prover generates w, a random integer modulo q, and computes commitment = g^w mod p. + Verifier provides challenge modulo q. + Prover computes response = w + x*challenge mod q, where x is the secret key. + """ + w = random.mpz_lt(self.pk.q) + commitment = pow(self.pk.g, w, self.pk.p) + challenge = challenge_generator(commitment) % self.pk.q + response = (w + (self.x * challenge)) % self.pk.q + return DLogProof(commitment, challenge, response) @classmethod def from_dict(cls, d): if not d: - return None + return None sk = cls() sk.x = int(d['x']) - if d.has_key('public_key'): - sk.pk = EGPublicKey.from_dict(d['public_key']) + if 'public_key' in d: + sk.pk = EGPublicKey.from_dict(d['public_key']) else: - sk.pk = None + sk.pk = None return sk fromJSONDict = from_dict + class EGPlaintext: - def __init__(self, m = None, pk = None): + def __init__(self, m=None, pk=None): self.m = m self.pk = pk def to_dict(self): - return {'m' : self.m} + return {'m': self.m} @classmethod def from_dict(cls, d): @@ -424,17 +328,17 @@ def __init__(self, alpha=None, beta=None, pk=None): self.alpha = alpha self.beta = beta - def __mul__(self,other): + def __mul__(self, other): """ Homomorphic Multiplication of ciphertexts. """ - if type(other) == int and (other == 0 or other == 1): - return self + if isinstance(other, int) and (other == 0 or other == 1): + return self if self.pk != other.pk: - logging.info(self.pk) - logging.info(other.pk) - raise Exception('different PKs!') + logging.info(self.pk) + logging.info(other.pk) + raise Exception('different PKs!') new = EGCiphertext() @@ -460,7 +364,7 @@ def reenc_return_r(self): """ Reencryption with fresh randomness, which is returned. """ - r = Utils.random_mpz_lt(self.pk.q) + r = random.mpz_lt(self.pk.q) new_c = self.reenc_with_r(r) return [new_c, r] @@ -471,189 +375,195 @@ def reenc(self): return self.reenc_return_r()[0] def __eq__(self, other): - """ - Check for ciphertext equality. - """ - if other == None: - return False + """ + Check for ciphertext equality. + """ + if other is None: + return False - return (self.alpha == other.alpha and self.beta == other.beta) + return self.alpha == other.alpha and self.beta == other.beta def generate_encryption_proof(self, plaintext, randomness, challenge_generator): - """ - Generate the disjunctive encryption proof of encryption - """ - # random W - w = Utils.random_mpz_lt(self.pk.q) + """ + Generate the disjunctive encryption proof of encryption + """ + # random W + w = random.mpz_lt(self.pk.q) - # build the proof - proof = EGZKProof() + # build the proof + proof = EGZKProof() - # compute A=g^w, B=y^w - proof.commitment['A'] = pow(self.pk.g, w, self.pk.p) - proof.commitment['B'] = pow(self.pk.y, w, self.pk.p) + # compute A=g^w, B=y^w + proof.commitment['A'] = pow(self.pk.g, w, self.pk.p) + proof.commitment['B'] = pow(self.pk.y, w, self.pk.p) - # generate challenge - proof.challenge = challenge_generator(proof.commitment); + # generate challenge + proof.challenge = challenge_generator(proof.commitment) - # Compute response = w + randomness * challenge - proof.response = (w + (randomness * proof.challenge)) % self.pk.q; + # Compute response = w + randomness * challenge + proof.response = (w + (randomness * proof.challenge)) % self.pk.q - return proof; + return proof def simulate_encryption_proof(self, plaintext, challenge=None): - # generate a random challenge if not provided - if not challenge: - challenge = Utils.random_mpz_lt(self.pk.q) + # generate a random challenge if not provided + if not challenge: + challenge = random.mpz_lt(self.pk.q) - proof = EGZKProof() - proof.challenge = challenge + proof = EGZKProof() + proof.challenge = challenge - # compute beta/plaintext, the completion of the DH tuple - beta_over_plaintext = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p + # compute beta/plaintext, the completion of the DH tuple + beta_over_plaintext = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p - # random response, does not even need to depend on the challenge - proof.response = Utils.random_mpz_lt(self.pk.q); + # random response, does not even need to depend on the challenge + proof.response = random.mpz_lt(self.pk.q) - # now we compute A and B - proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p - proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p + # now we compute A and B + proof.commitment['A'] = (number.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) + * pow(self.pk.g, proof.response, self.pk.p) + ) % self.pk.p + proof.commitment['B'] = (number.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow( + self.pk.y, proof.response, self.pk.p)) % self.pk.p - return proof + return proof def generate_disjunctive_encryption_proof(self, plaintexts, real_index, randomness, challenge_generator): - # note how the interface is as such so that the result does not reveal which is the real proof. + # note how the interface is as such so that the result does not reveal which is the real proof. - proofs = [None for p in plaintexts] + proofs = [None for _ in plaintexts] - # go through all plaintexts and simulate the ones that must be simulated. - for p_num in range(len(plaintexts)): - if p_num != real_index: - proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num]) + # go through all plaintexts and simulate the ones that must be simulated. + for p_num in range(len(plaintexts)): + if p_num != real_index: + proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num]) - # the function that generates the challenge - def real_challenge_generator(commitment): - # set up the partial real proof so we're ready to get the hash - proofs[real_index] = EGZKProof() - proofs[real_index].commitment = commitment + # the function that generates the challenge + def real_challenge_generator(commitment): + # set up the partial real proof so we're ready to get the hash + proofs[real_index] = EGZKProof() + proofs[real_index].commitment = commitment - # get the commitments in a list and generate the whole disjunctive challenge - commitments = [p.commitment for p in proofs] - disjunctive_challenge = challenge_generator(commitments); + # get the commitments in a list and generate the whole disjunctive challenge + commitments = [p.commitment for p in proofs] + disjunctive_challenge = challenge_generator(commitments) - # now we must subtract all of the other challenges from this challenge. - real_challenge = disjunctive_challenge - for p_num in range(len(proofs)): - if p_num != real_index: - real_challenge = real_challenge - proofs[p_num].challenge + # now we must subtract all of the other challenges from this challenge. + real_challenge = disjunctive_challenge + for p_num in range(len(proofs)): + if p_num != real_index: + real_challenge = real_challenge - proofs[p_num].challenge - # make sure we mod q, the exponent modulus - return real_challenge % self.pk.q + # make sure we mod q, the exponent modulus + return real_challenge % self.pk.q - # do the real proof - real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator) + # do the real proof + real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator) - # set the real proof - proofs[real_index] = real_proof + # set the real proof + proofs[real_index] = real_proof - return EGZKDisjunctiveProof(proofs) + return EGZKDisjunctiveProof(proofs) def verify_encryption_proof(self, plaintext, proof): - """ - Checks for the DDH tuple g, y, alpha, beta/plaintext. - (PoK of randomness r.) - - Proof contains commitment = {A, B}, challenge, response - """ - # check that A, B are in the correct group - if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1): - return False + """ + Checks for the DDH tuple g, y, alpha, beta/plaintext. + (PoK of randomness r.) + + Proof contains commitment = {A, B}, challenge, response + """ + # check that A, B are in the correct group + if not (pow(proof.commitment['A'], self.pk.q, self.pk.p) == 1 and pow(proof.commitment['B'], self.pk.q, + self.pk.p) == 1): + return False - # check that g^response = A * alpha^challenge - first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p)) + # check that g^response = A * alpha^challenge + first_check = (pow(self.pk.g, proof.response, self.pk.p) == ( + (pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p)) - # check that y^response = B * (beta/m)^challenge - beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p - second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p)) + # check that y^response = B * (beta/m)^challenge + beta_over_m = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p + second_check = (pow(self.pk.y, proof.response, self.pk.p) == ( + (pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p)) - # print "1,2: %s %s " % (first_check, second_check) - return (first_check and second_check) + # print "1,2: %s %s " % (first_check, second_check) + return first_check and second_check def verify_disjunctive_encryption_proof(self, plaintexts, proof, challenge_generator): - """ - plaintexts and proofs are all lists of equal length, with matching. + """ + plaintexts and proofs are all lists of equal length, with matching. - overall_challenge is what all of the challenges combined should yield. - """ - if len(plaintexts) != len(proof.proofs): - print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs))) - return False + overall_challenge is what all of the challenges combined should yield. + """ + if len(plaintexts) != len(proof.proofs): + print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs))) + return False - for i in range(len(plaintexts)): - # if a proof fails, stop right there - if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]): - print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]) - return False + for i in range(len(plaintexts)): + # if a proof fails, stop right there + if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]): + print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])) + return False - # logging.info("made it past the two encryption proofs") + # logging.info("made it past the two encryption proofs") - # check the overall challenge - return (challenge_generator([p.commitment for p in proof.proofs]) == (sum([p.challenge for p in proof.proofs]) % self.pk.q)) + # check the overall challenge + return (challenge_generator([p.commitment for p in proof.proofs]) == ( + sum([p.challenge for p in proof.proofs]) % self.pk.q)) def verify_decryption_proof(self, plaintext, proof): - """ - Checks for the DDH tuple g, alpha, y, beta/plaintext - (PoK of secret key x.) - """ - return False + """ + Checks for the DDH tuple g, alpha, y, beta/plaintext + (PoK of secret key x.) + """ + return False def verify_decryption_factor(self, dec_factor, dec_proof, public_key): - """ - when a ciphertext is decrypted by a dec factor, the proof needs to be checked - """ - pass + """ + when a ciphertext is decrypted by a dec factor, the proof needs to be checked + """ + pass def decrypt(self, decryption_factors, public_key): - """ - decrypt a ciphertext given a list of decryption factors (from multiple trustees) - For now, no support for threshold - """ - running_decryption = self.beta - for dec_factor in decryption_factors: - running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p + """ + decrypt a ciphertext given a list of decryption factors (from multiple trustees) + For now, no support for threshold + """ + running_decryption = self.beta + for dec_factor in decryption_factors: + running_decryption = (running_decryption * number.inverse(dec_factor, public_key.p)) % public_key.p - return running_decryption + return running_decryption def check_group_membership(self, pk): - """ - checks to see if an ElGamal element belongs to the group in the pk - """ - if not (1 < self.alpha < pk.p-1): - return False - - elif not (1 < self.beta < pk.p-1): - return False + """ + checks to see if an ElGamal element belongs to the group in the pk + """ + if not (1 < self.alpha < pk.p - 1): + return False - elif (pow(self.alpha, pk.q, pk.p)!=1): - return False + elif not (1 < self.beta < pk.p - 1): + return False - elif (pow(self.beta, pk.q, pk.p)!=1): - return False + elif pow(self.alpha, pk.q, pk.p) != 1: + return False - else: - return True + elif pow(self.beta, pk.q, pk.p) != 1: + return False + else: + return True def to_dict(self): return {'alpha': str(self.alpha), 'beta': str(self.beta)} - toJSONDict= to_dict + toJSONDict = to_dict def to_string(self): return "%s,%s" % (self.alpha, self.beta) @classmethod - def from_dict(cls, d, pk = None): + def from_dict(cls, d, pk=None): result = cls() result.alpha = int(d['alpha']) result.beta = int(d['beta']) @@ -668,127 +578,134 @@ def from_string(cls, str): expects alpha,beta """ split = str.split(",") - return cls.from_dict({'alpha' : split[0], 'beta' : split[1]}) + return cls.from_dict({'alpha': split[0], 'beta': split[1]}) + class EGZKProof(object): - def __init__(self): - self.commitment = {'A':None, 'B':None} - self.challenge = None - self.response = None + def __init__(self): + self.commitment = {'A': None, 'B': None} + self.challenge = None + self.response = None - @classmethod - def generate(cls, little_g, little_h, x, p, q, challenge_generator): - """ - generate a DDH tuple proof, where challenge generator is - almost certainly EG_fiatshamir_challenge_generator - """ + @classmethod + def generate(cls, little_g, little_h, x, p, q, challenge_generator): + """ + generate a DDH tuple proof, where challenge generator is + almost certainly EG_fiatshamir_challenge_generator + """ - # generate random w - w = Utils.random_mpz_lt(q) + # generate random w + w = random.mpz_lt(q) - # create proof instance - proof = cls() + # create proof instance + proof = cls() - # compute A = little_g^w, B=little_h^w - proof.commitment['A'] = pow(little_g, w, p) - proof.commitment['B'] = pow(little_h, w, p) + # compute A = little_g^w, B=little_h^w + proof.commitment['A'] = pow(little_g, w, p) + proof.commitment['B'] = pow(little_h, w, p) - # get challenge - proof.challenge = challenge_generator(proof.commitment) + # get challenge + proof.challenge = challenge_generator(proof.commitment) - # compute response - proof.response = (w + (x * proof.challenge)) % q + # compute response + proof.response = (w + (x * proof.challenge)) % q - # return proof - return proof + # return proof + return proof - @classmethod - def from_dict(cls, d): - p = cls() - p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])} - p.challenge = int(d['challenge']) - p.response = int(d['response']) - return p + @classmethod + def from_dict(cls, d): + p = cls() + p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])} + p.challenge = int(d['challenge']) + p.response = int(d['response']) + return p - fromJSONDict = from_dict + fromJSONDict = from_dict + + def to_dict(self): + return { + 'commitment': {'A': str(self.commitment['A']), 'B': str(self.commitment['B'])}, + 'challenge': str(self.challenge), + 'response': str(self.response) + } - def to_dict(self): - return { - 'commitment' : {'A' : str(self.commitment['A']), 'B' : str(self.commitment['B'])}, - 'challenge': str(self.challenge), - 'response': str(self.response) - } + toJSONDict = to_dict - def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None): - """ - Verify a DH tuple proof - """ - # check that A, B are in the correct group - if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1): - return False + def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None): + """ + Verify a DH tuple proof + """ + # check that A, B are in the correct group + if not (pow(self.commitment['A'], self.pk.q, self.pk.p) == 1 + and pow(self.commitment['B'], self.pk.q, self.pk.p) == 1): + return False - # check that little_g^response = A * big_g^challenge - first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p)) + # check that little_g^response = A * big_g^challenge + first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p)) - # check that little_h^response = B * big_h^challenge - second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p)) + # check that little_h^response = B * big_h^challenge + second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p)) - # check the challenge? - third_check = True + # check the challenge? + third_check = True - if challenge_generator: - third_check = (self.challenge == challenge_generator(self.commitment)) + if challenge_generator: + third_check = (self.challenge == challenge_generator(self.commitment)) - return (first_check and second_check and third_check) + return first_check and second_check and third_check - toJSONDict = to_dict class EGZKDisjunctiveProof: - def __init__(self, proofs = None): - self.proofs = proofs + def __init__(self, proofs=None): + self.proofs = proofs - @classmethod - def from_dict(cls, d): - dp = cls() - dp.proofs = [EGZKProof.from_dict(p) for p in d] - return dp + @classmethod + def from_dict(cls, d): + dp = cls() + dp.proofs = [EGZKProof.from_dict(p) for p in d] + return dp - def to_dict(self): - return [p.to_dict() for p in self.proofs] + def to_dict(self): + return [p.to_dict() for p in self.proofs] + + toJSONDict = to_dict - toJSONDict = to_dict class DLogProof(object): - def __init__(self, commitment, challenge, response): - self.commitment = commitment - self.challenge = challenge - self.response = response + def __init__(self, commitment, challenge, response): + self.commitment = commitment + self.challenge = challenge + self.response = response - def to_dict(self): - return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response' : str(self.response)} + def to_dict(self): + return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response': str(self.response)} - toJSONDict = to_dict + toJSONDict = to_dict - @classmethod - def from_dict(cls, d): - dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response'])) - return dlp + @classmethod + def from_dict(cls, d): + dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response'])) + return dlp + + fromJSONDict = from_dict - fromJSONDict = from_dict def EG_disjunctive_challenge_generator(commitments): - array_to_hash = [] - for commitment in commitments: - array_to_hash.append(str(commitment['A'])) - array_to_hash.append(str(commitment['B'])) + array_to_hash = [] + for commitment in commitments: + array_to_hash.append(str(commitment['A'])) + array_to_hash.append(str(commitment['B'])) + + string_to_hash = ",".join(array_to_hash) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16) - string_to_hash = ",".join(array_to_hash) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) # a challenge generator for Fiat-Shamir with A,B commitment def EG_fiatshamir_challenge_generator(commitment): - return EG_disjunctive_challenge_generator([commitment]) + return EG_disjunctive_challenge_generator([commitment]) + def DLog_challenge_generator(commitment): - string_to_hash = str(commitment) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) + string_to_hash = str(commitment) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16) diff --git a/helios/crypto/electionalgs.py b/helios/crypto/electionalgs.py index c67714043..605d9b4ab 100644 --- a/helios/crypto/electionalgs.py +++ b/helios/crypto/electionalgs.py @@ -4,778 +4,807 @@ Ben Adida 2008-08-30 """ - -import algs -import logging -import utils -import uuid import datetime +import uuid +import logging -class HeliosObject(object): - """ - A base class to ease serialization and de-serialization - crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out - and deserialized from jsonobjects on the way in - """ - FIELDS = [] - JSON_FIELDS = None - - def __init__(self, **kwargs): - self.set_from_args(**kwargs) - - # generate uuid if need be - if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid == None): - self.uuid = str(uuid.uuid4()) - - def set_from_args(self, **kwargs): - for f in self.FIELDS: - if kwargs.has_key(f): - new_val = self.process_value_in(f, kwargs[f]) - setattr(self, f, new_val) - else: - setattr(self, f, None) - - def set_from_other_object(self, o): - for f in self.FIELDS: - if hasattr(o, f): - setattr(self, f, self.process_value_in(f, getattr(o,f))) - else: - setattr(self, f, None) - - def toJSON(self): - return utils.to_json(self.toJSONDict()) - - def toJSONDict(self, alternate_fields=None): - val = {} - for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS): - val[f] = self.process_value_out(f, getattr(self, f)) - return val - - @classmethod - def fromJSONDict(cls, d): - # go through the keys and fix them - new_d = {} - for k in d.keys(): - new_d[str(k)] = d[k] - - return cls(**new_d) - - @classmethod - def fromOtherObject(cls, o): - obj = cls() - obj.set_from_other_object(o) - return obj - - def toOtherObject(self, o): - for f in self.FIELDS: - # FIXME: why isn't this working? - if hasattr(o, f): - # BIG HAMMER - try: - setattr(o, f, self.process_value_out(f, getattr(self,f))) - except: - pass - - @property - def hash(self): - s = utils.to_json(self.toJSONDict()) - return utils.hash_b64(s) - - def process_value_in(self, field_name, field_value): - """ - process some fields on the way into the object - """ - if field_value == None: - return None - - val = self._process_value_in(field_name, field_value) - if val != None: - return val - else: - return field_value +from helios.utils import to_json +from . import algs +from . import utils - def _process_value_in(self, field_name, field_value): - return None - def process_value_out(self, field_name, field_value): +class HeliosObject(object): """ - process some fields on the way out of the object + A base class to ease serialization and de-serialization + crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out + and deserialized from jsonobjects on the way in """ - if field_value == None: - return None - - val = self._process_value_out(field_name, field_value) - if val != None: - return val - else: - return field_value - - def _process_value_out(self, field_name, field_value): - return None + FIELDS = [] + JSON_FIELDS = None + + def __init__(self, **kwargs): + self.set_from_args(**kwargs) + + # generate uuid if need be + if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid is None): + self.uuid = str(uuid.uuid4()) + + def set_from_args(self, **kwargs): + for f in self.FIELDS: + if f in kwargs: + new_val = self.process_value_in(f, kwargs[f]) + setattr(self, f, new_val) + else: + setattr(self, f, None) + + def set_from_other_object(self, o): + for f in self.FIELDS: + if hasattr(o, f): + setattr(self, f, self.process_value_in(f, getattr(o, f))) + else: + setattr(self, f, None) + + def toJSON(self): + return to_json(self.toJSONDict()) + + def toJSONDict(self, alternate_fields=None): + val = {} + for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS): + val[f] = self.process_value_out(f, getattr(self, f)) + return val + + @classmethod + def fromJSONDict(cls, d): + # go through the keys and fix them + new_d = {} + for k in list(d.keys()): + new_d[str(k)] = d[k] + + return cls(**new_d) + + @classmethod + def fromOtherObject(cls, o): + obj = cls() + obj.set_from_other_object(o) + return obj + + def toOtherObject(self, o): + for f in self.FIELDS: + # FIXME: why isn't this working? + if hasattr(o, f): + # BIG HAMMER + try: + setattr(o, f, self.process_value_out(f, getattr(self, f))) + except: + pass + + @property + def hash(self): + s = to_json(self.toJSONDict()) + return utils.hash_b64(s) + + def process_value_in(self, field_name, field_value): + """ + process some fields on the way into the object + """ + if field_value is None: + return None + + val = self._process_value_in(field_name, field_value) + if val is not None: + return val + else: + return field_value + + def _process_value_in(self, field_name, field_value): + return None + + def process_value_out(self, field_name, field_value): + """ + process some fields on the way out of the object + """ + if field_value is None: + return None + + val = self._process_value_out(field_name, field_value) + if val is not None: + return val + else: + return field_value + + def _process_value_out(self, field_name, field_value): + return None + + def __eq__(self, other): + if not hasattr(self, 'uuid'): + return super(HeliosObject, self) == other + + return other is not None and self.uuid == other.uuid - def __eq__(self, other): - if not hasattr(self, 'uuid'): - return super(HeliosObject,self) == other - - return other != None and self.uuid == other.uuid class EncryptedAnswer(HeliosObject): - """ - An encrypted answer to a single election question - """ - - FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer'] - - # FIXME: remove this constructor and use only named-var constructor from HeliosObject - def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None): - self.choices = choices - self.individual_proofs = individual_proofs - self.overall_proof = overall_proof - self.randomness = randomness - self.answer = answer - - @classmethod - def generate_plaintexts(cls, pk, min=0, max=1): - plaintexts = [] - running_product = 1 - - # run the product up to the min - for i in range(max+1): - # if we're in the range, add it to the array - if i >= min: - plaintexts.append(algs.EGPlaintext(running_product, pk)) - - # next value in running product - running_product = (running_product * pk.g) % pk.p - - return plaintexts - - def verify_plaintexts_and_randomness(self, pk): """ - this applies only if the explicit answers and randomness factors are given - we do not verify the proofs here, that is the verify() method + An encrypted answer to a single election question """ - if not hasattr(self, 'answer'): - return False - - for choice_num in range(len(self.choices)): - choice = self.choices[choice_num] - choice.pk = pk - # redo the encryption - # WORK HERE (paste from below encryption) + FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer'] - return False + # FIXME: remove this constructor and use only named-var constructor from HeliosObject + def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None): + self.choices = choices + self.individual_proofs = individual_proofs + self.overall_proof = overall_proof + self.randomness = randomness + self.answer = answer - def verify(self, pk, min=0, max=1): - possible_plaintexts = self.generate_plaintexts(pk) - homomorphic_sum = 0 + @classmethod + def generate_plaintexts(cls, pk, min=0, max=1): + plaintexts = [] + running_product = 1 - for choice_num in range(len(self.choices)): - choice = self.choices[choice_num] - choice.pk = pk - individual_proof = self.individual_proofs[choice_num] + # run the product up to the min + for i in range(max + 1): + # if we're in the range, add it to the array + if i >= min: + plaintexts.append(algs.EGPlaintext(running_product, pk)) - # verify that elements belong to the proper group - if not choice.check_group_membership(pk): - return False - - # verify the proof on the encryption of that choice - if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof, algs.EG_disjunctive_challenge_generator): - return False + # next value in running product + running_product = (running_product * pk.g) % pk.p - # compute homomorphic sum if needed - if max != None: - homomorphic_sum = choice * homomorphic_sum + return plaintexts - if max != None: - # determine possible plaintexts for the sum - sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max) + def verify_plaintexts_and_randomness(self, pk): + """ + this applies only if the explicit answers and randomness factors are given + we do not verify the proofs here, that is the verify() method + """ + if not hasattr(self, 'answer'): + return False - # verify the sum - return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof, algs.EG_disjunctive_challenge_generator) - else: - # approval voting, no need for overall proof verification - return True + for choice_num in range(len(self.choices)): + choice = self.choices[choice_num] + choice.pk = pk - def toJSONDict(self, with_randomness=False): - value = { - 'choices': [c.to_dict() for c in self.choices], - 'individual_proofs' : [p.to_dict() for p in self.individual_proofs] - } + # redo the encryption + # WORK HERE (paste from below encryption) - if self.overall_proof: - value['overall_proof'] = self.overall_proof.to_dict() - else: - value['overall_proof'] = None - - if with_randomness: - value['randomness'] = [str(r) for r in self.randomness] - value['answer'] = self.answer - - return value - - @classmethod - def fromJSONDict(cls, d, pk=None): - ea = cls() - - ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']] - ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']] - - if d['overall_proof']: - ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof']) - else: - ea.overall_proof = None + return False - if d.has_key('randomness'): - ea.randomness = [int(r) for r in d['randomness']] - ea.answer = d['answer'] + def verify(self, pk, min=0, max=1): + possible_plaintexts = self.generate_plaintexts(pk) + homomorphic_sum = 0 + + for choice_num in range(len(self.choices)): + choice = self.choices[choice_num] + choice.pk = pk + individual_proof = self.individual_proofs[choice_num] + + # verify that elements belong to the proper group + if not choice.check_group_membership(pk): + return False + + # verify the proof on the encryption of that choice + if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof, + algs.EG_disjunctive_challenge_generator): + return False + + # compute homomorphic sum if needed + if max is not None: + homomorphic_sum = choice * homomorphic_sum + + if max is not None: + # determine possible plaintexts for the sum + sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max) + + # verify the sum + return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof, + algs.EG_disjunctive_challenge_generator) + else: + # approval voting, no need for overall proof verification + return True + + def toJSONDict(self, with_randomness=False): + value = { + 'choices': [c.to_dict() for c in self.choices], + 'individual_proofs': [p.to_dict() for p in self.individual_proofs] + } + + if self.overall_proof: + value['overall_proof'] = self.overall_proof.to_dict() + else: + value['overall_proof'] = None + + if with_randomness: + value['randomness'] = [str(r) for r in self.randomness] + value['answer'] = self.answer + + return value + + @classmethod + def fromJSONDict(cls, d, pk=None): + ea = cls() + + ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']] + ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']] + + if d['overall_proof']: + ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof']) + else: + ea.overall_proof = None + + if 'randomness' in d: + ea.randomness = [int(r) for r in d['randomness']] + ea.answer = d['answer'] + + return ea + + @classmethod + def fromElectionAndAnswer(cls, election, question_num, answer_indexes): + """ + Given an election, a question number, and a list of answers to that question + in the form of an array of 0-based indexes into the answer array, + produce an EncryptedAnswer that works. + """ + question = election.questions[question_num] + answers = question['answers'] + pk = election.public_key + + # initialize choices, individual proofs, randomness and overall proof + choices = [None for _ in range(len(answers))] + individual_proofs = [None for _ in range(len(answers))] + randomness = [None for _ in range(len(answers))] + + # possible plaintexts [0, 1] + plaintexts = cls.generate_plaintexts(pk) + + # keep track of number of options selected. + num_selected_answers = 0 + + # homomorphic sum of all + homomorphic_sum = 0 + randomness_sum = 0 + + # min and max for number of answers, useful later + min_answers = 0 + if 'min' in question: + min_answers = question['min'] + max_answers = question['max'] + + # go through each possible answer and encrypt either a g^0 or a g^1. + for answer_num in range(len(answers)): + plaintext_index = 0 + + # assuming a list of answers + if answer_num in answer_indexes: + plaintext_index = 1 + num_selected_answers += 1 + + # randomness and encryption + randomness[answer_num] = utils.random.mpz_lt(pk.q) + choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num]) + + # generate proof + individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts, + plaintext_index, + randomness[ + answer_num], + algs.EG_disjunctive_challenge_generator) + + # sum things up homomorphically if needed + if max_answers is not None: + homomorphic_sum = choices[answer_num] * homomorphic_sum + randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q + + # prove that the sum is 0 or 1 (can be "blank vote" for this answer) + # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded + + if num_selected_answers < min_answers: + raise Exception("Need to select at least %s answer(s)" % min_answers) + + if max_answers is not None: + sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers) + + # need to subtract the min from the offset + overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts, + num_selected_answers - min_answers, + randomness_sum, + algs.EG_disjunctive_challenge_generator); + else: + # approval voting + overall_proof = None + + return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes) - return ea - @classmethod - def fromElectionAndAnswer(cls, election, question_num, answer_indexes): +class EncryptedVote(HeliosObject): """ - Given an election, a question number, and a list of answers to that question - in the form of an array of 0-based indexes into the answer array, - produce an EncryptedAnswer that works. + An encrypted ballot """ - question = election.questions[question_num] - answers = question['answers'] - pk = election.public_key - - # initialize choices, individual proofs, randomness and overall proof - choices = [None for a in range(len(answers))] - individual_proofs = [None for a in range(len(answers))] - overall_proof = None - randomness = [None for a in range(len(answers))] - - # possible plaintexts [0, 1] - plaintexts = cls.generate_plaintexts(pk) - - # keep track of number of options selected. - num_selected_answers = 0; - - # homomorphic sum of all - homomorphic_sum = 0 - randomness_sum = 0 - - # min and max for number of answers, useful later - min_answers = 0 - if question.has_key('min'): - min_answers = question['min'] - max_answers = question['max'] + FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid'] + + def verify(self, election): + # correct number of answers + # noinspection PyUnresolvedReferences + n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0 + n_questions = len(election.questions) if election.questions is not None else 0 + if n_answers != n_questions: + logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})") + return False + + # check hash + # noinspection PyUnresolvedReferences + our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode() + actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode() + if our_election_hash != actual_election_hash: + logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ") + return False + + # check ID + # noinspection PyUnresolvedReferences + our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode() + actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode() + if our_election_uuid != actual_election_uuid: + logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_election_uuid} ") + return False + + # check proofs on all of answers + for question_num in range(len(election.questions)): + ea = self.encrypted_answers[question_num] + + question = election.questions[question_num] + min_answers = 0 + if 'min' in question: + min_answers = question['min'] + + if not ea.verify(election.public_key, min=min_answers, max=question['max']): + return False + + return True + + def get_hash(self): + return utils.hash_b64(to_json(self.toJSONDict())) + + def toJSONDict(self, with_randomness=False): + return { + 'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers], + 'election_hash': self.election_hash, + 'election_uuid': self.election_uuid + } + + @classmethod + def fromJSONDict(cls, d, pk=None): + ev = cls() + + ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']] + ev.election_hash = d['election_hash'] + ev.election_uuid = d['election_uuid'] + + return ev + + @classmethod + def fromElectionAndAnswers(cls, election, answers): + pk = election.public_key + + # each answer is an index into the answer array + encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for + answer_num in range(len(answers))] + return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid=election.uuid) - # go through each possible answer and encrypt either a g^0 or a g^1. - for answer_num in range(len(answers)): - plaintext_index = 0 - # assuming a list of answers - if answer_num in answer_indexes: - plaintext_index = 1 - num_selected_answers += 1 - - # randomness and encryption - randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q) - choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num]) - - # generate proof - individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts, plaintext_index, - randomness[answer_num], algs.EG_disjunctive_challenge_generator) - - # sum things up homomorphically if needed - if max_answers != None: - homomorphic_sum = choices[answer_num] * homomorphic_sum - randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q - - # prove that the sum is 0 or 1 (can be "blank vote" for this answer) - # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded - - if num_selected_answers < min_answers: - raise Exception("Need to select at least %s answer(s)" % min_answers) - - if max_answers != None: - sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers) +def one_question_winner(question, result, num_cast_votes): + """ + determining the winner for one question + """ + # sort the answers , keep track of the index + counts = sorted(enumerate(result), key=lambda x: x[1]) + counts.reverse() - # need to subtract the min from the offset - overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts, num_selected_answers - min_answers, randomness_sum, algs.EG_disjunctive_challenge_generator); - else: - # approval voting - overall_proof = None + # if there's a max > 1, we assume that the top MAX win + if question['max'] > 1: + return [c[0] for c in counts[:question['max']]] - return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes) + # if max = 1, then depends on absolute or relative + if question['result_type'] == 'absolute': + if counts[0][1] >= (num_cast_votes // 2 + 1): + return [counts[0][0]] + else: + return [] -class EncryptedVote(HeliosObject): - """ - An encrypted ballot - """ - FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid'] - - def verify(self, election): - # right number of answers - if len(self.encrypted_answers) != len(election.questions): - return False - - # check hash - if self.election_hash != election.hash: - # print "%s / %s " % (self.election_hash, election.hash) - return False - - # check ID - if self.election_uuid != election.uuid: - return False - - # check proofs on all of answers - for question_num in range(len(election.questions)): - ea = self.encrypted_answers[question_num] - - question = election.questions[question_num] - min_answers = 0 - if question.has_key('min'): - min_answers = question['min'] - - if not ea.verify(election.public_key, min=min_answers, max=question['max']): - return False + if question['result_type'] == 'relative': + return [counts[0][0]] - return True - - def get_hash(self): - return utils.hash_b64(utils.to_json(self.toJSONDict())) - - def toJSONDict(self, with_randomness=False): - return { - 'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers], - 'election_hash': self.election_hash, - 'election_uuid': self.election_uuid - } + +class Election(HeliosObject): + FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg', + 'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases', + 'voting_starts_at', 'voting_ends_at', 'election_type'] - @classmethod - def fromJSONDict(cls, d, pk=None): - ev = cls() + JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg', + 'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at'] - ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']] - ev.election_hash = d['election_hash'] - ev.election_uuid = d['election_uuid'] + # need to add in v3.1: use_advanced_audit_features, election_type, and probably more - return ev + def init_tally(self): + return Tally(election=self) - @classmethod - def fromElectionAndAnswers(cls, election, answers): - pk = election.public_key + def _process_value_in(self, field_name, field_value): + if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at': + if isinstance(field_value, str): + return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') - # each answer is an index into the answer array - encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for answer_num in range(len(answers))] - return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid = election.uuid) + if field_name == 'public_key': + return algs.EGPublicKey.fromJSONDict(field_value) + if field_name == 'private_key': + return algs.EGSecretKey.fromJSONDict(field_value) -def one_question_winner(question, result, num_cast_votes): - """ - determining the winner for one question - """ - # sort the answers , keep track of the index - counts = sorted(enumerate(result), key=lambda(x): x[1]) - counts.reverse() - - # if there's a max > 1, we assume that the top MAX win - if question['max'] > 1: - return [c[0] for c in counts[:question['max']]] - - # if max = 1, then depends on absolute or relative - if question['result_type'] == 'absolute': - if counts[0][1] >= (num_cast_votes/2 + 1): - return [counts[0][0]] - else: - return [] - - if question['result_type'] == 'relative': - return [counts[0][0]] + def _process_value_out(self, field_name, field_value): + # the date + if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at': + return str(field_value) -class Election(HeliosObject): + if field_name == 'public_key' or field_name == 'private_key': + return field_value.toJSONDict() - FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg', - 'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at', 'election_type'] + @property + def registration_status_pretty(self): + if self.openreg: + return "Open" + else: + return "Closed" - JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg', - 'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at'] + @property + def winners(self): + """ + Depending on the type of each question, determine the winners + returns an array of winners for each question, aka an array of arrays. + assumes that if there is a max to the question, that's how many winners there are. + """ + return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in + range(len(self.questions))] - # need to add in v3.1: use_advanced_audit_features, election_type, and probably more + @property + def pretty_result(self): + if not self.result: + return None - def init_tally(self): - return Tally(election=self) + # get the winners + winners = self.winners - def _process_value_in(self, field_name, field_value): - if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at': - if type(field_value) == str or type(field_value) == unicode: - return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') + raw_result = self.result + prettified_result = [] - if field_name == 'public_key': - return algs.EGPublicKey.fromJSONDict(field_value) + # loop through questions + for i in range(len(self.questions)): + q = self.questions[i] + pretty_question = [] - if field_name == 'private_key': - return algs.EGSecretKey.fromJSONDict(field_value) + # go through answers + for j in range(len(q['answers'])): + a = q['answers'][j] + count = raw_result[i][j] + pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])}) - def _process_value_out(self, field_name, field_value): - # the date - if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at': - return str(field_value) + prettified_result.append({'question': q['short_name'], 'answers': pretty_question}) - if field_name == 'public_key' or field_name == 'private_key': - return field_value.toJSONDict() + return prettified_result - @property - def registration_status_pretty(self): - if self.openreg: - return "Open" - else: - return "Closed" - @property - def winners(self): +class Voter(HeliosObject): """ - Depending on the type of each question, determine the winners - returns an array of winners for each question, aka an array of arrays. - assumes that if there is a max to the question, that's how many winners there are. + A voter in an election """ - return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in range(len(self.questions))] + FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias'] + JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name'] - @property - def pretty_result(self): - if not self.result: - return None + # alternative, for when the voter is aliased + ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias'] - # get the winners - winners = self.winners + def toJSONDict(self): + if self.alias is not None: + return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS) + else: + return super(Voter, self).toJSONDict() - raw_result = self.result - prettified_result = [] + @property + def voter_id_hash(self): + if self.voter_login_id: + # for backwards compatibility with v3.0, and since it doesn't matter + # too much if we hash the email or the unique login ID here. + return utils.hash_b64(self.voter_login_id) + else: + return utils.hash_b64(self.voter_id) - # loop through questions - for i in range(len(self.questions)): - q = self.questions[i] - pretty_question = [] - - # go through answers - for j in range(len(q['answers'])): - a = q['answers'][j] - count = raw_result[i][j] - pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])}) - - prettified_result.append({'question': q['short_name'], 'answers': pretty_question}) - - return prettified_result - - -class Voter(HeliosObject): - """ - A voter in an election - """ - FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias'] - JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name'] - - # alternative, for when the voter is aliased - ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias'] - - def toJSONDict(self): - fields = None - if self.alias != None: - return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS) - else: - return super(Voter,self).toJSONDict() - - @property - def voter_id_hash(self): - if self.voter_login_id: - # for backwards compatibility with v3.0, and since it doesn't matter - # too much if we hash the email or the unique login ID here. - return utils.hash_b64(self.voter_login_id) - else: - return utils.hash_b64(self.voter_id) class Trustee(HeliosObject): - """ - a trustee - """ - FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email'] - - def _process_value_in(self, field_name, field_value): - if field_name == 'public_key': - return algs.EGPublicKey.fromJSONDict(field_value) - - if field_name == 'pok': - return algs.DLogProof.fromJSONDict(field_value) - - def _process_value_out(self, field_name, field_value): - if field_name == 'public_key' or field_name == 'pok': - return field_value.toJSONDict() - -class CastVote(HeliosObject): - """ - A cast vote, which includes an encrypted vote and some cast metadata - """ - FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash'] - - def __init__(self, *args, **kwargs): - super(CastVote, self).__init__(*args, **kwargs) - self.election = None - - @classmethod - def fromJSONDict(cls, d, election=None): - o = cls() - o.election = election - o.set_from_args(**d) - return o - - def toJSONDict(self, include_vote=True): - result = super(CastVote,self).toJSONDict() - if not include_vote: - del result['vote'] - return result - - @classmethod - def fromOtherObject(cls, o, election): - obj = cls() - obj.election = election - obj.set_from_other_object(o) - return obj - - def _process_value_in(self, field_name, field_value): - if field_name == 'cast_at': - if type(field_value) == str: - return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') - - if field_name == 'vote': - return EncryptedVote.fromJSONDict(field_value, self.election.public_key) - - def _process_value_out(self, field_name, field_value): - # the date - if field_name == 'cast_at': - return str(field_value) - - if field_name == 'vote': - return field_value.toJSONDict() - - def issues(self, election): """ - Look for consistency problems + a trustee """ - issues = [] - - # check the election - if self.vote.election_uuid != election.uuid: - issues.append("the vote's election UUID does not match the election for which this vote is being cast") - - return issues - -class DLogTable(object): - """ - Keeping track of discrete logs - """ - - def __init__(self, base, modulus): - self.dlogs = {} - self.dlogs[1] = 0 - self.last_dlog_result = 1 - self.counter = 0 - - self.base = base - self.modulus = modulus + FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email'] - def increment(self): - self.counter += 1 + def _process_value_in(self, field_name, field_value): + if field_name == 'public_key': + return algs.EGPublicKey.fromJSONDict(field_value) - # new value - new_value = (self.last_dlog_result * self.base) % self.modulus + if field_name == 'pok': + return algs.DLogProof.fromJSONDict(field_value) - # record the discrete log - self.dlogs[new_value] = self.counter + def _process_value_out(self, field_name, field_value): + if field_name == 'public_key' or field_name == 'pok': + return field_value.toJSONDict() - # record the last value - self.last_dlog_result = new_value - - def precompute(self, up_to): - while self.counter < up_to: - self.increment() - - def lookup(self, value): - return self.dlogs.get(value, None) +class CastVote(HeliosObject): + """ + A cast vote, which includes an encrypted vote and some cast metadata + """ + FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash'] -class Tally(HeliosObject): - """ - A running homomorphic tally - """ + def __init__(self, *args, **kwargs): + super(CastVote, self).__init__(*args, **kwargs) + self.election = None - FIELDS = ['num_tallied', 'tally'] - JSON_FIELDS = ['num_tallied', 'tally'] + @classmethod + def fromJSONDict(cls, d, election=None): + o = cls() + o.election = election + o.set_from_args(**d) + return o - def __init__(self, *args, **kwargs): - super(Tally, self).__init__(*args, **kwargs) + def toJSONDict(self, include_vote=True): + result = super(CastVote, self).toJSONDict() + if not include_vote: + del result['vote'] + return result - self.election = kwargs.get('election',None) + @classmethod + def fromOtherObject(cls, o, election): + obj = cls() + obj.election = election + obj.set_from_other_object(o) + return obj - if self.election: - self.init_election(self.election) - else: - self.questions = None - self.public_key = None + def _process_value_in(self, field_name, field_value): + if field_name == 'cast_at': + if isinstance(field_value, str): + return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') - if not self.tally: - self.tally = None + if field_name == 'vote': + return EncryptedVote.fromJSONDict(field_value, self.election.public_key) - # initialize - if self.num_tallied == None: - self.num_tallied = 0 + def _process_value_out(self, field_name, field_value): + # the date + if field_name == 'cast_at': + return str(field_value) - def init_election(self, election): - """ - given the election, initialize some params - """ - self.questions = election.questions - self.public_key = election.public_key + if field_name == 'vote': + return field_value.toJSONDict() - if not self.tally: - self.tally = [[0 for a in q['answers']] for q in self.questions] + def issues(self, election): + """ + Look for consistency problems + """ + issues = [] - def add_vote_batch(self, encrypted_votes, verify_p=True): - """ - Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification - rather than a whole proof verif for each vote. - """ - for vote in encrypted_votes: - self.add_vote(vote, verify_p) - - def add_vote(self, encrypted_vote, verify_p=True): - # do we verify? - if verify_p: - if not encrypted_vote.verify(self.election): - raise Exception('Bad Vote') - - # for each question - for question_num in range(len(self.questions)): - question = self.questions[question_num] - answers = question['answers'] - - # for each possible answer to each question - for answer_num in range(len(answers)): - # do the homomorphic addition into the tally - enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num] - enc_vote_choice.pk = self.public_key - self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[answer_num] * self.tally[question_num][answer_num] - - self.num_tallied += 1 - - def decryption_factors_and_proofs(self, sk): - """ - returns an array of decryption factors and a corresponding array of decryption proofs. - makes the decryption factors into strings, for general Helios / JS compatibility. - """ - # for all choices of all questions (double list comprehension) - decryption_factors = [] - decryption_proof = [] + # check the election + if self.vote.election_uuid != election.uuid: + issues.append("the vote's election UUID does not match the election for which this vote is being cast") - for question_num, question in enumerate(self.questions): - answers = question['answers'] - question_factors = [] - question_proof = [] + return issues - for answer_num, answer in enumerate(answers): - # do decryption and proof of it - dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num]) - # look up appropriate discrete log - # this is the string conversion - question_factors.append(str(dec_factor)) - question_proof.append(proof.toJSONDict()) - - decryption_factors.append(question_factors) - decryption_proof.append(question_proof) - - return decryption_factors, decryption_proof - - def decrypt_and_prove(self, sk, discrete_logs=None): +class DLogTable(object): """ - returns an array of tallies and a corresponding array of decryption proofs. + Keeping track of discrete logs """ - # who's keeping track of discrete logs? - if not discrete_logs: - discrete_logs = self.discrete_logs + def __init__(self, base, modulus): + self.dlogs = {1: 0} + self.last_dlog_result = 1 + self.counter = 0 - # for all choices of all questions (double list comprehension) - decrypted_tally = [] - decryption_proof = [] + self.base = base + self.modulus = modulus - for question_num in range(len(self.questions)): - question = self.questions[question_num] - answers = question['answers'] - question_tally = [] - question_proof = [] + def increment(self): + self.counter += 1 - for answer_num in range(len(answers)): - # do decryption and proof of it - plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num]) + # new value + new_value = (self.last_dlog_result * self.base) % self.modulus - # look up appropriate discrete log - question_tally.append(discrete_logs[plaintext]) - question_proof.append(proof) + # record the discrete log + self.dlogs[new_value] = self.counter - decrypted_tally.append(question_tally) - decryption_proof.append(question_proof) + # record the last value + self.last_dlog_result = new_value - return decrypted_tally, decryption_proof + def precompute(self, up_to): + while self.counter < up_to: + self.increment() - def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator): - """ - decryption_factors is a list of lists of dec factors - decryption_proofs are the corresponding proofs - public_key is, of course, the public key of the trustee - """ + def lookup(self, value): + return self.dlogs.get(value, None) - # go through each one - for q_num, q in enumerate(self.tally): - for a_num, answer_tally in enumerate(q): - # parse the proof - proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num]) - # check that g, alpha, y, dec_factor is a DH tuple - if not proof.verify(public_key.g, answer_tally.alpha, public_key.y, int(decryption_factors[q_num][a_num]), public_key.p, public_key.q, challenge_generator): - return False - - return True - - def decrypt_from_factors(self, decryption_factors, public_key): +class Tally(HeliosObject): """ - decrypt a tally given decryption factors - - The decryption factors are a list of decryption factor sets, for each trustee. - Each decryption factor set is a list of lists of decryption factors (questions/answers). + A running homomorphic tally """ - # pre-compute a dlog table - dlog_table = DLogTable(base = public_key.g, modulus = public_key.p) - dlog_table.precompute(self.num_tallied) - - result = [] + FIELDS = ['num_tallied', 'tally'] + JSON_FIELDS = ['num_tallied', 'tally'] + + def __init__(self, *args, **kwargs): + super(Tally, self).__init__(*args, **kwargs) + + self.election = kwargs.get('election', None) + + if self.election: + self.init_election(self.election) + else: + self.questions = None + self.public_key = None + + if not self.tally: + self.tally = None + + # initialize + if self.num_tallied is None: + self.num_tallied = 0 + + def init_election(self, election): + """ + given the election, initialize some params + """ + self.questions = election.questions + self.public_key = election.public_key + + if not self.tally: + self.tally = [[0 for _ in q['answers']] for q in self.questions] + + def add_vote_batch(self, encrypted_votes, verify_p=True): + """ + Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification + rather than a whole proof verif for each vote. + """ + for vote in encrypted_votes: + self.add_vote(vote, verify_p) + + def add_vote(self, encrypted_vote, verify_p=True): + # do we verify? + if verify_p: + if not encrypted_vote.verify(self.election): + raise Exception('Bad Vote') + + # for each question + for question_num in range(len(self.questions)): + question = self.questions[question_num] + answers = question['answers'] + + # for each possible answer to each question + for answer_num in range(len(answers)): + # do the homomorphic addition into the tally + enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num] + enc_vote_choice.pk = self.public_key + self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[ + answer_num] * self.tally[question_num][answer_num] + + self.num_tallied += 1 + + def decryption_factors_and_proofs(self, sk): + """ + returns an array of decryption factors and a corresponding array of decryption proofs. + makes the decryption factors into strings, for general Helios / JS compatibility. + """ + # for all choices of all questions (double list comprehension) + decryption_factors = [] + decryption_proof = [] + + for question_num, question in enumerate(self.questions): + answers = question['answers'] + question_factors = [] + question_proof = [] + + for answer_num, answer in enumerate(answers): + # do decryption and proof of it + dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num]) + + # look up appropriate discrete log + # this is the string conversion + question_factors.append(str(dec_factor)) + question_proof.append(proof.toJSONDict()) + + decryption_factors.append(question_factors) + decryption_proof.append(question_proof) + + return decryption_factors, decryption_proof + + def decrypt_and_prove(self, sk, discrete_logs=None): + """ + returns an array of tallies and a corresponding array of decryption proofs. + """ + + # who's keeping track of discrete logs? + if not discrete_logs: + discrete_logs = self.discrete_logs + + # for all choices of all questions (double list comprehension) + decrypted_tally = [] + decryption_proof = [] + + for question_num in range(len(self.questions)): + question = self.questions[question_num] + answers = question['answers'] + question_tally = [] + question_proof = [] + + for answer_num in range(len(answers)): + # do decryption and proof of it + plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num]) + + # look up appropriate discrete log + question_tally.append(discrete_logs[plaintext]) + question_proof.append(proof) + + decrypted_tally.append(question_tally) + decryption_proof.append(question_proof) + + return decrypted_tally, decryption_proof + + def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator): + """ + decryption_factors is a list of lists of dec factors + decryption_proofs are the corresponding proofs + public_key is, of course, the public key of the trustee + """ + + # go through each one + for q_num, q in enumerate(self.tally): + for a_num, answer_tally in enumerate(q): + # parse the proof + proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num]) + + # check that g, alpha, y, dec_factor is a DH tuple + if not proof.verify(public_key.g, answer_tally.alpha, public_key.y, + int(decryption_factors[q_num][a_num]), public_key.p, public_key.q, + challenge_generator): + return False + + return True + + def decrypt_from_factors(self, decryption_factors, public_key): + """ + decrypt a tally given decryption factors + + The decryption factors are a list of decryption factor sets, for each trustee. + Each decryption factor set is a list of lists of decryption factors (questions/answers). + """ + + # pre-compute a dlog table + dlog_table = DLogTable(base=public_key.g, modulus=public_key.p) + dlog_table.precompute(self.num_tallied) + + result = [] - # go through each one - for q_num, q in enumerate(self.tally): - q_result = [] + # go through each one + for q_num, q in enumerate(self.tally): + q_result = [] - for a_num, a in enumerate(q): - # coalesce the decryption factors into one list - dec_factor_list = [df[q_num][a_num] for df in decryption_factors] - raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key) + for a_num, a in enumerate(q): + # coalesce the decryption factors into one list + dec_factor_list = [df[q_num][a_num] for df in decryption_factors] + raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key) - q_result.append(dlog_table.lookup(raw_value)) + q_result.append(dlog_table.lookup(raw_value)) - result.append(q_result) + result.append(q_result) - return result + return result - def _process_value_in(self, field_name, field_value): - if field_name == 'tally': - return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value] + def _process_value_in(self, field_name, field_value): + if field_name == 'tally': + return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value] - def _process_value_out(self, field_name, field_value): - if field_name == 'tally': - return [[a.toJSONDict() for a in q] for q in field_value] + def _process_value_out(self, field_name, field_value): + if field_name == 'tally': + return [[a.toJSONDict() for a in q] for q in field_value] diff --git a/helios/crypto/elgamal.py b/helios/crypto/elgamal.py index 88a08c01b..33eb03083 100644 --- a/helios/crypto/elgamal.py +++ b/helios/crypto/elgamal.py @@ -8,12 +8,13 @@ ben@adida.net """ -import math, hashlib, logging -import randpool, number +import logging -import numtheory +from Crypto.Hash import SHA1 +from Crypto.Util.number import inverse + +from helios.crypto.utils import random -from algs import Utils class Cryptosystem(object): def __init__(self): @@ -21,30 +22,6 @@ def __init__(self): self.q = None self.g = None - @classmethod - def generate(cls, n_bits): - """ - generate an El-Gamal environment. Returns an instance - of ElGamal(), with prime p, group size q, and generator g - """ - - EG = cls() - - # find a prime p such that (p-1)/2 is prime q - EG.p = Utils.random_safe_prime(n_bits) - - # q is the order of the group - # FIXME: not always p-1/2 - EG.q = (EG.p-1)/2 - - # find g that generates the q-order subgroup - while True: - EG.g = Utils.random_mpz_lt(EG.p) - if pow(EG.g, EG.q, EG.p) == 1: - break - - return EG - def generate_keypair(self): """ generates a keypair in the setting @@ -68,7 +45,7 @@ def generate(self, p, q, g): self.pk.p = p self.pk.q = q - self.sk.x = Utils.random_mpz_lt(q) + self.sk.x = random.mpz_lt(q) self.pk.y = pow(g, self.sk.x, p) self.sk.public_key = self.pk @@ -106,7 +83,7 @@ def encrypt_return_r(self, plaintext): """ Encrypt a plaintext and return the randomness just generated and used. """ - r = Utils.random_mpz_lt(self.q) + r = random.mpz_lt(self.q) ciphertext = self.encrypt_with_r(plaintext, r) return [ciphertext, r] @@ -181,7 +158,7 @@ def decrypt(self, ciphertext, dec_factor = None, decode_m=False): if not dec_factor: dec_factor = self.decryption_factor(ciphertext) - m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p + m = (inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p if decode_m: # get m back from the q-order subgroup @@ -207,15 +184,15 @@ def prove_decryption(self, ciphertext): and alpha^t = b * beta/m ^ c """ - m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p - beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p + m = (inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p + beta_over_m = (ciphertext.beta * inverse(m, self.pk.p)) % self.pk.p # pick a random w - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) a = pow(self.pk.g, w, self.pk.p) b = pow(ciphertext.alpha, w, self.pk.p) - c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16) + c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(),16) t = (w + self.x * c) % self.pk.q @@ -232,7 +209,7 @@ def prove_sk(self, challenge_generator): Verifier provides challenge modulo q. Prover computes response = w + x*challenge mod q, where x is the secret key. """ - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) commitment = pow(self.pk.g, w, self.pk.p) challenge = challenge_generator(commitment) % self.pk.q response = (w + (self.x * challenge)) % self.pk.q @@ -255,7 +232,7 @@ def __mul__(self,other): """ Homomorphic Multiplication of ciphertexts. """ - if type(other) == int and (other == 0 or other == 1): + if isinstance(other, int) and (other == 0 or other == 1): return self if self.pk != other.pk: @@ -287,7 +264,7 @@ def reenc_return_r(self): """ Reencryption with fresh randomness, which is returned. """ - r = Utils.random_mpz_lt(self.pk.q) + r = random.mpz_lt(self.pk.q) new_c = self.reenc_with_r(r) return [new_c, r] @@ -301,17 +278,17 @@ def __eq__(self, other): """ Check for ciphertext equality. """ - if other == None: + if other is None: return False - return (self.alpha == other.alpha and self.beta == other.beta) + return self.alpha == other.alpha and self.beta == other.beta def generate_encryption_proof(self, plaintext, randomness, challenge_generator): """ Generate the disjunctive encryption proof of encryption """ # random W - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) # build the proof proof = ZKProof() @@ -331,20 +308,20 @@ def generate_encryption_proof(self, plaintext, randomness, challenge_generator): def simulate_encryption_proof(self, plaintext, challenge=None): # generate a random challenge if not provided if not challenge: - challenge = Utils.random_mpz_lt(self.pk.q) + challenge = random.mpz_lt(self.pk.q) proof = ZKProof() proof.challenge = challenge # compute beta/plaintext, the completion of the DH tuple - beta_over_plaintext = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p + beta_over_plaintext = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p # random response, does not even need to depend on the challenge - proof.response = Utils.random_mpz_lt(self.pk.q); + proof.response = random.mpz_lt(self.pk.q); # now we compute A and B - proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p - proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p + proof.commitment['A'] = (inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p + proof.commitment['B'] = (inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p return proof @@ -397,7 +374,7 @@ def verify_encryption_proof(self, plaintext, proof): first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p)) # check that y^response = B * (beta/m)^challenge - beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p + beta_over_m = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p)) # print "1,2: %s %s " % (first_check, second_check) @@ -416,7 +393,7 @@ def verify_disjunctive_encryption_proof(self, plaintexts, proof, challenge_gener for i in range(len(plaintexts)): # if a proof fails, stop right there if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]): - print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]) + print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])) return False # logging.info("made it past the two encryption proofs") @@ -444,7 +421,7 @@ def decrypt(self, decryption_factors, public_key): """ running_decryption = self.beta for dec_factor in decryption_factors: - running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p + running_decryption = (running_decryption * inverse(dec_factor, public_key.p)) % public_key.p return running_decryption @@ -473,7 +450,7 @@ def generate(cls, little_g, little_h, x, p, q, challenge_generator): """ # generate random w - w = Utils.random_mpz_lt(q) + w = random.mpz_lt(q) # create proof instance proof = cls() @@ -526,7 +503,7 @@ def disjunctive_challenge_generator(commitments): array_to_hash.append(str(commitment['B'])) string_to_hash = ",".join(array_to_hash) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16) # a challenge generator for Fiat-Shamir with A,B commitment def fiatshamir_challenge_generator(commitment): @@ -534,5 +511,5 @@ def fiatshamir_challenge_generator(commitment): def DLog_challenge_generator(commitment): string_to_hash = str(commitment) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16) diff --git a/helios/crypto/number.py b/helios/crypto/number.py deleted file mode 100644 index 9d50563e9..000000000 --- a/helios/crypto/number.py +++ /dev/null @@ -1,201 +0,0 @@ -# -# number.py : Number-theoretic functions -# -# Part of the Python Cryptography Toolkit -# -# Distribute and use freely; there are no restrictions on further -# dissemination and usage except those imposed by the laws of your -# country of residence. This software is provided "as is" without -# warranty of fitness for use or suitability for any purpose, express -# or implied. Use at your own risk or not at all. -# - -__revision__ = "$Id: number.py,v 1.13 2003/04/04 18:21:07 akuchling Exp $" - -bignum = long -try: - from Crypto.PublicKey import _fastmath -except ImportError: - _fastmath = None - -# Commented out and replaced with faster versions below -## def long2str(n): -## s='' -## while n>0: -## s=chr(n & 255)+s -## n=n>>8 -## return s - -## import types -## def str2long(s): -## if type(s)!=types.StringType: return s # Integers will be left alone -## return reduce(lambda x,y : x*256+ord(y), s, 0L) - -def size (N): - """size(N:long) : int - Returns the size of the number N in bits. - """ - bits, power = 0,1L - while N >= power: - bits += 1 - power = power << 1 - return bits - -def getRandomNumber(N, randfunc): - """getRandomNumber(N:int, randfunc:callable):long - Return an N-bit random number.""" - - S = randfunc(N/8) - odd_bits = N % 8 - if odd_bits != 0: - char = ord(randfunc(1)) >> (8-odd_bits) - S = chr(char) + S - value = bytes_to_long(S) - value |= 2L ** (N-1) # Ensure high bit is set - assert size(value) >= N - return value - -def GCD(x,y): - """GCD(x:long, y:long): long - Return the GCD of x and y. - """ - x = abs(x) ; y = abs(y) - while x > 0: - x, y = y % x, x - return y - -def inverse(u, v): - """inverse(u:long, u:long):long - Return the inverse of u mod v. - """ - u3, v3 = long(u), long(v) - u1, v1 = 1L, 0L - while v3 > 0: - q=u3 / v3 - u1, v1 = v1, u1 - v1*q - u3, v3 = v3, u3 - v3*q - while u1<0: - u1 = u1 + v - return u1 - -# Given a number of bits to generate and a random generation function, -# find a prime number of the appropriate size. - -def getPrime(N, randfunc): - """getPrime(N:int, randfunc:callable):long - Return a random N-bit prime number. - """ - - number=getRandomNumber(N, randfunc) | 1 - while (not isPrime(number)): - number=number+2 - return number - -def isPrime(N): - """isPrime(N:long):bool - Return true if N is prime. - """ - if N == 1: - return 0 - if N in sieve: - return 1 - for i in sieve: - if (N % i)==0: - return 0 - - # Use the accelerator if available - if _fastmath is not None: - return _fastmath.isPrime(N) - - # Compute the highest bit that's set in N - N1 = N - 1L - n = 1L - while (n> 1L - - # Rabin-Miller test - for c in sieve[:7]: - a=long(c) ; d=1L ; t=n - while (t): # Iterate over the bits in N1 - x=(d*d) % N - if x==1L and d!=1L and d!=N1: - return 0 # Square root of 1 found - if N1 & t: - d=(x*a) % N - else: - d=x - t = t >> 1L - if d!=1L: - return 0 - return 1 - -# Small primes used for checking primality; these are all the primes -# less than 256. This should be enough to eliminate most of the odd -# numbers before needing to do a Rabin-Miller test at all. - -sieve=[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] - -# Improved conversion functions contributed by Barry Warsaw, after -# careful benchmarking - -import struct - -def long_to_bytes(n, blocksize=0): - """long_to_bytes(n:long, blocksize:int) : string - Convert a long integer to a byte string. - - If optional blocksize is given and greater than zero, pad the front of the - byte string with binary zeros so that the length is a multiple of - blocksize. - """ - # after much testing, this algorithm was deemed to be the fastest - s = '' - n = long(n) - pack = struct.pack - while n > 0: - s = pack('>I', n & 0xffffffffL) + s - n = n >> 32 - # strip off leading zeros - for i in range(len(s)): - if s[i] != '\000': - break - else: - # only happens when n == 0 - s = '\000' - i = 0 - s = s[i:] - # add back some pad bytes. this could be done more efficiently w.r.t. the - # de-padding being done above, but sigh... - if blocksize > 0 and len(s) % blocksize: - s = (blocksize - len(s) % blocksize) * '\000' + s - return s - -def bytes_to_long(s): - """bytes_to_long(string) : long - Convert a byte string to a long integer. - - This is (essentially) the inverse of long_to_bytes(). - """ - acc = 0L - unpack = struct.unpack - length = len(s) - if length % 4: - extra = (4 - length % 4) - s = '\000' * extra + s - length = length + extra - for i in range(0, length, 4): - acc = (acc << 32) + unpack('>I', s[i:i+4])[0] - return acc - -# For backwards compatibility... -import warnings -def long2str(n, blocksize=0): - warnings.warn("long2str() has been replaced by long_to_bytes()") - return long_to_bytes(n, blocksize) -def str2long(s): - warnings.warn("str2long() has been replaced by bytes_to_long()") - return bytes_to_long(s) diff --git a/helios/crypto/numtheory.py b/helios/crypto/numtheory.py index 16fcf9aa0..e16691745 100644 --- a/helios/crypto/numtheory.py +++ b/helios/crypto/numtheory.py @@ -103,7 +103,7 @@ def trial_division(n, bound=None): if n == 1: return 1 for p in [2, 3, 5]: if n%p == 0: return p - if bound == None: bound = n + if bound is None: bound = n dif = [6, 4, 2, 4, 2, 4, 6, 2] m = 7; i = 1 while m <= bound and m*m <= n: @@ -207,7 +207,7 @@ def inversemod(a, n): """ g, x, y = xgcd(a, n) if g != 1: - raise ZeroDivisionError, (a,n) + raise ZeroDivisionError(a,n) assert g == 1, "a must be coprime to n." return x%n @@ -225,7 +225,7 @@ def solve_linear(a,b,n): Examples: >>> solve_linear(4, 2, 10) 8 - >>> solve_linear(2, 1, 4) == None + >>> solve_linear(2, 1, 4) is None True """ g, c, _ = xgcd(a,n) # (1) @@ -1014,7 +1014,7 @@ def elliptic_curve_method(N, m, tries=5): E, P = randcurve(N) # (2) try: # (3) Q = ellcurve_mul(E, m, P) # (4) - except ZeroDivisionError, x: # (5) + except ZeroDivisionError as x: # (5) g = gcd(x[0],N) # (6) if g != 1 or g != N: return g # (7) return N @@ -1153,7 +1153,7 @@ def __mul__(self, other): return r def __neg__(self): v = {} - for m in self.v.keys(): + for m in list(self.v.keys()): v[m] = -self.v[m] return Poly(v) def __div__(self, other): @@ -1161,7 +1161,7 @@ def __div__(self, other): def __getitem__(self, m): # (6) m = tuple(m) - if not self.v.has_key(m): self.v[m] = 0 + if m not in self.v: self.v[m] = 0 return self.v[m] def __setitem__(self, m, c): self.v[tuple(m)] = c @@ -1169,7 +1169,7 @@ def __delitem__(self, m): del self.v[tuple(m)] def monomials(self): # (7) - return self.v.keys() + return list(self.v.keys()) def normalize(self): # (8) while True: finished = True @@ -1244,8 +1244,8 @@ def prove_associative(): # (15) - (x3 + x4)*(x3 - x4)*(x3 - x4)) s2 = (x3 - x4)*(x3 - x4)*((y1 - y5)*(y1 - y5) \ - (x1 + x5)*(x1 - x5)*(x1 - x5)) - print "Associative?" - print s1 == s2 # (17) + print("Associative?") + print(s1 == s2) # (17) diff --git a/helios/crypto/randpool.py b/helios/crypto/randpool.py deleted file mode 100644 index 53a8acc03..000000000 --- a/helios/crypto/randpool.py +++ /dev/null @@ -1,422 +0,0 @@ -# -# randpool.py : Cryptographically strong random number generation -# -# Part of the Python Cryptography Toolkit -# -# Distribute and use freely; there are no restrictions on further -# dissemination and usage except those imposed by the laws of your -# country of residence. This software is provided "as is" without -# warranty of fitness for use or suitability for any purpose, express -# or implied. Use at your own risk or not at all. -# - -__revision__ = "$Id: randpool.py,v 1.14 2004/05/06 12:56:54 akuchling Exp $" - -import time, array, types, warnings, os.path -from number import long_to_bytes -try: - import Crypto.Util.winrandom as winrandom -except: - winrandom = None - -STIRNUM = 3 - -class RandomPool: - """randpool.py : Cryptographically strong random number generation. - - The implementation here is similar to the one in PGP. To be - cryptographically strong, it must be difficult to determine the RNG's - output, whether in the future or the past. This is done by using - a cryptographic hash function to "stir" the random data. - - Entropy is gathered in the same fashion as PGP; the highest-resolution - clock around is read and the data is added to the random number pool. - A conservative estimate of the entropy is then kept. - - If a cryptographically secure random source is available (/dev/urandom - on many Unixes, Windows CryptGenRandom on most Windows), then use - it. - - Instance Attributes: - bits : int - Maximum size of pool in bits - bytes : int - Maximum size of pool in bytes - entropy : int - Number of bits of entropy in this pool. - - Methods: - add_event([s]) : add some entropy to the pool - get_bytes(int) : get N bytes of random data - randomize([N]) : get N bytes of randomness from external source - """ - - - def __init__(self, numbytes = 160, cipher=None, hash=None): - if hash is None: - from hashlib import sha1 as hash - - # The cipher argument is vestigial; it was removed from - # version 1.1 so RandomPool would work even in the limited - # exportable subset of the code - if cipher is not None: - warnings.warn("'cipher' parameter is no longer used") - - if isinstance(hash, types.StringType): - # ugly hack to force __import__ to give us the end-path module - hash = __import__('Crypto.Hash.'+hash, - None, None, ['new']) - warnings.warn("'hash' parameter should now be a hashing module") - - self.bytes = numbytes - self.bits = self.bytes*8 - self.entropy = 0 - self._hash = hash - - # Construct an array to hold the random pool, - # initializing it to 0. - self._randpool = array.array('B', [0]*self.bytes) - - self._event1 = self._event2 = 0 - self._addPos = 0 - self._getPos = hash().digest_size - self._lastcounter=time.time() - self.__counter = 0 - - self._measureTickSize() # Estimate timer resolution - self._randomize() - - def _updateEntropyEstimate(self, nbits): - self.entropy += nbits - if self.entropy < 0: - self.entropy = 0 - elif self.entropy > self.bits: - self.entropy = self.bits - - def _randomize(self, N = 0, devname = '/dev/urandom'): - """_randomize(N, DEVNAME:device-filepath) - collects N bits of randomness from some entropy source (e.g., - /dev/urandom on Unixes that have it, Windows CryptoAPI - CryptGenRandom, etc) - DEVNAME is optional, defaults to /dev/urandom. You can change it - to /dev/random if you want to block till you get enough - entropy. - """ - data = '' - if N <= 0: - nbytes = int((self.bits - self.entropy)/8+0.5) - else: - nbytes = int(N/8+0.5) - if winrandom: - # Windows CryptGenRandom provides random data. - data = winrandom.new().get_bytes(nbytes) - # GAE fix, benadida - #elif os.path.exists(devname): - # # Many OSes support a /dev/urandom device - # try: - # f=open(devname) - # data=f.read(nbytes) - # f.close() - # except IOError, (num, msg): - # if num!=2: raise IOError, (num, msg) - # # If the file wasn't found, ignore the error - if data: - self._addBytes(data) - # Entropy estimate: The number of bits of - # data obtained from the random source. - self._updateEntropyEstimate(8*len(data)) - self.stir_n() # Wash the random pool - - def randomize(self, N=0): - """randomize(N:int) - use the class entropy source to get some entropy data. - This is overridden by KeyboardRandomize(). - """ - return self._randomize(N) - - def stir_n(self, N = STIRNUM): - """stir_n(N) - stirs the random pool N times - """ - for i in xrange(N): - self.stir() - - def stir (self, s = ''): - """stir(s:string) - Mix up the randomness pool. This will call add_event() twice, - but out of paranoia the entropy attribute will not be - increased. The optional 's' parameter is a string that will - be hashed with the randomness pool. - """ - - entropy=self.entropy # Save inital entropy value - self.add_event() - - # Loop over the randomness pool: hash its contents - # along with a counter, and add the resulting digest - # back into the pool. - for i in range(self.bytes / self._hash().digest_size): - h = self._hash(self._randpool) - h.update(str(self.__counter) + str(i) + str(self._addPos) + s) - self._addBytes( h.digest() ) - self.__counter = (self.__counter + 1) & 0xFFFFffffL - - self._addPos, self._getPos = 0, self._hash().digest_size - self.add_event() - - # Restore the old value of the entropy. - self.entropy=entropy - - - def get_bytes (self, N): - """get_bytes(N:int) : string - Return N bytes of random data. - """ - - s='' - i, pool = self._getPos, self._randpool - h=self._hash() - dsize = self._hash().digest_size - num = N - while num > 0: - h.update( self._randpool[i:i+dsize] ) - s = s + h.digest() - num = num - dsize - i = (i + dsize) % self.bytes - if i>1, bits+1 - if bits>8: bits=8 - - self._event1, self._event2 = event, self._event1 - - self._updateEntropyEstimate(bits) - return bits - - # Private functions - def _noise(self): - # Adds a bit of noise to the random pool, by adding in the - # current time and CPU usage of this process. - # The difference from the previous call to _noise() is taken - # in an effort to estimate the entropy. - t=time.time() - delta = (t - self._lastcounter)/self._ticksize*1e6 - self._lastcounter = t - self._addBytes(long_to_bytes(long(1000*time.time()))) - self._addBytes(long_to_bytes(long(1000*time.clock()))) - self._addBytes(long_to_bytes(long(1000*time.time()))) - self._addBytes(long_to_bytes(long(delta))) - - # Reduce delta to a maximum of 8 bits so we don't add too much - # entropy as a result of this call. - delta=delta % 0xff - return int(delta) - - - def _measureTickSize(self): - # _measureTickSize() tries to estimate a rough average of the - # resolution of time that you can see from Python. It does - # this by measuring the time 100 times, computing the delay - # between measurements, and taking the median of the resulting - # list. (We also hash all the times and add them to the pool) - interval = [None] * 100 - h = self._hash(`(id(self),id(interval))`) - - # Compute 100 differences - t=time.time() - h.update(`t`) - i = 0 - j = 0 - while i < 100: - t2=time.time() - h.update(`(i,j,t2)`) - j += 1 - delta=int((t2-t)*1e6) - if delta: - interval[i] = delta - i += 1 - t=t2 - - # Take the median of the array of intervals - interval.sort() - self._ticksize=interval[len(interval)/2] - h.update(`(interval,self._ticksize)`) - # mix in the measurement times and wash the random pool - self.stir(h.digest()) - - def _addBytes(self, s): - "XOR the contents of the string S into the random pool" - i, pool = self._addPos, self._randpool - for j in range(0, len(s)): - pool[i]=pool[i] ^ ord(s[j]) - i=(i+1) % self.bytes - self._addPos = i - - # Deprecated method names: remove in PCT 2.1 or later. - def getBytes(self, N): - warnings.warn("getBytes() method replaced by get_bytes()", - DeprecationWarning) - return self.get_bytes(N) - - def addEvent (self, event, s=""): - warnings.warn("addEvent() method replaced by add_event()", - DeprecationWarning) - return self.add_event(s + str(event)) - -class PersistentRandomPool (RandomPool): - def __init__ (self, filename=None, *args, **kwargs): - RandomPool.__init__(self, *args, **kwargs) - self.filename = filename - if filename: - try: - # the time taken to open and read the file might have - # a little disk variability, modulo disk/kernel caching... - f=open(filename, 'rb') - self.add_event() - data = f.read() - self.add_event() - # mix in the data from the file and wash the random pool - self.stir(data) - f.close() - except IOError: - # Oh, well; the file doesn't exist or is unreadable, so - # we'll just ignore it. - pass - - def save(self): - if self.filename == "": - raise ValueError, "No filename set for this object" - # wash the random pool before save, provides some forward secrecy for - # old values of the pool. - self.stir_n() - f=open(self.filename, 'wb') - self.add_event() - f.write(self._randpool.tostring()) - f.close() - self.add_event() - # wash the pool again, provide some protection for future values - self.stir() - -# non-echoing Windows keyboard entry -_kb = 0 -if not _kb: - try: - import msvcrt - class KeyboardEntry: - def getch(self): - c = msvcrt.getch() - if c in ('\000', '\xe0'): - # function key - c += msvcrt.getch() - return c - def close(self, delay = 0): - if delay: - time.sleep(delay) - while msvcrt.kbhit(): - msvcrt.getch() - _kb = 1 - except: - pass - -# non-echoing Posix keyboard entry -if not _kb: - try: - import termios - class KeyboardEntry: - def __init__(self, fd = 0): - self._fd = fd - self._old = termios.tcgetattr(fd) - new = termios.tcgetattr(fd) - new[3]=new[3] & ~termios.ICANON & ~termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, new) - def getch(self): - termios.tcflush(0, termios.TCIFLUSH) # XXX Leave this in? - return os.read(self._fd, 1) - def close(self, delay = 0): - if delay: - time.sleep(delay) - termios.tcflush(self._fd, termios.TCIFLUSH) - termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._old) - _kb = 1 - except: - pass - -class KeyboardRandomPool (PersistentRandomPool): - def __init__(self, *args, **kwargs): - PersistentRandomPool.__init__(self, *args, **kwargs) - - def randomize(self, N = 0): - "Adds N bits of entropy to random pool. If N is 0, fill up pool." - import os, string, time - if N <= 0: - bits = self.bits - self.entropy - else: - bits = N*8 - if bits == 0: - return - print bits,'bits of entropy are now required. Please type on the keyboard' - print 'until enough randomness has been accumulated.' - kb = KeyboardEntry() - s='' # We'll save the characters typed and add them to the pool. - hash = self._hash - e = 0 - try: - while e < bits: - temp=str(bits-e).rjust(6) - os.write(1, temp) - s=s+kb.getch() - e += self.add_event(s) - os.write(1, 6*chr(8)) - self.add_event(s+hash.new(s).digest() ) - finally: - kb.close() - print '\n\007 Enough. Please wait a moment.\n' - self.stir_n() # wash the random pool. - kb.close(4) - -if __name__ == '__main__': - pool = RandomPool() - print 'random pool entropy', pool.entropy, 'bits' - pool.add_event('something') - print `pool.get_bytes(100)` - import tempfile, os - fname = tempfile.mktemp() - pool = KeyboardRandomPool(filename=fname) - print 'keyboard random pool entropy', pool.entropy, 'bits' - pool.randomize() - print 'keyboard random pool entropy', pool.entropy, 'bits' - pool.randomize(128) - pool.save() - saved = open(fname, 'rb').read() - print 'saved', `saved` - print 'pool ', `pool._randpool.tostring()` - newpool = PersistentRandomPool(fname) - print 'persistent random pool entropy', pool.entropy, 'bits' - os.remove(fname) diff --git a/helios/crypto/utils.py b/helios/crypto/utils.py index dd395a598..2fcce307f 100644 --- a/helios/crypto/utils.py +++ b/helios/crypto/utils.py @@ -1,23 +1,31 @@ """ Crypto Utils """ +import base64 +import math + +from Crypto.Hash import SHA256 +from Crypto.Random.random import StrongRandom + +random = StrongRandom() + + +def random_mpz_lt(maximum, strong_random=random): + n_bits = int(math.floor(math.log(maximum, 2))) + res = strong_random.getrandbits(n_bits) + while res >= maximum: + res = strong_random.getrandbits(n_bits) + return res + + +random.mpz_lt = random_mpz_lt -import hmac, base64, json -from hashlib import sha256 - def hash_b64(s): - """ - hash the string using sha1 and produce a base64 output - removes the trailing "=" - """ - hasher = sha256(s) - result= base64.b64encode(hasher.digest())[:-1] - return result - -def to_json(d): - return json.dumps(d, sort_keys=True) - -def from_json(json_str): - if not json_str: return None - return json.loads(json_str) + """ + hash the string using sha256 and produce a base64 output + removes the trailing "=" + """ + hasher = SHA256.new(s.encode('utf-8')) + result = base64.b64encode(hasher.digest())[:-1].decode('ascii') + return result diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py index b0ede25f2..574f8eb93 100644 --- a/helios/datatypes/__init__.py +++ b/helios/datatypes/__init__.py @@ -25,6 +25,7 @@ # but is not necessary for full JSON-LD objects. LDObject.deserialize(json_string, type=...) """ +import importlib from helios import utils from helios.crypto import utils as cryptoutils @@ -33,32 +34,32 @@ ## utility function ## def recursiveToDict(obj): - if obj == None: + if obj is None: return None - if type(obj) == list: + if isinstance(obj, list): return [recursiveToDict(el) for el in obj] else: return obj.toDict() def get_class(datatype): # already done? - if not isinstance(datatype, basestring): + if not isinstance(datatype, str): return datatype # parse datatype string "v31/Election" --> from v31 import Election parsed_datatype = datatype.split("/") # get the module - dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1) + dynamic_module = importlib.import_module("helios.datatypes." + (".".join(parsed_datatype[:-1]))) if not dynamic_module: - raise Exception("no module for %s" % datatpye) + raise Exception("no module for %s" % datatype) # go down the attributes to get to the class try: dynamic_ptr = dynamic_module - for attr in parsed_datatype[1:]: + for attr in parsed_datatype[-1:]: dynamic_ptr = getattr(dynamic_ptr, attr) dynamic_cls = dynamic_ptr except AttributeError: @@ -119,7 +120,7 @@ def __init__(self, wrapped_obj): @classmethod def instantiate(cls, obj, datatype=None): - "FIXME: should datatype override the object's internal datatype? probably not" + """FIXME: should datatype override the object's internal datatype? probably not""" if isinstance(obj, LDObject): return obj @@ -130,7 +131,7 @@ def instantiate(cls, obj, datatype=None): raise Exception("no datatype found") # nulls - if obj == None: + if obj is None: return None # the class @@ -149,9 +150,11 @@ def _setattr_wrapped(self, attr, val): setattr(self.wrapped_obj, attr, val) def loadData(self): - "load data using from the wrapped object" + """ + load data using from the wrapped object + """ # go through the subfields and instantiate them too - for subfield_name, subfield_type in self.STRUCTURED_FIELDS.iteritems(): + for subfield_name, subfield_type in self.STRUCTURED_FIELDS.items(): self.structured_fields[subfield_name] = self.instantiate(self._getattr_wrapped(subfield_name), datatype = subfield_type) def loadDataFromDict(self, d): @@ -160,7 +163,7 @@ def loadDataFromDict(self, d): """ # the structured fields - structured_fields = self.STRUCTURED_FIELDS.keys() + structured_fields = list(self.STRUCTURED_FIELDS.keys()) # go through the fields and set them properly # on the newly instantiated object @@ -171,7 +174,7 @@ def loadDataFromDict(self, d): self.structured_fields[f] = sub_ld_object # set the field on the wrapped object too - if sub_ld_object != None: + if sub_ld_object is not None: self._setattr_wrapped(f, sub_ld_object.wrapped_obj) else: self._setattr_wrapped(f, None) @@ -190,12 +193,12 @@ def toDict(self, alternate_fields=None, complete=False): fields = self.FIELDS if not self.structured_fields: - if self.wrapped_obj.alias != None: + if self.wrapped_obj.alias is not None: fields = self.ALIASED_VOTER_FIELDS for f in (alternate_fields or fields): # is it a structured subfield? - if self.structured_fields.has_key(f): + if f in self.structured_fields: val[f] = recursiveToDict(self.structured_fields[f]) else: val[f] = self.process_value_out(f, self._getattr_wrapped(f)) @@ -214,7 +217,7 @@ def toDict(self, alternate_fields=None, complete=False): @classmethod def fromDict(cls, d, type_hint=None): # null objects - if d == None: + if d is None: return None # the LD type is either in d or in type_hint @@ -248,11 +251,11 @@ def process_value_in(self, field_name, field_value): """ process some fields on the way into the object """ - if field_value == None: + if field_value is None: return None val = self._process_value_in(field_name, field_value) - if val != None: + if val is not None: return val else: return field_value @@ -264,23 +267,25 @@ def process_value_out(self, field_name, field_value): """ process some fields on the way out of the object """ - if field_value == None: + if field_value is None: return None val = self._process_value_out(field_name, field_value) - if val != None: + if val is not None: return val else: return field_value def _process_value_out(self, field_name, field_value): + if isinstance(field_value, bytes): + return field_value.decode('utf-8') return None - + def __eq__(self, other): if not hasattr(self, 'uuid'): - return super(LDObject,self) == other + return super(LDObject, self) == other - return other != None and self.uuid == other.uuid + return other is not None and self.uuid == other.uuid class BaseArrayOfObjects(LDObject): diff --git a/helios/datatypes/djangofield.py b/helios/datatypes/djangofield.py index e0eb1b4fb..a299ace6c 100644 --- a/helios/datatypes/djangofield.py +++ b/helios/datatypes/djangofield.py @@ -6,15 +6,12 @@ and adapted to LDObject """ -import datetime -import json from django.db import models -from django.db.models import signals -from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder +from helios import utils from . import LDObject + class LDObjectField(models.TextField): """ LDObject is a generic textfield that neatly serializes/unserializes @@ -23,9 +20,6 @@ class LDObjectField(models.TextField): deserialization_params added on 2011-01-09 to provide additional hints at deserialization time """ - # Used so to_python() is called - __metaclass__ = models.SubfieldBase - def __init__(self, type_hint=None, **kwargs): self.type_hint = type_hint super(LDObjectField, self).__init__(**kwargs) @@ -34,35 +28,29 @@ def to_python(self, value): """Convert our string value to LDObject after we load it from the DB""" # did we already convert this? - if not isinstance(value, basestring): + if not isinstance(value, str): return value - if value == None: - return None + return self.from_db_value(value) + # noinspection PyUnusedLocal + def from_db_value(self, value, *args, **kwargs): # in some cases, we're loading an existing array or dict, - # we skip this part but instantiate the LD object - if isinstance(value, basestring): - try: - parsed_value = json.loads(value) - except: - raise Exception("value is not JSON parseable, that's bad news") - else: - parsed_value = value - - if parsed_value != None: - "we give the wrapped object back because we're not dealing with serialization types" - return_val = LDObject.fromDict(parsed_value, type_hint = self.type_hint).wrapped_obj - return return_val - else: + # from_json takes care of this duality + parsed_value = utils.from_json(value) + if parsed_value is None: return None + # we give the wrapped object back because we're not dealing with serialization types + return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj + return return_val + def get_prep_value(self, value): """Convert our JSON object to a string before we save""" - if isinstance(value, basestring): + if isinstance(value, str): return value - if value == None: + if value is None: return None # instantiate the proper LDObject to dump it appropriately @@ -71,4 +59,4 @@ def get_prep_value(self, value): def value_to_string(self, obj): value = self._get_val_from_obj(obj) - return self.get_db_prep_value(value) + return self.get_db_prep_value(value, None) diff --git a/helios/datatypes/legacy.py b/helios/datatypes/legacy.py index c0a24ffc2..d469b4418 100644 --- a/helios/datatypes/legacy.py +++ b/helios/datatypes/legacy.py @@ -77,7 +77,7 @@ def toDict(self, complete=False): """ depending on whether the voter is aliased, use different fields """ - if self.wrapped_obj.alias != None: + if self.wrapped_obj.alias is not None: return super(Voter, self).toDict(self.ALIASED_VOTER_FIELDS, complete = complete) else: return super(Voter,self).toDict(complete = complete) diff --git a/helios/datetimewidget.py b/helios/datetimewidget.py index 5a9e0d40a..dfd7ec04a 100644 --- a/helios/datetimewidget.py +++ b/helios/datetimewidget.py @@ -14,7 +14,7 @@ from django.utils.safestring import mark_safe # DATETIMEWIDGET -calbtn = u'''calendar +calbtn = '''calendar ''' class DateTimeWidget(forms.widgets.TextInput): + template_name = '' + class Media: css = { 'all': ( @@ -49,13 +51,13 @@ def render(self, name, value, attrs=None): except: final_attrs['value'] = \ force_unicode(value) - if not final_attrs.has_key('id'): - final_attrs['id'] = u'%s_id' % (name) + if 'id' not in final_attrs: + final_attrs['id'] = '%s_id' % (name) id = final_attrs['id'] jsdformat = self.dformat #.replace('%', '%%') cal = calbtn % (settings.MEDIA_URL, id, id, jsdformat, id) - a = u'%s%s' % (forms.util.flatatt(final_attrs), self.media, cal) + a = '%s%s' % (forms.util.flatatt(final_attrs), self.media, cal) return mark_safe(a) def value_from_datadict(self, data, files, name): @@ -82,12 +84,12 @@ def _has_changed(self, initial, data): Copy of parent's method, but modify value with strftime function before final comparsion """ if data is None: - data_value = u'' + data_value = '' else: data_value = data if initial is None: - initial_value = u'' + initial_value = '' else: initial_value = initial diff --git a/helios/election_url_names.py b/helios/election_url_names.py new file mode 100644 index 000000000..0700de268 --- /dev/null +++ b/helios/election_url_names.py @@ -0,0 +1,70 @@ +ELECTION_HOME="election@home" +ELECTION_VIEW="election@view" +ELECTION_META="election@meta" +ELECTION_EDIT="election@edit" +ELECTION_SCHEDULE="election@schedule" +ELECTION_EXTEND="election@extend" +ELECTION_ARCHIVE="election@archive" +ELECTION_DELETE="election@delete" +ELECTION_COPY="election@copy" +ELECTION_BADGE="election@badge" + +ELECTION_TRUSTEES_HOME="election@trustees" +ELECTION_TRUSTEES_VIEW="election@trustees@view" +ELECTION_TRUSTEES_NEW="election@trustees@new" +ELECTION_TRUSTEES_ADD_HELIOS="election@trustees@add-helios" +ELECTION_TRUSTEES_DELETE="election@trustees@delete" + +ELECTION_ADMINS_LIST="election@admins" +ELECTION_ADMINS_ADD="election@admins@add" +ELECTION_ADMINS_REMOVE="election@admins@remove" + +ELECTION_TRUSTEE_HOME="election@trustee" +ELECTION_TRUSTEE_SEND_URL="election@trustee@send-url" +ELECTION_TRUSTEE_KEY_GENERATOR="election@trustee@key-generator" +ELECTION_TRUSTEE_CHECK_SK="election@trustee@check-sk" +ELECTION_TRUSTEE_UPLOAD_PK="election@trustee@upload-pk" +ELECTION_TRUSTEE_DECRYPT_AND_PROVE="election@trustee@decrypt-and-prove" +ELECTION_TRUSTEE_UPLOAD_DECRYPTION="election@trustee@upload-decryption" + +ELECTION_RESULT="election@result" +ELECTION_RESULT_PROOF="election@result@proof" +ELECTION_BBOARD="election@bboard" +ELECTION_AUDITED_BALLOTS="election@audited-ballots" + +ELECTION_GET_RANDOMNESS="election@get-randomness" +ELECTION_QUESTIONS="election@questions" +ELECTION_SET_REG="election@set-reg" +ELECTION_SET_FEATURED="election@set-featured" +ELECTION_SAVE_QUESTIONS="election@save-questions" +ELECTION_REGISTER="election@register" +ELECTION_FREEZE="election@freeze" + +ELECTION_COMPUTE_TALLY="election@compute-tally" +ELECTION_COMBINE_DECRYPTIONS="election@combine-decryptions" +ELECTION_RELEASE_RESULT="election@release-result" + +ELECTION_CAST="election@cast" +ELECTION_CAST_CONFIRM="election@cast-confirm" +ELECTION_PASSWORD_VOTER_LOGIN="election@password-voter-login" +ELECTION_PASSWORD_VOTER_RESEND="election@password-voter-resend" +ELECTION_CAST_DONE="election@cast-done" + +ELECTION_POST_AUDITED_BALLOT="election@post-audited-ballot" + +ELECTION_VOTERS_HOME="election@voters" +ELECTION_VOTERS_UPLOAD="election@voters@upload" +ELECTION_VOTERS_UPLOAD_CANCEL="election@voters@upload-cancel" +ELECTION_VOTERS_LIST="election@voters@list" +ELECTION_VOTERS_LIST_PRETTY="election@voters@list-pretty" +ELECTION_VOTERS_ELIGIBILITY="election@voters@eligibility" +ELECTION_VOTERS_EMAIL="election@voters@email" +ELECTION_VOTER="election@voter" +ELECTION_VOTER_DELETE="election@voter@delete" +ELECTION_VOTERS_CLEAR="election@voters@clear" + +ELECTION_BALLOTS_LIST="election@ballots@list" +ELECTION_BALLOTS_VOTER="election@ballots@voter" +ELECTION_BALLOTS_VOTER_LAST="election@ballots@voter@last" + +ELECTION_LOG_DOWNLOAD_CSV="election@log@download-csv" diff --git a/helios/election_urls.py b/helios/election_urls.py index 6622c5568..db01c871e 100644 --- a/helios/election_urls.py +++ b/helios/election_urls.py @@ -4,91 +4,108 @@ Ben Adida (ben@adida.net) """ -from django.conf.urls import * +from django.urls import path, re_path -from helios.views import * +from helios import views +from helios import election_url_names as names -urlpatterns = patterns('', +urlpatterns = [ # election data that is cryptographically verified - (r'^$', one_election), + path('', views.one_election, name=names.ELECTION_HOME), # metadata that need not be verified - (r'^/meta$', one_election_meta), + path('/meta', views.one_election_meta, name=names.ELECTION_META), # edit election params - (r'^/edit$', one_election_edit), - (r'^/schedule$', one_election_schedule), - (r'^/extend$', one_election_extend), - (r'^/archive$', one_election_archive), - (r'^/copy$', one_election_copy), + path('/edit', views.one_election_edit, name=names.ELECTION_EDIT), + path('/schedule', views.one_election_schedule, name=names.ELECTION_SCHEDULE), + path('/extend', views.one_election_extend, name=names.ELECTION_EXTEND), + path('/archive', views.one_election_archive, name=names.ELECTION_ARCHIVE), + path('/delete', views.one_election_delete, name=names.ELECTION_DELETE), + path('/copy', views.one_election_copy, name=names.ELECTION_COPY), # badge - (r'^/badge$', election_badge), + path('/badge', views.election_badge, name=names.ELECTION_BADGE), # adding trustees - (r'^/trustees/$', list_trustees), - (r'^/trustees/view$', list_trustees_view), - (r'^/trustees/new$', new_trustee), - (r'^/trustees/add-helios$', new_trustee_helios), - (r'^/trustees/delete$', delete_trustee), + path('/trustees/', views.list_trustees, name=names.ELECTION_TRUSTEES_HOME), + path('/trustees/view', views.list_trustees_view, name=names.ELECTION_TRUSTEES_VIEW), + path('/trustees/new', views.new_trustee, name=names.ELECTION_TRUSTEES_NEW), + path('/trustees/add-helios', views.new_trustee_helios, name=names.ELECTION_TRUSTEES_ADD_HELIOS), + path('/trustees/delete', views.delete_trustee, name=names.ELECTION_TRUSTEES_DELETE), + + # managing administrators + path('/admins/', views.election_admin_list, name=names.ELECTION_ADMINS_LIST), + path('/admins/add', views.election_admin_add, name=names.ELECTION_ADMINS_ADD), + path('/admins/remove', views.election_admin_remove, name=names.ELECTION_ADMINS_REMOVE), # trustee pages - (r'^/trustees/(?P[^/]+)/home$', trustee_home), - (r'^/trustees/(?P[^/]+)/sendurl$', trustee_send_url), - (r'^/trustees/(?P[^/]+)/keygenerator$', trustee_keygenerator), - (r'^/trustees/(?P[^/]+)/check-sk$', trustee_check_sk), - (r'^/trustees/(?P[^/]+)/upoad-pk$', trustee_upload_pk), - (r'^/trustees/(?P[^/]+)/decrypt-and-prove$', trustee_decrypt_and_prove), - (r'^/trustees/(?P[^/]+)/upload-decryption$', trustee_upload_decryption), + path('/trustees//home', + views.trustee_home, name=names.ELECTION_TRUSTEE_HOME), + path('/trustees//sendurl', + views.trustee_send_url, name=names.ELECTION_TRUSTEE_SEND_URL), + path('/trustees//keygenerator', + views.trustee_keygenerator, name=names.ELECTION_TRUSTEE_KEY_GENERATOR), + path('/trustees//check-sk', + views.trustee_check_sk, name=names.ELECTION_TRUSTEE_CHECK_SK), + path('/trustees//upoad-pk', + views.trustee_upload_pk, name=names.ELECTION_TRUSTEE_UPLOAD_PK), + path('/trustees//decrypt-and-prove', + views.trustee_decrypt_and_prove, name=names.ELECTION_TRUSTEE_DECRYPT_AND_PROVE), + path('/trustees//upload-decryption', + views.trustee_upload_decryption, name=names.ELECTION_TRUSTEE_UPLOAD_DECRYPTION), # election voting-process actions - (r'^/view$', one_election_view), - (r'^/result$', one_election_result), - (r'^/result_proof$', one_election_result_proof), - # (r'^/bboard$', one_election_bboard), - (r'^/audited-ballots/$', one_election_audited_ballots), + path('/view', views.one_election_view, name=names.ELECTION_VIEW), + path('/result', views.one_election_result, name=names.ELECTION_RESULT), + path('/result_proof', views.one_election_result_proof, name=names.ELECTION_RESULT_PROOF), + # url(r'^/bboard$', views.one_election_bboard, name=names.ELECTION_BBOARD), + path('/audited-ballots/', views.one_election_audited_ballots, name=names.ELECTION_AUDITED_BALLOTS), # get randomness - (r'^/get-randomness$', get_randomness), - - # server-side encryption - (r'^/encrypt-ballot$', encrypt_ballot), + path('/get-randomness', views.get_randomness, name=names.ELECTION_GET_RANDOMNESS), # construct election - (r'^/questions$', one_election_questions), - (r'^/set_reg$', one_election_set_reg), - (r'^/set_featured$', one_election_set_featured), - (r'^/save_questions$', one_election_save_questions), - (r'^/register$', one_election_register), - (r'^/freeze$', one_election_freeze), # includes freeze_2 as POST target + path('/questions', views.one_election_questions, name=names.ELECTION_QUESTIONS), + path('/set_reg', views.one_election_set_reg, name=names.ELECTION_SET_REG), + path('/set_featured', views.one_election_set_featured, name=names.ELECTION_SET_FEATURED), + path('/save_questions', views.one_election_save_questions, name=names.ELECTION_SAVE_QUESTIONS), + path('/register', views.one_election_register, name=names.ELECTION_REGISTER), + path('/freeze', views.one_election_freeze, name=names.ELECTION_FREEZE), # includes freeze_2 as POST target # computing tally - (r'^/compute_tally$', one_election_compute_tally), - (r'^/combine_decryptions$', combine_decryptions), - (r'^/release_result$', release_result), + path('/compute_tally', views.one_election_compute_tally, name=names.ELECTION_COMPUTE_TALLY), + path('/combine_decryptions', views.combine_decryptions, name=names.ELECTION_COMBINE_DECRYPTIONS), + path('/release_result', views.release_result, name=names.ELECTION_RELEASE_RESULT), # casting a ballot before we know who the voter is - (r'^/cast$', one_election_cast), - (r'^/cast_confirm$', one_election_cast_confirm), - (r'^/password_voter_login$', password_voter_login), - (r'^/cast_done$', one_election_cast_done), + path('/cast', views.one_election_cast, name=names.ELECTION_CAST), + path('/cast_confirm', views.one_election_cast_confirm, name=names.ELECTION_CAST_CONFIRM), + path('/password_voter_login', views.password_voter_login, name=names.ELECTION_PASSWORD_VOTER_LOGIN), + path('/password_voter_resend', views.password_voter_resend, name=names.ELECTION_PASSWORD_VOTER_RESEND), + path('/cast_done', views.one_election_cast_done, name=names.ELECTION_CAST_DONE), # post audited ballot - (r'^/post-audited-ballot', post_audited_ballot), + re_path(r'^/post-audited-ballot', views.post_audited_ballot, name=names.ELECTION_POST_AUDITED_BALLOT), # managing voters - (r'^/voters/$', voter_list), - (r'^/voters/upload$', voters_upload), - (r'^/voters/upload-cancel$', voters_upload_cancel), - (r'^/voters/list$', voters_list_pretty), - (r'^/voters/eligibility$', voters_eligibility), - (r'^/voters/email$', voters_email), - (r'^/voters/(?P[^/]+)$', one_voter), - (r'^/voters/(?P[^/]+)/delete$', voter_delete), + path('/voters/', views.voter_list, name=names.ELECTION_VOTERS_LIST), + path('/voters/upload', views.voters_upload, name=names.ELECTION_VOTERS_UPLOAD), + path('/voters/upload-cancel', views.voters_upload_cancel, name=names.ELECTION_VOTERS_UPLOAD_CANCEL), + path('/voters/list', views.voters_list_pretty, name=names.ELECTION_VOTERS_LIST_PRETTY), + path('/voters/download-csv', views.voters_download_csv, name='election@voters@download-csv'), + path('/voters/eligibility', views.voters_eligibility, name=names.ELECTION_VOTERS_ELIGIBILITY), + path('/voters/email', views.voters_email, name=names.ELECTION_VOTERS_EMAIL), + path('/voters/clear', views.voters_clear, name=names.ELECTION_VOTERS_CLEAR), + path('/voters/', views.one_voter, name=names.ELECTION_VOTER), + path('/voters//delete', views.voter_delete, name=names.ELECTION_VOTER_DELETE), # ballots - (r'^/ballots/$', ballot_list), - (r'^/ballots/(?P[^/]+)/all$', voter_votes), - (r'^/ballots/(?P[^/]+)/last$', voter_last_vote), + path('/ballots/', views.ballot_list, name=names.ELECTION_BALLOTS_LIST), + path('/ballots//all', views.voter_votes, name=names.ELECTION_BALLOTS_VOTER), + path('/ballots//last', views.voter_last_vote, name=names.ELECTION_BALLOTS_VOTER_LAST), + + # election log + path('/log/download-csv', views.election_log_download_csv, name=names.ELECTION_LOG_DOWNLOAD_CSV), -) +] diff --git a/helios/fields.py b/helios/fields.py index cf2ad6c3e..da57fb58f 100644 --- a/helios/fields.py +++ b/helios/fields.py @@ -1,9 +1,9 @@ -from time import strptime, strftime import datetime -from django import forms -from django.db import models + from django.forms import fields -from widgets import SplitSelectDateTimeWidget + +from .widgets import SplitSelectDateTimeWidget, DateTimeLocalWidget + class SplitDateTimeField(fields.MultiValueField): widget = SplitSelectDateTimeWidget @@ -28,3 +28,17 @@ def compress(self, data_list): return datetime.datetime.combine(*data_list) return None + +class DateTimeLocalField(fields.DateTimeField): + """ + A field for HTML5 datetime-local input widget. + Handles datetime input in the format: YYYY-MM-DDTHH:MM + """ + widget = DateTimeLocalWidget + input_formats = ['%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S'] + + def __init__(self, *args, **kwargs): + if 'input_formats' not in kwargs or kwargs.get('input_formats') is None: + kwargs['input_formats'] = self.input_formats + super(DateTimeLocalField, self).__init__(*args, **kwargs) + diff --git a/helios/fixtures/election.json b/helios/fixtures/election.json index cd7a61655..1d54b4e44 100644 --- a/helios/fixtures/election.json +++ b/helios/fixtures/election.json @@ -1,25 +1,30 @@ -[{"pk": 1000, - "model": "helios.election", - "fields": - { - "admin": 1, - "uuid" : "206ef039-05c9-4e9c-bb8f-963da50c08d4", - "short_name" : "test", - "name" : "Test Election", - "election_type" : "election", - "use_advanced_audit_features" : true, - "created_at" : "2013-02-22 12:00:00", - "modified_at" : "2013-02-22 12:00:00", - "private_p" : false, - "description" : "test description", - "public_key" : null, - "private_key" : null, - "questions" : [], - "eligibility": null, - "openreg": true, - "featured_p": false, - "use_voter_aliases" : false, - "cast_url" : "/helios/elections/206ef039-05c9-4e9c-bb8f-963da50c08d4/cast" - } +[ + { + "pk": 1, + "model": "helios.election", + "fields": { + "admin": 1, + "uuid": "43b498b7-2b84-4e4e-ba82-3c2ba1a63519", + "datatype": "legacy/Election", + "short_name": "test", + "name": "Test Election", + "election_type": "election", + "private_p": false, + "description": "Test Election for Unit Tests", + "public_key": null, + "private_key": null, + "questions": [{"answer_urls": [null, null, null], "answers": ["a", "b", "c"], "choice_type": "approval", "max": 1, "min": 0, "question": "Test Question?", "result_type": "absolute", "short_name": "q1", "tally_type": "homomorphic"}], + "eligibility": null, + "openreg": false, + "featured_p": false, + "use_voter_aliases": false, + "use_advanced_audit_features": true, + "randomize_answer_order": false, + "cast_url": "", + "created_at": "2023-01-01T00:00:00Z", + "modified_at": "2023-01-01T00:00:00Z", + "frozen_at": null, + "archived_at": null + } } - ] +] \ No newline at end of file diff --git a/helios/fixtures/users.json b/helios/fixtures/users.json index c588ce475..c6e18b53d 100644 --- a/helios/fixtures/users.json +++ b/helios/fixtures/users.json @@ -1 +1,40 @@ -[{"pk": 1, "model": "helios_auth.user", "fields": {"info": "{}", "user_id": "ben@adida.net", "name": "Ben Adida", "user_type": "google", "token": null, "admin_p": false}},{"pk": 2, "model": "helios_auth.user", "fields": {"info": "{}", "user_id": "12345", "name": "Ben Adida", "user_type": "facebook", "token": {"access_token":"1234"}, "admin_p": false}}] \ No newline at end of file +[ + { + "pk": 1, + "model": "helios_auth.user", + "fields": { + "info": "{}", + "user_id": "ben@adida.net", + "name": "Ben Adida", + "user_type": "google", + "token": null, + "admin_p": false + } + }, + { + "pk": 2, + "model": "helios_auth.user", + "fields": { + "info": "{}", + "user_id": "12345", + "name": "Ben Adida", + "user_type": "facebook", + "token": { + "access_token": "1234" + }, + "admin_p": false + } + }, + { + "pk": 3, + "model": "helios_auth.user", + "fields": { + "info": "{}", + "user_id": "mccio@github.com", + "name": "Marco Ciotola", + "user_type": "google", + "token": null, + "admin_p": true + } + } +] \ No newline at end of file diff --git a/helios/fixtures/voter-badfile.csv b/helios/fixtures/voter-badfile.csv index fd674a999..cc858ace2 100644 --- a/helios/fixtures/voter-badfile.csv +++ b/helios/fixtures/voter-badfile.csv @@ -1,5 +1,5 @@ -Ben78@adida.net,Ben78 Adida - benadida5,ben5@adida.net , Ben5 Adida -benadida6,ben6@adida.net,Ben6 Adida -benadida7,ben7@adida.net,Ben7 Adida -ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios \ No newline at end of file +password,Ben78@adida.net,Ben78 Adida +password, benadida5,ben5@adida.net , Ben5 Adida +password,benadida6,ben6@adida.net,Ben6 Adida +password,benadida7,ben7@adida.net,Ben7 Adida +password,ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios \ No newline at end of file diff --git a/helios/fixtures/voter-file-2.csv b/helios/fixtures/voter-file-2.csv new file mode 100644 index 000000000..a8cf727f7 --- /dev/null +++ b/helios/fixtures/voter-file-2.csv @@ -0,0 +1,3 @@ +password,voter8,voter8@example.com,Voter Eight +password,voter9,voter9@example.com,Voter Nine +password,voter10,voter10@example.com,Voter Ten diff --git a/helios/fixtures/voter-file-latin1.csv b/helios/fixtures/voter-file-latin1.csv new file mode 100644 index 000000000..4c2e42a48 --- /dev/null +++ b/helios/fixtures/voter-file-latin1.csv @@ -0,0 +1,4 @@ +password, benadida5,ben5@adida.net , Ben5 Adida +password,benadida6,ben6@adida.net,Ben6 Adida +password,benadida7,ben7@adida.net,Ben7 Adida +password,testlatin1,Test Latin1,J�NIO LUIZ CORREIA J�NIOR diff --git a/helios/fixtures/voter-file.csv b/helios/fixtures/voter-file.csv index b94bd1a4f..70795def3 100644 --- a/helios/fixtures/voter-file.csv +++ b/helios/fixtures/voter-file.csv @@ -1,4 +1,4 @@ - benadida5,ben5@adida.net , Ben5 Adida -benadida6,ben6@adida.net,Ben6 Adida -benadida7,ben7@adida.net,Ben7 Adida -ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios \ No newline at end of file +password, benadida5,ben5@adida.net , Ben5 Adida +password,benadida6,ben6@adida.net,Ben6 Adida +password,benadida7,ben7@adida.net,Ben7 Adida +password,ernesto,helios-testing-ernesto@adida.net,Erñesto Testing Helios \ No newline at end of file diff --git a/helios/forms.py b/helios/forms.py index cb10cfab8..7a6f6a42c 100644 --- a/helios/forms.py +++ b/helios/forms.py @@ -3,11 +3,11 @@ """ from django import forms -from models import Election -from widgets import * -from fields import * from django.conf import settings +from .fields import DateTimeLocalField +from .models import Election + class ElectionForm(forms.Form): short_name = forms.SlugField(max_length=40, help_text='no spaces, will be part of the URL for your election, e.g. my-club-2010') @@ -24,14 +24,14 @@ class ElectionForm(forms.Form): election_info_url = forms.CharField(required=False, initial="", label="Election Info Download URL", help_text="the URL of a PDF document that contains extra election information, e.g. candidate bios and statements") # times - voting_starts_at = SplitDateTimeField(help_text = 'UTC date and time when voting begins', - widget=SplitSelectDateTimeWidget, required=False) - voting_ends_at = SplitDateTimeField(help_text = 'UTC date and time when voting ends', - widget=SplitSelectDateTimeWidget, required=False) + voting_starts_at = DateTimeLocalField(help_text = 'UTC date and time when voting begins', + required=False) + voting_ends_at = DateTimeLocalField(help_text = 'UTC date and time when voting ends', + required=False) class ElectionTimeExtensionForm(forms.Form): - voting_extended_until = SplitDateTimeField(help_text = 'UTC date and time voting extended to', - widget=SplitSelectDateTimeWidget, required=False) + voting_extended_until = DateTimeLocalField(help_text = 'UTC date and time voting extended to', + required=False) class EmailVotersForm(forms.Form): subject = forms.CharField(max_length=80) @@ -47,3 +47,6 @@ class VoterPasswordForm(forms.Form): voter_id = forms.CharField(max_length=50, label="Voter ID") password = forms.CharField(widget=forms.PasswordInput(), max_length=100) +class VoterPasswordResendForm(forms.Form): + voter_id = forms.CharField(max_length=50, label="Voter ID", help_text="Enter the voter ID you were assigned for this election") + diff --git a/helios/management/commands/helios_trustee_decrypt.py b/helios/management/commands/helios_trustee_decrypt.py index 3dc75a4af..478833020 100644 --- a/helios/management/commands/helios_trustee_decrypt.py +++ b/helios/management/commands/helios_trustee_decrypt.py @@ -8,12 +8,10 @@ 2010-05-22 """ -from django.core.management.base import BaseCommand, CommandError -import csv, datetime +from django.core.management.base import BaseCommand -from helios import utils as helios_utils +from helios.models import Trustee -from helios.models import * class Command(BaseCommand): args = '' diff --git a/helios/management/commands/load_voter_files.py b/helios/management/commands/load_voter_files.py index 5b82285a6..549434702 100644 --- a/helios/management/commands/load_voter_files.py +++ b/helios/management/commands/load_voter_files.py @@ -8,12 +8,15 @@ 2010-05-22 """ -from django.core.management.base import BaseCommand, CommandError -import csv, datetime +import datetime + +import csv +import uuid +from django.core.management.base import BaseCommand from helios import utils as helios_utils +from helios.models import User, Voter, VoterFile -from helios.models import * ## ## UTF8 craziness for CSV @@ -25,44 +28,47 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs): dialect=dialect, **kwargs) for row in csv_reader: # decode UTF-8 back to Unicode, cell by cell: - yield [unicode(cell, 'utf-8') for cell in row] + yield [str(cell, 'utf-8') for cell in row] + def utf_8_encoder(unicode_csv_data): for line in unicode_csv_data: yield line.encode('utf-8') - + + def process_csv_file(election, f): reader = unicode_csv_reader(f) - + num_voters = 0 for voter in reader: - # bad line - if len(voter) < 1: - continue - - num_voters += 1 - voter_id = voter[0] - name = voter_id - email = voter_id - - if len(voter) > 1: - email = voter[1] - - if len(voter) > 2: - name = voter[2] - - # create the user - user = User.update_or_create(user_type='password', user_id=voter_id, info = {'password': helios_utils.random_string(10), 'email': email, 'name': name}) - user.save() - - # does voter for this user already exist - voter = Voter.get_by_election_and_user(election, user) - - # create the voter - if not voter: - voter_uuid = str(uuid.uuid1()) - voter = Voter(uuid= voter_uuid, voter_type = 'password', voter_id = voter_id, name = name, election = election) - voter.save() + # bad line + if len(voter) < 1: + continue + + num_voters += 1 + voter_id = voter[0] + name = voter_id + email = voter_id + + if len(voter) > 1: + email = voter[1] + + if len(voter) > 2: + name = voter[2] + + # create the user + user = User.update_or_create(user_type='password', user_id=voter_id, + info={'password': helios_utils.random_string(10), 'email': email, 'name': name}) + user.save() + + # does voter for this user already exist + voter = Voter.get_by_election_and_user(election, user) + + # create the voter + if not voter: + voter_uuid = str(uuid.uuid4()) + voter = Voter(uuid=voter_uuid, voter_type='password', voter_id=voter_id, name=name, election=election) + voter.save() return num_voters @@ -70,7 +76,7 @@ def process_csv_file(election, f): class Command(BaseCommand): args = '' help = 'load up voters from unprocessed voter files' - + def handle(self, *args, **options): # load up the voter files in order of last uploaded files_to_process = VoterFile.objects.filter(processing_started_at=None).order_by('uploaded_at') @@ -86,5 +92,3 @@ def handle(self, *args, **options): file_to_process.processing_finished_at = datetime.datetime.utcnow() file_to_process.num_voters = num_voters file_to_process.save() - - diff --git a/helios/management/commands/verify_cast_votes.py b/helios/management/commands/verify_cast_votes.py index 5b7f39253..e2fab7186 100644 --- a/helios/management/commands/verify_cast_votes.py +++ b/helios/management/commands/verify_cast_votes.py @@ -6,12 +6,10 @@ 2010-05-22 """ -from django.core.management.base import BaseCommand, CommandError -import csv, datetime +from django.core.management.base import BaseCommand -from helios import utils as helios_utils +from helios.models import CastVote -from helios.models import * def get_cast_vote_to_verify(): # fixme: add "select for update" functionality here diff --git a/helios/media/datetime-local.css b/helios/media/datetime-local.css new file mode 100644 index 000000000..893001df7 --- /dev/null +++ b/helios/media/datetime-local.css @@ -0,0 +1,21 @@ +/* Helios DateTime Picker Styles */ + +.helios-datetime-input { + padding: 8px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; + width: 250px; + box-sizing: border-box; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.helios-datetime-input:focus { + border-color: #4CAF50; + outline: none; + box-shadow: 0 0 5px rgba(76, 175, 80, 0.3); +} + +.helios-datetime-input:hover { + border-color: #999; +} diff --git a/helios/media/static_templates/question.html b/helios/media/static_templates/question.html index 5e576916d..c3c1fd200 100644 --- a/helios/media/static_templates/question.html +++ b/helios/media/static_templates/question.html @@ -1,132 +1,480 @@ - {#foreach $T.questions as question} -
-

-{#if $T.admin_p}[ -{#if $T.question$index > 0}^] [{#/if} -x] [edit] {#/if}{$T.question$index + 1}. {$T.question.question} ({$T.question.choice_type}, select between {$T.question.min} and {#if $T.question.max != null}{$T.question.max}{#else}unlimited{#/if} answers, result type {$T.question.result_type}.)

-
    -{#foreach $T.question.answers as answer} -
  • {$T.answer} -{#if $T.question.answer_urls[$T.answer$index]} - [more] -{#/if} -
  • -{#/for} -
+
+
+

+ {#if $T.admin_p} + + {#if $T.question$index > 0}[]{#/if} + [edit] + [×] + + {#/if} + {$T.question$index + 1}. {$T.question.question} +

+
+ +
+ + Selection: {$T.question.min} to {#if $T.question.max != null}{$T.question.max}{#else}unlimited{#/if} + + + Result type: {$T.question.result_type} + + {#if $T.question.randomize_answer_order} + + Order: randomized + + {#/if} +
+ +
    + {#foreach $T.question.answers as answer} +
  • + {$T.answer} + {#if $T.question.answer_urls[$T.answer$index]} + + 🔗 + + {#/if} +
  • + {#/for} +
- diff --git a/helios/templates/election_freeze.html b/helios/templates/election_freeze.html index a0aad4dd0..7e3c34baf 100644 --- a/helios/templates/election_freeze.html +++ b/helios/templates/election_freeze.html @@ -3,8 +3,7 @@ {% block content %}

{{election.name}} — Freeze Ballot

-Once the ballot is frozen, the questions and options can no longer be modified.
-The list of trustees and their public keys will also be frozen. +Once an election is frozen, the questions, options, and trustees can no longer be modified.

@@ -29,14 +28,14 @@

{{election.name}} — Freeze Ballot

  • {{issue.action}}
  • {% endfor %} - go back to the election + go back to the election

    {% else %}
    - +
    {% endif %} diff --git a/helios/templates/election_keygenerator.html b/helios/templates/election_keygenerator.html index 60a2c55c7..47afbfb05 100644 --- a/helios/templates/election_keygenerator.html +++ b/helios/templates/election_keygenerator.html @@ -35,7 +35,7 @@ $('#generator').hide(); // get some more server-side randomness for keygen - $.getJSON('../../get-randomness', function(result) { + $.getJSON('{% url "election@get-randomness" election.uuid %}', function(result) { sjcl.random.addEntropy(result.randomness); BigInt.setup(function() { ELGAMAL_PARAMS = ElGamal.Params.fromJSONObject({{eg_params_json|safe}}); @@ -76,11 +76,23 @@ } function download_sk() { - UTILS.open_window_with_content(jQuery.toJSON(SECRET_KEY), "application/json"); + $('#pk_content').show(); + $('#sk_content').html(jQuery.toJSON(SECRET_KEY)); +} + +function download_sk_to_file(filename) { + var element = document.createElement('a'); + element.setAttribute('href','data:text/plain;charset=utf-8,'+ encodeURIComponent(jQuery.toJSON(SECRET_KEY))); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); } function show_pk() { $('#sk_download').hide(); + $('#pk_content').hide(); $('#pk_hash').show(); $('#pk_form').show(); } @@ -99,9 +111,9 @@

    {{election.name}} — Trustee {{trustee.name}} — Key

    - + -

    +
    If you've already generated a keypair, you can reuse it.

    @@ -115,7 +127,7 @@

    Reusing a Key


    - +
    @@ -126,15 +138,28 @@

    Your Secret Key

    - +

    + - + + -
    +

    Your Public Key

    It's time to upload the public key to the server. @@ -146,7 +171,7 @@

    Your Public Key

    - +
    diff --git a/helios/templates/election_new.html b/helios/templates/election_new.html index 1e5e2ec0f..b1e382dd4 100644 --- a/helios/templates/election_new.html +++ b/helios/templates/election_new.html @@ -16,7 +16,7 @@

    Create a New Election

    {{election_form.as_table}}
    - +
    diff --git a/helios/templates/election_new_2.html b/helios/templates/election_new_2.html index 16061c2d0..4fa0d23d5 100644 --- a/helios/templates/election_new_2.html +++ b/helios/templates/election_new_2.html @@ -6,11 +6,11 @@ var SECRET_KEY; function before_create() { -{% ifequal election_type "one" %} +{% if election_type == "one" %} return confirm('Have you made sure to copy the private key to a safe place?\n\nOnce you click OK, Helios will not be able to recover the secret key without your help!'); {% else %} return true; -{% endifequal %} +{% endif %} } function generate_keypair() { @@ -26,44 +26,44 @@ $('#pk').val(jQuery.toJSON(SECRET_KEY.pk)); -{% ifequal election_type "one" %} +{% if election_type == "one" %} $('#sk_textarea').val(jQuery.toJSON(SECRET_KEY)); $('#sk_form').show(); {% else %} -{% ifequal election_type "helios" %} +{% if election_type == "helios" %} $('#sk').val(jQuery.toJSON(SECRET_KEY)); -{% endifequal %} -{% endifequal %} +{% endif %} +{% endif %} $('#submit').show(); } $(document).ready(function() { -{% ifnotequal election_type "multiple" %} +{% if election_type != "multiple" %} $('#submit').hide(); -{% endifnotequal %} +{% endif %} $('#sk_form').hide(); });

    Create a New Election: {{name}}

    -{% ifequal election_type "helios" %} +{% if election_type == "helios" %} An election managed by Helios. {% else %} -{% ifequal election_type "one" %} +{% if election_type == "one" %} An election managed by you, the single administrator. {% else %} An election managed by multiple trustees. -{% endifequal %} -{% endifequal %} +{% endif %} +{% endif %}

    -{% ifnotequal election_type "multiple" %} - +{% if election_type != "multiple" %} + {% else %}

    Trustees (up to 5)

    @@ -73,12 +73,12 @@

    Trustees (up to 5)



    -{% endifnotequal %} +{% endif %}

    - +


    -{% ifequal election_type "one" %} +{% if election_type == "one" %}
    Your Election's Secret Key:
    @@ -88,5 +88,5 @@

    Trustees (up to 5)

    (You need to copy and paste this key and keep it safe,
    otherwise you won't be able to tally your election.)
    -{% endifequal %} +{% endif %} {% endblock %} \ No newline at end of file diff --git a/helios/templates/election_not_started.html b/helios/templates/election_not_started.html index 4ba944e1a..7e123be88 100644 --- a/helios/templates/election_not_started.html +++ b/helios/templates/election_not_started.html @@ -1,14 +1,20 @@ {% extends TEMPLATE_BASE %} +{% load timezone_tags %} {% block content %}

    Election {{election.name}} Not Yet Open

    - This election is not yet open. You probably got here from the Ballot Preview. + This election is not yet open.

    - back to the election + {% if election.voting_start_at %}Voting start at {{election.voting_start_at|utc_time}}
    {% endif %} + {% if election.voting_end_at %}Voting end at {{election.voting_end_at|utc_time}}
    {% endif %} +

    + +

    + back to the election

    {% endblock %} diff --git a/helios/templates/election_questions.html b/helios/templates/election_questions.html index d7b8f7353..b9fbfdbcb 100644 --- a/helios/templates/election_questions.html +++ b/helios/templates/election_questions.html @@ -2,7 +2,7 @@ {% block title %}Questions for {{election.name}}{% endblock %} {% block content %} -

    {{election.name}} — Questions [back to election]

    +

    {{election.name}} — Questions [back to election]

    - - - diff --git a/helios/templates/stats.html b/helios/templates/stats.html index 37468b715..b1dbba1ce 100644 --- a/helios/templates/stats.html +++ b/helios/templates/stats.html @@ -5,11 +5,13 @@

    Admin

    -

    {{num_votes_in_queue}} votes in queue. {% if num_votes_in_queue %}[force it]{% endif %}

    +

    {{num_votes_in_queue}} votes in queue. {% if num_votes_in_queue %}[force it]{% endif %}

    {% endblock %} diff --git a/helios/templates/stats_deleted_elections.html b/helios/templates/stats_deleted_elections.html new file mode 100644 index 000000000..728183408 --- /dev/null +++ b/helios/templates/stats_deleted_elections.html @@ -0,0 +1,44 @@ +{% extends TEMPLATE_BASE %} +{% load timezone_tags %} +{% block title %}Deleted Elections{% endblock %} + +{% block content %} +

    Deleted Elections

    + +

    +

    +search: + clear search +
    +

    + + +

    +{% if elections_page.has_previous %} +previous {{limit}}    +{% endif %} + +Elections {{elections_page.start_index}} - {{elections_page.end_index}} (of {{total_elections}})   + +{% if elections_page.has_next %} +next {{limit}}    +{% endif %} +

    + +{% if elections %} +{% for election in elections %} +

    +{{election.name}} by {{election.admin.pretty_name}}
    +Deleted at: {{election.deleted_at|utc_time}}
    +{{election.num_voters}} voters / {{election.num_cast_votes}} cast votes
    +

    + + +
    +

    +{% endfor %} +{% else %} +

    No deleted elections found.

    +{% endif %} + +{% endblock %} diff --git a/helios/templates/stats_elections.html b/helios/templates/stats_elections.html index 717969833..e9a498ef8 100644 --- a/helios/templates/stats_elections.html +++ b/helios/templates/stats_elections.html @@ -5,7 +5,7 @@

    Elections

    -

    + search: clear search
    @@ -26,7 +26,7 @@

    Elections

    {% for election in elections %}

    -{{election.name}} by {{election.admin.pretty_name}} -- {{election.num_voters}} voters / {{election.num_cast_votes}} cast votes +{{election.name}} by {{election.admin.pretty_name}} -- {{election.num_voters}} voters / {{election.num_cast_votes}} cast votes

    {% endfor %} diff --git a/helios/templates/stats_problem_elections.html b/helios/templates/stats_problem_elections.html index 9f8c1dadd..2f82f74b9 100644 --- a/helios/templates/stats_problem_elections.html +++ b/helios/templates/stats_problem_elections.html @@ -8,7 +8,7 @@

    Problematic Elections

    {% for election in elections %}

    -{{election.name}} -- {{election.num_voters}} voters +{{election.name}} -- {{election.num_voters}} voters

    {% endfor %} diff --git a/helios/templates/stats_recent_votes.html b/helios/templates/stats_recent_votes.html index 37c074161..bf360a284 100644 --- a/helios/templates/stats_recent_votes.html +++ b/helios/templates/stats_recent_votes.html @@ -8,7 +8,7 @@

    Recent Votes

    {% for election in elections %}

    -{{election.name}} -- {{election.last_cast_vote}} {{election.num_recent_cast_votes}} recently cast votes +{{election.name}} -- {{election.last_cast_vote}} {{election.num_recent_cast_votes}} recently cast votes

    {% endfor %} diff --git a/helios/templates/stats_user_search.html b/helios/templates/stats_user_search.html new file mode 100644 index 000000000..400bfc5e0 --- /dev/null +++ b/helios/templates/stats_user_search.html @@ -0,0 +1,83 @@ +{% extends TEMPLATE_BASE %} +{% block title %}User Search{% endblock %} + +{% block content %} +

    User Search

    + +

    +

    +Search for user: + clear search +
    +

    + +{% if q %} + {% if users_with_elections %} +

    Found {{users_with_elections|length}} user(s)

    + + {% for item in users_with_elections %} +
    +

    {{item.user.pretty_name}}

    +

    + User ID: {{item.user.user_id}}
    + User Type: {{item.user.user_type}}
    + Site Admin: {% if item.user.admin_p %}Yes{% else %}No{% endif %} +

    + +

    Elections as Administrator ({{item.elections_as_admin|length}})

    + {% if item.elections_as_admin %} +
      + {% for election in item.elections_as_admin %} +
    • + {{election.name}} + ({{election.num_voters}} voters / {{election.num_cast_votes}} cast votes) + {% if election.frozen_at %}[frozen]{% endif %} + {% if election.archived_at %}[archived]{% endif %} +
    • + {% endfor %} +
    + {% else %} +

    None

    + {% endif %} + +

    Elections as Voter ({{item.elections_as_voter|length}})

    + {% if item.elections_as_voter %} +
      + {% for election in item.elections_as_voter %} +
    • + {{election.name}} + ({{election.num_voters}} voters / {{election.num_cast_votes}} cast votes) + {% if election.frozen_at %}[frozen]{% endif %} + {% if election.archived_at %}[archived]{% endif %} +
    • + {% endfor %} +
    + {% else %} +

    None

    + {% endif %} + +

    Elections as Trustee ({{item.elections_as_trustee|length}})

    + {% if item.elections_as_trustee %} +
      + {% for election in item.elections_as_trustee %} +
    • + {{election.name}} + ({{election.num_voters}} voters / {{election.num_cast_votes}} cast votes) + {% if election.frozen_at %}[frozen]{% endif %} + {% if election.archived_at %}[archived]{% endif %} +
    • + {% endfor %} +
    + {% else %} +

    None

    + {% endif %} +
    + {% endfor %} + {% else %} +

    No users found matching "{{q}}"

    + {% endif %} +{% else %} +

    Enter a search term to find users

    +{% endif %} + +{% endblock %} diff --git a/helios/templates/trustee_check_sk.html b/helios/templates/trustee_check_sk.html index f6b615e43..b5791eff8 100644 --- a/helios/templates/trustee_check_sk.html +++ b/helios/templates/trustee_check_sk.html @@ -26,13 +26,21 @@ try { var secret_key = ElGamal.SecretKey.fromJSONObject(jQuery.secureEvalJSON(sk_value)); + // Check 1: Verify the public key hash matches var pk_hash = b64_sha256(jQuery.toJSON(secret_key.pk)); - var key_ok_p = (pk_hash == PK_HASH); + var hash_ok = (pk_hash == PK_HASH); + + // Check 2: Verify that g^x = y mod p (cryptographic verification) + // This ensures the secret exponent actually corresponds to the public key + var computed_y = secret_key.pk.g.modPow(secret_key.x, secret_key.pk.p); + var crypto_ok = computed_y.equals(secret_key.pk.y); + + var key_ok_p = hash_ok && crypto_ok; } catch (e) { debugger; var key_ok_p = false; } - + $('#processing').hide(); var reset_link = "
    try again"; @@ -43,7 +51,7 @@ } } -

    {{election.name}} — Trustee {{trustee.name}} — Check Secret Key [back to trustee home]

    +

    {{election.name}} — Trustee {{trustee.name}} — Check Secret Key [back to trustee home]

    Your public key fingerprint is: {{trustee.public_key_hash}} @@ -59,10 +67,10 @@

    {{election.name}} — Trustee {{trustee.name}} — Che

    -
    - +
    diff --git a/helios/templates/trustee_decrypt_and_prove.html b/helios/templates/trustee_decrypt_and_prove.html index d4cbe4384..6e1e63079 100644 --- a/helios/templates/trustee_decrypt_and_prove.html +++ b/helios/templates/trustee_decrypt_and_prove.html @@ -157,10 +157,10 @@

    Trustee {{trustee.name}} — Decrypt Result for {{election

    FIRST STEP: enter your secret key

    - +

    - +

    @@ -187,17 +187,15 @@

    SECOND STEP: upload your partial decryption

    When you're ready, you can submit this result to the server.

    Your partial decryption:
    -
    -

    - -
    -
    - reset and restart decryption process -
    +

    + + +

    +

    reset and restart decryption process

    diff --git a/helios/templates/trustee_home.html b/helios/templates/trustee_home.html index 5f6ee9c54..8d6dd1151 100644 --- a/helios/templates/trustee_home.html +++ b/helios/templates/trustee_home.html @@ -7,9 +7,9 @@

    {{election.name}} — Trustee {{trustee.name}} Home {% if trustee.public_key_hash %} You have successfully uploaded your public key.
    Your public key fingerprint is: {{trustee.public_key_hash}}.
    -You can verify that you have the right secret key. +You can verify that you have the right secret key. {% else %} -setup your key +setup your key {% endif %}

    @@ -19,7 +19,7 @@

    {{election.name}} — Trustee {{trustee.name}} Home You have successfully uploaded your decryption. {% else %} The encrypted tally for this election is ready.
    - decrypt with your key + decrypt with your key {% endif %} {% else %} Once the tally is computed, come back here to provide your secret key for decryption purposes.
    diff --git a/helios/templates/voters_eligibility.html b/helios/templates/voters_eligibility.html index 51343befd..8d4c7a159 100644 --- a/helios/templates/voters_eligibility.html +++ b/helios/templates/voters_eligibility.html @@ -2,7 +2,7 @@ {% block title %}Voter Eligibility for {{election.name}}{% endblock %} {% block content %} -

    {{election.name}} — Voter Eligibility [back to voters]

    +

    {{election.name}} — Voter Eligibility [back to voters]

    {{election.pretty_eligibility|safe}} @@ -20,7 +20,7 @@

    {{election.name}} — Voter Eligibility {{category.name}} {% endfor %} - + {% endblock %} diff --git a/helios/templates/voters_email.html b/helios/templates/voters_email.html index 535f7533f..62a6c7d63 100644 --- a/helios/templates/voters_email.html +++ b/helios/templates/voters_email.html @@ -9,7 +9,7 @@ {% endif %} -

    {{election.name}} — Contact Voters [back to election]

    +

    {{election.name}} — Contact Voters [back to election]

    {% if voter %}

    @@ -55,7 +55,7 @@

    {{election.name}} — Contact Voters - Done, go back to election. + Done, go back to election.