From e33b91f852393bee55e6d24aee061fa0246bb78b Mon Sep 17 00:00:00 2001 From: vince0656 Date: Sat, 7 Jul 2018 09:52:47 +0100 Subject: [PATCH] Implemented full end-to-end encryption and decryption of an event using trustee public and secret keys. This required modification of the NodeJS crypto server to receive post data from the crypto_rpc py methods and for combining SKs together. Additionally, the new binary voting encoding scheme has been implemented for encryption and decryption of the event. General UI improvements have been made and as well as some other bug fixes --- Node/index.js | 208 +++++++++----- Node/package.json | 3 +- allauthdemo/polls/crypto_rpc.py | 84 ++++-- allauthdemo/polls/models.py | 82 ++++-- allauthdemo/polls/tasks.py | 185 +++++++++--- .../polls/templatetags/custom_filters_tags.py | 16 +- allauthdemo/polls/urls.py | 41 +-- allauthdemo/polls/utils/EventModelAdaptor.py | 42 +-- allauthdemo/polls/views.py | 266 ++++++++++-------- .../templates/allauth/account/login.html | 2 +- .../templates/bases/bootstrap-jquery.html | 72 +++-- allauthdemo/templates/bases/bootstrap.html | 2 +- .../templates/polls/event_decrypt.html | 32 +-- ...launch.html => event_detail_advanced.html} | 0 .../templates/polls/event_detail_base.html | 20 +- ...nisers.html => event_detail_entities.html} | 0 .../templates/polls/event_detail_polls.html | 2 +- allauthdemo/templates/polls/event_list.html | 8 +- allauthdemo/templates/polls/event_setup.html | 2 + allauthdemo/templates/polls/event_vote.html | 81 ++++++ allauthdemo/templates/polls/poll_detail.html | 85 ------ static/css/main.css | 2 +- static/js/decrypt_event.js | 20 ++ 23 files changed, 809 insertions(+), 446 deletions(-) rename allauthdemo/templates/polls/{event_detail_launch.html => event_detail_advanced.html} (100%) rename allauthdemo/templates/polls/{event_detail_organisers.html => event_detail_entities.html} (100%) create mode 100755 allauthdemo/templates/polls/event_vote.html delete mode 100755 allauthdemo/templates/polls/poll_detail.html create mode 100644 static/js/decrypt_event.js diff --git a/Node/index.js b/Node/index.js index 6d29284..e7e82ed 100755 --- a/Node/index.js +++ b/Node/index.js @@ -1,30 +1,33 @@ /* -Code by Thomas Smith +Code by Bingsheng Zhang, Thomas Smith, Vincent de Almeida +Dependencies can be found in 'package.json' and installed using 'npm install' */ var port = 8080; -var express = require('express'); var Buffer = require('buffer').Buffer; -var CTX = require('milagro-crypto-js') -var app = express(); -/* -var cors = require('cors') -app.use(cors()); -*/ +var CTX = require('milagro-crypto-js'); +var express = require('express'); +var bodyParser = require("body-parser"); +var app = express(); + +// Express server configuration app.use(express.static('test')); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); + //default test app.get('/', function(request, response){ var data = { message: 'hello world', value: 5 - } + }; //response.send('Hey there'+request.ip); @@ -40,13 +43,13 @@ app.get('/param', function(request, response){ console.log('Generated Param:' + param); response.json(param); -}) +}); //combine public keys and return the full combined one - JSON Version app.get('/combpk', function(request, response){ - + console.log('\nEndpoint /combpk called'); - var partials = request.query['PK'] + var partials = request.query['PK']; var parsed = []; @@ -57,45 +60,40 @@ app.get('/combpk', function(request, response){ parsed.push(JSON.parse(partials[i])); } - var PK = combine(parsed); + var PK = combine_pks(parsed); response.json(PK); -}) +}); //byte array version -app.get('/cmpkstring', function(request, response){ +app.post('/cmpkstring', function(request, response){ + console.log('\nEndpoint /cmpkstring called'); var ctx = new CTX("BN254CX"); - var partials = request.query['PK'] - //if there is only one key, partials will be an array of the individual bytes - //if more than one, it will be an array of arrays - //we need to factor for this in code - var noOfKeys = request.query['number']; + var partials = request.body.PKs; var parsed = []; - if(noOfKeys == partials.length)//if we're submitting more than one key + if(partials.length > 1)//if we're submitting more than one key { - console.log('Combining' + noOfKeys + " keys..."); + console.log('Combining ' + partials.length + " public keys into one..."); for (var i = partials.length - 1; i >= 0; i--) { - console.log('PK' +i+ ': '+partials[i]); - var bytes = Buffer.from(partials[i].split(','), 'hex'); - console.log(bytes) - var pk = new ctx.ECP.fromBytes(bytes); - parsed.push(pk); + console.log('PK' + i + ': ' + partials[i]); + var bytes = Buffer.from(partials[i].split(','), 'hex'); + var pk = new ctx.ECP.fromBytes(bytes); + parsed.push(pk); } } - else if(noOfKeys == 1) + else if(partials.length === 1) { - console.log("Combining just one key"); - var bytes = Buffer.from(partials.split(','), 'hex'); - console.log(bytes); + console.log("Combining just one public key..."); + var bytes = Buffer.from(partials[0].split(','), 'hex'); var pk = new ctx.ECP.fromBytes(bytes); parsed.push(pk); } - response.json(combine(parsed)); -}) + response.json(combine_pks(parsed)); +}); //addition function on homomorphically encrypted variables @@ -155,20 +153,19 @@ app.get('/addec', function(request, response){ response.json(add(parsed)); -}) - +}); //tally partially decrypted ciphertexts app.get('/tally', function(request, response){ - console.log("called tally"); + 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(paramString); + 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(); @@ -183,47 +180,117 @@ app.get('/tally', function(request, response){ n:n, g1:g1, g2:g2 - } + }; //re-build partial decryptions - var partials = [] + 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("Only one partial decryption received") - console.log(paramString) + 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) - } + D : new ctx.ECP.fromBytes(bytes) + }; + partials.push(dec); } //re-build combined ciphertext var tempCipher = JSON.parse(ciphertextString); - cipher = { + var cipher = { C1: new ctx.ECP(), C2: new ctx.ECP() - } + }; + 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()); +}); + +app.post('/get_tally', function(request, response){ + const COUNT = request.body.count; + 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; + + 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 + var n = new ctx.BIG(); + var g1 = new ctx.ECP(); + var g2 = new ctx.ECP2(); + + n.copy(TEMP_PARAMS.n); + g1.copy(TEMP_PARAMS.g1); + g2.copy(TEMP_PARAMS.g2); + + var params = { + n:n, + g1:g1, + g2:g2 + }; + + //rebuild our secret key + var skBytes = SK.split(","); + var sk = new ctx.BIG.fromBytes(skBytes); + + var tally = 0; + + for(var i = 0; i < 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: newC2}; + tally += decrypt(params, sk, cipher).M; + } + + console.log("Tally: " + tally + "\n"); + + response.send("" + tally); +}); var server = app.listen(port, function(){ var host = server.address().address; @@ -277,7 +344,7 @@ gpGen = function(){ g1:P, g2:Q } -} +}; //creates ElGamal public and secret key @@ -304,12 +371,12 @@ keyGen=function(params){ PK:pk, SK:sk } -} +}; //combine multiple public key together //the input is an array of PKs -combine=function(PKs){ +combine_pks=function(PKs){ var ctx = new CTX("BN254CX"); var pk=new ctx.ECP(); //copy the first pk @@ -319,11 +386,25 @@ combine=function(PKs){ pk.add(PKs[i]); } - return{ - PK:pk + return { + PK : pk } -} +}; +// 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) { + // 'add' the rest of the sks to the first + var sk = SKs[0]; + + for(var i = 1; i < SKs.length; i++) { + sk.add(SKs[i]); + } + + return { + SK: sk + } +}; //ElGamal encryption encrypt=function(params,PK, m){ @@ -356,7 +437,7 @@ encrypt=function(params,PK, m){ C1:C1, C2:C2 } -} +}; //add ciphertexts @@ -380,7 +461,7 @@ add=function(Ciphers){ C1:s1, C2:s2 } -} +}; //ElGamal decryption @@ -410,9 +491,7 @@ decrypt=function(params,SK, C){ return{ M: "Error" } -} - - +}; //ElGamal partial decryption @@ -424,8 +503,7 @@ partDec=function(SK, C){ return{ D: D } -} - +}; @@ -446,24 +524,24 @@ tally=function(params,Ds, C){ gM.copy(C.C2); gM.sub(D); -//search for message by brute force + //search for message by brute force var B; - for (j = 0; j < 1000; j++) { + for (var j = 0; j < 1000; j++) { //use D as temp var B = new ctx.BIG(j); D = ctx.PAIR.G1mul(params.g1,B); if (D.equals(gM)) return{ - M:j + M: j } - }; + } return{ M: "Error" } -} +}; diff --git a/Node/package.json b/Node/package.json index 8eda884..be592f5 100644 --- a/Node/package.json +++ b/Node/package.json @@ -14,9 +14,10 @@ "url": "https://github.com/vincentmdealmeida/DEMOS2" }, "keywords": [], - "author": "Bingsheng Zang, Thomas Smith", + "author": "Bingsheng Zang, Thomas Smith, Vincent de Almeida", "license": "ISC", "dependencies": { + "body-parser": "^1.18.3", "express": "^4.16.3", "milagro-crypto-js": "git+https://github.com/milagro-crypto/milagro-crypto-js.git" } diff --git a/allauthdemo/polls/crypto_rpc.py b/allauthdemo/polls/crypto_rpc.py index a85cc8a..2e1d915 100755 --- a/allauthdemo/polls/crypto_rpc.py +++ b/allauthdemo/polls/crypto_rpc.py @@ -1,6 +1,3 @@ -import os -import shlex -import subprocess import json import urllib2 @@ -10,26 +7,40 @@ All functions in this file have been re-implemenented by Thomas Smith File then updated by Vincent de Almeida. Changes include: -Update filename to 'crypto_rpc' to reflect the RPC nature of the methods + -Modified RPC calls that send data to POST requests to avoid large query URLs ''' + + +def send_post_req(url, data): + data = json.dumps(data) + + # Create a request specifying the Content-Type + req = urllib2.Request(url, data, {'Content-Type': 'application/json'}) + f = urllib2.urlopen(req) + response = f.read() + f.close() + + return response + + def param(): - url = 'http://localhost:8080/param' # RPC URL + url = 'http://localhost:8080/param' jsondict = json.load(urllib2.urlopen(url)) return json.dumps(jsondict) -def combpk(amount, pks): - url = 'http://localhost:8080/cmpkstring' # RPC URL - querystring = '?number='+str(amount) - for pk in pks: - querystring += '&PK='+pk - print(url+querystring) - jsondict = json.load(urllib2.urlopen(url+querystring)) - print(json.dumps(jsondict)) - return json.dumps(jsondict) +def combpk(pks): + url = 'http://localhost:8080/cmpkstring' + + data = {} + data['PKs'] = pks + + return send_post_req(url, data) + def addec(amount, ciphers): - url = 'http://localhost:8080/addec' # RPC URL + url = 'http://localhost:8080/addec' querystring = '?number='+str(amount) c1s = ciphers['c1s'] c2s = ciphers['c2s'] @@ -42,23 +53,44 @@ def addec(amount, ciphers): print(json.dumps(jsondict)) return json.dumps(jsondict) -def tally(amount, param, decs, cipher): - url = 'http://localhost:8080/tally' # RPC URL - querystring = '?number='+str(amount) - querystring += '¶m='+urllib2.quote(str(param)) - testquerystring = '?number='+str(amount) - testquerystring += '¶m='+str(param) +# Deprecated functionality and has been superseded by get_tally +def tally(amount, group_param, decs, cipher): + url = 'http://localhost:8080/tally' + querystring = '?number='+str(amount) + querystring += '¶m='+urllib2.quote(str(group_param)) for i, value in enumerate(decs): querystring += "&decs="+str(value) - testquerystring += "&decs="+str(value) querystring += '&cipher=' + urllib2.quote(str(cipher)) - testquerystring += '&cipher=' + str(cipher) - print(url+querystring) - print(url+testquerystring) jsondict = json.load(urllib2.urlopen(url+querystring)) - print('tally: ' + str(jsondict['M'])) - return str(jsondict['M']) \ No newline at end of file + + return str(jsondict['M']) + + +def combine_sks(sks): + url = 'http://localhost:8080/comb_sks' + + # Construct POST data + data = {} + data['SKs'] = sks + + # Return the new combined SK + return send_post_req(url, data) + + +def get_tally(count, ciphers, sk, group_param): + url = 'http://localhost:8080/get_tally' + + # Construct POST data + data = {} + data['count'] = count + data['ciphers'] = ciphers + data['sk'] = sk + data['param'] = group_param + + # Return the tally of votes for the option + return send_post_req(url, data) + diff --git a/allauthdemo/polls/models.py b/allauthdemo/polls/models.py index 84a7524..74b227f 100755 --- a/allauthdemo/polls/models.py +++ b/allauthdemo/polls/models.py @@ -28,6 +28,7 @@ class Event(models.Model): start_time = models.DateTimeField() end_time = models.DateTimeField() prepared = models.BooleanField(default=False) + ended = models.BooleanField(default=False) public_key = models.CharField(null=True, blank=False, max_length=1024) title = models.CharField(max_length=1024) EID = models.CharField(max_length=2048, blank=True) @@ -35,6 +36,7 @@ class Event(models.Model): c_email = models.CharField(max_length=512, blank=True) trustees = models.CharField(max_length=4096) + # Custom helper methods def EID_hr(self): EID_json = json.loads(self.EID) return EID_json['hr'] @@ -68,15 +70,36 @@ class Event(models.Model): # future event present = timezone.now() - if present >= self.start_time and present <= self.end_time: - status_str = "Active" - elif present > self.end_time: - status_str = "Expired" - elif present < self.start_time: - status_str = "Future" + if self.ended is False: + if present < self.start_time and self.public_key is None: + status_str = "Future" + elif present < self.start_time and self.public_key is not None: + status_str = "Prepared" + elif present >= self.start_time and present <= self.end_time and self.public_key is not None: + status_str = "Active" + elif present > self.end_time and self.public_key is not None: + status_str = "Expired" + else: + if self.event_sk.all().count() == 1: + status_str = "Decrypted" + elif self.event_sk.all().count() == 0: + status_str = "Ended" return status_str + ''' + The result applies to all polls for an event so True will only be returned when votes have + been received for every poll. + ''' + def has_received_votes(self): + received_votes = True + + for poll in self.polls.all(): + if Ballot.objects.filter(poll=poll, cast=True).count() == 0: + received_votes = False + + return received_votes + def __str__(self): return self.title @@ -84,12 +107,12 @@ class Event(models.Model): class TrusteeKey(models.Model): event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="trustee_keys") user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_keys") - key = models.CharField(max_length=1024, unique=True) # ideally composite key here, but django doesn't really support yet + 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=1024, unique=True) # ideally composite key here, but django doesn't really support yet + key = models.CharField(max_length=255, unique=True) #total = models.IntegerField(blank=True, null=True, default=0) @@ -115,20 +138,6 @@ class Poll(models.Model): def __str__(self): return self.question_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) - -#some modification to this class -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") - cipher_text_c1 = models.CharField(max_length=4096)#the encryption system uses two byte strings - cipher_text_c2 = models.CharField(max_length=4096) - cast = models.BooleanField(default=False) - class PollOption(models.Model): choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) @@ -138,6 +147,35 @@ class PollOption(models.Model): 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 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) diff --git a/allauthdemo/polls/tasks.py b/allauthdemo/polls/tasks.py index 0390b28..6b433af 100755 --- a/allauthdemo/polls/tasks.py +++ b/allauthdemo/polls/tasks.py @@ -5,14 +5,11 @@ import json from os import urandom from celery import task -from django.core.exceptions import ValidationError -from django.core.validators import EmailValidator -from django.core.mail import send_mail from django.conf import settings -from allauthdemo.polls.models import AccessKey +from allauthdemo.polls.models import AccessKey, Ballot, Decryption, TrusteeSK, EventSK -from .crypto_rpc import param, combpk, addec, tally +from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks ''' Goal: This py file defines celery tasks that can be initiated @@ -25,25 +22,43 @@ from .crypto_rpc import param, combpk, addec, tally # Will store the result of the initial cal to param() from .cpp_calls group_param = None -def is_valid_email(email): - try: - valid_email = EmailValidator(whitelist=None) - valid_email(email) - return True - except ValidationError: - return False - -@task() -def create_ballots(poll): - for voter in poll.event.voters.all(): - ballot = poll.ballots.create(voter=voter, poll=poll) - ''' - Will generate a key for accessing either the event preparation page or the voting page + Helper functions + + gen_access_key - Will generate an a key for accessing either the event preparation page, voting page and decryption page ''' 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" + url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/decrypt/?key=" + email_body = email_body + url_base + + 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) + +@task() +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) + +@task() +def create_ballots_for_poll(poll): + for voter in poll.event.voters.all(): + ballot = poll.ballots.create(voter=voter, poll=poll) + + ''' Emails an event preparation URL containing an access key for all of the trustees for an event ''' @@ -64,19 +79,31 @@ def email_trustees_prep(trustees, event): trustee.send_email(email_subject, email_body + key) ''' - Emails the access keys for all of the voters for an event + Emails a URL containing an access key for all of the voters for an event ''' @task() -def email_voters_a_key(voters, event): +def email_voters_vote_url(voters, event): email_subject = "Voting Access for Event '" + event.title + "'" - email_body = 'Key: ' + + # 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 + + 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() 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) - voter.send_email(email_subject, email_body + key) + # 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 + + 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()) @@ -93,6 +120,83 @@ def update_EID(event): event.EID = json.dumps(EID) event.save() +@task() +def event_ended(event): + # Email all trustees to request 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(): @@ -101,39 +205,42 @@ def tally_results(event): decs.append(dec.text) amount = len(decs) result = tally(amount, event.EID, decs, poll.enc) - send_mail( - 'Your Results:', - poll.question_text + ": " + result, - 'from@example.com', - ["fake@fake.com"], - fail_silently=False, - ) + + # TODO: Email organisers using email_user method? + print(poll.question_text + ": " + result) @task() def generate_combpk(event): pks = list() + for tkey in event.trustee_keys.all(): pks.append(str(tkey.key)) - amount = len(pks) - event.public_key = combpk(amount, pks) + + event.public_key = combpk(pks) + event.prepared = True event.save() @task def generate_enc(poll): - c1s = list()#c1 components of ciphertexts - c2s = list()#c1 components of ciphertexts + # c1 and c2 components of ciphertexts + c1s = list() + c2s = list() + for ballot in poll.ballots.all(): - if (ballot.cast): + if ballot.cast: c1s.append(str(ballot.cipher_text_c1)) c2s.append(str(ballot.cipher_text_c2)) + ciphers = { - 'c1s':c1s, - 'c2s':c2s + 'c1s': c1s, + 'c2s': c2s } - amount = len(c1s) - poll.enc = addec(amount, ciphers) + + count = len(c1s) + + poll.enc = addec(count, ciphers) poll.save() diff --git a/allauthdemo/polls/templatetags/custom_filters_tags.py b/allauthdemo/polls/templatetags/custom_filters_tags.py index 2b0a804..0a17210 100755 --- a/allauthdemo/polls/templatetags/custom_filters_tags.py +++ b/allauthdemo/polls/templatetags/custom_filters_tags.py @@ -5,5 +5,17 @@ register = template.Library() #get a value for additively homomorphic encryption ballots #we can't do maths in the template normally so a filter is a way around it @register.filter -def get_ballot_value(value): - return pow(10, value-1) +def get_ballot_value(option_no, options_count): + ballot_value = "" + + for i in range(options_count): + + if (i+1) == option_no: + ballot_value = ballot_value + "1" + else: + ballot_value = ballot_value + "0" + + if not i == (options_count-1): + ballot_value = ballot_value + "," + + return ballot_value diff --git a/allauthdemo/polls/urls.py b/allauthdemo/polls/urls.py index a2d5938..b0a40a5 100755 --- a/allauthdemo/polls/urls.py +++ b/allauthdemo/polls/urls.py @@ -1,37 +1,24 @@ from django.conf.urls import url -from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.decorators import login_required from . import views app_name = 'polls' -#urlpatterns = [ -# url(r'^$', views.index, name='index'), - # ex: /polls/5/ -# url(r'^(?P[0-9]+)/$', views.detail, name='detail'), - # ex: /polls/5/results/ -# url(r'^(?P[0-9]+)/results/$', views.results, name='results'), - # ex: /polls/5/vote/ -# url(r'^(?P[0-9]+)/vote/$', views.vote, name='vote'), -#] - urlpatterns = [ - url(r'^vote/(?P[0-9]+)/$', views.test_poll_vote, name='vote-poll'), - url(r'^(?P[0-9]+)/$', views.EventDetailView.as_view(), name='view-event'), - url(r'^(?P[0-9]+)/polls$', views.EventDetailPollsView.as_view(), name='event-polls'), - url(r'^(?P[0-9]+)/organisers$', views.EventDetailOrganisersView.as_view(), name='event-organisers'), - url(r'^$', views.EventListView.as_view(), name='index'), + 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]+)/decrypt/$', login_required(views.event_trustee_decrypt), name='decrypt-event'), - url(r'^(?P[0-9]+)/prepare/$', login_required(views.event_trustee_setup), name='prepare-event'), - url(r'^(?P[0-9]+)/encrypt/$', login_required(views.event_addec), name='enc-event'), - url(r'^(?P[0-9]+)/launch/$', views.EventDetailLaunchView.as_view(), name='launch-event'), - url(r'^edit/(?P[0-9]+)/$', login_required(views.edit_event), name='edit-event'), - url(r'^delete/(?P[0-9]+)/$', login_required(views.del_event), name='del-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]+)/$', login_required(views.view_poll), name='view-poll'), - url(r'^(?P[0-9]+)/poll/(?P[0-9]+)/edit$', login_required(views.edit_poll), name='edit-poll'), - #url(r'^(?P[0-9]+)/$', login_required(views.DetailView.as_view()), name='detail'), - #url(r'^(?P[0-9]+)/results/$', login_required(views.ResultsView.as_view()), name='results'), - #url(r'^(?P[0-9]+)/vote/$', login_required(views.vote), name='vote'), + url(r'^(?P[0-9]+)/poll/(?P[0-9]+)/edit$', login_required(views.edit_poll), name='edit-poll') ] diff --git a/allauthdemo/polls/utils/EventModelAdaptor.py b/allauthdemo/polls/utils/EventModelAdaptor.py index 5b5c968..86ddfa6 100644 --- a/allauthdemo/polls/utils/EventModelAdaptor.py +++ b/allauthdemo/polls/utils/EventModelAdaptor.py @@ -539,12 +539,9 @@ class EventModelAdaptor: # Extract the list of trustees trustees_list = self.form_data.pop('trustee-email-input') - for trustee in trustees_list: - if trustee != '': - if EmailUser.objects.filter(email=trustee).exists(): - self.trustees.append(EmailUser.objects.filter(email=trustee).get()) - else: - self.trustees.append(EmailUser(email=trustee)) + for trustee_email in trustees_list: + if trustee_email != '': + self.trustees.append(trustee_email) # Extract the email list of voters voters_csv_string = self.form_data.pop('voters-list-input')[0].replace(' ', '') @@ -552,17 +549,21 @@ class EventModelAdaptor: for voter_email in voters_email_list: if voter_email != '': - if EmailUser.objects.filter(email=voter_email).exists(): - self.voters.append(EmailUser.objects.filter(email=voter_email).get()) - else: - self.voters.append(EmailUser(email=voter_email)) + self.voters.append(voter_email) # Create the Event model object - this does not persist it to the DB + creator = "" + if self.user.first_name is not None: + creator += self.user.first_name + " " + + if self.user.last_name is not None: + creator += self.user.last_name + self.event = Event(start_time=self.starts_at, end_time=self.ends_at, title=self.event_name, EID=self.identifier, - creator=self.user.first_name + ' ' + self.user.last_name, + creator=creator, c_email=self.user.email, trustees=voters_csv_string) @@ -629,20 +630,21 @@ class EventModelAdaptor: # so it can just be added self.event.users_organisers = self.organisers - # Add the list of trustees to the event, making sure they're instantiated + # Add the list of trustees to the event + db_trustees = list() for trustee in self.trustees: - if not EmailUser.objects.filter(email=trustee.email).exists(): - trustee.save() + user, created = EmailUser.objects.get_or_create(email=trustee) + db_trustees.append(user) - self.event.users_trustees = self.trustees + self.event.users_trustees = db_trustees - # Add the list of voters to the event, making sure they're instantiated - # Additionally, generating the AccessKey for voters + # Add the list of voters to the event + db_voters = list() for voter in self.voters: - if not EmailUser.objects.filter(email=voter.email).exists(): - voter.save() + user, created = EmailUser.objects.get_or_create(email=voter) + db_voters.append(user) - self.event.voters = self.voters + self.event.voters = db_voters # Extract all the poll data for the event and associated poll option data # This can only be done at this point as the event has been persisted diff --git a/allauthdemo/polls/views.py b/allauthdemo/polls/views.py index a12f20d..e2a1301 100755 --- a/allauthdemo/polls/views.py +++ b/allauthdemo/polls/views.py @@ -2,25 +2,23 @@ import urllib import urllib2 import json -from io import StringIO from django.contrib import messages from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.http.response import HttpResponseNotAllowed from django.core.urlresolvers import reverse -from django.shortcuts import get_object_or_404, render, render_to_response -from django.utils import timezone +from django.shortcuts import get_object_or_404, render from django.views import generic from django.conf import settings -from django.core import serializers -from .forms import EventForm, PollForm, OptionFormset, QuestionFormset, OrganiserFormSet, TrusteeFormSet, VoteForm, EventSetupForm, EventEditForm, DecryptionFormset, DecryptionFormSetHelper -from .models import Event, Poll, PollOption, EmailUser, Ballot, TrusteeKey, Decryption +from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm +from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, TrusteeSK from allauthdemo.auth.models import DemoUser -from .tasks import email_trustees_prep, update_EID, generate_combpk, generate_enc, tally_results +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 .utils.EventModelAdaptor import EventModelAdaptor + class EventListView(generic.ListView): model = Event @@ -30,6 +28,7 @@ class EventListView(generic.ListView): #context['now'] = timezone.now() return context + class EventDetailView(generic.DetailView): template_name="polls/event_detail_details.html" model = Event @@ -37,38 +36,32 @@ class EventDetailView(generic.DetailView): 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['decrypted'] = self.object.status() == "Decrypted" - #context['now'] = timezone.now() return context class EventDetailPollsView(EventDetailView): - template_name="polls/event_detail_polls.html" + template_name = "polls/event_detail_polls.html" -class EventDetailOrganisersView(EventDetailView): - template_name="polls/event_detail_organisers.html" -class EventDetailLaunchView(EventDetailView): - template_name="polls/event_detail_launch.html" +class EventDetailEntitiesView(EventDetailView): + template_name = "polls/event_detail_entities.html" + + +class EventDetailAdvancedView(EventDetailView): + template_name = "polls/event_detail_advanced.html" + class PollDetailView(generic.View): - model = Poll def get_context_data(self, **kwargs): context = super(PollDetailView, self).get_context_data(**kwargs) - #context['now'] = timezone.now() context['form'] = VoteForm(instance=self.object) context['poll_count'] = self.object.event.polls.all().count() return context -#my_value = self.kwargs.get('key', 'default_value') - -def test_poll_detail(request, event_id, poll_num, key=None): - context = {} - context['form'] = VoteForm(instance=self.object) - context['poll_count'] = self.object.event.polls.all().count() - return render(request, "polls/event_setup.html", context) def util_get_poll_by_event_index(event, poll_num): try: @@ -80,78 +73,114 @@ def util_get_poll_by_event_index(event, poll_num): return None return poll + def edit_poll(request, event_id, poll_num): event = get_object_or_404(Event, pk=event_id) - event_poll_count = event.polls.all().count() poll = util_get_poll_by_event_index(event, poll_num) if (poll == None): raise Http404("Poll does not exist") - form = PollForm(instance=poll, prefix="main") - formset = OptionFormset(instance=poll, prefix="formset_options") - return render(request, "polls/generic_form.html", {'form_title': "Edit Poll: " + poll.question_text, 'form': form, 'option_formset': formset}) + if request.method == 'GET': + form = PollForm(instance=poll, prefix="main") + formset = OptionFormset(instance=poll, prefix="formset_options") + return render(request, "polls/generic_form.html", {'form_title': "Edit Poll: " + poll.question_text, 'form': form, 'option_formset': formset}) + elif request.method == 'POST': + form = PollForm(request.POST, instance=poll, prefix="main") -def view_poll(request, event_id, poll_num): - #return HttpResponse(param("012345")) - #return HttpResponse(combpk(param("012345"), "ABzqvL+pqTi+DNLLRcM62RwCoaZTaXVbOs3sk4fc0+Dc 0 AAaQd6S1x+bcgnkDp2ev5mTt34ICQdZIzP9GaqG4x5sy 0" "ABhQay9jI4pZvkAETNwfo8iwJ8eBMkjqplqAiu/FZxMy 0 ABPxj0jVj3rt0VW54iv4tV02gYtujnR41t5gf97asrPs 0 ABfoiW03bsYIUgfAThmjurmOViKy9L89vfkIavhQIblm 1 ABhQay9jI4pZvkAETNwfo8iwJ8eBMkjqplqAiu/FZxMy 0 ABPxj0jVj3rt0VW54iv4tV02gYtujnR41t5gf97asrPs 0 ABfoiW03bsYIUgfAThmjurmOViKy9L89vfkIavhQIblm 1 ABhQay9jI4pZvkAETNwfo8iwJ8eBMkjqplqAiu/FZxMy 0 ABPxj0jVj3rt0VW54iv4tV02gYtujnR41t5gf97asrPs 0 ABfoiW03bsYIUgfAThmjurmOViKy9L89vfkIavhQIblm 1")) - #return HttpResponse(addec("ACMW70Yj3+mJ/FO+6VOSDGYPYHf7NoTXdpInbfzUqYpH 0 ABV4Mo496B0FW3AW/7gY6Fs+oz6BwfwilonMYeriUyV/ 0 AAg+bdGhs3sxSxAc/wcKdBNUy+el8A2b4yVYShNOb8uX 0 AAspJbn5V2AaY4CgLkzCkHwUWbC5nyxrBzw+o4Az8HVM 1 ABKI7o5Yhgi44XwpFnPpLnH0/czbXA8y5vM4ucV8vojo 1 AAwVrT9+dcQsqRZYoI7+QsJvWOgd7JaJpfI6envmC2jU 1 ABIZO0DK4OrdROD805of6iRk2RenonGYmo2qG2IB1sj/ 1 ACMUHQdjGN0wyCd2AgDHMk9u0TpnywNVtamHWopGho8L 0 ABNT5lbE4siC3QklQXRvTwSQPwtme91+UrIr9iXT3y84 1 ABib0mmQ9ZVCrErqFwDgoRp3jHPpjHGQR2vsMVlwM+vI 0 ABvf3cg1NSS8fn6EKJNnTomeoflcEY1WBxkPPKrBBFl+ 0 ACBUZAtolN4HNh+mw4jLZuHzD+/rYHKR5av16PUc6BJF 0", "2")) - #return HttpResponse(tally("ACNQLLQlh+lNm1Dc+X+dEI0ECVLTkxRHjRnzX1OA+HtW 0 AAWOsUZK/G/cjhUee/gPAXop3Bc0CTVG3iDdQxD6+XqV 0", "ACNQLLQlh+lNm1Dc+X+dEI0ECVLTkxRHjRnzX1OA+HtW 0 0 2", "2")) + if form.is_valid(): + form.save() + + formset = OptionFormset(request.POST, instance=poll, prefix="formset_options") + + if formset.is_valid(): + formset.save() + return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id])) + + +def event_vote(request, event_id, poll_num): event = get_object_or_404(Event, pk=event_id) - if (not event.prepared): + + 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, vote_count = False, False, "", 0 + can_vote, has_voted, voter_email = False, False, "" poll = util_get_poll_by_event_index(event, poll_num) - if (poll == None): - raise Http404("Poll does not exist") + if poll is None: + messages.add_message(request, messages.ERROR, "There was an error loading the voting page.") + return HttpResponseRedirect(reverse("user_home")) - form = VoteForm(instance=poll) - poll_num = int(poll_num) # now known to be safe as it suceeded in the util function + poll_num = int(poll_num) # now known to be safe as it succeeded in the util function - if (poll_num > 1): + if poll_num > 1: prev_poll_index = (poll_num - 1) - if (poll_num < event_poll_count): + if poll_num < event_poll_count: next_poll_index = (poll_num + 1) access_key = request.GET.get('key', None) email_key = event.keys.filter(key=access_key) - vote_count = Ballot.objects.filter(poll=poll, cast=True).count() - if (email_key.exists() and event.voters.filter(email=email_key[0].user.email).exists()): - ballot = Ballot.objects.filter(voter=email_key[0].user, poll=poll) - if (ballot.exists() and ballot[0].cast): - has_voted = True - - if (access_key and email_key.exists()): #or (can_vote(request.user, event)) + 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 can_vote = True - if (request.method == "POST"): - form = VoteForm(request.POST, instance=poll) - if (email_key.exists()): - #return HttpResponse(email_key[0].key) - ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)[0] + # Check whether this is the first time a user is voting + ballot = Ballot.objects.filter(voter=email_key[0].user, poll=poll) + 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")) - if (form.is_valid()): - ballot.cipher_text_c1 = request.POST["cipher_text_c1"] - ballot.cipher_text_c2 = request.POST["cipher_text_c2"] - ballot.cast = True - ballot.save() - if (next_poll_index): - return HttpResponseRedirect(reverse('polls:view-poll', kwargs={'event_id': event.id, 'poll_num': next_poll_index }) + "?key=" + email_key[0].key) - else: - return HttpResponse("Voted successfully!") # finished all polls in event + if request.method == "POST": + if ballot is None: + ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll) - return render(request, "polls/poll_detail.html", - {"object": poll, "poll_num": poll_num , "event": event, "form": form, "poll_count": event.polls.all().count(), - "prev_index": prev_poll_index , "next_index": next_poll_index, - "can_vote": can_vote, "voter_email": voter_email, "has_voted": has_voted, "vote_count": vote_count + # Will store the fragments of the encoding scheme that define the vote + encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot[0])[0] + + # Clear any existing fragments - a voter changing their vote + encrypted_vote.fragment.all().delete() + + # Add in the new ciphers + fragment_count = int(request.POST['vote_frag_count']) + for i in range(fragment_count): + i_str = str(i) + + cipher_c1 = request.POST['cipher_c1_frag_' + i_str] + cipher_c2 = request.POST['cipher_c2_frag_' + i_str] + + encrypted_vote.fragment.create(encrypted_vote=encrypted_vote, + cipher_text_c1=cipher_c1, + cipher_text_c2=cipher_c2) + + ballot[0].cast = True + ballot[0].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) + else: + # The user has finished voting in the event + success_msg = 'You have successfully cast your vote(s)!' + messages.add_message(request, messages.SUCCESS, success_msg) + + return HttpResponseRedirect(reverse("user_home")) + + 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 }) + def event_trustee_setup(request, event_id): # Obtain the event and the event preparation access key that's been supplied event = get_object_or_404(Event, pk=event_id) @@ -178,68 +207,82 @@ def event_trustee_setup(request, event_id): # The event will now be ready to receive votes on the various polls that have been defined - # voters therefore need to be informed if event.trustee_keys.count() == event.users_trustees.count(): + create_ballots.delay(event) generate_combpk.delay(event) - # TODO: Create Celery task that generates voting URLs for voters as well as creates the ballots + email_voters_vote_url.delay(event.voters.all(), event) - success_msg = 'You have successfully submitted your public key for this event' + success_msg = 'You have successfully submitted your public key for this event!' messages.add_message(request, messages.SUCCESS, success_msg) - # This re-direct may not be appropriate for trustees that don't have logins return HttpResponseRedirect(reverse("user_home")) else: form = EventSetupForm() - return render(request, "polls/event_setup.html", {"event": event, "form": form}) + return render(request, "polls/event_setup.html", {"event": event, "form": form, "user_email": email_key[0].user.email}) #if no key or is invalid? messages.add_message(request, messages.WARNING, 'You do not have permission to access: ' + request.path) return HttpResponseRedirect(reverse("user_home")) -def event_addec(request, event_id): + +def event_end(request, event_id): event = get_object_or_404(Event, pk=event_id) - for poll in event.polls.all(): - generate_enc.delay(poll) - return HttpResponse("Generating enc.") + + if not event.ended: + event_ended.delay(event) + + # Mark the event as ended + event.ended = True + event.save() + + return HttpResponseRedirect(reverse('polls:view-event', args=[event_id])) + + +# Returns a JSONed version of the results +def results(request, event_id): + event = get_object_or_404(Event, pk=event_id) + polls = event.polls.all() + + results = "" + results += "{\"polls\":[" + for poll in polls: + results += poll.enc + + results += "]}" + + return HttpResponse(results) + def event_trustee_decrypt(request, event_id): event = get_object_or_404(Event, pk=event_id) access_key = request.GET.get('key', None) - if (access_key): - 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 (Decryption.objects.filter(event=event, user=email_key[0].user).exists()): - messages.add_message(request, messages.WARNING, 'You have already provided your decryptions for this event') - #if (event.decryptions.count() == (event.polls.count() * event.users_trustees.count())): - # tally_results.delay(event) # all keys are in - return HttpResponseRedirect(reverse("user_home")) - elif (request.method == "GET"): - initial = [] - for poll in event.polls.all(): - initial.append({'text': poll.enc }) - formset = DecryptionFormset(initial=initial) - else: - formset = DecryptionFormset(request.POST) - data = [] - for form in formset: - if form.is_valid(): - data.append(form.cleaned_data.get('text')) - if (len(data) == event.polls.count()): - for dec, poll in zip(data, event.polls.all()): - Decryption.objects.get_or_create(user=email_key[0].user, event=event, poll=poll, text=dec) - messages.add_message(request, messages.SUCCESS, 'Decryption complete.') - if (event.decryptions.count() == (event.polls.count() * event.users_trustees.count())): - tally_results.delay(event) # all keys are in - else: - messages.add_message(request, messages.ERROR, 'You didn\'t provide decryptions for every poll. Please try again.') - return HttpResponseRedirect(reverse("user_home")) - return render(request, "polls/event_decrypt.html", {"event": event, "formset": formset, "helper": DecryptionFormSetHelper() }) + if access_key: + 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') + 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}) + elif request.method == "POST": + sk = request.POST['secret-key'] + + TrusteeSK.objects.create(event=event, + trustee=email_key[0].user, + key=sk) + + 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) + + messages.add_message(request, messages.SUCCESS, 'Your secret key has been successfully submitted') + return HttpResponseRedirect(reverse("user_home")) + + # Without an access key, the client does not have permission to access this page messages.add_message(request, messages.WARNING, 'You do not have permission to decrypt this Event.') return HttpResponseRedirect(reverse("user_home")) -def test_poll_vote(request, poll_id): - poll = get_object_or_404(Poll, pk=poll_id) - form = VoteForm(instance=poll) - return render(request, "polls/vote_poll.html", {"vote_form": form, "poll": poll}) def manage_questions(request, event_id): @@ -262,7 +305,7 @@ def manage_questions(request, event_id): formset = OptionFormset(request.POST, prefix="formset_organiser", instance=poll) if formset.is_valid(): formset.save() - #create_ballots.delay(poll) + create_ballots_for_poll.delay(poll) messages.add_message(request, messages.SUCCESS, 'Poll created successfully') return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id])) @@ -274,6 +317,7 @@ def manage_questions(request, event_id): else: return HttpResponseNotAllowed() + def render_invalid(request, events, demo_users, invalid_fields): return render(request, "polls/create_event.html", @@ -285,6 +329,7 @@ def render_invalid(request, events, demo_users, invalid_fields): "invalid_fields": invalid_fields }) + def create_event(request): # Obtain context data for the rendering of the html template and validation events = Event.objects.all() @@ -347,6 +392,7 @@ def create_event(request): else: return HttpResponseNotAllowed() + def edit_event(request, event_id): event = get_object_or_404(Event, pk=event_id) if request.method == "GET": @@ -384,7 +430,6 @@ def edit_event(request, event_id): return render(request, "polls/generic_form.html", {"form_title": "Edit Event: " + event.title, "form": form}) #"organiser_formset": organiser_formset, "trustee_formset": trustee_formset}) #trustee_formset = TrusteeFormSet(request.POST, prefix="formset_trustee", instance=event) -#class CreatePoll(generic.View): def del_event(request, event_id): event = get_object_or_404(Event, pk=event_id) @@ -392,9 +437,4 @@ def del_event(request, event_id): return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.id}) elif request.method == "POST": event.delete() - return HttpResponseRedirect(reverse('polls:index')) - -def can_vote(user, event): - if event.voters.filter(email=user.email).exists(): - return True - return False \ No newline at end of file + return HttpResponseRedirect(reverse('polls:index')) \ No newline at end of file diff --git a/allauthdemo/templates/allauth/account/login.html b/allauthdemo/templates/allauth/account/login.html index 8960086..f19af77 100755 --- a/allauthdemo/templates/allauth/account/login.html +++ b/allauthdemo/templates/allauth/account/login.html @@ -14,7 +14,7 @@
- + {% bootstrap_messages %} {% if socialaccount_providers %}
{% include "allauth/account/provider_panel.html" with process="login" %} diff --git a/allauthdemo/templates/bases/bootstrap-jquery.html b/allauthdemo/templates/bases/bootstrap-jquery.html index 5ee398b..44dcc47 100755 --- a/allauthdemo/templates/bases/bootstrap-jquery.html +++ b/allauthdemo/templates/bases/bootstrap-jquery.html @@ -18,6 +18,7 @@ crossorigin="anonymous"> + @@ -74,15 +75,19 @@ //new function demosEncrypt.encryptAndSubmit = function() { - var ctx = new CTX("BN254CX"); //new context we can use + // Disable the enc and submit button to prevent fn from being called twice + $('#keygen-btn').prop("disabled", true); + + // Elliptic curve cryptography params used for encryption of encrypted vote + // fragments + var ctx = new CTX("BN254CX"); var n = new ctx.BIG(); var g1 = new ctx.ECP(); var g2 = new ctx.ECP2(); - var param = $('#event-param').val(); - //console.log(param); + var parameter = $('#event-param').val(); + var tempParams = JSON.parse(JSON.parse(parameter).crypto); - var tempParams = JSON.parse(param); //copying the values n.copy(tempParams.n); g1.copy(tempParams.g1); @@ -92,26 +97,55 @@ n:n, g1:g1, g2:g2 - } - - var tempPK = JSON.parse($('#comb_pk').val()); + }; + var tempPK = JSON.parse($('#comb_pk').val()); var pk = new ctx.ECP(0); pk.copy(tempPK.PK); - var answer = $('#poll-options').val(); - console.log(answer); - var cipher = encrypt(params, pk, answer); - - var c1Bytes = []; - cipher.C1.toBytes(c1Bytes); - var c2Bytes = []; - cipher.C2.toBytes(c2Bytes); - $('#id_cipher_text_c1').val(c1Bytes.toString()); - $('#id_cipher_text_c2').val(c2Bytes.toString()); + // Obtain the user's selection (their vote) and encrypt the fragments of the binary encoding + const selection = $('#poll-options').val(); + const selectionFragments = selection.split(','); + var cipherForm = document.getElementById("cipher-form"); + + for(var i = 0; i < selectionFragments.length; i++) { + // Encrypt this fragment for the selection + var cipher = encrypt(params, pk, parseInt(selectionFragments[i])); + + // Store C1 and C2 from the cipher in 2 arrays + var c1Bytes = []; + cipher.C1.toBytes(c1Bytes); + + var c2Bytes = []; + cipher.C2.toBytes(c2Bytes); + + // Inject hidden input controls into the form that represents a single ballot + var c1Input = document.createElement("input"); + c1Input.setAttribute("type", "hidden"); + c1Input.setAttribute("name", "cipher_c1_frag_" + i); + c1Input.setAttribute("value", c1Bytes.toString()); + + var c2Input = document.createElement("input"); + c2Input.setAttribute("type", "hidden"); + c2Input.setAttribute("name", "cipher_c2_frag_" + i); + c2Input.setAttribute("value", c2Bytes.toString()); + + cipherForm.appendChild(c1Input); + cipherForm.appendChild(c2Input); + } + + // Inject a final input control into the form which specifies the number of fragments + // That make up an encrypted vote + var fragCountInput = document.createElement("input"); + fragCountInput.setAttribute("type", "hidden"); + fragCountInput.setAttribute("name", "vote_frag_count"); + fragCountInput.setAttribute("value", "" + selectionFragments.length); + cipherForm.appendChild(fragCountInput); + + // Submit the encrypted vote to the server $('#cipher-form').submit(); - } + }; //new function @@ -149,7 +183,7 @@ //new function demosEncrypt.generateKeys = function() { - parameter = $("#event-param").val(); + var parameter = $("#event-param").val(); var tempParams = JSON.parse(JSON.parse(parameter).crypto); //the full objects need to be initalised as per the library, then copy the values we need into it //I follow Bingsheng's code as to what objects are used in the parameter object diff --git a/allauthdemo/templates/bases/bootstrap.html b/allauthdemo/templates/bases/bootstrap.html index aea4224..c26d9e8 100755 --- a/allauthdemo/templates/bases/bootstrap.html +++ b/allauthdemo/templates/bases/bootstrap.html @@ -7,7 +7,7 @@ - {% block title %}dẽmos 2{% endblock %} + {% block title %}DĒMOS 2{% endblock %} diff --git a/allauthdemo/templates/polls/event_decrypt.html b/allauthdemo/templates/polls/event_decrypt.html index 9178179..f1a55e1 100755 --- a/allauthdemo/templates/polls/event_decrypt.html +++ b/allauthdemo/templates/polls/event_decrypt.html @@ -7,24 +7,24 @@ {% block content %}
-

