2018-06-12 12:31:38 +00:00
|
|
|
from __future__ import absolute_import
|
2018-07-02 09:06:05 +00:00
|
|
|
|
2018-06-12 12:31:38 +00:00
|
|
|
import base64
|
2018-07-02 09:06:05 +00:00
|
|
|
import json
|
|
|
|
from os import urandom
|
2018-06-12 12:31:38 +00:00
|
|
|
from celery import task
|
2018-07-02 09:06:05 +00:00
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
2018-07-13 16:19:35 +00:00
|
|
|
from allauthdemo.polls.models import AccessKey, Ballot, CombinedBallot, PartialBallotDecryption, EncryptedVote, CombinedEncryptedVote, VoteFragment
|
2018-07-02 09:06:05 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
from .crypto_rpc import param, combpk, add_ciphers, get_tally
|
2018-07-02 09:06:05 +00:00
|
|
|
|
|
|
|
'''
|
|
|
|
Goal: This py file defines celery tasks that can be initiated
|
|
|
|
|
|
|
|
The following tasks were re-implemented by Thomas Smith: generate_event_param, tally_results, generate_combpk, generate_enc
|
|
|
|
|
2018-07-23 13:08:17 +00:00
|
|
|
This file was also updated by Vincent de Almeida and Ben Goldsworthy
|
2018-07-02 09:06:05 +00:00
|
|
|
'''
|
|
|
|
|
|
|
|
# Will store the result of the initial cal to param() from .cpp_calls
|
|
|
|
group_param = None
|
2018-06-12 12:31:38 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
|
2018-07-07 08:52:47 +00:00
|
|
|
'''
|
|
|
|
Helper functions
|
|
|
|
|
|
|
|
gen_access_key - Will generate an a key for accessing either the event preparation page, voting page and decryption page
|
2018-07-11 13:25:36 +00:00
|
|
|
email_trustees_dec - Will email trustees a link to begin decrypting the event
|
|
|
|
|
2018-07-07 08:52:47 +00:00
|
|
|
'''
|
|
|
|
def gen_access_key():
|
|
|
|
return base64.urlsafe_b64encode(urandom(16)).decode('utf-8')
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
|
2018-07-07 08:52:47 +00:00
|
|
|
def email_trustees_dec(event):
|
|
|
|
email_subject = "Event Ballot Decryption for '" + event.title + "'"
|
|
|
|
|
|
|
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body_base = str("")
|
|
|
|
email_body_base += "Dear Trustee,\n\n"
|
|
|
|
email_body_base += "You're now required to decrypt the event: " + event.title + \
|
|
|
|
". This will require uploading your secret key that you have previously backed up.\n\n"
|
|
|
|
email_body_base += "Please visit the following URL to submit your trustee secret key to begin event decryption:\n\n"
|
2018-07-07 08:52:47 +00:00
|
|
|
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/decrypt/?key="
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body_base += url_base
|
|
|
|
|
|
|
|
sign_off = get_email_sign_off()
|
2018-07-07 08:52:47 +00:00
|
|
|
|
|
|
|
for trustee in event.users_trustees.all():
|
|
|
|
# Generate a key and create an AccessKey object
|
|
|
|
key = gen_access_key()
|
|
|
|
AccessKey.objects.create(user=trustee, event=event, key=key)
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body = str(email_body_base + key)
|
|
|
|
email_body += sign_off
|
|
|
|
|
|
|
|
trustee.send_email(email_subject, email_body)
|
|
|
|
|
2018-07-13 16:19:35 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
def get_email_sign_off():
|
|
|
|
sign_off = str("")
|
|
|
|
sign_off += "\n\nPlease note: This email address is not monitored so please don't reply to this email.\n\n"
|
|
|
|
sign_off += "Kind Regards,\n"
|
|
|
|
sign_off += "DEMOS 2 Admin - Lancaster University"
|
|
|
|
|
|
|
|
return sign_off
|
|
|
|
|
2018-07-13 16:19:35 +00:00
|
|
|
|
2018-09-03 14:39:42 +00:00
|
|
|
def email_e_results_ready(event):
|
|
|
|
event_title = event.title
|
|
|
|
email_subject = "Event Results for '" + event_title + "' Have Been Decrypted"
|
|
|
|
|
|
|
|
# Construct the email body. This can be later replaced by a HTML email.
|
|
|
|
email_body = str("")
|
|
|
|
email_body += "Dear Event Organiser,\n\n"
|
|
|
|
email_body += "This email is to inform you that all of the partial decryptions for the event '" + event_title + \
|
|
|
|
"' have been supplied. These have been used to decrypt the results which are now available.\n\n"
|
|
|
|
email_body += "As an organiser, the event results can be found at the following URL:\n\n"
|
|
|
|
email_body += "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/results/"
|
|
|
|
email_body += get_email_sign_off()
|
|
|
|
|
|
|
|
# Get all of the organisers for the event and send them an email
|
|
|
|
organisers = event.users_organisers.all()
|
|
|
|
|
|
|
|
for organiser in organisers:
|
|
|
|
organiser.email_user(email_subject, email_body)
|
|
|
|
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
'''
|
|
|
|
Combines all of the voter ballots for a poll option into a single 'CombinedBallot'
|
|
|
|
'''
|
|
|
|
def combine_ballots(polls):
|
|
|
|
for poll in polls:
|
|
|
|
options = poll.options.all()
|
|
|
|
opt_count = len(options)
|
|
|
|
ballots = Ballot.objects.filter(poll=poll)
|
|
|
|
|
|
|
|
for i in range(opt_count):
|
|
|
|
option = options[i]
|
|
|
|
|
|
|
|
# Collect all fragments for this opt
|
|
|
|
frags_c1 = list()
|
|
|
|
frags_c2 = list()
|
|
|
|
|
|
|
|
for ballot in ballots:
|
2018-07-13 16:19:35 +00:00
|
|
|
enc_vote = ballot.comb_encrypted_vote.get()
|
2018-07-11 13:25:36 +00:00
|
|
|
|
|
|
|
if enc_vote is not None:
|
|
|
|
fragments = enc_vote.fragment.all()
|
|
|
|
frags_c1.append(fragments[i].cipher_text_c1)
|
|
|
|
frags_c2.append(fragments[i].cipher_text_c2)
|
|
|
|
|
|
|
|
ciphers = {
|
|
|
|
'c1s': frags_c1,
|
|
|
|
'c2s': frags_c2
|
|
|
|
}
|
|
|
|
|
|
|
|
combined_cipher = add_ciphers(ciphers)
|
|
|
|
|
2018-09-03 14:39:42 +00:00
|
|
|
# If a combined ballot already exists, clear it
|
|
|
|
if CombinedBallot.objects.filter(poll=poll, option=option).exists():
|
|
|
|
CombinedBallot.objects.filter(poll=poll, option=option).delete()
|
|
|
|
|
|
|
|
# Create a combined ballot for this option in the poll
|
2018-07-11 13:25:36 +00:00
|
|
|
CombinedBallot.objects.create(poll=poll,
|
|
|
|
option=option,
|
|
|
|
cipher_text_c1=combined_cipher['C1'],
|
|
|
|
cipher_text_c2=combined_cipher['C2'])
|
2018-06-12 12:31:38 +00:00
|
|
|
|
2018-07-13 16:19:35 +00:00
|
|
|
@task()
|
|
|
|
def combine_encrypted_votes(voter, poll):
|
|
|
|
poll_options_count = poll.options.all().count()
|
|
|
|
ballot = Ballot.objects.get_or_create(voter=voter, poll=poll)[0]
|
|
|
|
e_votes = EncryptedVote.objects.filter(ballot=ballot)
|
|
|
|
|
|
|
|
CombinedEncryptedVote.objects.filter(ballot=ballot).delete()
|
|
|
|
comb_e_vote = CombinedEncryptedVote.objects.create(ballot=ballot)
|
|
|
|
|
|
|
|
for i in range(poll_options_count):
|
|
|
|
frags_c1 = list()
|
|
|
|
frags_c2 = list()
|
|
|
|
|
|
|
|
for e_vote in e_votes:
|
|
|
|
fragments = e_vote.fragment.all()
|
|
|
|
frags_c1.append(fragments[i].cipher_text_c1)
|
|
|
|
frags_c2.append(fragments[i].cipher_text_c2)
|
|
|
|
|
|
|
|
ciphers = {
|
|
|
|
'c1s': frags_c1,
|
|
|
|
'c2s': frags_c2
|
|
|
|
}
|
|
|
|
|
|
|
|
combined_cipher = add_ciphers(ciphers)
|
|
|
|
VoteFragment.objects.create(comb_encrypted_vote=comb_e_vote,
|
|
|
|
cipher_text_c1=combined_cipher['C1'],
|
|
|
|
cipher_text_c2=combined_cipher['C2'])
|
|
|
|
|
2018-06-12 12:31:38 +00:00
|
|
|
@task()
|
2018-07-07 08:52:47 +00:00
|
|
|
def create_ballots(event):
|
|
|
|
voters = event.voters.all()
|
|
|
|
|
|
|
|
for poll in event.polls.all():
|
|
|
|
for voter in voters:
|
|
|
|
ballot = poll.ballots.create(voter=voter, poll=poll)
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
|
2018-07-07 08:52:47 +00:00
|
|
|
@task()
|
|
|
|
def create_ballots_for_poll(poll):
|
2018-06-12 12:31:38 +00:00
|
|
|
for voter in poll.event.voters.all():
|
|
|
|
ballot = poll.ballots.create(voter=voter, poll=poll)
|
|
|
|
|
2018-07-02 09:06:05 +00:00
|
|
|
|
|
|
|
'''
|
|
|
|
Emails an event preparation URL containing an access key for all of the trustees for an event
|
|
|
|
'''
|
2018-06-12 12:31:38 +00:00
|
|
|
@task()
|
2018-07-02 09:06:05 +00:00
|
|
|
def email_trustees_prep(trustees, event):
|
|
|
|
email_subject = "Key Generation and Preparation for Event '" + event.title + "'"
|
|
|
|
|
|
|
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body_base = str("")
|
|
|
|
email_body_base += "Dear Trustee,\n\n"
|
|
|
|
email_body_base += "You have been enrolled as a trustee onto the event: " + event.title + \
|
|
|
|
". You are required to visit the URL below to generate your secret key and associated public" \
|
|
|
|
" key that will be used to encrypt the event.\n\n You will need to ensure that you back up" \
|
|
|
|
" your secret key as this will be needed to decrypt the event - please don't lose this as it" \
|
|
|
|
" cannot be re-generated. DEMOS2 will never and cannot store your secret key.\n\n"
|
|
|
|
email_body_base += "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n"
|
2018-07-02 09:06:05 +00:00
|
|
|
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/prepare/?key="
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body_base += url_base
|
|
|
|
|
|
|
|
sign_off = get_email_sign_off()
|
2018-07-02 09:06:05 +00:00
|
|
|
|
|
|
|
for trustee in trustees:
|
|
|
|
# Generate a key and create an AccessKey object
|
|
|
|
key = gen_access_key()
|
|
|
|
AccessKey.objects.create(user=trustee, event=event, key=key)
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body = str(email_body_base + key)
|
|
|
|
email_body += sign_off
|
|
|
|
|
|
|
|
trustee.send_email(email_subject, email_body)
|
|
|
|
|
2018-07-02 09:06:05 +00:00
|
|
|
|
2018-09-03 14:39:42 +00:00
|
|
|
'''
|
|
|
|
Task triggered when all trustees have supplied their partial public keys and the event has been prepared
|
|
|
|
'''
|
|
|
|
@task()
|
|
|
|
def email_organisers_next_steps(event):
|
|
|
|
event_title = event.title
|
|
|
|
email_subject = "Event '" + event_title + "' Successfully Prepared by All Trustees"
|
|
|
|
|
|
|
|
email_body = str("")
|
|
|
|
email_body += "Dear Event Organiser,\n\n"
|
|
|
|
email_body += "This email is to inform you that all trustees have supplied their public keys for the event '" \
|
|
|
|
+ event_title + "'. The event is therefore prepared and ready to accept votes when voting opens.\n\n"
|
|
|
|
email_body += "Once voting has ended, you need to visit the following URL to begin the decryption process:\n\n"
|
|
|
|
email_body += "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/\n\n"
|
|
|
|
email_body += "Once you've accessed the page, simply hit the 'End' button to email all trustees to ask for their " \
|
|
|
|
"partial decryptions for all polls for this event."
|
|
|
|
email_body += get_email_sign_off()
|
|
|
|
|
|
|
|
# Get the list of organisers that need emailing and email them
|
|
|
|
organisers = event.users_organisers.all()
|
|
|
|
|
|
|
|
for organiser in organisers:
|
|
|
|
organiser.email_user(email_subject, email_body)
|
|
|
|
|
2018-06-12 12:31:38 +00:00
|
|
|
'''
|
2018-07-07 08:52:47 +00:00
|
|
|
Emails a URL containing an access key for all of the voters for an event
|
2018-07-02 09:06:05 +00:00
|
|
|
'''
|
|
|
|
@task()
|
2018-07-07 08:52:47 +00:00
|
|
|
def email_voters_vote_url(voters, event):
|
2018-07-02 09:06:05 +00:00
|
|
|
email_subject = "Voting Access for Event '" + event.title + "'"
|
2018-07-07 08:52:47 +00:00
|
|
|
|
|
|
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
2018-07-11 13:25:36 +00:00
|
|
|
# TODO: The URL needs updating and it could be replaced with a single UUID that's unique
|
|
|
|
# TODO: for the voter for an event which would shorten the URL
|
|
|
|
email_body_base = str("")
|
|
|
|
email_body_base += "Dear Voter,\n\n"
|
|
|
|
email_body_base += "You have been enrolled as a voter onto the event: " + event.title + ".\n\nYou can vote between the following dates and times:\n"
|
|
|
|
email_body_base += "Start: " + event.start_time_formatted_utc() + "\n"
|
|
|
|
email_body_base += "End: " + event.end_time_formatted_utc() + "\n\n"
|
|
|
|
email_body_base += "Please visit the following URL in order to vote on the event where further instructions can be found on the page:\n\n"
|
|
|
|
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/poll/" + str(event.polls.all()[0].uuid) + "/vote/?key="
|
|
|
|
email_body_base += url_base
|
|
|
|
|
|
|
|
sign_off = get_email_sign_off()
|
2018-06-12 12:31:38 +00:00
|
|
|
|
2018-07-02 09:06:05 +00:00
|
|
|
for voter in voters:
|
|
|
|
# Generate a key and create an AccessKey object
|
|
|
|
key = gen_access_key()
|
|
|
|
AccessKey.objects.create(user=voter, event=event, key=key)
|
2018-06-12 12:31:38 +00:00
|
|
|
|
2018-07-07 08:52:47 +00:00
|
|
|
# Update the email body to incl the access key as well as the duration information
|
|
|
|
email_body = str(email_body_base + key)
|
2018-07-11 13:25:36 +00:00
|
|
|
email_body += sign_off
|
2018-07-07 08:52:47 +00:00
|
|
|
|
|
|
|
voter.send_email(email_subject, email_body)
|
2018-07-02 09:06:05 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
|
2018-08-31 18:39:12 +00:00
|
|
|
@task()
|
|
|
|
def email_voting_success(voter, ballotHandle, eventTitle):
|
|
|
|
email_subject = "Vote(s) received for Event '" + eventTitle + "'"
|
|
|
|
|
|
|
|
# Plain text email - this could be replaced for a HTML-based email in the future
|
|
|
|
email_body_base = str("")
|
|
|
|
email_body_base += "Dear Voter,\n\n"
|
|
|
|
email_body_base += "Thank you for your vote(s) for the event: " + eventTitle + ". This has been securely encrypted "
|
|
|
|
email_body_base += "and anonymously stored in our system.\n\n"
|
|
|
|
email_body_base += "For your reference, the identifier for your selected ballot is:\n"
|
|
|
|
email_body_base += ballotHandle
|
|
|
|
email_body_base += get_email_sign_off()
|
|
|
|
|
|
|
|
voter.send_email(email_subject, email_body_base)
|
|
|
|
|
|
|
|
|
2018-07-02 09:06:05 +00:00
|
|
|
'''
|
|
|
|
Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param())
|
2018-06-12 12:31:38 +00:00
|
|
|
'''
|
|
|
|
@task()
|
2018-07-02 09:06:05 +00:00
|
|
|
def update_EID(event):
|
|
|
|
global group_param
|
|
|
|
if group_param is None:
|
|
|
|
group_param = param()
|
|
|
|
|
|
|
|
EID = {}
|
|
|
|
EID['hr'] = event.EID
|
|
|
|
EID['crypto'] = group_param
|
|
|
|
event.EID = json.dumps(EID)
|
2018-06-12 12:31:38 +00:00
|
|
|
event.save()
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
|
2018-07-07 08:52:47 +00:00
|
|
|
@task()
|
|
|
|
def event_ended(event):
|
2018-07-11 13:25:36 +00:00
|
|
|
# Combine all the ballots for every option in every poll which will be decrypted by the trustees
|
|
|
|
polls = event.polls.all()
|
|
|
|
combine_ballots(polls)
|
|
|
|
|
|
|
|
# Email all trustees to request their partial decryptions using their secret keys
|
2018-07-07 08:52:47 +00:00
|
|
|
email_trustees_dec(event)
|
|
|
|
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
@task()
|
|
|
|
def generate_combpk(event):
|
|
|
|
pks = list()
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
for tkey in event.trustee_keys.all():
|
|
|
|
pks.append(str(tkey.key))
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
event.public_key = combpk(pks)
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
event.prepared = True
|
|
|
|
event.save()
|
2018-07-07 08:52:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
@task()
|
2018-07-11 13:25:36 +00:00
|
|
|
def combine_decryptions_and_tally(event):
|
2018-07-07 08:52:47 +00:00
|
|
|
polls = event.polls.all()
|
2018-07-11 13:25:36 +00:00
|
|
|
polls_count = len(polls)
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
for i in range(polls_count):
|
2018-07-07 08:52:47 +00:00
|
|
|
poll = polls[i]
|
|
|
|
result = str("")
|
|
|
|
result += "{\"name\": \"" + poll.question_text + "\","
|
|
|
|
|
|
|
|
options = poll.options.all()
|
|
|
|
opt_count = len(options)
|
|
|
|
result += "\"options\": ["
|
|
|
|
for j in range(opt_count):
|
2018-07-11 13:25:36 +00:00
|
|
|
option = options[j]
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
# Find the combined ballot for the current option of the current poll
|
|
|
|
# and then extract the C1 and C2 components of the cipher that contains the tally
|
|
|
|
combined_ballot = CombinedBallot.objects.filter(poll=poll,
|
|
|
|
option=option)[0]
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
ballot_cipher = {}
|
|
|
|
ballot_cipher['C1'] = combined_ballot.cipher_text_c1
|
|
|
|
ballot_cipher['C2'] = combined_ballot.cipher_text_c2
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
# Collect all the partial decryptions for the ballot cipher which will decrypt the result
|
|
|
|
part_decs = PartialBallotDecryption.objects.filter(event=event,
|
|
|
|
poll=poll,
|
|
|
|
option=option)
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
part_decs_text = list()
|
|
|
|
for part_dec in part_decs:
|
|
|
|
part_decs_text.append(part_dec.text)
|
2018-07-07 08:52:47 +00:00
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
# Get the vote tally for this option and add it to the results
|
|
|
|
voters_count = event.voters.all().count()
|
|
|
|
votes = get_tally(ballot_cipher, part_decs_text, event.EID, voters_count)
|
|
|
|
result += "{\"option\": \"" + str(option.choice_text) + "\", \"votes\": \"" + str(votes) + "\"}"
|
2018-07-07 08:52:47 +00:00
|
|
|
|
|
|
|
if j != (opt_count-1):
|
|
|
|
result += ","
|
|
|
|
|
|
|
|
result += "]}"
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
if i != (polls_count - 1):
|
2018-07-07 08:52:47 +00:00
|
|
|
result += ","
|
|
|
|
|
2018-07-11 13:25:36 +00:00
|
|
|
poll.result_json = result
|
2018-07-07 08:52:47 +00:00
|
|
|
poll.save()
|
|
|
|
|
2018-09-03 14:39:42 +00:00
|
|
|
# Email the list of organisers to inform them that the results for this event are ready
|
|
|
|
email_e_results_ready(event)
|
|
|
|
|