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