A terminal-based voting client for the Criptocracia Electoral Commission. Communicates over Nostr using NIP-59 Gift Wrap messages to register voters, obtain anonymous blind-RSA voting tokens, and cast ballots from unlinkable keypairs.
- Anonymous voting via blind RSA signatures (RFC 9474) — your vote cannot be linked to your identity
- End-to-end encrypted communication with the Electoral Commission over Nostr NIP-59
- Plurality and STV elections — single-choice and ranked-choice voting
- Optional password protection for your identity using age encryption
- Keyboard-driven TUI with vim-style navigation
- Local state persistence — registrations and tokens survive restarts
- No telemetry — communicates only with configured Nostr relays
- Rust stable toolchain (edition 2024)
- A running Electoral Commission daemon on configured Nostr relays
cargo build --release
./target/release/voterOn first launch you'll be prompted to create or import a Nostr identity. Optionally protect it with a password.
- Browse elections — the client subscribes to election announcements from Nostr relays
- Register — enter your out-of-band registration token to bind your identity to an election
- Request a voting token — the client blinds a random nonce, sends it to the EC, receives a blind signature, and unblinds it locally
- Cast your vote — select candidates and confirm; the vote is sent from a throwaway keypair that cannot be linked back to you
Configuration is stored at ~/.config/voter/voter.toml (created with defaults on first run).
[nostr]
relays = ["wss://relay.mostro.network", "wss://nos.lol"]
[identity]
path = "~/.config/voter/identity.json"
[ui]
theme = "dark"| Path | Purpose |
|---|---|
~/.config/voter/voter.toml |
Relay URLs, identity path, UI theme |
~/.config/voter/identity.json |
Voter Nostr keypair (plaintext) |
~/.config/voter/identity.age |
Voter Nostr keypair (password-encrypted) |
~/.config/voter/state.json |
Registrations and voting tokens |
~/.config/voter/voter.log |
Debug log (development builds only) |
| Key | Action |
|---|---|
q |
Quit |
? |
Toggle help overlay |
Esc |
Go back / cancel |
| Key | Action |
|---|---|
j / Down |
Move down |
k / Up |
Move up |
Enter |
Confirm / select |
Tab |
Next field |
s |
Open settings (from election list) |
| Key | Action |
|---|---|
v |
Cast vote |
t |
Request voting token |
r |
View results |
Enter / Space |
Add candidate to ranking (STV) |
d |
Remove from ranking (STV) |
cargo build # Build debug
cargo test # Run all tests
cargo test <test_name> # Run a single test
cargo fmt --check # Check formatting
cargo clippy # Lint (zero warnings enforced)Integration tests that require a running EC daemon are ignored by default:
RUN_EC_INTEGRATION=1 cargo test -- --ignoredThe EC protocol defines three interactions, all wrapped in NIP-59 Gift Wrap:
Voter Electoral Commission
| |
|-- Register (identity + token) ---------> |
|<------------- Ok / Error --------------- |
| |
|-- RequestToken (blinded nonce) --------> |
|<------------- BlindSignature ----------- |
| [unblind locally] |
| |
|-- CastVote (throwaway key + token) ----> |
|<------------- Ok / Error --------------- |
- Blind RSA signatures: the EC signs a blinded nonce without seeing the final token
- Throwaway keypairs: votes are sent from ephemeral keys discarded after submission
- No linkability: the unblinded token is never associated with the voter's identity
- Fail-secure: invalid cryptographic operations are rejected immediately
- No custom crypto: uses only audited libraries (
blind-rsa-signatures,nostr-sdk,age)
src/
├── main.rs # Entry point, event loop
├── app.rs # App state, screen management, input handling
├── config.rs # Configuration (TOML)
├── identity.rs # Keypair generation, save/load, age encryption
├── state.rs # Persistent state (registrations, tokens)
├── error.rs # Error types
├── crypto/
│ ├── blind_rsa.rs # Blind/unblind operations
│ └── token.rs # VotingToken, nonce generation
├── nostr/
│ ├── client.rs # Nostr client, relay connection, Gift Wrap
│ ├── events.rs # Election & results structs
│ └── messages.rs # Protocol message types
└── ui/
├── welcome.rs # Identity setup
├── password.rs # Encrypted identity unlock
├── election_list.rs # Browse elections
├── election_detail.rs# Register, request token
├── vote.rs # Candidate selection
├── results.rs # Election results
├── settings.rs # Relay/theme config
├── help.rs # Keyboard shortcuts overlay
└── widgets/ # Status bar, confirm dialog
MIT