Event: {{event.title}}

-

Trustee Decrypt

+

Trustee Event Decryption for Event '{{ event.title }}'

+
-
Secret Key
+
Submit your Secret Key as '{{ user_email }}'
- -

Use your secret key to generate a decrypted cipher

- -
-
-
-
Encrypted Ciphers
-
- {% load crispy_forms_tags %} -
- {% crispy formset helper %} - -
+
+ {% csrf_token %} + + + + + +
diff --git a/allauthdemo/templates/polls/event_detail_launch.html b/allauthdemo/templates/polls/event_detail_advanced.html similarity index 100% rename from allauthdemo/templates/polls/event_detail_launch.html rename to allauthdemo/templates/polls/event_detail_advanced.html diff --git a/allauthdemo/templates/polls/event_detail_base.html b/allauthdemo/templates/polls/event_detail_base.html index 03e417f..ba7fe76 100755 --- a/allauthdemo/templates/polls/event_detail_base.html +++ b/allauthdemo/templates/polls/event_detail_base.html @@ -9,12 +9,22 @@ {% if is_organiser %}
-
+

Event: {{object.title}}

-
- +
+ {% if object.has_received_votes and object.ended == False %} + + End + + {% endif %} + {% if decrypted == True and object.ended == True %} + + Results + + {% endif %} + Edit
@@ -40,11 +50,11 @@ Polls ({{ object.polls.count }})
  • - Entities + Entities
  • {% if is_organiser %}
  • - Advanced + Advanced
  • {% endif %} diff --git a/allauthdemo/templates/polls/event_detail_organisers.html b/allauthdemo/templates/polls/event_detail_entities.html similarity index 100% rename from allauthdemo/templates/polls/event_detail_organisers.html rename to allauthdemo/templates/polls/event_detail_entities.html diff --git a/allauthdemo/templates/polls/event_detail_polls.html b/allauthdemo/templates/polls/event_detail_polls.html index 493901b..72fcdb5 100755 --- a/allauthdemo/templates/polls/event_detail_polls.html +++ b/allauthdemo/templates/polls/event_detail_polls.html @@ -6,7 +6,7 @@ {% block event_content %} {% if object.polls.all %} {% for poll in object.polls.all %} -

    Poll: {{ poll.question_text }} (Edit)

    +

    Poll: {{ poll.question_text }} (Edit)


    Poll Options:

      diff --git a/allauthdemo/templates/polls/event_list.html b/allauthdemo/templates/polls/event_list.html index 64b7296..aa85c1e 100755 --- a/allauthdemo/templates/polls/event_list.html +++ b/allauthdemo/templates/polls/event_list.html @@ -46,9 +46,13 @@
      + {% if event.status == 'Expired' %}btn-danger{% endif %} + {% if event.status == 'Ended' %}btn-danger{% endif %} + {% if event.status == 'Decrypted' %}btn-primary{% endif %} + "> {{ event.status }}
      diff --git a/allauthdemo/templates/polls/event_setup.html b/allauthdemo/templates/polls/event_setup.html index 886d224..e1d58c8 100755 --- a/allauthdemo/templates/polls/event_setup.html +++ b/allauthdemo/templates/polls/event_setup.html @@ -9,6 +9,8 @@

      Trustee Event Setup for Event '{{ event.title }}'


      +

      Key Generation For: {{ user_email }}

      +
      Step 1: Generate Your Secret Key
      diff --git a/allauthdemo/templates/polls/event_vote.html b/allauthdemo/templates/polls/event_vote.html new file mode 100755 index 0000000..257e0dd --- /dev/null +++ b/allauthdemo/templates/polls/event_vote.html @@ -0,0 +1,81 @@ +{% extends "bases/bootstrap-with-nav.html" %} +{% load staticfiles %} +{% load bootstrap3 %} + +{% block app_js_vars %} + var option_count = {{ object.options.count }}; +{% endblock %} + +{% block content %} + +
      + + + + +

      Event Voting Page for the Event '{{ object.event.title }}'

      + + Voting status: + {% if has_voted %} + Voted - Re-Submitting will Change your Vote + {% else %} + Not Voted + {% endif %} + +
      + Number of polls for this event: {{ poll_count }} +
      +
      + Instructions: + You will be shown each poll for this event one by one where you will need to make a selection for the current + poll before moving onto the next poll. For this specific poll you need to make a + minimum of {{ min_selection }} option selection(s) and a maximum of + {{ max_selection }}. Please make your choice below. + +
      + {% if prev_index %} + + + + {% endif %} + {% if next_index %} + + + + {% endif %} +
      + {% if object.options.all %} +

      Poll {{ poll_num }} of {{ poll_count }}: {{object.question_text}}

      + {% if can_vote %} + {% load crispy_forms_tags %} +
      +
      Options
      +
      + +
      + +
      + {% csrf_token %} +
      +
      +
      + {% else %} + + {% endif %} + {% else %} +

      No options are available.

      + {% endif %} +
      +
      +
      +{% endblock %} diff --git a/allauthdemo/templates/polls/poll_detail.html b/allauthdemo/templates/polls/poll_detail.html deleted file mode 100755 index 34501a6..0000000 --- a/allauthdemo/templates/polls/poll_detail.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "bases/bootstrap-with-nav.html" %} -{% load staticfiles %} -{% load bootstrap3 %} - -{% block app_js_vars %} - - - var option_count = {{ object.options.count }}; -{% endblock %} - -{% block content %} - -
      - - - -

      Poll: {{object.question_text}}

      - Poll {{ poll_num }} of {{ poll_count }} in Event: {{ object.event.title }} -
      - {% if prev_index %} - - - - {% endif %} - {% if next_index %} - - - - {% endif %} -
      - Edit Poll - {% if object.options.all %} -

      Options

      -

      {{ vote_count }} vote(s) have been cast

      - {% if can_vote %} - {% if has_voted %} -

      You have already voted in this poll. Resubmitting the form will change your vote.

      - {% endif %} -

      Voting as {{ voter_email }} -- Do NOT share this url

      - {% load crispy_forms_tags %} -
      -
      Options
      -
      - - - -
      - {% crispy form %} - {% csrf_token %} -
      -
      -
      - {% else %} - - {% endif %} - {% else %} -

      No options are available.

      - {% endif %} -
      -
      -
      -POLL ENC {{ object.enc }} - -{% if form.errors %} - {% for field in form %} - {% for error in field.errors %} -
      - {{ error|escape }} -
      - {% endfor %} - {% endfor %} - {% for error in form.non_field_errors %} -
      - {{ error|escape }} -
      - {% endfor %} -{% endif %} -{% endblock %} diff --git a/static/css/main.css b/static/css/main.css index 009d179..f07dd7c 100755 --- a/static/css/main.css +++ b/static/css/main.css @@ -157,7 +157,7 @@ input[type="file"] { /* Events List page / Events Detail */ .statusBtn { - width: 74px; + width: 89px; } .marginTopEventList { diff --git a/static/js/decrypt_event.js b/static/js/decrypt_event.js new file mode 100644 index 0000000..862acdf --- /dev/null +++ b/static/js/decrypt_event.js @@ -0,0 +1,20 @@ +function processFileSKChange(event) { + var files = event.target.files; + + if(files !== undefined + && files[0] !== undefined) { + var reader = new FileReader(); + + reader.onload = function(e) { + $('input#secret-key').val(reader.result); + }; + + reader.readAsText(files[0]); + } +} + +var filesHandleSK = document.getElementById('files_sk_upload'); + +if(filesHandleSK) { + filesHandleSK.addEventListener('change', processFileSKChange, false); +} \ No newline at end of file