{% 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 %}