Updated the voting and event decryption pages. On the voting page, added the ability to vote by selecting checkboxes instead of a drop down select menu. This adds flexibility at event creation to specify a wide range of min and max option selections for polls. On the event decryption page, SK validation was added based on the stored trustee PK as well as a dialog to display any validation errors. Most of the updates took place behind the scenes at the backend.
This commit is contained in:
parent
3fd9173666
commit
745fdf06b1
10 changed files with 503 additions and 152 deletions
|
@ -51,7 +51,7 @@ class Event(models.Model):
|
||||||
try:
|
try:
|
||||||
EID_json = json.loads(self.EID)
|
EID_json = json.loads(self.EID)
|
||||||
EID_crypto_str = EID_json['crypto']
|
EID_crypto_str = EID_json['crypto']
|
||||||
return json.loads(EID_crypto_str)
|
return json.dumps(json.loads(EID_crypto_str))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return "None - Event not Initialised"
|
return "None - Event not Initialised"
|
||||||
|
|
||||||
|
@ -213,8 +213,13 @@ class EncryptedVote(models.Model):
|
||||||
ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="encrypted_vote")
|
ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="encrypted_vote")
|
||||||
|
|
||||||
|
|
||||||
|
class CombinedEncryptedVote(models.Model):
|
||||||
|
ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="comb_encrypted_vote")
|
||||||
|
|
||||||
|
|
||||||
class VoteFragment(models.Model):
|
class VoteFragment(models.Model):
|
||||||
encrypted_vote = models.ForeignKey(EncryptedVote, on_delete=models.CASCADE, related_name="fragment")
|
encrypted_vote = models.ForeignKey(EncryptedVote, on_delete=models.CASCADE, related_name="fragment", null=True)
|
||||||
|
comb_encrypted_vote = models.ForeignKey(CombinedEncryptedVote, on_delete=models.CASCADE, related_name="fragment", null=True)
|
||||||
cipher_text_c1 = models.CharField(max_length=4096)
|
cipher_text_c1 = models.CharField(max_length=4096)
|
||||||
cipher_text_c2 = models.CharField(max_length=4096)
|
cipher_text_c2 = models.CharField(max_length=4096)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from celery import task
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from allauthdemo.polls.models import AccessKey, Ballot, CombinedBallot, PartialBallotDecryption
|
from allauthdemo.polls.models import AccessKey, Ballot, CombinedBallot, PartialBallotDecryption, EncryptedVote, CombinedEncryptedVote, VoteFragment
|
||||||
|
|
||||||
from .crypto_rpc import param, combpk, add_ciphers, get_tally
|
from .crypto_rpc import param, combpk, add_ciphers, get_tally
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ def email_trustees_dec(event):
|
||||||
|
|
||||||
trustee.send_email(email_subject, email_body)
|
trustee.send_email(email_subject, email_body)
|
||||||
|
|
||||||
|
|
||||||
def get_email_sign_off():
|
def get_email_sign_off():
|
||||||
sign_off = str("")
|
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 += "\n\nPlease note: This email address is not monitored so please don't reply to this email.\n\n"
|
||||||
|
@ -66,6 +67,7 @@ def get_email_sign_off():
|
||||||
|
|
||||||
return sign_off
|
return sign_off
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Combines all of the voter ballots for a poll option into a single 'CombinedBallot'
|
Combines all of the voter ballots for a poll option into a single 'CombinedBallot'
|
||||||
'''
|
'''
|
||||||
|
@ -83,7 +85,7 @@ def combine_ballots(polls):
|
||||||
frags_c2 = list()
|
frags_c2 = list()
|
||||||
|
|
||||||
for ballot in ballots:
|
for ballot in ballots:
|
||||||
enc_vote = ballot.encrypted_vote.get()
|
enc_vote = ballot.comb_encrypted_vote.get()
|
||||||
|
|
||||||
if enc_vote is not None:
|
if enc_vote is not None:
|
||||||
fragments = enc_vote.fragment.all()
|
fragments = enc_vote.fragment.all()
|
||||||
|
@ -102,6 +104,34 @@ def combine_ballots(polls):
|
||||||
cipher_text_c1=combined_cipher['C1'],
|
cipher_text_c1=combined_cipher['C1'],
|
||||||
cipher_text_c2=combined_cipher['C2'])
|
cipher_text_c2=combined_cipher['C2'])
|
||||||
|
|
||||||
|
@task()
|
||||||
|
def combine_encrypted_votes(voter, poll):
|
||||||
|
poll_options_count = poll.options.all().count()
|
||||||
|
ballot = Ballot.objects.get_or_create(voter=voter, poll=poll)[0]
|
||||||
|
e_votes = EncryptedVote.objects.filter(ballot=ballot)
|
||||||
|
|
||||||
|
CombinedEncryptedVote.objects.filter(ballot=ballot).delete()
|
||||||
|
comb_e_vote = CombinedEncryptedVote.objects.create(ballot=ballot)
|
||||||
|
|
||||||
|
for i in range(poll_options_count):
|
||||||
|
frags_c1 = list()
|
||||||
|
frags_c2 = list()
|
||||||
|
|
||||||
|
for e_vote in e_votes:
|
||||||
|
fragments = e_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)
|
||||||
|
VoteFragment.objects.create(comb_encrypted_vote=comb_e_vote,
|
||||||
|
cipher_text_c1=combined_cipher['C1'],
|
||||||
|
cipher_text_c2=combined_cipher['C2'])
|
||||||
|
|
||||||
@task()
|
@task()
|
||||||
def create_ballots(event):
|
def create_ballots(event):
|
||||||
voters = event.voters.all()
|
voters = event.voters.all()
|
||||||
|
|
|
@ -11,11 +11,11 @@ from django.views import generic
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm
|
from .forms import PollForm, OptionFormset, VoteForm, EventSetupForm, EventEditForm
|
||||||
from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, PartialBallotDecryption, CombinedBallot
|
from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, PartialBallotDecryption, CombinedBallot, VoteFragment
|
||||||
from allauthdemo.auth.models import DemoUser
|
from allauthdemo.auth.models import DemoUser
|
||||||
|
|
||||||
from .tasks import email_trustees_prep, update_EID, generate_combpk, event_ended, create_ballots
|
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 .tasks import create_ballots_for_poll, email_voters_vote_url, combine_decryptions_and_tally, combine_encrypted_votes
|
||||||
|
|
||||||
from .utils.EventModelAdaptor import EventModelAdaptor
|
from .utils.EventModelAdaptor import EventModelAdaptor
|
||||||
|
|
||||||
|
@ -153,39 +153,35 @@ def event_vote(request, event_id, poll_id):
|
||||||
cant_vote_reason = "The event either isn't ready for voting or it has expired and therefore you cannot vote."
|
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 request.method == "POST":
|
||||||
ballot = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)[0]
|
data = json.loads(request.POST.lists()[0][0])
|
||||||
|
ballot_json = data['ballot']
|
||||||
|
encrypted_votes_json = ballot_json['encryptedVotes']
|
||||||
|
|
||||||
|
# Before storing the encrypted votes, we need the voter's ballot
|
||||||
|
ballot, created = Ballot.objects.get_or_create(voter=email_key[0].user, poll=poll)
|
||||||
|
EncryptedVote.objects.filter(ballot=ballot).delete()
|
||||||
|
|
||||||
|
for e_vote in encrypted_votes_json:
|
||||||
# Will store the fragments of the encoding scheme that define the vote
|
# Will store the fragments of the encoding scheme that define the vote
|
||||||
encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot)[0]
|
encrypted_vote = EncryptedVote.objects.create(ballot=ballot)
|
||||||
|
fragments_json = e_vote['fragments']
|
||||||
|
|
||||||
# Clear any existing fragments - a voter changing their vote
|
for fragment in fragments_json:
|
||||||
encrypted_vote.fragment.all().delete()
|
VoteFragment.objects.create(encrypted_vote=encrypted_vote,
|
||||||
|
cipher_text_c1=fragment['C1'],
|
||||||
# Add in the new ciphers
|
cipher_text_c2=fragment['C2'])
|
||||||
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.cast = True
|
ballot.cast = True
|
||||||
ballot.save()
|
ballot.save()
|
||||||
|
|
||||||
|
combine_encrypted_votes.delay(email_key[0].user, poll)
|
||||||
|
|
||||||
if next_poll_uuid:
|
if next_poll_uuid:
|
||||||
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid,
|
return HttpResponseRedirect(reverse('polls:event-vote', kwargs={'event_id': event.uuid,
|
||||||
'poll_id': next_poll_uuid})
|
'poll_id': next_poll_uuid})
|
||||||
+ "?key=" + email_key_str)
|
+ "?key=" + email_key_str)
|
||||||
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 HttpResponse('Voted Successfully!')
|
||||||
|
|
||||||
return render(request, "polls/event_vote.html",
|
return render(request, "polls/event_vote.html",
|
||||||
{
|
{
|
||||||
|
@ -273,16 +269,20 @@ def event_trustee_decrypt(request, event_id):
|
||||||
|
|
||||||
if access_key:
|
if access_key:
|
||||||
email_key = event.keys.filter(key=access_key)
|
email_key = event.keys.filter(key=access_key)
|
||||||
|
trustee = email_key[0].user
|
||||||
|
|
||||||
if email_key.exists() and event.users_trustees.filter(email=email_key[0].user.email).exists():
|
if email_key.exists() and event.users_trustees.filter(email=trustee.email).exists():
|
||||||
|
|
||||||
if PartialBallotDecryption.objects.filter(event=event, user=email_key[0].user).count() == event.total_num_opts():
|
if PartialBallotDecryption.objects.filter(event=event, user=trustee).count() == event.total_num_opts():
|
||||||
|
|
||||||
warning_msg = 'You have already provided your decryption key for this event - Thank You'
|
warning_msg = 'You have already provided your decryption key for this event - Thank You'
|
||||||
messages.add_message(request, messages.WARNING, warning_msg)
|
messages.add_message(request, messages.WARNING, warning_msg)
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse("user_home"))
|
return HttpResponseRedirect(reverse("user_home"))
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
|
# Get the Trustee's original PK - used in the template for SK validation
|
||||||
|
trustee_pk = TrusteeKey.objects.get(event=event, user=trustee).key
|
||||||
|
|
||||||
# Gen a list of ciphers from the combined ballots for every opt of every poll
|
# Gen a list of ciphers from the combined ballots for every opt of every poll
|
||||||
polls = event.polls.all()
|
polls = event.polls.all()
|
||||||
poll_ciphers = []
|
poll_ciphers = []
|
||||||
|
@ -305,7 +305,8 @@ def event_trustee_decrypt(request, event_id):
|
||||||
"polls/event_decrypt.html",
|
"polls/event_decrypt.html",
|
||||||
{
|
{
|
||||||
"event": event,
|
"event": event,
|
||||||
"user_email": email_key[0].user.email,
|
"user_email": trustee.email,
|
||||||
|
"trustee_pk": trustee_pk,
|
||||||
"poll_ciphers": poll_ciphers
|
"poll_ciphers": poll_ciphers
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -318,7 +319,7 @@ def event_trustee_decrypt(request, event_id):
|
||||||
options_count = len(options)
|
options_count = len(options)
|
||||||
|
|
||||||
for j in range(options_count):
|
for j in range(options_count):
|
||||||
input_name = ""
|
input_name = str("")
|
||||||
input_name = "poll-" + str(i) + "-cipher-" + str(j)
|
input_name = "poll-" + str(i) + "-cipher-" + str(j)
|
||||||
|
|
||||||
part_dec = request.POST[input_name]
|
part_dec = request.POST[input_name]
|
||||||
|
@ -326,7 +327,7 @@ def event_trustee_decrypt(request, event_id):
|
||||||
PartialBallotDecryption.objects.create(event=event,
|
PartialBallotDecryption.objects.create(event=event,
|
||||||
poll=polls[i],
|
poll=polls[i],
|
||||||
option=options[j],
|
option=options[j],
|
||||||
user=email_key[0].user,
|
user=trustee,
|
||||||
text=part_dec)
|
text=part_dec)
|
||||||
|
|
||||||
if event.all_part_decs_received():
|
if event.all_part_decs_received():
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<script src="{% static 'js/papaparse.min.js' %}" type="text/javascript"></script>
|
<script src="{% static 'js/papaparse.min.js' %}" type="text/javascript"></script>
|
||||||
<script src="{% static 'js/create-event-poll.js' %}" type="text/javascript"></script>
|
<script src="{% static 'js/create-event-poll.js' %}" type="text/javascript"></script>
|
||||||
<script src="{% static 'js/decrypt_event.js' %}" type="text/javascript"></script>
|
<script src="{% static 'js/decrypt_event.js' %}" type="text/javascript"></script>
|
||||||
|
<script src="{% static 'js/event_vote.js' %}" type="text/javascript"></script>
|
||||||
<script src="{% static 'js/encrypt.js' %}" type="text/javascript"></script>
|
<script src="{% static 'js/encrypt.js' %}" type="text/javascript"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{% static 'js/core/rand.js' %}"></script>
|
<script type="text/javascript" src="{% static 'js/core/rand.js' %}"></script>
|
||||||
|
@ -64,113 +65,6 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Code written with "New function" comments have
|
|
||||||
been totally or mostly re-implemented by Thomas Smith
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
dropDownFragsNotZero = function(frags) {
|
|
||||||
var valid = false;
|
|
||||||
|
|
||||||
for(var i = 0; i < frags.length; i++) {
|
|
||||||
var frag = frags[i];
|
|
||||||
|
|
||||||
if(frag !== "0") {
|
|
||||||
valid = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid;
|
|
||||||
};
|
|
||||||
|
|
||||||
//new function
|
|
||||||
demosEncrypt.encryptAndSubmit = function() {
|
|
||||||
// Drop down option selection validation
|
|
||||||
if(min_selections === 1 && max_selections === 1) {
|
|
||||||
var fragments = $('#poll-options').val().split(",");
|
|
||||||
|
|
||||||
if(!dropDownFragsNotZero(fragments)) {
|
|
||||||
alert("You have to select an option in order to vote.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} // TODO: Checkbox validation goes here
|
|
||||||
|
|
||||||
// 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 parameter = $('#event-param').val();
|
|
||||||
var tempParams = JSON.parse(JSON.parse(parameter).crypto);
|
|
||||||
|
|
||||||
//copying the values
|
|
||||||
n.copy(tempParams.n);
|
|
||||||
g1.copy(tempParams.g1);
|
|
||||||
g2.copy(tempParams.g2);
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
n:n,
|
|
||||||
g1:g1,
|
|
||||||
g2:g2
|
|
||||||
};
|
|
||||||
|
|
||||||
var tempPK = JSON.parse($('#comb_pk').val());
|
|
||||||
var pk = new ctx.ECP(0);
|
|
||||||
pk.copy(tempPK.PK);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
};
|
|
||||||
|
|
||||||
function getBytes(arr) {
|
function getBytes(arr) {
|
||||||
for(var i = 0; i < arr.length; i++) {
|
for(var i = 0; i < arr.length; i++) {
|
||||||
arr[i] = parseInt(arr[i]);
|
arr[i] = parseInt(arr[i]);
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
{% if not forloop.first %},{% endif %}
|
{% if not forloop.first %},{% endif %}
|
||||||
{
|
{
|
||||||
title: "{{ event.title }}",
|
title: "{{ event.title }}",
|
||||||
slug: "{{ event.EID }}"
|
slug: "{{ event.EID_hr }}"
|
||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
@ -34,6 +34,7 @@
|
||||||
<!-- The DEMOS1 repository can be found at: https://github.com/mlevogiannis/demos-voting -->
|
<!-- The DEMOS1 repository can be found at: https://github.com/mlevogiannis/demos-voting -->
|
||||||
<!-- TODO: look into i18n translations as this was a feature implemented in DEMOS1 to enable automatic translations -->
|
<!-- TODO: look into i18n translations as this was a feature implemented in DEMOS1 to enable automatic translations -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Create New Event with Polls</h2>
|
<h2>Create New Event with Polls</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,13 @@
|
||||||
{% block sk-file-name %}{{ event.title|safe }}{% endblock %}
|
{% block sk-file-name %}{{ event.title|safe }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
// This is what we expect the SK supplied by the trustee to generate
|
||||||
|
var trustee_pk = "{{ trustee_pk }}";
|
||||||
|
|
||||||
|
var tempParams = "{{ event.EID_crypto|escapejs }}";
|
||||||
|
tempParams = JSON.parse(tempParams);
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Trustee Event Decryption for Event '{{ event.title }}'</h2>
|
<h2>Trustee Event Decryption for Event '{{ event.title }}'</h2>
|
||||||
|
@ -49,4 +56,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Information Dialog called upon request -->
|
||||||
|
<div class="modal fade" id="modalDialog" role="dialog">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
|
||||||
|
<!-- Dialog content-->
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h4 class="modal-title" style="text-align: center"><strong>Thank You</strong></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
{% block app_js_vars %}
|
{% block app_js_vars %}
|
||||||
var option_count = {{ object.options.count }};
|
const OPTION_COUNT = {{ object.options.count }};
|
||||||
var min_selections = {{ min_selection }};
|
const MIN_SELECTIONS = {{ min_selection }};
|
||||||
var max_selections = {{ max_selection }};
|
const MAX_SELECTIONS = {{ max_selection }};
|
||||||
|
var selectedCount = 0;
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% if can_vote %}
|
{% if can_vote %}
|
||||||
<!-- Hidden fields -->
|
<!-- Hidden fields -->
|
||||||
|
{% csrf_token %}
|
||||||
<input id="event-param" type="text" value="{{event.EID}}" hidden/>
|
<input id="event-param" type="text" value="{{event.EID}}" hidden/>
|
||||||
<input id="comb_pk" type="text" value="{{event.public_key}}" hidden/>
|
<input id="comb_pk" type="text" value="{{event.public_key}}" hidden/>
|
||||||
|
|
||||||
|
@ -37,7 +39,15 @@
|
||||||
You will be shown each poll for this event one by one where you will need to make a selection for the current
|
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. <strong>For this specific poll</strong> you need to make a <strong>
|
poll before moving onto the next poll. <strong>For this specific poll</strong> you need to make a <strong>
|
||||||
minimum</strong> of {{ min_selection }} option selection(s) and a <strong>maximum</strong> of
|
minimum</strong> of {{ min_selection }} option selection(s) and a <strong>maximum</strong> of
|
||||||
{{ max_selection }}. <br/><br/>Please make your choice below.
|
{{ max_selection }}.
|
||||||
|
|
||||||
|
{% if min_selection == 0 %}
|
||||||
|
<br/><br/>Due to the fact that you are permitted to select nothing, simply hitting submit will submit a
|
||||||
|
'blank' vote so please be aware of this. You can always re-visit this page before the event ends if you
|
||||||
|
change your mind.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<br/><br/>Please make your choice below.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Poll Voting Section -->
|
<!-- Poll Voting Section -->
|
||||||
|
@ -48,18 +58,30 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Options</strong></div>
|
<div class="panel-heading"><strong>Options</strong></div>
|
||||||
<div class="panel panel-body">
|
<div class="panel panel-body">
|
||||||
<select class="radio-inline select form-control" id="poll-options" name="options">
|
{% comment %}<select class="radio-inline select form-control" id="poll-options" name="options">
|
||||||
{% load custom_filters_tags %}
|
{% load custom_filters_tags %}
|
||||||
<option value="{{ -1|get_ballot_value:object.options.all.count }}">Please Select...</option>
|
<option value="{{ -1|get_ballot_value:object.options.all.count }}">Please Select...</option>
|
||||||
{% for option in object.options.all %}
|
{% for option in object.options.all %}
|
||||||
<option value="{{forloop.counter|get_ballot_value:object.options.all.count}}">{{ option.choice_text }}</option>
|
<option value="{{forloop.counter|get_ballot_value:object.options.all.count}}">{{ option.choice_text }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>{% endcomment %}
|
||||||
|
{% for option in object.options.all %}
|
||||||
|
<div class="checkbox">
|
||||||
|
{% load custom_filters_tags %}
|
||||||
|
<label><input type="checkbox" value="{{forloop.counter|get_ballot_value:object.options.all.count}}">{{ option.choice_text }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
<hr/>
|
<hr/>
|
||||||
<button id="keygen-btn" onclick="demosEncrypt.encryptAndSubmit()" class="btn btn-primary">Submit</button>
|
<div id="ballot-gen-progress-area">
|
||||||
<form id="cipher-form" method="post" action="" class="">
|
<button id="gen-ballots-btn" class="btn btn-primary">Generate Ballots</button>
|
||||||
{% csrf_token %}
|
<!-- Progress bar which is used during encryption -->
|
||||||
</form>
|
<h4 id="progress-bar-description" class="hidden">Generating Ballots...</h4>
|
||||||
|
<div id="progress-bar-container" class="progress hidden">
|
||||||
|
<div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
||||||
|
<span class="sr-only">70% Complete</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -78,6 +100,27 @@
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Information Dialog called upon request -->
|
||||||
|
<div class="modal fade" id="modalDialog" role="dialog">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
|
||||||
|
<!-- Dialog content-->
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h4 class="modal-title" style="text-align: center"><strong>Ballot</strong></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% else %} <!-- for: { if can_vote %} -->
|
{% else %} <!-- for: { if can_vote %} -->
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<p>{{ cant_vote_reason }}</p>
|
<p>{{ cant_vote_reason }}</p>
|
||||||
|
|
|
@ -179,3 +179,8 @@ input[type="file"] {
|
||||||
.overviewPadding {
|
.overviewPadding {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Voting page */
|
||||||
|
.big-checkbox {
|
||||||
|
width: 30px; height: 30px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,59 @@
|
||||||
|
//SK checking algorithm - If PK and SK matches, it returns True; otherwise, it returns false.
|
||||||
|
// Written by Bingsheng Zhang
|
||||||
|
function skCheck(ctx, params, SK, PK) {
|
||||||
|
var D = ctx.PAIR.G1mul(params.g1, SK);
|
||||||
|
return D.equals(PK)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSKFromString(SKStr) {
|
||||||
|
// Re-create the SK from the string byte definition
|
||||||
|
let ctx = new CTX("BN254CX");
|
||||||
|
|
||||||
|
let skBytes = SKStr.split(",");
|
||||||
|
let sk = new ctx.BIG.fromBytes(skBytes);
|
||||||
|
|
||||||
|
// Re-create the params
|
||||||
|
let n = new ctx.BIG();
|
||||||
|
let g1 = new ctx.ECP();
|
||||||
|
let g2 = new ctx.ECP2();
|
||||||
|
|
||||||
|
n.copy(tempParams.n);
|
||||||
|
g1.copy(tempParams.g1);
|
||||||
|
g2.copy(tempParams.g2);
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
n:n,
|
||||||
|
g1:g1,
|
||||||
|
g2:g2
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-create the trustee PK from the string byte definition
|
||||||
|
let pkBytes = trustee_pk.split(',').map(function(byteStr) {
|
||||||
|
return parseInt(byteStr)
|
||||||
|
});
|
||||||
|
|
||||||
|
let pk = new ctx.ECP.fromBytes(pkBytes);
|
||||||
|
|
||||||
|
// Check that the SK supplies generates the PK we know about
|
||||||
|
return skCheck(ctx, params, sk, pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDialog(titleTxt, bodyTxt) {
|
||||||
|
var modalDialog = $('#modalDialog');
|
||||||
|
var title = modalDialog.find('.modal-title');
|
||||||
|
var body = modalDialog.find('.modal-body');
|
||||||
|
|
||||||
|
title.text(titleTxt);
|
||||||
|
var bodyText = bodyTxt;
|
||||||
|
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.innerHTML = bodyText;
|
||||||
|
body.empty();
|
||||||
|
body.append( p );
|
||||||
|
|
||||||
|
modalDialog.modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
function processFileSKChange(event) {
|
function processFileSKChange(event) {
|
||||||
var files = event.target.files;
|
var files = event.target.files;
|
||||||
|
|
||||||
|
@ -6,7 +62,23 @@ function processFileSKChange(event) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
$('input#secret-key').val(reader.result);
|
var SKStr = reader.result;
|
||||||
|
|
||||||
|
// Check that the SK string is not blank
|
||||||
|
if(SKStr === '') {
|
||||||
|
// Show a dialog informing the user that they've uploaded a blank file
|
||||||
|
showDialog('Error', 'The file you have uploaded is blank.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = validateSKFromString(SKStr);
|
||||||
|
|
||||||
|
if(valid) {
|
||||||
|
$('input#secret-key').val(SKStr);
|
||||||
|
} else {
|
||||||
|
// Show a dialog informing the user that they've supplied an invalid SK
|
||||||
|
showDialog('Error',
|
||||||
|
'The secret key you have supplied is invalid and doesn\'t match with the recorded public key.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsText(files[0]);
|
reader.readAsText(files[0]);
|
||||||
|
|
273
static/js/event_vote.js
Normal file
273
static/js/event_vote.js
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
// This should stop people ticking more than the maximum permitted
|
||||||
|
function updateCheckboxInteractivity() {
|
||||||
|
var inputs = $("label input[type=checkbox]");
|
||||||
|
|
||||||
|
if(selectedCount === MAX_SELECTIONS) {
|
||||||
|
inputs.each(function() {
|
||||||
|
var input = $(this);
|
||||||
|
|
||||||
|
if(!input.prop('checked')){
|
||||||
|
input.prop('disabled', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
inputs.each(function() {
|
||||||
|
var input = $(this);
|
||||||
|
|
||||||
|
if(!input.prop('checked')) {
|
||||||
|
input.prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$("label input[type=checkbox]").change(function() {
|
||||||
|
// Increment the selectedCount counter if a box has been checked
|
||||||
|
if(this.checked) {
|
||||||
|
selectedCount += 1;
|
||||||
|
} else {
|
||||||
|
selectedCount -= 1;
|
||||||
|
|
||||||
|
// Just incase this falls below zero to avoid any nasty bugs
|
||||||
|
if(selectedCount < 0) {
|
||||||
|
selectedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCheckboxInteractivity();
|
||||||
|
});
|
||||||
|
|
||||||
|
function dropDownFragsNotZero(frags) {
|
||||||
|
var valid = false;
|
||||||
|
|
||||||
|
for(var i = 0; i < frags.length; i++) {
|
||||||
|
var frag = frags[i];
|
||||||
|
|
||||||
|
if(frag !== "0") {
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVotingInputValid() {
|
||||||
|
var valid = true;
|
||||||
|
|
||||||
|
// First establish if the user's selection count is valid
|
||||||
|
if(!(selectedCount >= MIN_SELECTIONS && selectedCount <= MAX_SELECTIONS)) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will highlight when people haven't selected enough options
|
||||||
|
|
||||||
|
if(!valid) {
|
||||||
|
var modalDialog = $('#modalDialog');
|
||||||
|
var title = modalDialog.find('.modal-title');
|
||||||
|
var body = modalDialog.find('.modal-body');
|
||||||
|
var errText = "You've only selected " + selectedCount
|
||||||
|
+ " option(s). The minimum number you need to select is " + MIN_SELECTIONS
|
||||||
|
+ " and the maximum is " + MAX_SELECTIONS + ". Please go back and correct this.";
|
||||||
|
|
||||||
|
title.text('Voting Error');
|
||||||
|
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.innerHTML = errText;
|
||||||
|
body.empty();
|
||||||
|
body.append( p );
|
||||||
|
|
||||||
|
modalDialog.modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a blank vote as a string using the binary encoding scheme
|
||||||
|
function genBlankVote() {
|
||||||
|
var vote = "";
|
||||||
|
|
||||||
|
for(var i = 0; i < OPTION_COUNT; i++) {
|
||||||
|
vote += "0";
|
||||||
|
|
||||||
|
if (i !== (OPTION_COUNT - 1)) {
|
||||||
|
vote += ",";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = 0;
|
||||||
|
var progressBar = document.getElementById("progress-bar");
|
||||||
|
|
||||||
|
// Based on the user's vote in the current poll, this generates a ballot which
|
||||||
|
// does not leak information about how many options the user has selected
|
||||||
|
function generateBallot() {
|
||||||
|
// 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 parameter = $('#event-param').val();
|
||||||
|
var tempParams = JSON.parse(JSON.parse(parameter).crypto);
|
||||||
|
|
||||||
|
//copying the values
|
||||||
|
n.copy(tempParams.n);
|
||||||
|
g1.copy(tempParams.g1);
|
||||||
|
g2.copy(tempParams.g2);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
n:n,
|
||||||
|
g1:g1,
|
||||||
|
g2:g2
|
||||||
|
};
|
||||||
|
|
||||||
|
var tempPK = JSON.parse($('#comb_pk').val());
|
||||||
|
var pk = new ctx.ECP(0);
|
||||||
|
pk.copy(tempPK.PK);
|
||||||
|
|
||||||
|
// Collect together the unencrypted votes (which correspond to selected options)
|
||||||
|
var checkboxInputs = $("label input[type=checkbox]");
|
||||||
|
var unencryptedVotes = [];
|
||||||
|
checkboxInputs.each(function() {
|
||||||
|
var checkbox = $(this);
|
||||||
|
|
||||||
|
// Push the selected option values to an array
|
||||||
|
if(checkbox.prop('checked')) {
|
||||||
|
unencryptedVotes.push(checkbox.val());
|
||||||
|
}
|
||||||
|
// For whatever hasn't been selected, push a blank vote to the array
|
||||||
|
else {
|
||||||
|
unencryptedVotes.push(genBlankVote());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encrypt all of the votes for this ballot
|
||||||
|
var encryptedVotes = [];
|
||||||
|
unencryptedVotes.forEach(function(unencryptedVote) {
|
||||||
|
var encFragments = [];
|
||||||
|
|
||||||
|
// Encrypt each fragment of the unencrypted vote
|
||||||
|
unencryptedVote.split(',').forEach(function(fragment) {
|
||||||
|
var cipher = encrypt(params, pk, parseInt(fragment));
|
||||||
|
|
||||||
|
// Store C1 and C2 from the cipher in the fragment
|
||||||
|
var c1Bytes = [];
|
||||||
|
cipher.C1.toBytes(c1Bytes);
|
||||||
|
|
||||||
|
var c2Bytes = [];
|
||||||
|
cipher.C2.toBytes(c2Bytes);
|
||||||
|
|
||||||
|
encFragments.push({
|
||||||
|
C1 : c1Bytes.toString(),
|
||||||
|
C2 : c2Bytes.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store all fragments in a single 'encrypted vote'
|
||||||
|
encryptedVotes.push({
|
||||||
|
fragments: encFragments
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var ballot = {
|
||||||
|
encryptedVotes: encryptedVotes
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return ballot;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#gen-ballots-btn').click(function() {
|
||||||
|
// Ensure that the user selections are valid
|
||||||
|
if(isVotingInputValid()) {
|
||||||
|
// Hide the button
|
||||||
|
$(this).toggleClass('hidden');
|
||||||
|
|
||||||
|
// Inject the description progress bar which can then be updated by the encrypt btn
|
||||||
|
$('#progress-bar-description').toggleClass('hidden');
|
||||||
|
$('#progress-bar-container').toggleClass('hidden');
|
||||||
|
|
||||||
|
setTimeout(generateBallotsAndShowUsr, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function voteSuccessfullyReceived() {
|
||||||
|
var modalDialog = $('#modalDialog');
|
||||||
|
var title = modalDialog.find('.modal-title');
|
||||||
|
var body = modalDialog.find('.modal-body');
|
||||||
|
|
||||||
|
title.text('Vote Successfully Received');
|
||||||
|
var bodyText = "Thank you for voting!";
|
||||||
|
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.innerHTML = bodyText;
|
||||||
|
body.empty();
|
||||||
|
body.append( p );
|
||||||
|
|
||||||
|
modalDialog.modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
var CSRF = $( "input[name='csrfmiddlewaretoken']" ).val();
|
||||||
|
function csrfSafeMethod(method) {
|
||||||
|
// these HTTP methods do not require CSRF protection
|
||||||
|
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendBallotToServer(ballot) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", CSRF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url : window.location,
|
||||||
|
data : JSON.stringify({ ballot: ballot}),
|
||||||
|
success : function(){
|
||||||
|
voteSuccessfullyReceived();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBallotB(ballotA) {
|
||||||
|
var ballotB = generateBallot();
|
||||||
|
progressBar.setAttribute("style", "width: 100%;");
|
||||||
|
|
||||||
|
var ballots = {
|
||||||
|
A : ballotA,
|
||||||
|
B : ballotB
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Implement ballot choice UI and QR func here. At the moment the code automatically
|
||||||
|
// TODO: submits the first ballot (as if the user selected it) to the server but this needs updating.
|
||||||
|
// TODO: Currently, there is a dialog already implemented in the event_vote.html page which is
|
||||||
|
// TODO: used for voting error information but could be used to display the ballot choices.
|
||||||
|
// This delay allows the execution thread to update the above CSS on the progress bar
|
||||||
|
var selectedBallot = ballots.A;
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
sendBallotToServer(selectedBallot);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBallotsAndShowUsr() {
|
||||||
|
// Generate Ballot A and Ballot B to be displayed to the user
|
||||||
|
// This fn starts the process
|
||||||
|
var ballotA = generateBallot();
|
||||||
|
|
||||||
|
// Update the progress bar once the generation has completed
|
||||||
|
progressBar.setAttribute("style", "width: 50%;");
|
||||||
|
|
||||||
|
// This delay allows the execution thread to update the above CSS on the progress bar
|
||||||
|
setTimeout(function () {
|
||||||
|
generateBallotB(ballotA);
|
||||||
|
}, 125);
|
||||||
|
}
|
Reference in a new issue