From 5b746ad40654a19fc7e8a51cf74d4aa94a4de214 Mon Sep 17 00:00:00 2001 From: vince0656 Date: Wed, 11 Jul 2018 14:25:36 +0100 Subject: [PATCH] Full end-to-end voting is working using the new binary encoding scheme, ballot combination and trustee partial decryption with tallies working perfectly. This required updating the Node server as well as Django models and views to support this. Emails to voters and trustees have also been updated to be more informative and look more professional. It could probably do at this point with using email templates and in the future HTML emails. --- Node/index.js | 293 +++++++----------- allauthdemo/polls/admin.py | 2 +- allauthdemo/polls/crypto_rpc.py | 57 +--- allauthdemo/polls/forms.py | 2 +- allauthdemo/polls/models.py | 107 ++++--- allauthdemo/polls/tasks.py | 259 +++++++++------- allauthdemo/polls/urls.py | 26 +- allauthdemo/polls/views.py | 159 +++++++--- .../templates/bases/bootstrap-jquery.html | 63 +++- .../templates/polls/event_decrypt.html | 48 ++- .../templates/polls/event_detail_base.html | 14 +- .../templates/polls/event_detail_polls.html | 4 +- allauthdemo/templates/polls/event_list.html | 8 +- allauthdemo/templates/polls/event_vote.html | 144 ++++----- 14 files changed, 631 insertions(+), 555 deletions(-) diff --git a/Node/index.js b/Node/index.js index e7e82ed..f8f6945 100755 --- a/Node/index.js +++ b/Node/index.js @@ -40,7 +40,7 @@ app.get('/', function(request, response){ app.get('/param', function(request, response){ var param = gpGen(); - console.log('Generated Param:' + param); + console.log('Generated Group Param'); response.json(param); }); @@ -98,164 +98,84 @@ app.post('/cmpkstring', function(request, response){ //addition function on homomorphically encrypted variables //this may need some work, different method of serialisation maybe? -app.get('/addec', function(request, response){ - var c1 = request.query['C1']; - var c2 = request.query['C2']; - var number = request.query['number']; //number of ciphertexts to add - //all the list of ciphertext objects to give to the function - var parsed = []; +app.post('/add_ciphers', function(request, response){ + console.log("\nEndpoint /add_ciphers called"); + const C1s = request.body.ciphers.c1s; + const C2s = request.body.ciphers.c2s; + const CIPHER_COUNT = C1s.length; + // Will store a list of parsed ciphers from the C1s and C2s arrays passed in + var parsedCiphers = []; var ctx = new CTX("BN254CX"); - console.log('Addec:'); - if(number == c1.length) + if(CIPHER_COUNT > 1) { - for (var i = 0; i < c1.length; i++) { - console.log(i + ".C1: " + c1[i]); - var c1Bytes = Buffer.from(c1[i].split(','), 'hex'); + console.log("Combining " + CIPHER_COUNT + " ciphers"); + + for (var i = 0; i < CIPHER_COUNT; i++) { + + var c1Bytes = Buffer.from(C1s[i].split(','), 'hex'); var newC1 = new ctx.ECP.fromBytes(c1Bytes); + + var c2Bytes = Buffer.from(C2s[i].split(','), 'hex'); + var newC2 = new ctx.ECP.fromBytes(c2Bytes); - var cipher = - { - C1:newC1, - C2:null + var cipher = { + C1 : newC1, + C2 : newC2 }; - parsed.push(cipher); - + + parsedCiphers.push(cipher); } - for (var j = 0; j < c2.length; j++) { - console.log(j + ".C2: " + c2[j]); - var c2Bytes = Buffer.from(c2[j].split(','), 'hex'); - var newC2 = new ctx.ECP.fromBytes(c2Bytes); - - parsed[j].C2 = newC2; - } - } + } else if(CIPHER_COUNT === 1) { + console.log("Combining only one cipher"); - else if(number == 1) - { - console.log("only one cipher"); - var c1Bytes = Buffer.from(c1.split(','), 'hex'); + var c1Bytes = Buffer.from(C1s[0].split(','), 'hex'); var newC1 = new ctx.ECP.fromBytes(c1Bytes); - console.log("C1: " + c1); - var c2Bytes = Buffer.from(c2.split(','), 'hex'); + + + var c2Bytes = Buffer.from(C2s[0].split(','), 'hex'); var newC2 = new ctx.ECP.fromBytes(c2Bytes); - console.log("C2: " + c2); var cipher = { - C1:newC1, - C2:newC2 - }; - parsed.push(cipher); - } - - - response.json(add(parsed)); -}); - - -//tally partially decrypted ciphertexts -app.get('/tally', function(request, response){ - console.log("\nEndpoint /tally called"); - var amount = request.query['number'];//number of decryptions taking in - var paramString = request.query['param'];//event group parameter in JSON - var partialsStrings = request.query['decs'];//array of partial decryption(s) in bytes - var ciphertextString = request.query['cipher'];//ciphertext being decrypted in JSON - - //re-build parameters - var tempParams = JSON.parse(JSON.parse(paramString).crypto); - var ctx = new CTX("BN254CX"); //new context we can use - var n = new ctx.BIG(); - var g1 = new ctx.ECP(); - var g2 = new ctx.ECP2(); - - //copying the values - n.copy(tempParams.n); - g1.copy(tempParams.g1); - g2.copy(tempParams.g2); - - var params = { - n:n, - g1:g1, - g2:g2 - }; - - //re-build partial decryptions - var partials = []; - if(amount == partialsStrings.length) - { - console.log(amount + " partial decryptions"); - for(var i = 0; i < partialsStrings.length; i++) - { - var bytes = Buffer.from(partialsStrings[i].split(','), 'hex'); - - var dec = { - D:new ctx.ECP.fromBytes(bytes) - }; - - partials.push(dec); - } - } - else if(amount == 1) - { - console.log("\nOnly one partial decryption received\n"); - console.log(JSON.parse(paramString).crypto + "\n"); - - var bytes = Buffer.from(partialsStrings.split(','), 'hex'); - var dec = { - D : new ctx.ECP.fromBytes(bytes) + C1 : newC1, + C2 : newC2 }; - partials.push(dec); + parsedCiphers.push(cipher); } - //re-build combined ciphertext - var tempCipher = JSON.parse(ciphertextString); + // Combine the ciphers here + var combinedCipher = add(parsedCiphers); - var cipher = { - C1: new ctx.ECP(), - C2: new ctx.ECP() + // Get the byte string of the C1 and C2 part for transmission + var C1Bytes = []; + combinedCipher.C1.toBytes(C1Bytes); + + var C2Bytes = []; + combinedCipher.C2.toBytes(C2Bytes); + + var responseData = { + C1: C1Bytes.toString(), + C2: C2Bytes.toString() }; - cipher.C1.copy(tempCipher.C1); - cipher.C2.copy(tempCipher.C2); - - response.json(tally(params, partials, cipher)) -}); - -app.post('/comb_sks', function(request, response){ - console.log("\nEndpoint /comb_sks called"); - const SKsAsStrings = request.body.SKs; - - // Parse and combine the secret keys - var ctx = new CTX("BN254CX"); - var parsedSKs = []; - - for(var i = 0; i < SKsAsStrings.length; i++) { - var skBytes = SKsAsStrings[i].split(","); - parsedSKs.push(new ctx.BIG.fromBytes(skBytes)); - } - - console.log("Combining " + parsedSKs.length + " SKs..."); - var SKBytes = []; - combine_sks(parsedSKs).SK.toBytes(SKBytes); - - response.send(SKBytes.toString()); + response.json(responseData); }); app.post('/get_tally', function(request, response){ - const COUNT = request.body.count; + console.log("\nEndpoint /get_tally called"); + + // Extract the data from the request const TEMP_PARAMS = JSON.parse(JSON.parse(request.body.param).crypto); - const C1s = request.body.ciphers.c1s; - const C2s = request.body.ciphers.c2s; - const SK = request.body.sk; + const BALLOT_CIPHER = request.body.ballot_cipher; + const PART_DECS = request.body.part_decs; + const VOTERS_COUNT = request.body.voters_count; - console.log("\nFrom /get_tally - C1 array length (num of voters for the opt): " + C1s.length); - - //re-build parameters - var ctx = new CTX("BN254CX"); //new context we can use + // Re-build parameters + var ctx = new CTX("BN254CX"); var n = new ctx.BIG(); var g1 = new ctx.ECP(); var g2 = new ctx.ECP2(); @@ -265,31 +185,39 @@ app.post('/get_tally', function(request, response){ g2.copy(TEMP_PARAMS.g2); var params = { - n:n, - g1:g1, - g2:g2 + n : n, + g1 : g1, + g2 : g2 }; - //rebuild our secret key - var skBytes = SK.split(","); - var sk = new ctx.BIG.fromBytes(skBytes); + // Initialise the ballot cipher + var c1Bytes = Buffer.from(BALLOT_CIPHER.C1.split(','), 'hex'); + var newC1 = new ctx.ECP.fromBytes(c1Bytes); - var tally = 0; + var c2Bytes = Buffer.from(BALLOT_CIPHER.C2.split(','), 'hex'); + var newC2 = new ctx.ECP.fromBytes(c2Bytes); - for(var i = 0; i < COUNT; i++) { - var c1Bytes = Buffer.from(C1s[i].split(','), 'hex'); - var newC1 = new ctx.ECP.fromBytes(c1Bytes); + var cipher = + { + C1 : newC1, + C2 : newC2 + }; - var c2Bytes = Buffer.from(C2s[i].split(','), 'hex'); - var newC2 = new ctx.ECP.fromBytes(c2Bytes); + // Initialise all of the partial decryptions + var partials = []; + for(var i = 0; i < PART_DECS.length; i++) + { + var bytes = Buffer.from(PART_DECS[i].split(','), 'hex'); - var cipher = {C1: newC1, C2: newC2}; - tally += decrypt(params, sk, cipher).M; + var dec = { + D : new ctx.ECP.fromBytes(bytes) + }; + + partials.push(dec); } - console.log("Tally: " + tally + "\n"); - - response.send("" + tally); + // Send the decrypted cipher value (vote tally for an option) + response.send("" + getCipherVal(params, partials, cipher, VOTERS_COUNT).M); }); var server = app.listen(port, function(){ @@ -311,7 +239,7 @@ https://github.com/milagro-crypto/milagro-crypto-js //Group parameter generator: returns rng object and generators g1,g2 for G1,G2 as well as order -gpGen = function(){ +gpGen = function() { //init, and base generators var ctx = new CTX("BN254CX"); @@ -348,7 +276,7 @@ gpGen = function(){ //creates ElGamal public and secret key -keyGen=function(params){ +keyGen = function(params) { var ctx = new CTX("BN254CX"); //set rng var RAW = []; @@ -376,7 +304,7 @@ keyGen=function(params){ //combine multiple public key together //the input is an array of PKs -combine_pks=function(PKs){ +combine_pks = function(PKs) { var ctx = new CTX("BN254CX"); var pk=new ctx.ECP(); //copy the first pk @@ -393,7 +321,7 @@ combine_pks=function(PKs){ // Written by Vincent de Almeida: Combines multiple secret keys together // The SKs in the SKs array should already have been initialised using 'new ctx.BIG.fromBytes()' -combine_sks=function(SKs) { +combine_sks = function(SKs) { // 'add' the rest of the sks to the first var sk = SKs[0]; @@ -407,7 +335,7 @@ combine_sks=function(SKs) { }; //ElGamal encryption -encrypt=function(params,PK, m){ +encrypt = function(params,PK, m) { var ctx = new CTX("BN254CX"); //set rand var RAW = []; @@ -441,31 +369,30 @@ encrypt=function(params,PK, m){ //add ciphertexts -add=function(Ciphers){ +add = function(Ciphers) { var ctx = new CTX("BN254CX"); - var s1=new ctx.ECP(); - var s2=new ctx.ECP(); + var s1 = new ctx.ECP(); + var s2 = new ctx.ECP(); + //copy the first cipher s1.copy(Ciphers[0].C1); s2.copy(Ciphers[0].C2); + //multiple the rest ciphertexts - for(i=1;i= self.start_time and present <= self.end_time and self.public_key is not None: + elif present >= self.start_time and present <= self.end_time and self.prepared is True: status_str = "Active" - elif present > self.end_time and self.public_key is not None: + elif present >= self.start_time and present <= self.end_time and self.prepared is False: + status_str = "Future" + elif present > self.end_time: status_str = "Expired" else: - if self.event_sk.all().count() == 1: + if self.all_part_decs_received(): status_str = "Decrypted" - elif self.event_sk.all().count() == 0: + else: status_str = "Ended" return status_str @@ -109,13 +147,12 @@ class TrusteeKey(models.Model): user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_keys") key = models.CharField(max_length=255, unique=True) + class AccessKey(models.Model): event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="keys") user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="keys") key = models.CharField(max_length=255, unique=True) - #total = models.IntegerField(blank=True, null=True, default=0) - def has_started(self): return timezone.now() >= self.start @@ -125,60 +162,60 @@ class AccessKey(models.Model): def __unicode__(self): return self.title + class Poll(models.Model): question_text = models.CharField(max_length=200) total_votes = models.IntegerField(default=0) min_num_selections = models.IntegerField(default=0) max_num_selections = models.IntegerField(default=1) event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="polls") - enc = models.CharField(max_length=4096, null=True) - - #index = models.IntegerField() + combined_ballots = models.CharField(max_length=4096, null=True) + result_json = models.CharField(max_length=4096, null=True) + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) def __str__(self): return self.question_text + class PollOption(models.Model): choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) question = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="options") - #index = models.IntegerField() def __str__(self): return self.choice_text -class Decryption(models.Model): - event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="decryptions") - poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="decryptions") - user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="decryptions") - text = models.CharField(max_length=1024) -class TrusteeSK(models.Model): - event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="trustee_sk") - trustee = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_sk") - key = models.CharField(max_length=1024) +class CombinedBallot(models.Model): + poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="combined_ballot") + option = models.ForeignKey(PollOption, on_delete=models.CASCADE, related_name="combined_ballot") + cipher_text_c1 = models.CharField(max_length=4096) + cipher_text_c2 = models.CharField(max_length=4096) + + +# A partial decryption supplied by a trustee for a combined ballot that relates to a poll option +class PartialBallotDecryption(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="decryption") + poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="decryption") + option = models.ForeignKey(PollOption, on_delete=models.CASCADE, related_name="decryption") + user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="decryption") + text = models.CharField(max_length=4096) -class EventSK(models.Model): - event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="event_sk") - key = models.CharField(max_length=1024) class Ballot(models.Model): voter = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="ballots") poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="ballots") cast = models.BooleanField(default=False) + # Implements the new binary encoding scheme class EncryptedVote(models.Model): ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="encrypted_vote") + class VoteFragment(models.Model): encrypted_vote = models.ForeignKey(EncryptedVote, on_delete=models.CASCADE, related_name="fragment") cipher_text_c1 = models.CharField(max_length=4096) cipher_text_c2 = models.CharField(max_length=4096) -class Organiser(models.Model): - index = models.IntegerField(default=0) - email = models.CharField(max_length=100, blank=False, null=False) - event = models.ForeignKey(Event, on_delete=models.CASCADE) - diff --git a/allauthdemo/polls/tasks.py b/allauthdemo/polls/tasks.py index 6b433af..db65344 100755 --- a/allauthdemo/polls/tasks.py +++ b/allauthdemo/polls/tasks.py @@ -7,9 +7,9 @@ from celery import task from django.conf import settings -from allauthdemo.polls.models import AccessKey, Ballot, Decryption, TrusteeSK, EventSK +from allauthdemo.polls.models import AccessKey, Ballot, CombinedBallot, PartialBallotDecryption -from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks +from .crypto_rpc import param, combpk, add_ciphers, get_tally ''' Goal: This py file defines celery tasks that can be initiated @@ -22,28 +22,85 @@ from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks # Will store the result of the initial cal to param() from .cpp_calls group_param = None + ''' Helper functions gen_access_key - Will generate an a key for accessing either the event preparation page, voting page and decryption page + email_trustees_dec - Will email trustees a link to begin decrypting the event + ''' def gen_access_key(): return base64.urlsafe_b64encode(urandom(16)).decode('utf-8') + 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 - email_body = "Please visit the following URL to submit your trustee secret key to begin event decryption:\n\n" + 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" url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/decrypt/?key=" - email_body = email_body + url_base + email_body_base += url_base + + sign_off = get_email_sign_off() 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) - trustee.send_email(email_subject, email_body + key) + email_body = str(email_body_base + key) + email_body += sign_off + + trustee.send_email(email_subject, email_body) + +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 + +''' + 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: + enc_vote = ballot.encrypted_vote.get() + + 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) + + CombinedBallot.objects.create(poll=poll, + option=option, + cipher_text_c1=combined_cipher['C1'], + cipher_text_c2=combined_cipher['C2']) @task() def create_ballots(event): @@ -53,6 +110,7 @@ def create_ballots(event): for voter in voters: ballot = poll.ballots.create(voter=voter, poll=poll) + @task() def create_ballots_for_poll(poll): for voter in poll.event.voters.all(): @@ -67,16 +125,29 @@ 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 - email_body = "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n" + 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" url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/prepare/?key=" - email_body = email_body + url_base + email_body_base += url_base + + sign_off = get_email_sign_off() 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) - trustee.send_email(email_subject, email_body + key) + email_body = str(email_body_base + key) + email_body += sign_off + + trustee.send_email(email_subject, email_body) + ''' Emails a URL containing an access key for all of the voters for an event @@ -86,13 +157,18 @@ def email_voters_vote_url(voters, event): email_subject = "Voting Access for Event '" + event.title + "'" # Plain text email - this could be replaced for a HTML-based email in the future - email_body_base = "Please visit the following URL in order to vote on the event '" + event.title + "':\n\n" - url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/poll/1/vote/?key=" - email_body_base = email_body_base + url_base + # 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 - duration_info = "\n\nYou can vote between the following dates and times:\n" - duration_info = duration_info + "Start: " + event.start_time_formatted_utc() + "\n" - duration_info = duration_info + "End: " + event.end_time_formatted_utc() + sign_off = get_email_sign_off() for voter in voters: # Generate a key and create an AccessKey object @@ -101,10 +177,11 @@ def email_voters_vote_url(voters, event): # Update the email body to incl the access key as well as the duration information email_body = str(email_body_base + key) - email_body = email_body + duration_info + email_body += sign_off voter.send_email(email_subject, email_body) + ''' Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param()) ''' @@ -120,95 +197,16 @@ def update_EID(event): event.EID = json.dumps(EID) event.save() + @task() def event_ended(event): - # Email all trustees to request their secret keys + # 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 email_trustees_dec(event) -@task() -def gen_event_sk_and_dec(event): - trustee_sks = TrusteeSK.objects.filter(event=event) - t_sks_count = len(trustee_sks) - - # Combine SKs if there's more than one - event_sk = None - if t_sks_count == 1: - event_sk = trustee_sks.get().key - else: - t_sks_str_list = list() - - for t_sk in trustee_sks: - t_sks_str_list.append(t_sk.key) - - event_sk = combine_sks(t_sks_str_list) - - EventSK.objects.create(event=event, key=event_sk) - - # With the event sk created, we can decrypt the event - decrypt_and_tally(event) - -@task() -def decrypt_and_tally(event): - polls = event.polls.all() - sk = EventSK.objects.filter(event=event).get().key - - for i in range(len(polls)): - poll = polls[i] - result = str("") - result += "{\"name\": \"" + poll.question_text + "\"," - - # get num of opts and ballots - options = poll.options.all() - opt_count = len(options) - ballots = Ballot.objects.filter(poll=poll, cast=True) - - result += "\"options\": [" - for j in range(opt_count): - # Collect all fragments for this opt - frags_c1 = list() - frags_c2 = list() - - for ballot in ballots: - enc_vote = ballot.encrypted_vote.get() - - if enc_vote is not None: - fragments = enc_vote.fragment.all() - frags_c1.append(fragments[j].cipher_text_c1) - frags_c2.append(fragments[j].cipher_text_c2) - - ciphers = { - 'c1s': frags_c1, - 'c2s': frags_c2 - } - - count = len(frags_c1) - votes = get_tally(count, ciphers, sk, event.EID) - - result += "{\"option\": \"" + str(options[j].choice_text) + "\", \"votes\": " + str(votes) + "}" - - if j != (opt_count-1): - result += "," - - result += "]}" - - if i != (len(polls) - 1): - result += "," - - poll.enc = result - poll.save() - -@task() -def tally_results(event): - for poll in event.polls.all(): - decs = list() - for dec in poll.decryptions.all(): - decs.append(dec.text) - amount = len(decs) - result = tally(amount, event.EID, decs, poll.enc) - - # TODO: Email organisers using email_user method? - - print(poll.question_text + ": " + result) @task() def generate_combpk(event): @@ -222,25 +220,54 @@ def generate_combpk(event): event.prepared = True event.save() -@task -def generate_enc(poll): - # c1 and c2 components of ciphertexts - c1s = list() - c2s = list() - for ballot in poll.ballots.all(): - if ballot.cast: - c1s.append(str(ballot.cipher_text_c1)) - c2s.append(str(ballot.cipher_text_c2)) +@task() +def combine_decryptions_and_tally(event): + polls = event.polls.all() + polls_count = len(polls) - ciphers = { - 'c1s': c1s, - 'c2s': c2s - } + for i in range(polls_count): + poll = polls[i] + result = str("") + result += "{\"name\": \"" + poll.question_text + "\"," - count = len(c1s) + options = poll.options.all() + opt_count = len(options) + result += "\"options\": [" + for j in range(opt_count): + option = options[j] - poll.enc = addec(count, ciphers) - poll.save() + # 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] + ballot_cipher = {} + ballot_cipher['C1'] = combined_ballot.cipher_text_c1 + ballot_cipher['C2'] = combined_ballot.cipher_text_c2 + + # 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) + + part_decs_text = list() + for part_dec in part_decs: + part_decs_text.append(part_dec.text) + + # 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) + "\"}" + + if j != (opt_count-1): + result += "," + + result += "]}" + + if i != (polls_count - 1): + result += "," + + poll.result_json = result + poll.save() diff --git a/allauthdemo/polls/urls.py b/allauthdemo/polls/urls.py index b0a40a5..8304719 100755 --- a/allauthdemo/polls/urls.py +++ b/allauthdemo/polls/urls.py @@ -8,17 +8,17 @@ app_name = 'polls' urlpatterns = [ url(r'^$', login_required(views.EventListView.as_view()), name='index'), url(r'^create/$', login_required(views.create_event), name='create-event'), - url(r'^(?P[0-9]+)/$', login_required(views.EventDetailView.as_view()), name='view-event'), - url(r'^(?P[0-9]+)/polls/$', login_required(views.EventDetailPollsView.as_view()), name='event-polls'), - url(r'^(?P[0-9]+)/entities/$', login_required(views.EventDetailEntitiesView.as_view()), name='event-entities'), - url(r'^(?P[0-9]+)/advanced/$', login_required(views.EventDetailAdvancedView.as_view()), name='event-advanced'), - url(r'^(?P[0-9]+)/end/$', login_required(views.event_end), name='end-event'), - url(r'^(?P[0-9]+)/results/$', login_required(views.results), name='event-results'), - url(r'^(?P[0-9]+)/edit/$', login_required(views.edit_event), name='edit-event'), - url(r'^(?P[0-9]+)/delete/$', login_required(views.del_event), name='del-event'), - url(r'^(?P[0-9]+)/decrypt/$', views.event_trustee_decrypt, name='decrypt-event'), - url(r'^(?P[0-9]+)/prepare/$', views.event_trustee_setup, name='prepare-event'), - url(r'^(?P[0-9]+)/poll/(?P[0-9]+)/vote/$', views.event_vote, name='event-vote'), - url(r'^(?P[0-9]+)/create/poll/$', login_required(views.manage_questions), name='create-poll'), - url(r'^(?P[0-9]+)/poll/(?P[0-9]+)/edit$', login_required(views.edit_poll), name='edit-poll') + url(r'^(?P[0-9a-f-]+)/$', login_required(views.EventDetailView.as_view()), name='view-event'), + url(r'^(?P[0-9a-f-]+)/polls/$', login_required(views.EventDetailPollsView.as_view()), name='event-polls'), + url(r'^(?P[0-9a-f-]+)/entities/$', login_required(views.EventDetailEntitiesView.as_view()), name='event-entities'), + url(r'^(?P[0-9a-f-]+)/advanced/$', login_required(views.EventDetailAdvancedView.as_view()), name='event-advanced'), + url(r'^(?P[0-9a-f-]+)/end/$', login_required(views.event_end), name='end-event'), + url(r'^(?P[0-9a-f-]+)/results/$', login_required(views.results), name='event-results'), + url(r'^(?P[0-9a-f-]+)/edit/$', login_required(views.edit_event), name='edit-event'), + url(r'^(?P[0-9a-f-]+)/delete/$', login_required(views.del_event), name='del-event'), + url(r'^(?P[0-9a-f-]+)/decrypt/$', views.event_trustee_decrypt, name='decrypt-event'), + url(r'^(?P[0-9a-f-]+)/prepare/$', views.event_trustee_setup, name='prepare-event'), + url(r'^(?P[0-9a-f-]+)/poll/(?P[0-9a-f-]+)/vote/$', views.event_vote, name='event-vote'), + url(r'^(?P[0-9a-f-]+)/create/poll/$', login_required(views.manage_questions), name='create-poll'), + url(r'^(?P[0-9a-f-]+)/poll/(?P[0-9a-f-]+)/edit$', login_required(views.edit_poll), name='edit-poll') ] diff --git a/allauthdemo/polls/views.py b/allauthdemo/polls/views.py index e2a1301..a528616 100755 --- a/allauthdemo/polls/views.py +++ b/allauthdemo/polls/views.py @@ -11,10 +11,11 @@ from django.views import generic from django.conf import settings from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm -from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, TrusteeSK +from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, PartialBallotDecryption, CombinedBallot from allauthdemo.auth.models import DemoUser -from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots, create_ballots_for_poll, email_voters_vote_url, gen_event_sk_and_dec +from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots +from .tasks import create_ballots_for_poll, email_voters_vote_url, combine_decryptions_and_tally from .utils.EventModelAdaptor import EventModelAdaptor @@ -25,17 +26,16 @@ class EventListView(generic.ListView): def get_context_data(self, **kwargs): context = super(EventListView, self).get_context_data(**kwargs) - #context['now'] = timezone.now() return context class EventDetailView(generic.DetailView): - template_name="polls/event_detail_details.html" + template_name = "polls/event_detail_details.html" model = Event def get_context_data(self, **kwargs): context = super(EventDetailView, self).get_context_data(**kwargs) - context['is_organiser'] = ((not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists())) + context['is_organiser'] = (not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists()) context['decrypted'] = self.object.status() == "Decrypted" return context @@ -63,20 +63,13 @@ class PollDetailView(generic.View): return context -def util_get_poll_by_event_index(event, poll_num): - try: - poll_num = int(poll_num) - if ((poll_num < 1) or (poll_num > event.polls.all().count())): - return None - poll = event.polls.filter().order_by('id')[poll_num-1] # index field eventually - except ValueError: - return None - return poll +def util_get_poll_by_event_index(event, poll_id): + return event.polls.get(uuid=poll_id) -def edit_poll(request, event_id, poll_num): +def edit_poll(request, event_id, poll_id): event = get_object_or_404(Event, pk=event_id) - poll = util_get_poll_by_event_index(event, poll_num) + poll = util_get_poll_by_event_index(event, poll_id) if (poll == None): raise Http404("Poll does not exist") @@ -98,33 +91,50 @@ def edit_poll(request, event_id, poll_num): return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id])) -def event_vote(request, event_id, poll_num): +def event_vote(request, event_id, poll_id): event = get_object_or_404(Event, pk=event_id) if not event.prepared: messages.add_message(request, messages.WARNING, "This Event isn\'t ready for voting yet.") return HttpResponseRedirect(reverse("user_home")) - event_poll_count = event.polls.all().count() - prev_poll_index, next_poll_index = False, False - can_vote, has_voted, voter_email = False, False, "" - poll = util_get_poll_by_event_index(event, poll_num) + # Lookup the specified poll + poll = event.polls.get(uuid=poll_id) if poll is None: messages.add_message(request, messages.ERROR, "There was an error loading the voting page.") return HttpResponseRedirect(reverse("user_home")) - poll_num = int(poll_num) # now known to be safe as it succeeded in the util function + polls = event.polls.all() + event_poll_count = len(polls) + prev_poll_uuid, next_poll_uuid, poll_num = False, False, 0 + can_vote, cant_vote_reason, has_voted, voter_email = False, "", False, "" - if poll_num > 1: - prev_poll_index = (poll_num - 1) - if poll_num < event_poll_count: - next_poll_index = (poll_num + 1) + for i in range(event_poll_count): + poll = polls[i] + poll_uuid = str(poll.uuid) + req_poll_uuid = str(poll_id) + + if poll_uuid == req_poll_uuid: + poll_num = str(i+1) + + # If current voting request isn't for the last poll, then make sure we link to the next + if i != event_poll_count - 1: + # Only set the previous poll's uuid if we're not looking at the first poll + if i != 0: + prev_poll_uuid = str(polls[i - 1].uuid) + + next_poll_uuid = str(polls[i + 1].uuid) + else: + if i != 0: + prev_poll_uuid = str(polls[i - 1].uuid) + + break access_key = request.GET.get('key', None) email_key = event.keys.filter(key=access_key) + email_key_str = email_key[0].key - ballot = None if email_key.exists() and event.voters.filter(email=email_key[0].user.email).exists(): # Passing this test means the user can vote voter_email = email_key[0].user.email @@ -135,15 +145,18 @@ def event_vote(request, event_id, poll_num): if ballot.exists() and ballot[0].cast: has_voted = True else: - messages.add_message(request, messages.ERROR, "You don\'t have permission to vote in this event.") - return HttpResponseRedirect(reverse("user_home")) + can_vote = False + cant_vote_reason = "You don't have permission to access this page." + + if event.status() != "Active": + can_vote = False + cant_vote_reason = "The event either isn't ready for voting or it has expired and therefore you cannot vote." if request.method == "POST": - if ballot is None: - ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll) + ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)[0] # Will store the fragments of the encoding scheme that define the vote - encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot[0])[0] + encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot)[0] # Clear any existing fragments - a voter changing their vote encrypted_vote.fragment.all().delete() @@ -160,11 +173,13 @@ def event_vote(request, event_id, poll_num): cipher_text_c1=cipher_c1, cipher_text_c2=cipher_c2) - ballot[0].cast = True - ballot[0].save() + ballot.cast = True + ballot.save() - if next_poll_index: - return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.id, 'poll_num': next_poll_index }) + "?key=" + email_key[0].key) + if next_poll_uuid: + return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid, + 'poll_id': next_poll_uuid}) + + "?key=" + email_key_str) else: # The user has finished voting in the event success_msg = 'You have successfully cast your vote(s)!' @@ -175,9 +190,9 @@ def event_vote(request, event_id, poll_num): return render(request, "polls/event_vote.html", { "object": poll, "poll_num": poll_num, "event": event, "poll_count": event.polls.all().count(), - "prev_index": prev_poll_index, "next_index": next_poll_index, "min_selection": poll.min_num_selections, - "max_selection": poll.max_num_selections, "can_vote": can_vote, "voter_email": voter_email, - "has_voted": has_voted + "prev_uuid": prev_poll_uuid, "next_uuid": next_poll_uuid, "min_selection": poll.min_num_selections, + "max_selection": poll.max_num_selections, "can_vote": can_vote, "cant_vote_reason": cant_vote_reason, + "voter_email": voter_email, "has_voted": has_voted, "a_key": email_key_str }) @@ -245,7 +260,7 @@ def results(request, event_id): results = "" results += "{\"polls\":[" for poll in polls: - results += poll.enc + results += poll.result_json results += "]}" @@ -260,21 +275,63 @@ def event_trustee_decrypt(request, event_id): email_key = event.keys.filter(key=access_key) if email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists(): - if TrusteeSK.objects.filter(event=event, trustee=email_key[0].user).exists(): - messages.add_message(request, messages.WARNING, 'You have already provided your decryption key for this event') + + if PartialBallotDecryption.objects.filter(event=event, user=email_key[0].user).count() == event.total_num_opts(): + + warning_msg = 'You have already provided your decryption key for this event - Thank You' + messages.add_message(request, messages.WARNING, warning_msg) + return HttpResponseRedirect(reverse("user_home")) elif request.method == "GET": - return render(request, "polls/event_decrypt.html", {"event": event, "user_email": email_key[0].user.email}) + # Gen a list of ciphers from the combined ballots for every opt of every poll + polls = event.polls.all() + poll_ciphers = [] + + for poll in polls: + options = poll.options.all() + + options_ciphers = [] + for option in options: + combined_ballot = CombinedBallot.objects.filter(poll=poll, option=option).get() + + cipher = {} + cipher['C1'] = combined_ballot.cipher_text_c1 + cipher['C2'] = combined_ballot.cipher_text_c2 + options_ciphers.append(cipher) + + poll_ciphers.append(options_ciphers) + + return render(request, + "polls/event_decrypt.html", + { + "event": event, + "user_email": email_key[0].user.email, + "poll_ciphers": poll_ciphers + }) + elif request.method == "POST": - sk = request.POST['secret-key'] + polls = event.polls.all() + polls_count = len(polls) - TrusteeSK.objects.create(event=event, - trustee=email_key[0].user, - key=sk) + for i in range(polls_count): + options = polls[i].options.all() + options_count = len(options) - if event.trustee_sk.count() == event.users_trustees.count(): - # Generate the event SK and decrypt the event to tally the results - gen_event_sk_and_dec.delay(event) + for j in range(options_count): + input_name = "" + input_name = "poll-" + str(i) + "-cipher-" + str(j) + + part_dec = request.POST[input_name] + + PartialBallotDecryption.objects.create(event=event, + poll=polls[i], + option=options[j], + user=email_key[0].user, + text=part_dec) + + if event.all_part_decs_received(): + # TODO: Combine partial decryptions and gen results + combine_decryptions_and_tally.delay(event) messages.add_message(request, messages.SUCCESS, 'Your secret key has been successfully submitted') return HttpResponseRedirect(reverse("user_home")) @@ -434,7 +491,7 @@ def edit_event(request, event_id): def del_event(request, event_id): event = get_object_or_404(Event, pk=event_id) if request.method == "GET": - return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.id}) + return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.uuid}) elif request.method == "POST": event.delete() return HttpResponseRedirect(reverse('polls:index')) \ No newline at end of file diff --git a/allauthdemo/templates/bases/bootstrap-jquery.html b/allauthdemo/templates/bases/bootstrap-jquery.html index 44dcc47..d49a34e 100755 --- a/allauthdemo/templates/bases/bootstrap-jquery.html +++ b/allauthdemo/templates/bases/bootstrap-jquery.html @@ -56,7 +56,6 @@