Full end-to-end voting is working using the new binary encoding scheme, ballot combination and trustee partial decryption with tallies working perfectly. This required updating the Node server as well as Django models and views to support this. Emails to voters and trustees have also been updated to be more informative and look more professional. It could probably do at this point with using email templates and in the future HTML emails.

This commit is contained in:
vince0656 2018-07-11 14:25:36 +01:00
parent 571cd723bc
commit 5b746ad406
14 changed files with 631 additions and 555 deletions

View file

@ -3,7 +3,7 @@ from django.contrib import admin
# Register your models here.
from allauthdemo.auth.models import DemoUser
from .models import Event, PollOption, Poll, Organiser
from .models import Event, PollOption, Poll
"""

View file

@ -3,11 +3,12 @@ import urllib2
'''
All functions in this file have been re-implemenented by Thomas Smith
All functions in this file have been re-implemenented by Vincent de Almeida
File then updated by Vincent de Almeida. Changes include:
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
-Modified RPC calls that send data to POST requests to avoid large query URLs (using a helper function)
-Added a new cipher combination and tally function
'''
@ -39,58 +40,24 @@ def combpk(pks):
return send_post_req(url, data)
def addec(amount, ciphers):
url = 'http://localhost:8080/addec'
querystring = '?number='+str(amount)
c1s = ciphers['c1s']
c2s = ciphers['c2s']
for i, value in enumerate(c1s):
querystring += "&C1="+str(c1s[i])
querystring += "&C2="+str(c2s[i])
def add_ciphers(ciphers):
url = 'http://localhost:8080/add_ciphers'
print(url+querystring)
jsondict = json.load(urllib2.urlopen(url+querystring))
print(json.dumps(jsondict))
return json.dumps(jsondict)
# 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 += '&param='+urllib2.quote(str(group_param))
for i, value in enumerate(decs):
querystring += "&decs="+str(value)
querystring += '&cipher=' + urllib2.quote(str(cipher))
jsondict = json.load(urllib2.urlopen(url+querystring))
return str(jsondict['M'])
def combine_sks(sks):
url = 'http://localhost:8080/comb_sks'
# Construct POST data
data = {}
data['SKs'] = sks
data['ciphers'] = ciphers
# Return the new combined SK
return send_post_req(url, data)
return json.loads(send_post_req(url, data))
def get_tally(count, ciphers, sk, group_param):
def get_tally(ballot_cipher, part_decs, group_param, voters_count):
url = 'http://localhost:8080/get_tally'
# Construct POST data
data = {}
data['count'] = count
data['ciphers'] = ciphers
data['sk'] = sk
data['ballot_cipher'] = ballot_cipher
data['part_decs'] = part_decs
data['param'] = group_param
data['voters_count'] = voters_count
# Return the tally of votes for the option
return send_post_req(url, data)

View file

@ -11,7 +11,7 @@ from crispy_forms.layout import LayoutObject, Layout, TEMPLATE_PACK, Fieldset, B
from crispy_forms.bootstrap import StrictButton, TabHolder, Tab, FormActions, PrependedText, PrependedAppendedText, Accordion, AccordionGroup
from captcha.fields import ReCaptchaField
from allauthdemo.auth.models import DemoUser
from .models import Event, Poll, PollOption, Organiser
from .models import Event, Poll, PollOption
def is_valid_email(email):
try:

View file

@ -1,6 +1,7 @@
from __future__ import unicode_literals
import json
import uuid
from django.core.mail import send_mail
from django.db import models
@ -8,6 +9,7 @@ from django.utils import timezone
from allauthdemo.auth.models import DemoUser
class EmailUser(models.Model):
email = models.CharField(max_length=80, unique=True)
@ -35,16 +37,23 @@ class Event(models.Model):
creator = models.CharField(max_length=256, blank=True)
c_email = models.CharField(max_length=512, blank=True)
trustees = models.CharField(max_length=4096)
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Custom helper methods
def EID_hr(self):
EID_json = json.loads(self.EID)
return EID_json['hr']
try:
EID_json = json.loads(self.EID)
return EID_json['hr']
except ValueError:
return self.EID
def EID_crypto(self):
EID_json = json.loads(self.EID)
EID_crypto_str = EID_json['crypto']
return json.loads(EID_crypto_str)
try:
EID_json = json.loads(self.EID)
EID_crypto_str = EID_json['crypto']
return json.loads(EID_crypto_str)
except ValueError:
return "None - Event not Initialised"
def duration(self):
duration_str = self.start_time_formatted()
@ -63,26 +72,55 @@ class Event(models.Model):
def end_time_formatted_utc(self):
return self.end_time.strftime("%d-%m-%y %H:%M %Z")
# Total number of options in all polls
def total_num_opts(self):
polls = self.polls.all()
count = 0
for poll in polls:
count += poll.options.all().count()
return count
def total_num_partial_decs(self):
polls = self.polls.all()
count = 0
for poll in polls:
count += PartialBallotDecryption.objects.filter(poll=poll).count()
return count
def all_part_decs_received(self):
received = False
if self.total_num_partial_decs() == self.total_num_opts() * self.users_trustees.all().count():
received = True
return received
def status(self):
status_str = ""
# Get the current date and time to compare against to establish if this is a past, current or
# future event
# future event. Prepared means the public key has been initialised
present = timezone.now()
if self.ended is False:
if present < self.start_time and self.public_key is None:
if present < self.start_time and self.prepared is False:
status_str = "Future"
elif present < self.start_time and self.public_key is not None:
elif present < self.start_time and self.prepared is True:
status_str = "Prepared"
elif present >= self.start_time and present <= self.end_time and self.public_key is not None:
elif present >= self.start_time and present <= self.end_time and self.prepared is True:
status_str = "Active"
elif present > self.end_time and self.public_key is not None:
elif present >= self.start_time and present <= self.end_time and self.prepared is False:
status_str = "Future"
elif present > self.end_time:
status_str = "Expired"
else:
if self.event_sk.all().count() == 1:
if self.all_part_decs_received():
status_str = "Decrypted"
elif self.event_sk.all().count() == 0:
else:
status_str = "Ended"
return status_str
@ -109,13 +147,12 @@ class TrusteeKey(models.Model):
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_keys")
key = models.CharField(max_length=255, unique=True)
class AccessKey(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="keys")
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="keys")
key = models.CharField(max_length=255, unique=True)
#total = models.IntegerField(blank=True, null=True, default=0)
def has_started(self):
return timezone.now() >= self.start
@ -125,60 +162,60 @@ class AccessKey(models.Model):
def __unicode__(self):
return self.title
class Poll(models.Model):
question_text = models.CharField(max_length=200)
total_votes = models.IntegerField(default=0)
min_num_selections = models.IntegerField(default=0)
max_num_selections = models.IntegerField(default=1)
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="polls")
enc = models.CharField(max_length=4096, null=True)
#index = models.IntegerField()
combined_ballots = models.CharField(max_length=4096, null=True)
result_json = models.CharField(max_length=4096, null=True)
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
def __str__(self):
return self.question_text
class PollOption(models.Model):
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
question = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="options")
#index = models.IntegerField()
def __str__(self):
return self.choice_text
class Decryption(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="decryptions")
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="decryptions")
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="decryptions")
text = models.CharField(max_length=1024)
class TrusteeSK(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="trustee_sk")
trustee = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="trustee_sk")
key = models.CharField(max_length=1024)
class CombinedBallot(models.Model):
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="combined_ballot")
option = models.ForeignKey(PollOption, on_delete=models.CASCADE, related_name="combined_ballot")
cipher_text_c1 = models.CharField(max_length=4096)
cipher_text_c2 = models.CharField(max_length=4096)
# A partial decryption supplied by a trustee for a combined ballot that relates to a poll option
class PartialBallotDecryption(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="decryption")
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="decryption")
option = models.ForeignKey(PollOption, on_delete=models.CASCADE, related_name="decryption")
user = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="decryption")
text = models.CharField(max_length=4096)
class EventSK(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="event_sk")
key = models.CharField(max_length=1024)
class Ballot(models.Model):
voter = models.ForeignKey(EmailUser, on_delete=models.CASCADE, related_name="ballots")
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="ballots")
cast = models.BooleanField(default=False)
# Implements the new binary encoding scheme
class EncryptedVote(models.Model):
ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="encrypted_vote")
class VoteFragment(models.Model):
encrypted_vote = models.ForeignKey(EncryptedVote, on_delete=models.CASCADE, related_name="fragment")
cipher_text_c1 = models.CharField(max_length=4096)
cipher_text_c2 = models.CharField(max_length=4096)
class Organiser(models.Model):
index = models.IntegerField(default=0)
email = models.CharField(max_length=100, blank=False, null=False)
event = models.ForeignKey(Event, on_delete=models.CASCADE)

View file

@ -7,9 +7,9 @@ from celery import task
from django.conf import settings
from allauthdemo.polls.models import AccessKey, Ballot, Decryption, TrusteeSK, EventSK
from allauthdemo.polls.models import AccessKey, Ballot, CombinedBallot, PartialBallotDecryption
from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks
from .crypto_rpc import param, combpk, add_ciphers, get_tally
'''
Goal: This py file defines celery tasks that can be initiated
@ -22,28 +22,85 @@ from .crypto_rpc import param, combpk, addec, tally, get_tally, combine_sks
# Will store the result of the initial cal to param() from .cpp_calls
group_param = None
'''
Helper functions
gen_access_key - Will generate an a key for accessing either the event preparation page, voting page and decryption page
email_trustees_dec - Will email trustees a link to begin decrypting the event
'''
def gen_access_key():
return base64.urlsafe_b64encode(urandom(16)).decode('utf-8')
def email_trustees_dec(event):
email_subject = "Event Ballot Decryption for '" + event.title + "'"
# Plain text email - this could be replaced for a HTML-based email in the future
email_body = "Please visit the following URL to submit your trustee secret key to begin event decryption:\n\n"
email_body_base = str("")
email_body_base += "Dear Trustee,\n\n"
email_body_base += "You're now required to decrypt the event: " + event.title + \
". This will require uploading your secret key that you have previously backed up.\n\n"
email_body_base += "Please visit the following URL to submit your trustee secret key to begin event decryption:\n\n"
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/decrypt/?key="
email_body = email_body + url_base
email_body_base += url_base
sign_off = get_email_sign_off()
for trustee in event.users_trustees.all():
# Generate a key and create an AccessKey object
key = gen_access_key()
AccessKey.objects.create(user=trustee, event=event, key=key)
trustee.send_email(email_subject, email_body + key)
email_body = str(email_body_base + key)
email_body += sign_off
trustee.send_email(email_subject, email_body)
def get_email_sign_off():
sign_off = str("")
sign_off += "\n\nPlease note: This email address is not monitored so please don't reply to this email.\n\n"
sign_off += "Kind Regards,\n"
sign_off += "DEMOS 2 Admin - Lancaster University"
return sign_off
'''
Combines all of the voter ballots for a poll option into a single 'CombinedBallot'
'''
def combine_ballots(polls):
for poll in polls:
options = poll.options.all()
opt_count = len(options)
ballots = Ballot.objects.filter(poll=poll)
for i in range(opt_count):
option = options[i]
# Collect all fragments for this opt
frags_c1 = list()
frags_c2 = list()
for ballot in ballots:
enc_vote = ballot.encrypted_vote.get()
if enc_vote is not None:
fragments = enc_vote.fragment.all()
frags_c1.append(fragments[i].cipher_text_c1)
frags_c2.append(fragments[i].cipher_text_c2)
ciphers = {
'c1s': frags_c1,
'c2s': frags_c2
}
combined_cipher = add_ciphers(ciphers)
CombinedBallot.objects.create(poll=poll,
option=option,
cipher_text_c1=combined_cipher['C1'],
cipher_text_c2=combined_cipher['C2'])
@task()
def create_ballots(event):
@ -53,6 +110,7 @@ def create_ballots(event):
for voter in voters:
ballot = poll.ballots.create(voter=voter, poll=poll)
@task()
def create_ballots_for_poll(poll):
for voter in poll.event.voters.all():
@ -67,16 +125,29 @@ def email_trustees_prep(trustees, event):
email_subject = "Key Generation and Preparation for Event '" + event.title + "'"
# Plain text email - this could be replaced for a HTML-based email in the future
email_body = "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n"
email_body_base = str("")
email_body_base += "Dear Trustee,\n\n"
email_body_base += "You have been enrolled as a trustee onto the event: " + event.title + \
". You are required to visit the URL below to generate your secret key and associated public" \
" key that will be used to encrypt the event.\n\n You will need to ensure that you back up" \
" your secret key as this will be needed to decrypt the event - please don't lose this as it" \
" cannot be re-generated. DEMOS2 will never and cannot store your secret key.\n\n"
email_body_base += "Please visit the following URL to prepare the event and generate your trustee secret key:\n\n"
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/prepare/?key="
email_body = email_body + url_base
email_body_base += url_base
sign_off = get_email_sign_off()
for trustee in trustees:
# Generate a key and create an AccessKey object
key = gen_access_key()
AccessKey.objects.create(user=trustee, event=event, key=key)
trustee.send_email(email_subject, email_body + key)
email_body = str(email_body_base + key)
email_body += sign_off
trustee.send_email(email_subject, email_body)
'''
Emails a URL containing an access key for all of the voters for an event
@ -86,13 +157,18 @@ def email_voters_vote_url(voters, event):
email_subject = "Voting Access for Event '" + event.title + "'"
# Plain text email - this could be replaced for a HTML-based email in the future
email_body_base = "Please visit the following URL in order to vote on the event '" + event.title + "':\n\n"
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/poll/1/vote/?key="
email_body_base = email_body_base + url_base
# TODO: The URL needs updating and it could be replaced with a single UUID that's unique
# TODO: for the voter for an event which would shorten the URL
email_body_base = str("")
email_body_base += "Dear Voter,\n\n"
email_body_base += "You have been enrolled as a voter onto the event: " + event.title + ".\n\nYou can vote between the following dates and times:\n"
email_body_base += "Start: " + event.start_time_formatted_utc() + "\n"
email_body_base += "End: " + event.end_time_formatted_utc() + "\n\n"
email_body_base += "Please visit the following URL in order to vote on the event where further instructions can be found on the page:\n\n"
url_base = "http://" + settings.DOMAIN + "/event/" + str(event.pk) + "/poll/" + str(event.polls.all()[0].uuid) + "/vote/?key="
email_body_base += url_base
duration_info = "\n\nYou can vote between the following dates and times:\n"
duration_info = duration_info + "Start: " + event.start_time_formatted_utc() + "\n"
duration_info = duration_info + "End: " + event.end_time_formatted_utc()
sign_off = get_email_sign_off()
for voter in voters:
# Generate a key and create an AccessKey object
@ -101,10 +177,11 @@ def email_voters_vote_url(voters, event):
# Update the email body to incl the access key as well as the duration information
email_body = str(email_body_base + key)
email_body = email_body + duration_info
email_body += sign_off
voter.send_email(email_subject, email_body)
'''
Updates the EID of an event to contain 2 event IDs: a human readable one (hr) and a crypto one (GP from param())
'''
@ -120,95 +197,16 @@ def update_EID(event):
event.EID = json.dumps(EID)
event.save()
@task()
def event_ended(event):
# Email all trustees to request their secret keys
# Combine all the ballots for every option in every poll which will be decrypted by the trustees
polls = event.polls.all()
combine_ballots(polls)
# Email all trustees to request their partial decryptions using their secret keys
email_trustees_dec(event)
@task()
def gen_event_sk_and_dec(event):
trustee_sks = TrusteeSK.objects.filter(event=event)
t_sks_count = len(trustee_sks)
# Combine SKs if there's more than one
event_sk = None
if t_sks_count == 1:
event_sk = trustee_sks.get().key
else:
t_sks_str_list = list()
for t_sk in trustee_sks:
t_sks_str_list.append(t_sk.key)
event_sk = combine_sks(t_sks_str_list)
EventSK.objects.create(event=event, key=event_sk)
# With the event sk created, we can decrypt the event
decrypt_and_tally(event)
@task()
def decrypt_and_tally(event):
polls = event.polls.all()
sk = EventSK.objects.filter(event=event).get().key
for i in range(len(polls)):
poll = polls[i]
result = str("")
result += "{\"name\": \"" + poll.question_text + "\","
# get num of opts and ballots
options = poll.options.all()
opt_count = len(options)
ballots = Ballot.objects.filter(poll=poll, cast=True)
result += "\"options\": ["
for j in range(opt_count):
# Collect all fragments for this opt
frags_c1 = list()
frags_c2 = list()
for ballot in ballots:
enc_vote = ballot.encrypted_vote.get()
if enc_vote is not None:
fragments = enc_vote.fragment.all()
frags_c1.append(fragments[j].cipher_text_c1)
frags_c2.append(fragments[j].cipher_text_c2)
ciphers = {
'c1s': frags_c1,
'c2s': frags_c2
}
count = len(frags_c1)
votes = get_tally(count, ciphers, sk, event.EID)
result += "{\"option\": \"" + str(options[j].choice_text) + "\", \"votes\": " + str(votes) + "}"
if j != (opt_count-1):
result += ","
result += "]}"
if i != (len(polls) - 1):
result += ","
poll.enc = result
poll.save()
@task()
def tally_results(event):
for poll in event.polls.all():
decs = list()
for dec in poll.decryptions.all():
decs.append(dec.text)
amount = len(decs)
result = tally(amount, event.EID, decs, poll.enc)
# TODO: Email organisers using email_user method?
print(poll.question_text + ": " + result)
@task()
def generate_combpk(event):
@ -222,25 +220,54 @@ def generate_combpk(event):
event.prepared = True
event.save()
@task
def generate_enc(poll):
# c1 and c2 components of ciphertexts
c1s = list()
c2s = list()
for ballot in poll.ballots.all():
if ballot.cast:
c1s.append(str(ballot.cipher_text_c1))
c2s.append(str(ballot.cipher_text_c2))
@task()
def combine_decryptions_and_tally(event):
polls = event.polls.all()
polls_count = len(polls)
ciphers = {
'c1s': c1s,
'c2s': c2s
}
for i in range(polls_count):
poll = polls[i]
result = str("")
result += "{\"name\": \"" + poll.question_text + "\","
count = len(c1s)
options = poll.options.all()
opt_count = len(options)
result += "\"options\": ["
for j in range(opt_count):
option = options[j]
poll.enc = addec(count, ciphers)
poll.save()
# Find the combined ballot for the current option of the current poll
# and then extract the C1 and C2 components of the cipher that contains the tally
combined_ballot = CombinedBallot.objects.filter(poll=poll,
option=option)[0]
ballot_cipher = {}
ballot_cipher['C1'] = combined_ballot.cipher_text_c1
ballot_cipher['C2'] = combined_ballot.cipher_text_c2
# Collect all the partial decryptions for the ballot cipher which will decrypt the result
part_decs = PartialBallotDecryption.objects.filter(event=event,
poll=poll,
option=option)
part_decs_text = list()
for part_dec in part_decs:
part_decs_text.append(part_dec.text)
# Get the vote tally for this option and add it to the results
voters_count = event.voters.all().count()
votes = get_tally(ballot_cipher, part_decs_text, event.EID, voters_count)
result += "{\"option\": \"" + str(option.choice_text) + "\", \"votes\": \"" + str(votes) + "\"}"
if j != (opt_count-1):
result += ","
result += "]}"
if i != (polls_count - 1):
result += ","
poll.result_json = result
poll.save()

View file

@ -8,17 +8,17 @@ app_name = 'polls'
urlpatterns = [
url(r'^$', login_required(views.EventListView.as_view()), name='index'),
url(r'^create/$', login_required(views.create_event), name='create-event'),
url(r'^(?P<pk>[0-9]+)/$', login_required(views.EventDetailView.as_view()), name='view-event'),
url(r'^(?P<pk>[0-9]+)/polls/$', login_required(views.EventDetailPollsView.as_view()), name='event-polls'),
url(r'^(?P<pk>[0-9]+)/entities/$', login_required(views.EventDetailEntitiesView.as_view()), name='event-entities'),
url(r'^(?P<pk>[0-9]+)/advanced/$', login_required(views.EventDetailAdvancedView.as_view()), name='event-advanced'),
url(r'^(?P<event_id>[0-9]+)/end/$', login_required(views.event_end), name='end-event'),
url(r'^(?P<event_id>[0-9]+)/results/$', login_required(views.results), name='event-results'),
url(r'^(?P<event_id>[0-9]+)/edit/$', login_required(views.edit_event), name='edit-event'),
url(r'^(?P<event_id>[0-9]+)/delete/$', login_required(views.del_event), name='del-event'),
url(r'^(?P<event_id>[0-9]+)/decrypt/$', views.event_trustee_decrypt, name='decrypt-event'),
url(r'^(?P<event_id>[0-9]+)/prepare/$', views.event_trustee_setup, name='prepare-event'),
url(r'^(?P<event_id>[0-9]+)/poll/(?P<poll_num>[0-9]+)/vote/$', views.event_vote, name='event-vote'),
url(r'^(?P<event_id>[0-9]+)/create/poll/$', login_required(views.manage_questions), name='create-poll'),
url(r'^(?P<event_id>[0-9]+)/poll/(?P<poll_num>[0-9]+)/edit$', login_required(views.edit_poll), name='edit-poll')
url(r'^(?P<pk>[0-9a-f-]+)/$', login_required(views.EventDetailView.as_view()), name='view-event'),
url(r'^(?P<pk>[0-9a-f-]+)/polls/$', login_required(views.EventDetailPollsView.as_view()), name='event-polls'),
url(r'^(?P<pk>[0-9a-f-]+)/entities/$', login_required(views.EventDetailEntitiesView.as_view()), name='event-entities'),
url(r'^(?P<pk>[0-9a-f-]+)/advanced/$', login_required(views.EventDetailAdvancedView.as_view()), name='event-advanced'),
url(r'^(?P<event_id>[0-9a-f-]+)/end/$', login_required(views.event_end), name='end-event'),
url(r'^(?P<event_id>[0-9a-f-]+)/results/$', login_required(views.results), name='event-results'),
url(r'^(?P<event_id>[0-9a-f-]+)/edit/$', login_required(views.edit_event), name='edit-event'),
url(r'^(?P<event_id>[0-9a-f-]+)/delete/$', login_required(views.del_event), name='del-event'),
url(r'^(?P<event_id>[0-9a-f-]+)/decrypt/$', views.event_trustee_decrypt, name='decrypt-event'),
url(r'^(?P<event_id>[0-9a-f-]+)/prepare/$', views.event_trustee_setup, name='prepare-event'),
url(r'^(?P<event_id>[0-9a-f-]+)/poll/(?P<poll_id>[0-9a-f-]+)/vote/$', views.event_vote, name='event-vote'),
url(r'^(?P<event_id>[0-9a-f-]+)/create/poll/$', login_required(views.manage_questions), name='create-poll'),
url(r'^(?P<event_id>[0-9a-f-]+)/poll/(?P<poll_id>[0-9a-f-]+)/edit$', login_required(views.edit_poll), name='edit-poll')
]

View file

@ -11,10 +11,11 @@ from django.views import generic
from django.conf import settings
from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm
from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, TrusteeSK
from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, PartialBallotDecryption, CombinedBallot
from allauthdemo.auth.models import DemoUser
from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots, create_ballots_for_poll, email_voters_vote_url, gen_event_sk_and_dec
from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots
from .tasks import create_ballots_for_poll, email_voters_vote_url, combine_decryptions_and_tally
from .utils.EventModelAdaptor import EventModelAdaptor
@ -25,17 +26,16 @@ class EventListView(generic.ListView):
def get_context_data(self, **kwargs):
context = super(EventListView, self).get_context_data(**kwargs)
#context['now'] = timezone.now()
return context
class EventDetailView(generic.DetailView):
template_name="polls/event_detail_details.html"
template_name = "polls/event_detail_details.html"
model = Event
def get_context_data(self, **kwargs):
context = super(EventDetailView, self).get_context_data(**kwargs)
context['is_organiser'] = ((not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists()))
context['is_organiser'] = (not self.request.user.is_anonymous()) and (self.object.users_organisers.filter(email=self.request.user.email).exists())
context['decrypted'] = self.object.status() == "Decrypted"
return context
@ -63,20 +63,13 @@ class PollDetailView(generic.View):
return context
def util_get_poll_by_event_index(event, poll_num):
try:
poll_num = int(poll_num)
if ((poll_num < 1) or (poll_num > event.polls.all().count())):
return None
poll = event.polls.filter().order_by('id')[poll_num-1] # index field eventually
except ValueError:
return None
return poll
def util_get_poll_by_event_index(event, poll_id):
return event.polls.get(uuid=poll_id)
def edit_poll(request, event_id, poll_num):
def edit_poll(request, event_id, poll_id):
event = get_object_or_404(Event, pk=event_id)
poll = util_get_poll_by_event_index(event, poll_num)
poll = util_get_poll_by_event_index(event, poll_id)
if (poll == None):
raise Http404("Poll does not exist")
@ -98,33 +91,50 @@ def edit_poll(request, event_id, poll_num):
return HttpResponseRedirect(reverse('polls:event-polls', args=[poll.event_id]))
def event_vote(request, event_id, poll_num):
def event_vote(request, event_id, poll_id):
event = get_object_or_404(Event, pk=event_id)
if not event.prepared:
messages.add_message(request, messages.WARNING, "This Event isn\'t ready for voting yet.")
return HttpResponseRedirect(reverse("user_home"))
event_poll_count = event.polls.all().count()
prev_poll_index, next_poll_index = False, False
can_vote, has_voted, voter_email = False, False, ""
poll = util_get_poll_by_event_index(event, poll_num)
# Lookup the specified poll
poll = event.polls.get(uuid=poll_id)
if poll is None:
messages.add_message(request, messages.ERROR, "There was an error loading the voting page.")
return HttpResponseRedirect(reverse("user_home"))
poll_num = int(poll_num) # now known to be safe as it succeeded in the util function
polls = event.polls.all()
event_poll_count = len(polls)
prev_poll_uuid, next_poll_uuid, poll_num = False, False, 0
can_vote, cant_vote_reason, has_voted, voter_email = False, "", False, ""
if poll_num > 1:
prev_poll_index = (poll_num - 1)
if poll_num < event_poll_count:
next_poll_index = (poll_num + 1)
for i in range(event_poll_count):
poll = polls[i]
poll_uuid = str(poll.uuid)
req_poll_uuid = str(poll_id)
if poll_uuid == req_poll_uuid:
poll_num = str(i+1)
# If current voting request isn't for the last poll, then make sure we link to the next
if i != event_poll_count - 1:
# Only set the previous poll's uuid if we're not looking at the first poll
if i != 0:
prev_poll_uuid = str(polls[i - 1].uuid)
next_poll_uuid = str(polls[i + 1].uuid)
else:
if i != 0:
prev_poll_uuid = str(polls[i - 1].uuid)
break
access_key = request.GET.get('key', None)
email_key = event.keys.filter(key=access_key)
email_key_str = email_key[0].key
ballot = None
if email_key.exists() and event.voters.filter(email=email_key[0].user.email).exists():
# Passing this test means the user can vote
voter_email = email_key[0].user.email
@ -135,15 +145,18 @@ def event_vote(request, event_id, poll_num):
if ballot.exists() and ballot[0].cast:
has_voted = True
else:
messages.add_message(request, messages.ERROR, "You don\'t have permission to vote in this event.")
return HttpResponseRedirect(reverse("user_home"))
can_vote = False
cant_vote_reason = "You don't have permission to access this page."
if event.status() != "Active":
can_vote = False
cant_vote_reason = "The event either isn't ready for voting or it has expired and therefore you cannot vote."
if request.method == "POST":
if ballot is None:
ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)
ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)[0]
# Will store the fragments of the encoding scheme that define the vote
encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot[0])[0]
encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot)[0]
# Clear any existing fragments - a voter changing their vote
encrypted_vote.fragment.all().delete()
@ -160,11 +173,13 @@ def event_vote(request, event_id, poll_num):
cipher_text_c1=cipher_c1,
cipher_text_c2=cipher_c2)
ballot[0].cast = True
ballot[0].save()
ballot.cast = True
ballot.save()
if next_poll_index:
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.id, 'poll_num': next_poll_index }) + "?key=" + email_key[0].key)
if next_poll_uuid:
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid,
'poll_id': next_poll_uuid})
+ "?key=" + email_key_str)
else:
# The user has finished voting in the event
success_msg = 'You have successfully cast your vote(s)!'
@ -175,9 +190,9 @@ def event_vote(request, event_id, poll_num):
return render(request, "polls/event_vote.html",
{
"object": poll, "poll_num": poll_num, "event": event, "poll_count": event.polls.all().count(),
"prev_index": prev_poll_index, "next_index": next_poll_index, "min_selection": poll.min_num_selections,
"max_selection": poll.max_num_selections, "can_vote": can_vote, "voter_email": voter_email,
"has_voted": has_voted
"prev_uuid": prev_poll_uuid, "next_uuid": next_poll_uuid, "min_selection": poll.min_num_selections,
"max_selection": poll.max_num_selections, "can_vote": can_vote, "cant_vote_reason": cant_vote_reason,
"voter_email": voter_email, "has_voted": has_voted, "a_key": email_key_str
})
@ -245,7 +260,7 @@ def results(request, event_id):
results = ""
results += "{\"polls\":["
for poll in polls:
results += poll.enc
results += poll.result_json
results += "]}"
@ -260,21 +275,63 @@ def event_trustee_decrypt(request, event_id):
email_key = event.keys.filter(key=access_key)
if email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists():
if TrusteeSK.objects.filter(event=event, trustee=email_key[0].user).exists():
messages.add_message(request, messages.WARNING, 'You have already provided your decryption key for this event')
if PartialBallotDecryption.objects.filter(event=event, user=email_key[0].user).count() == event.total_num_opts():
warning_msg = 'You have already provided your decryption key for this event - Thank You'
messages.add_message(request, messages.WARNING, warning_msg)
return HttpResponseRedirect(reverse("user_home"))
elif request.method == "GET":
return render(request, "polls/event_decrypt.html", {"event": event, "user_email": email_key[0].user.email})
# Gen a list of ciphers from the combined ballots for every opt of every poll
polls = event.polls.all()
poll_ciphers = []
for poll in polls:
options = poll.options.all()
options_ciphers = []
for option in options:
combined_ballot = CombinedBallot.objects.filter(poll=poll, option=option).get()
cipher = {}
cipher['C1'] = combined_ballot.cipher_text_c1
cipher['C2'] = combined_ballot.cipher_text_c2
options_ciphers.append(cipher)
poll_ciphers.append(options_ciphers)
return render(request,
"polls/event_decrypt.html",
{
"event": event,
"user_email": email_key[0].user.email,
"poll_ciphers": poll_ciphers
})
elif request.method == "POST":
sk = request.POST['secret-key']
polls = event.polls.all()
polls_count = len(polls)
TrusteeSK.objects.create(event=event,
trustee=email_key[0].user,
key=sk)
for i in range(polls_count):
options = polls[i].options.all()
options_count = len(options)
if event.trustee_sk.count() == event.users_trustees.count():
# Generate the event SK and decrypt the event to tally the results
gen_event_sk_and_dec.delay(event)
for j in range(options_count):
input_name = ""
input_name = "poll-" + str(i) + "-cipher-" + str(j)
part_dec = request.POST[input_name]
PartialBallotDecryption.objects.create(event=event,
poll=polls[i],
option=options[j],
user=email_key[0].user,
text=part_dec)
if event.all_part_decs_received():
# TODO: Combine partial decryptions and gen results
combine_decryptions_and_tally.delay(event)
messages.add_message(request, messages.SUCCESS, 'Your secret key has been successfully submitted')
return HttpResponseRedirect(reverse("user_home"))
@ -434,7 +491,7 @@ def edit_event(request, event_id):
def del_event(request, event_id):
event = get_object_or_404(Event, pk=event_id)
if request.method == "GET":
return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.id})
return render(request, "polls/del_event.html", {"event_title": event.title, "event_id": event.uuid})
elif request.method == "POST":
event.delete()
return HttpResponseRedirect(reverse('polls:index'))