diff --git a/allauthdemo/polls/models.py b/allauthdemo/polls/models.py index 0cc0d7f..581e88e 100755 --- a/allauthdemo/polls/models.py +++ b/allauthdemo/polls/models.py @@ -51,7 +51,7 @@ class Event(models.Model): try: EID_json = json.loads(self.EID) EID_crypto_str = EID_json['crypto'] - return json.loads(EID_crypto_str) + return json.dumps(json.loads(EID_crypto_str)) except ValueError: 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") +class CombinedEncryptedVote(models.Model): + ballot = models.ForeignKey(Ballot, on_delete=models.CASCADE, related_name="comb_encrypted_vote") + + 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_c2 = models.CharField(max_length=4096) diff --git a/allauthdemo/polls/tasks.py b/allauthdemo/polls/tasks.py index db65344..ebe53ad 100755 --- a/allauthdemo/polls/tasks.py +++ b/allauthdemo/polls/tasks.py @@ -7,7 +7,7 @@ from celery import task 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 @@ -58,6 +58,7 @@ def email_trustees_dec(event): 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" @@ -66,6 +67,7 @@ def get_email_sign_off(): return sign_off + ''' 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() for ballot in ballots: - enc_vote = ballot.encrypted_vote.get() + enc_vote = ballot.comb_encrypted_vote.get() if enc_vote is not None: fragments = enc_vote.fragment.all() @@ -102,6 +104,34 @@ def combine_ballots(polls): cipher_text_c1=combined_cipher['C1'], 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() def create_ballots(event): voters = event.voters.all() diff --git a/allauthdemo/polls/views.py b/allauthdemo/polls/views.py index a528616..a6c8235 100755 --- a/allauthdemo/polls/views.py +++ b/allauthdemo/polls/views.py @@ -11,11 +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, PartialBallotDecryption, CombinedBallot +from .models import Event, Poll, Ballot, EncryptedVote, TrusteeKey, PartialBallotDecryption, CombinedBallot, VoteFragment from allauthdemo.auth.models import DemoUser 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 @@ -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." 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'] - # Will store the fragments of the encoding scheme that define the vote - encrypted_vote = EncryptedVote.objects.get_or_create(ballot=ballot)[0] + # 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() - # Clear any existing fragments - a voter changing their vote - encrypted_vote.fragment.all().delete() + for e_vote in encrypted_votes_json: + # Will store the fragments of the encoding scheme that define the vote + encrypted_vote = EncryptedVote.objects.create(ballot=ballot) + fragments_json = e_vote['fragments'] - # 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) + for fragment in fragments_json: + VoteFragment.objects.create(encrypted_vote=encrypted_vote, + cipher_text_c1=fragment['C1'], + cipher_text_c2=fragment['C2']) ballot.cast = True ballot.save() + combine_encrypted_votes.delay(email_key[0].user, poll) + 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)!' - messages.add_message(request, messages.SUCCESS, success_msg) - return HttpResponseRedirect(reverse("user_home")) + return HttpResponse('Voted Successfully!') return render(request, "polls/event_vote.html", { @@ -273,16 +269,20 @@ def event_trustee_decrypt(request, event_id): if 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' messages.add_message(request, messages.WARNING, warning_msg) return HttpResponseRedirect(reverse("user_home")) 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 polls = event.polls.all() poll_ciphers = [] @@ -305,7 +305,8 @@ def event_trustee_decrypt(request, event_id): "polls/event_decrypt.html", { "event": event, - "user_email": email_key[0].user.email, + "user_email": trustee.email, + "trustee_pk": trustee_pk, "poll_ciphers": poll_ciphers }) @@ -318,7 +319,7 @@ def event_trustee_decrypt(request, event_id): options_count = len(options) for j in range(options_count): - input_name = "" + input_name = str("") input_name = "poll-" + str(i) + "-cipher-" + str(j) part_dec = request.POST[input_name] @@ -326,7 +327,7 @@ def event_trustee_decrypt(request, event_id): PartialBallotDecryption.objects.create(event=event, poll=polls[i], option=options[j], - user=email_key[0].user, + user=trustee, text=part_dec) if event.all_part_decs_received(): diff --git a/allauthdemo/templates/bases/bootstrap-jquery.html b/allauthdemo/templates/bases/bootstrap-jquery.html index d49a34e..e19f8b0 100755 --- a/allauthdemo/templates/bases/bootstrap-jquery.html +++ b/allauthdemo/templates/bases/bootstrap-jquery.html @@ -19,6 +19,7 @@ + @@ -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) { for(var i = 0; i < arr.length; i++) { arr[i] = parseInt(arr[i]); diff --git a/allauthdemo/templates/polls/create_event.html b/allauthdemo/templates/polls/create_event.html index 3728649..b20b051 100755 --- a/allauthdemo/templates/polls/create_event.html +++ b/allauthdemo/templates/polls/create_event.html @@ -12,7 +12,7 @@ {% if not forloop.first %},{% endif %} { title: "{{ event.title }}", - slug: "{{ event.EID }}" + slug: "{{ event.EID_hr }}" } {% endfor %} ]; @@ -34,6 +34,7 @@
{{ cant_vote_reason }}
diff --git a/static/css/main.css b/static/css/main.css index f07dd7c..79df47c 100755 --- a/static/css/main.css +++ b/static/css/main.css @@ -178,4 +178,9 @@ input[type="file"] { .overviewPadding { padding-left: 16px; -} \ No newline at end of file +} + +/* Voting page */ +.big-checkbox { + width: 30px; height: 30px; +} diff --git a/static/js/decrypt_event.js b/static/js/decrypt_event.js index 862acdf..7e6768f 100644 --- a/static/js/decrypt_event.js +++ b/static/js/decrypt_event.js @@ -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) { var files = event.target.files; @@ -6,7 +62,23 @@ function processFileSKChange(event) { var reader = new FileReader(); 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]); diff --git a/static/js/event_vote.js b/static/js/event_vote.js new file mode 100644 index 0000000..634652e --- /dev/null +++ b/static/js/event_vote.js @@ -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); +} \ No newline at end of file