2018-06-12 13:31:38 +01:00
from __future__ import absolute_import
2018-07-02 10:06:05 +01:00
2018-06-12 13:31:38 +01:00
import base64
2018-07-02 10:06:05 +01:00
import json
from os import urandom
2018-06-12 13:31:38 +01:00
from celery import task
2018-07-02 10:06:05 +01:00
from django . conf import settings
2018-07-13 17:19:35 +01:00
from allauthdemo . polls . models import AccessKey , Ballot , CombinedBallot , PartialBallotDecryption , EncryptedVote , CombinedEncryptedVote , VoteFragment
2018-07-02 10:06:05 +01:00
2018-07-11 14:25:36 +01:00
from . crypto_rpc import param , combpk , add_ciphers , get_tally
2018-07-02 10:06:05 +01: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 14:08:17 +01:00
This file was also updated by Vincent de Almeida and Ben Goldsworthy
2018-07-02 10:06:05 +01:00
'''
# Will store the result of the initial cal to param() from .cpp_calls
group_param = None
2018-06-12 13:31:38 +01:00
2018-07-11 14:25:36 +01:00
2018-07-07 09:52:47 +01: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 14:25:36 +01:00
email_trustees_dec - Will email trustees a link to begin decrypting the event
2018-07-07 09:52:47 +01:00
'''
def gen_access_key ( ) :
return base64 . urlsafe_b64encode ( urandom ( 16 ) ) . decode ( ' utf-8 ' )
2018-07-11 14:25:36 +01:00
2018-07-07 09:52:47 +01: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 14:25:36 +01: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 09:52:47 +01:00
url_base = " http:// " + settings . DOMAIN + " /event/ " + str ( event . pk ) + " /decrypt/?key= "
2018-07-11 14:25:36 +01:00
email_body_base + = url_base
sign_off = get_email_sign_off ( )
2018-07-07 09:52:47 +01: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 14:25:36 +01:00
email_body = str ( email_body_base + key )
email_body + = sign_off
trustee . send_email ( email_subject , email_body )
2018-07-13 17:19:35 +01:00
2018-07-11 14:25:36 +01:00
def get_email_sign_off ( ) :
sign_off = str ( " " )
sign_off + = " \n \n Please 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 17:19:35 +01:00
2018-09-03 15:39:42 +01: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 14:25:36 +01: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 17:19:35 +01:00
enc_vote = ballot . comb_encrypted_vote . get ( )
2018-07-11 14:25:36 +01: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 15:39:42 +01: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 14:25:36 +01:00
CombinedBallot . objects . create ( poll = poll ,
option = option ,
cipher_text_c1 = combined_cipher [ ' C1 ' ] ,
cipher_text_c2 = combined_cipher [ ' C2 ' ] )
2018-06-12 13:31:38 +01:00
2018-07-13 17:19:35 +01: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 13:31:38 +01:00
@task ( )
2018-07-07 09:52:47 +01: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 14:25:36 +01:00
2018-07-07 09:52:47 +01:00
@task ( )
def create_ballots_for_poll ( poll ) :
2018-06-12 13:31:38 +01:00
for voter in poll . event . voters . all ( ) :
ballot = poll . ballots . create ( voter = voter , poll = poll )
2018-07-02 10:06:05 +01:00
'''
Emails an event preparation URL containing an access key for all of the trustees for an event
'''
2018-06-12 13:31:38 +01:00
@task ( )
2018-07-02 10:06:05 +01: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 14:25:36 +01: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 10:06:05 +01:00
url_base = " http:// " + settings . DOMAIN + " /event/ " + str ( event . pk ) + " /prepare/?key= "
2018-07-11 14:25:36 +01:00
email_body_base + = url_base
sign_off = get_email_sign_off ( )
2018-07-02 10:06:05 +01: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 14:25:36 +01:00
email_body = str ( email_body_base + key )
email_body + = sign_off
trustee . send_email ( email_subject , email_body )
2018-07-02 10:06:05 +01:00
2018-09-03 15:39:42 +01: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 13:31:38 +01:00
'''
2018-07-07 09:52:47 +01:00
Emails a URL containing an access key for all of the voters for an event
2018-07-02 10:06:05 +01:00
'''
@task ( )
2018-07-07 09:52:47 +01:00
def email_voters_vote_url ( voters , event ) :
2018-07-02 10:06:05 +01:00
email_subject = " Voting Access for Event ' " + event . title + " ' "
2018-07-07 09:52:47 +01:00
# Plain text email - this could be replaced for a HTML-based email in the future
2018-07-11 14:25:36 +01: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 \n You 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 13:31:38 +01:00
2018-07-02 10:06:05 +01: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 13:31:38 +01:00
2018-07-07 09:52:47 +01: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 14:25:36 +01:00
email_body + = sign_off
2018-07-07 09:52:47 +01:00
voter . send_email ( email_subject , email_body )
2018-07-02 10:06:05 +01:00
2018-07-11 14:25:36 +01:00
2018-08-31 19:39:12 +01: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 10:06:05 +01: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 13:31:38 +01:00
'''
@task ( )
2018-07-02 10:06:05 +01: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 13:31:38 +01:00
event . save ( )
2018-07-11 14:25:36 +01:00
2018-07-07 09:52:47 +01:00
@task ( )
def event_ended ( event ) :
2018-07-11 14:25:36 +01: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 09:52:47 +01:00
email_trustees_dec ( event )
2018-07-11 14:25:36 +01:00
@task ( )
def generate_combpk ( event ) :
pks = list ( )
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01:00
for tkey in event . trustee_keys . all ( ) :
pks . append ( str ( tkey . key ) )
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01:00
event . public_key = combpk ( pks )
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01:00
event . prepared = True
event . save ( )
2018-07-07 09:52:47 +01:00
@task ( )
2018-07-11 14:25:36 +01:00
def combine_decryptions_and_tally ( event ) :
2018-07-07 09:52:47 +01:00
polls = event . polls . all ( )
2018-07-11 14:25:36 +01:00
polls_count = len ( polls )
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01:00
for i in range ( polls_count ) :
2018-07-07 09:52:47 +01: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 14:25:36 +01:00
option = options [ j ]
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01: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 09:52:47 +01:00
2018-07-11 14:25:36 +01:00
ballot_cipher = { }
ballot_cipher [ ' C1 ' ] = combined_ballot . cipher_text_c1
ballot_cipher [ ' C2 ' ] = combined_ballot . cipher_text_c2
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01: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 09:52:47 +01:00
2018-07-11 14:25:36 +01:00
part_decs_text = list ( )
for part_dec in part_decs :
part_decs_text . append ( part_dec . text )
2018-07-07 09:52:47 +01:00
2018-07-11 14:25:36 +01: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 09:52:47 +01:00
if j != ( opt_count - 1 ) :
result + = " , "
result + = " ]} "
2018-07-11 14:25:36 +01:00
if i != ( polls_count - 1 ) :
2018-07-07 09:52:47 +01:00
result + = " , "
2018-07-11 14:25:36 +01:00
poll . result_json = result
2018-07-07 09:52:47 +01:00
poll . save ( )
2018-09-03 15:39:42 +01:00
# Email the list of organisers to inform them that the results for this event are ready
email_e_results_ready ( event )