220 lines
9.5 KiB
HTML
220 lines
9.5 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{#
|
|
Eos - Verifiable elections
|
|
Copyright © 2017-18 RunasSudo (Yingtong Li)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#}
|
|
|
|
{% block title %}Ballot auditor{% endblock %}
|
|
|
|
{% block content %}
|
|
<h2>Ballot auditor</h2>
|
|
|
|
<div class="ui form">
|
|
<div class="field">
|
|
<label for="audit_election_url">Election URL</label>
|
|
<input id="audit_election_url" placeholder="https://example.com/01234567-89ab-cdef-ghij-klmnopqrstuv" type="text">
|
|
</div>
|
|
<div class="ui error message" id="audit_error_load">
|
|
<p>There was an error loading the election data. Please check the election URL and try again. If the problem persists, contact the election administrator.</p>
|
|
<p class="techdetails"></p>
|
|
</div>
|
|
<div class="field">
|
|
<label for="audit_ballot">Ballot data</label>
|
|
<textarea id="audit_ballot" placeholder='{"type": "eos.base.election.AuditBallot", "value": …}'></textarea>
|
|
</div>
|
|
<button class="ui primary button" onclick="loadElection();">Audit ballot</button>
|
|
</div>
|
|
<div class="ui hidden warning message" id="audit_contents">
|
|
<div class="header">Ballot contents</div>
|
|
<div id="audit_contents_inner"></div>
|
|
</div>
|
|
<div class="ui hidden negative message" id="audit_not_yet_cast">
|
|
<p>Your vote has <span class="superem">not</span> yet been cast. If you are satisfied with your ballot, you must return to the voting booth by closing this page and following the instructions.</p>
|
|
</div>
|
|
<div class="ui hidden message" id="audit_result">
|
|
<div class="header">Audit result</div>
|
|
<div id="audit_result_inner"></div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block basecontent %}
|
|
{{ super() }}
|
|
|
|
<script src="{{ url_for('static', filename='js/eosjs.js') }}"></script>
|
|
|
|
<script>
|
|
var election = null;
|
|
var auditBallot = null;
|
|
|
|
function loadElection() {
|
|
$.ajax({ url: $("#audit_election_url").val(), dataType: "text" })
|
|
.done(function(data) {
|
|
try {
|
|
election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json(data), null);
|
|
} catch (err) {
|
|
loadError(err);
|
|
throw err;
|
|
}
|
|
electionLoaded();
|
|
})
|
|
.fail(function(xhr, status, err) {
|
|
loadError(err);
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
function electionLoaded() {
|
|
$("#audit_error_load").removeClass("visible");
|
|
|
|
// Audit ballot
|
|
var auditResult = document.querySelector("#audit_result");
|
|
auditResult.className = "ui message";
|
|
|
|
var auditResultInner = document.querySelector("#audit_result_inner");
|
|
auditResultInner.innerHTML = '<p>Auditing your ballot… Please wait.</p>';
|
|
|
|
try {
|
|
var result = doAuditBallot(auditResult, auditResultInner);
|
|
if (result) {
|
|
auditResult.className = "ui success message";
|
|
} else {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> <b>The ballot has not been correctly prepared.</b></p>';
|
|
auditResult.className = "ui error message";
|
|
}
|
|
} catch (err) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Unknown error: ' + err + '</p>';
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> <b>The ballot has not been correctly prepared.</b></p>';
|
|
auditResult.className = "ui error message";
|
|
throw err;
|
|
} finally {
|
|
document.querySelector("#audit_not_yet_cast").className = "ui negative message";
|
|
}
|
|
}
|
|
|
|
function doAuditBallot(auditResult, auditResultInner) {
|
|
auditBallot = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json($("#audit_ballot").val()));
|
|
//auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> Data is in ballot format.</p>';
|
|
|
|
if (!eosjs.isinstance(auditBallot, eosjs.eos.base.election.__all__.Ballot)) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The data is not in ballot format.</p>';
|
|
return false;
|
|
}
|
|
if (!auditBallot.answers || !auditBallot.answers.__len__ || auditBallot.answers.__len__() === 0) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The data is not in audit ballot format.</p>';
|
|
return false;
|
|
}
|
|
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> The data is in audit ballot format.</p>';
|
|
|
|
if (auditBallot.election_id !== election._id) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The ballot corresponds to a different election.</p>';
|
|
return false;
|
|
}
|
|
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> The ballot election ID is correct.</p>';
|
|
|
|
if (auditBallot.election_hash !== eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64()) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The ballot corresponds to a different election.</p>';
|
|
return false;
|
|
}
|
|
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> The ballot election hash is correct.</p>';
|
|
|
|
for (var questionNum = 0; questionNum < auditBallot.encrypted_answers.__len__(); questionNum++) {
|
|
auditResultInner.innerHTML += '<p><i class="info circle icon"></i> Question number ' + (questionNum + 1) + ':</p>';
|
|
|
|
// Compute expected plaintexts
|
|
var pt = eosjs.eos.core.objects.__all__.EosObject.to_json(eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(auditBallot.answers.__getitem__(questionNum)));
|
|
var bs = eosjs.eos.psr.bitstream.__all__.BitStream();
|
|
bs.write_string(pt);
|
|
bs.multiple_of(election.public_key.nbits(), true);
|
|
|
|
var messages = [];
|
|
function callback(val) {
|
|
messages.push(val);
|
|
}
|
|
bs.map(callback, election.public_key.nbits());
|
|
|
|
var encryptedAnswer = auditBallot.encrypted_answers.__getitem__(questionNum);
|
|
|
|
for (var blockNum = 0; blockNum < encryptedAnswer.blocks.__len__(); blockNum++) {
|
|
var block = encryptedAnswer.blocks.__getitem__(blockNum);
|
|
|
|
// TODO: Implement this in Python
|
|
|
|
if (!block.randomness) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' ciphertext is not a valid audit ciphertext.</p>';
|
|
return false;
|
|
}
|
|
|
|
if (!block.is_signature_valid()) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' signature is not valid.</p>';
|
|
return false;
|
|
}
|
|
|
|
if (block.randomness.__lt__(eosjs.eos.core.bigint.__all__.ONE) || block.randomness.__gt__(election.public_key.group.p.__sub__(eosjs.eos.core.bigint.__all__.TWO))) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' randomness is not valid.</p>';
|
|
return false;
|
|
}
|
|
|
|
if (!block.is_randomness_valid()) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' randomness does not match ciphertext.</p>';
|
|
return false;
|
|
}
|
|
|
|
if (!block.m0.__eq__(election.public_key.message_to_m0(messages[blockNum]))) {
|
|
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' plaintext does not match claimed plaintext.</p>';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> Question number ' + (questionNum + 1) + ' passed validation.</p>';
|
|
}
|
|
|
|
// Passed validation
|
|
|
|
var auditContents = document.querySelector("#audit_contents");
|
|
auditContents.className = "ui warning message";
|
|
|
|
var auditContentsInner = document.querySelector("#audit_contents_inner");
|
|
auditContentsInner.innerHTML = '';
|
|
|
|
auditContentsInner.innerHTML += '<p><i class="info circle icon"></i> <b>Please check that the following details match your intended selections:</b></p>';
|
|
for (var questionNum = 0; questionNum < auditBallot.encrypted_answers.__len__(); questionNum++) {
|
|
auditContentsInner.innerHTML += '<p><i class="icon"></i> Question ' + (questionNum + 1) + ': ' + election.questions.__getitem__(questionNum).pretty_answer(auditBallot.answers.__getitem__(questionNum)) + '</p>';
|
|
}
|
|
|
|
auditContentsInner.innerHTML += '<p><i class="info circle icon"></i> <b>Please check that the ballot fingerprint you recorded matches the following computed ballot fingerprint: <span class="hash">' + eosjs.eos.core.hashing.__all__.SHA256().update_obj(auditBallot).hash_as_b64() + '</span>.</b></p>';
|
|
auditContentsInner.innerHTML += '<p><i class="checkmark icon"></i> If the selections are correct, and the ballot fingerprint matches, then the ballot has been prepared correctly.</p>';
|
|
|
|
return true;
|
|
}
|
|
|
|
function loadError(err) {
|
|
if (err) {
|
|
$("#audit_error_load .techdetails").text("Technical details: " + err);
|
|
$("#audit_error_load .techdetails").show();
|
|
} else {
|
|
$("#audit_error_load .techdetails").hide();
|
|
}
|
|
$("#audit_error_load").addClass("visible");
|
|
}
|
|
|
|
if (location.search.indexOf("electionUrl=") >= 0) {
|
|
var electionUrl = (location.search.split('electionUrl=')[1]||'').split('&')[0];
|
|
$("#audit_election_url").val(electionUrl);
|
|
}
|
|
</script>
|
|
{% endblock %}
|