Course-scale voting platform with a web UI at /, a JSON API under /api, and cryptography aligned with docs/ (proposal, CDDR, interface spec, key lifecycle).
The root path / must serve the app. This project now serves GET / as the SecureVote UI. JSON APIs live under /api/... (for example /api/login). Hitting a path that does not exist still returns JSON Not Found from FastAPI.
| Property | Mechanism |
|---|---|
| Vote confidentiality | Per-election RSA-2048 OAEP (SHA-256) |
| Integrity of stored ciphertext | SHA-256 over ciphertext |
| Passwords | bcrypt |
| Sessions | JWT (Bearer) |
| Ballot origin | RSA-PSS + SHA-256 over UTF-8 encrypted_vote ‖ | ‖ timestamp |
Voter signing private keys are returned once at registration and stored in browser localStorage for the demo UI (not kept on the server). Each election has its own RSA key pair in the database.
.
├── docs/
├── src/ # main.py, api_routes.py, crypto_service.py, database.py, …
├── static/ # CSS + JS for the UI
├── templates/ # app.html (single-page shell)
├── tests/
├── data/ # runtime DB + uploads (gitignored)
└── requirements.txt
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp config/.env.example .env # set JWT_SECRET and ADMIN_PASSWORDuvicorn src.main:app --reload --host 127.0.0.1 --port 8000Open http://127.0.0.1:8000/ in a browser.
- Voter tab: sign in or create an account using an authorized
@umsystem.eduemail. Login is password + OTP. - Admin tab: password is
ADMIN_PASSWORDfrom.env(hashed once into the DB on first run). - Admin: Create election (class / department / campus), add contestant names + photos, set open/close times. Approvals for registration requests. Manage to Close an election, then Publish results so voters see counts in Results.
| Variable | Purpose |
|---|---|
JWT_SECRET |
HMAC key for JWTs |
ADMIN_PASSWORD |
Used to seed the first admin account if the admins table is empty (bcrypt in DB) |
ADMIN_USERNAME |
Username for that seeded account (default admin) |
STUDENT_EMAIL_ALLOWLIST |
Comma-separated authorized student emails (e.g. ts8md@umsystem.edu,spc7p@umsystem.edu) |
OTP_EXPIRE_SECONDS |
OTP lifetime in seconds (default 300) |
OTP_DELIVERY_MODE |
demo (logs/returns OTP) or smtp (sends real OTP email) |
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM_EMAIL, SMTP_USE_STARTTLS |
SMTP settings used when OTP_DELIVERY_MODE=smtp |
EXPOSE_OTP_IN_RESPONSE |
Demo/testing helper to include OTP in API response (true by default; set false in production) |
DATABASE_PATH |
Optional SQLite path (default ./data/voting.db) |
DATA_DIR |
Optional data root for DB default path + uploads/ |
All under prefix /api:
POST /api/register,POST /api/login,GET /api/admin/setup-status,POST /api/admin/register-first(only if no admins),POST /api/admin/login(username + password),GET /api/admin/dashboard-summaryGET /api/elections(optionalAuthorization: Bearervoter token for per-user status)GET /api/elections/{id}/detailPOST /api/elections/{id}/register(voter)GET /api/admin/registrations?status_filter=pendingPOST /api/admin/registrations/{id}/approve|rejectPOST /api/admin/elections(multipart: title, category, starts_at, ends_at, contestant_names JSON, photos[])POST /api/elections/{id}/vote— body:voter_id,encrypted_vote(base64),signature,timestamp; plaintext JSON is{"contestant_id": <int>}POST /api/admin/elections/{id}/close,POST /api/admin/elections/{id}/publish-resultsGET /api/elections/{id}/results(after publish)
Legacy shims for the original single-election flow: /api/legacy/....
pytest- Set these in
.env:
OTP_DELIVERY_MODE=smtp
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USERNAME=<your-mail@umsystem.edu>
SMTP_PASSWORD=<smtp/app-password>
SMTP_FROM_EMAIL=<your-mail@umsystem.edu>
SMTP_USE_STARTTLS=true
EXPOSE_OTP_IN_RESPONSE=false- Restart the server.
- Login sends OTP to the student email in
STUDENT_EMAIL_ALLOWLIST.
MIT — see LICENSE.