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 @@