|
#!/usr/bin/env python3 |
|
|
|
BOARD_ELECTION_VOTE_LIMIT = 5 # at most 5 positions on the board |
|
MOCK_ELECTION_VOTE_LIMIT = 2 |
|
|
|
ELECTIONS = ( |
|
('2022-02-22 board', '2022-02-22_boardcandidates.txt', |
|
[], BOARD_ELECTION_VOTE_LIMIT), |
|
('2022-02-22 mock', '2022-02-22_mockcandidates.txt', |
|
[], MOCK_ELECTION_VOTE_LIMIT), |
|
) |
|
|
|
SHORT_MASTER_KEY_LENGTH = 8 # 64 bits, 8 bytes |
|
SUB_KEY_SIG_TRUNCATE = 8 # 64 bits, 8 bytes |
|
BALLOT_SIZE_BYTES = 8 |
|
MASTERKEY_PREFIX = "masterkey:" |
|
REKEY_PREFIX = "rekey:" |
|
BALLOT_PREFIX = "ballot:" |
|
|
|
from random import SystemRandom |
|
import hmac |
|
import hashlib |
|
import base64 |
|
import binascii |
|
import sys |
|
from os.path import exists |
|
|
|
if sys.version_info[0] < 3: |
|
print("Python 3 required. (tested on 3.5.2)") |
|
exit(1) |
|
|
|
# load candidate lists into the empty lists ELECTIONS[0][2] |
|
# and ELECTIONS[1][2] above |
|
for election, candidate_list_filename, candidate_list, limit in ELECTIONS: |
|
if not exists(candidate_list_filename): |
|
print( |
|
"%s was not found in current working directory" % |
|
candidate_list_filename) |
|
exit(1) |
|
|
|
with open(candidate_list_filename) as f: |
|
candidate_list.extend( line.strip() for line in f ) |
|
|
|
SystemRandom().shuffle(candidate_list) |
|
|
|
def create_code_signed_ballot(sub_key, vote): |
|
ballot_hmac = hmac.new(sub_key, vote.encode('UTF-8'), |
|
digestmod=hashlib.sha256) |
|
base_64_bytes = base64.b64encode(ballot_hmac.digest()[0:BALLOT_SIZE_BYTES]) |
|
return BALLOT_PREFIX + base_64_bytes.decode('ascii') |
|
|
|
def valid_candidate(candidate_selection, |
|
candidates, candidate_lookup_by_number): |
|
if candidate_selection in candidates: |
|
return candidate_selection |
|
else: |
|
try: |
|
candidate_number = int(candidate_selection) |
|
except ValueError: |
|
return None |
|
else: # only if conversion of candidate_selection to int works |
|
if candidate_number in candidate_lookup_by_number: |
|
return candidate_lookup_by_number[candidate_number] |
|
return None |
|
|
|
def get_valid_votes(candidates, max_votes): |
|
candidates_enumerated = list(enumerate(candidates, 1)) |
|
candidate_lookup_by_number = dict( candidates_enumerated ) |
|
|
|
print("Vote for up to %d candidates by matching name or number, " |
|
"seperated by comma (',')" % max_votes) |
|
print("write-in ballots are not supported by this interface") |
|
print() |
|
|
|
while True: |
|
print() # blank line |
|
for i, candidate in candidates_enumerated: |
|
print("%d) %s" % (i, candidate)) |
|
candidate_selection_w_newline = input("Who are you voting for? > ") |
|
candidate_selection = candidate_selection_w_newline.strip() |
|
|
|
# split up the candidate selection by comma |
|
selected_candidates = candidate_selection.split(",") |
|
|
|
# ask again if too many were voted for or none were |
|
if len(selected_candidates) > max_votes or len(selected_candidates) <1: |
|
continue |
|
|
|
# remove any whitespace |
|
selected_candidates = [ c.strip() for c in selected_candidates ] |
|
|
|
# validate each candidate selection, replacing with None if |
|
# invalid, and replacing any numbers with names |
|
selected_candidates = [ |
|
valid_candidate(c, candidates, candidate_lookup_by_number) |
|
for c in selected_candidates] |
|
# verify all selected candidates came out okay, no None |
|
if all( c !=None for c in selected_candidates ): |
|
return selected_candidates |
|
|
|
def get_valid_master_code(): |
|
while True: |
|
code_w_newline = input("What's your original masterkey? > ") |
|
mastercode_ascii = code_w_newline.strip() |
|
|
|
if mastercode_ascii.startswith(MASTERKEY_PREFIX): |
|
# remove masterkey: prefix |
|
mastercode_ascii = mastercode_ascii[len(MASTERKEY_PREFIX):] |
|
prefix_seen = True |
|
else: |
|
prefix_seen = False |
|
|
|
try: |
|
decoded_code = base64.b64decode(mastercode_ascii, validate=True) |
|
except binascii.Error: |
|
print("invalid code") |
|
else: |
|
if len(decoded_code) == SHORT_MASTER_KEY_LENGTH: |
|
break |
|
else: |
|
print("invalid code") |
|
|
|
print("Your code is:") |
|
print(mastercode_ascii) |
|
print() |
|
return decoded_code |
|
|
|
def get_election_choice(): |
|
while True: |
|
print("Which election are you voting in?") |
|
# start enumeration at 1 so they are numbered 1), 2)... |
|
for i, (election_name, a, b, c) in enumerate(ELECTIONS, 1): |
|
print("%d) %s" % (i, election_name) ) |
|
try: |
|
election_choice = int(input("> ")) |
|
except ValueError: |
|
print() |
|
else: |
|
if 1<= election_choice <= len(ELECTIONS): |
|
# election_choice-1 because the UI enumerates from 1) |
|
choice_of_election = ELECTIONS[election_choice-1] |
|
return ( |
|
choice_of_election[0], # election name |
|
choice_of_election[2], # candidate list |
|
choice_of_election[3], # vote limit |
|
) # end tuple |
|
|
|
def get_valid_sub_key_derivation_string(election_name): |
|
print("If you provided the election officer a sub-key derivation string " |
|
"add it here. Otherwise, leave this blank (hit enter) or " |
|
"type default") |
|
print() |
|
|
|
rekey_prefix = REKEY_PREFIX + election_name + ' ' |
|
print("""accepted input formats for non-default are: |
|
%sYOUR_DERIVATION_STRING |
|
or |
|
YOUR_DERIVATION_STRING |
|
""" % rekey_prefix) |
|
|
|
while True: |
|
sub_key_derivation_string = input( |
|
"sub-key derivation string > ").strip() |
|
|
|
if ( sub_key_derivation_string.startswith(rekey_prefix) and |
|
len(sub_key_derivation_string) > len(rekey_prefix) ): |
|
return sub_key_derivation_string[ len(rekey_prefix): ] |
|
elif sub_key_derivation_string in ("", "default", election_name): |
|
return election_name |
|
else: |
|
return sub_key_derivation_string |
|
|
|
def main(): |
|
election_name, candidates, vote_limit = get_election_choice() |
|
|
|
master_key_64 = get_valid_master_code() |
|
master_key_256_sha = hashlib.sha256(master_key_64) |
|
master_key_256 = master_key_256_sha.digest() |
|
|
|
subkey_derivation_string = get_valid_sub_key_derivation_string( |
|
election_name) |
|
print() |
|
|
|
if subkey_derivation_string == election_name: |
|
print("you picked the default sub-key derivation string") |
|
else: |
|
print("your custom sub-key derivation string was") |
|
print(subkey_derivation_string) |
|
|
|
rekey_msg = ( |
|
REKEY_PREFIX + election_name + ' ' + subkey_derivation_string) |
|
sub_key_hmac = hmac.new( |
|
master_key_256, rekey_msg.encode('ascii'), hashlib.sha256) |
|
sig_64 = sub_key_hmac.digest()[0:SUB_KEY_SIG_TRUNCATE] |
|
print("your signature when you provided this to the election officer " |
|
"was: ") |
|
print("rekeysig:" + base64.b64encode(sig_64).decode('ascii')) |
|
print() |
|
|
|
sub_key_sha256 = master_key_256_sha.copy() |
|
sub_key_sha256.update(subkey_derivation_string.encode('ascii')) |
|
sub_key = sub_key_sha256.digest() |
|
|
|
votes = get_valid_votes(candidates, vote_limit) |
|
|
|
print("your ballots are:") |
|
for vote in votes: |
|
code_signed_ballot = create_code_signed_ballot(sub_key, vote) |
|
print(code_signed_ballot) |
|
|
|
if __name__ == "__main__": |
|
main() |