diff --git a/eos/base/election.py b/eos/base/election.py index b30cd0d..0d7bc7e 100644 --- a/eos/base/election.py +++ b/eos/base/election.py @@ -32,10 +32,12 @@ class NullEncryptedAnswer(EncryptedAnswer): class Ballot(EmbeddedObject): #_id = UUIDField() encrypted_answers = EmbeddedObjectListField() + election_id = UUIDField() + election_hash = StringField() class Vote(EmbeddedObject): ballot = EmbeddedObjectField() - cast_at = StringField() + cast_at = DateTimeField() class Voter(EmbeddedObject): _id = UUIDField() diff --git a/eos/base/tests.py b/eos/base/tests.py index 03a6436..3306d2b 100644 --- a/eos/base/tests.py +++ b/eos/base/tests.py @@ -81,6 +81,8 @@ class ElectionTestCase(EosTestCase): except Exception: pass + election_hash = SHA256().update_obj(election).hash_as_b64() + # Open voting self.do_task_assert(election, 'eos.base.workflow.TaskOpenVoting', 'eos.base.workflow.TaskCloseVoting') election.save() @@ -89,12 +91,12 @@ class ElectionTestCase(EosTestCase): VOTES = [[[0], [0]], [[0, 1], [1]], [[2], [0]]] for i in range(3): - ballot = Ballot() + ballot = Ballot(election_id=election._id, election_hash=election_hash) for j in range(2): answer = ApprovalAnswer(choices=VOTES[i][j]) encrypted_answer = NullEncryptedAnswer(answer=answer) ballot.encrypted_answers.append(encrypted_answer) - vote = Vote(ballot=ballot) + vote = Vote(ballot=ballot, cast_at=DateTimeField.now()) election.voters[i].votes.append(vote) election.save() diff --git a/eos/base/workflow.py b/eos/base/workflow.py index f42b5a1..ec69fac 100644 --- a/eos/base/workflow.py +++ b/eos/base/workflow.py @@ -29,6 +29,7 @@ class WorkflowTask(EmbeddedObject): provides = [] status = IntField(default=0, is_hashed=False) + exited_at = DateTimeField(is_hashed=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -81,7 +82,7 @@ class WorkflowTask(EmbeddedObject): listener() def on_exit(self): - pass + self.exited_at = DateTimeField.now() def exit(self): if self.status is not WorkflowTask.Status.ENTERED: diff --git a/eos/core/bigint/js.py b/eos/core/bigint/js.py index e8a90d9..d58d744 100644 --- a/eos/core/bigint/js.py +++ b/eos/core/bigint/js.py @@ -76,7 +76,6 @@ class BigInt(EosObject): setattr(self, key, make_operator_func(func)) for key, func in [ - ('__eq__', lambda x: x == 0), ('__ne__', lambda x: x != 0), ('__lt__', lambda x: x < 0), ('__gt__', lambda x: x > 0), @@ -104,6 +103,12 @@ class BigInt(EosObject): def __str__(self): return str(self.impl) + # TNYI: Transcrypt doesn't like that we've defined __eq__ in EosObject + def __eq__(self, other): + if not isinstance(other, BigInt): + other = BigInt(other) + return self.impl.compareTo(other.impl) == 0 + def __int__(self): # WARNING: This will yield unexpected results for large numbers return int(str(self.impl)) diff --git a/eos/core/objects/__init__.py b/eos/core/objects/__init__.py index 86ead73..077b290 100644 --- a/eos/core/objects/__init__.py +++ b/eos/core/objects/__init__.py @@ -31,6 +31,7 @@ if is_python: from bson.binary import UUIDLegacy import base64 + from datetime import datetime import hashlib import json import uuid @@ -119,6 +120,37 @@ if is_python: else: UUIDField = PrimitiveField +class DateTimeField(Field): + def pad(self, number): + if number < 10: + return '0' + str(number) + return str(number) + + def serialise(self, value, for_hash=False, should_protect=False): + if value is None: + return None + + if is_python: + return value.strftime('%Y-%m-%dT%H:%M:%SZ') + else: + return value.getUTCFullYear() + '-' + self.pad(value.getUTCMonth() + 1) + '-' + self.pad(value.getUTCDate()) + 'T' + self.pad(value.getUTCHours()) + ':' + self.pad(value.getUTCMinutes()) + ':' + self.pad(value.getUTCSeconds()) + 'Z' + + def deserialise(self, value): + if value is None: + return None + + if is_python: + return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') + else: + return Date.parse(value) + + @staticmethod + def now(): + if is_python: + return datetime.utcnow() + else: + return __pragma__('js', '{}', 'new Date()') + # Objects # ======= diff --git a/eos/psr/tests.py b/eos/psr/tests.py index 418806b..f9cbff3 100644 --- a/eos/psr/tests.py +++ b/eos/psr/tests.py @@ -16,7 +16,7 @@ from eos.core.tests import * -from eos.core.objects import __pragma__ +from eos.core.objects import * from eos.core.bigint import * from eos.core.hashing import * from eos.psr.bitstream import * @@ -26,6 +26,8 @@ from eos.psr.mixnet import * from eos.psr.secretsharing import * from eos.psr.workflow import * +from eos.core.objects import __pragma__ + class GroupValidityTestCase(EosTestCase): # HAC 4.24 def miller_rabin_test(self, n, t): @@ -252,6 +254,8 @@ class ElectionTestCase(EosTestCase): # Freeze election self.do_task_assert(election, 'eos.base.workflow.TaskConfigureElection', 'eos.base.workflow.TaskOpenVoting') + election_hash = SHA256().update_obj(election).hash_as_b64() # Keep track of the hash and make sure it doesn't change + # Open voting self.do_task_assert(election, 'eos.base.workflow.TaskOpenVoting', 'eos.base.workflow.TaskCloseVoting') election.save() @@ -260,12 +264,12 @@ class ElectionTestCase(EosTestCase): VOTES = [[[0], [0]], [[0, 1], [1]], [[2], [0]]] for i in range(3): - ballot = Ballot() + ballot = Ballot(election_id=election._id, election_hash=election_hash) for j in range(2): answer = ApprovalAnswer(choices=VOTES[i][j]) encrypted_answer = BlockEncryptedAnswer.encrypt(election.sk.public_key, answer) ballot.encrypted_answers.append(encrypted_answer) - vote = Vote(ballot=ballot) + vote = Vote(ballot=ballot, cast_at=DateTimeField.now()) election.voters[i].votes.append(vote) election.save() @@ -301,6 +305,9 @@ class ElectionTestCase(EosTestCase): # Release result self.do_task_assert(election, 'eos.base.workflow.TaskReleaseResults', None) election.save() + + # Check the hash hasn't changed during that + self.assertEqual(SHA256().update_obj(election).hash_as_b64(), election_hash) class PVSSTestCase(EosTestCase): @py_only diff --git a/eosweb/core/main.py b/eosweb/core/main.py index 1132f57..b08bdfa 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -17,6 +17,7 @@ import click import flask +from eos.core.objects import * from eos.base.election import * from eos.psr.crypto import * from eos.psr.election import * @@ -170,7 +171,7 @@ def election_api_cast_vote(election): # Cast the vote ballot = EosObject.deserialise_and_unwrap(data['ballot']) - vote = Vote(ballot=ballot, cast_at=datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')) + vote = Vote(ballot=ballot, cast_at=DateTimeField.now()) voter.votes.append(vote) election.save() diff --git a/eosweb/core/templates/election/view.html b/eosweb/core/templates/election/view.html index c31e45c..a169fe3 100644 --- a/eosweb/core/templates/election/view.html +++ b/eosweb/core/templates/election/view.html @@ -37,13 +37,13 @@

Voting in this election {% if election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status == Status.EXITED %} - opened + opened at {{ election.workflow.get_task('eos.base.workflow.TaskOpenVoting').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC {% else %} is scheduled to open {% endif %} at the administrators' discretion, and {% if election.workflow.get_task('eos.base.workflow.TaskCloseVoting').status == Status.EXITED %} - closed + closed at {{ election.workflow.get_task('eos.base.workflow.TaskCloseVoting').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC {% else %} is scheduled to close {% endif %} @@ -58,6 +58,8 @@ {% if election.workflow.get_task('eos.base.workflow.TaskReleaseResults').status == Status.EXITED %}

Results

+

Results were released at {{ election.workflow.get_task('eos.base.workflow.TaskReleaseResults').exited_at.strftime('%Y-%m-%d %H:%M:%S') }} UTC.

+ {% for question in election.questions %}

{{ loop.index }}. {{ question.prompt }}

{% include eosweb.core.main.model_view_map[question.__class__]['result_raw'] %}