diff --git a/eos/base/election.py b/eos/base/election.py index ea07bd1..1149b66 100644 --- a/eos/base/election.py +++ b/eos/base/election.py @@ -34,6 +34,16 @@ class Ballot(EmbeddedObject): encrypted_answers = EmbeddedObjectListField() election_id = UUIDField() election_hash = StringField() + + answers = EmbeddedObjectListField(is_hashed=False) + + def deaudit(self): + encrypted_answers_deaudit = EosList() + + for i in range(len(self.encrypted_answers)): + encrypted_answers_deaudit.append(self.encrypted_answers[i].deaudit()) + + return Ballot(encrypted_answers=encrypted_answers_deaudit, election_id=self.election_id, election_hash=self.election_hash) class Vote(EmbeddedObject): ballot = EmbeddedObjectField() @@ -96,7 +106,7 @@ class ApprovalQuestion(Question): max_choices = IntField() def pretty_answer(self, answer): - return ', '.join([self.choices[choice] for choice in answer.choices]) + return ', '.join([self.choices[answer.choices[i]] for i in range(len(answer.choices))]) class ApprovalAnswer(Answer): choices = ListField(IntField()) @@ -107,7 +117,7 @@ class PreferentialQuestion(Question): max_choices = IntField() def pretty_answer(self, answer): - return ', '.join([self.choices[choice] for choice in answer.choices]) + return ', '.join([self.choices[answer.choices[i]] for i in range(len(answer.choices))]) class PreferentialAnswer(Answer): choices = ListField(IntField()) diff --git a/eos/core/bigint/js.py b/eos/core/bigint/js.py index d58d744..7ca157d 100644 --- a/eos/core/bigint/js.py +++ b/eos/core/bigint/js.py @@ -130,6 +130,8 @@ class BigInt(EosObject): @classmethod def deserialise(cls, value): + if value is None: + return None return cls(value) # Returns a random BigInt from lower_bound to upper_bound, both inclusive diff --git a/eos/core/bigint/python.py b/eos/core/bigint/python.py index 0238cf6..268dca1 100644 --- a/eos/core/bigint/python.py +++ b/eos/core/bigint/python.py @@ -51,6 +51,8 @@ class BigInt(EosObject): @classmethod def deserialise(cls, value): + if value is None: + return None return cls(value) # Returns a random BigInt from lower_bound to upper_bound, both inclusive diff --git a/eos/psr/crypto.py b/eos/psr/crypto.py index 685596e..c333958 100644 --- a/eos/psr/crypto.py +++ b/eos/psr/crypto.py @@ -56,36 +56,48 @@ class EGPublicKey(EmbeddedObject): return self.group.q.nbits() - 1 # HAC 8.18 - def _encrypt(self, message): + def _encrypt(self, message, randomness=None): if message <= ZERO: raise Exception('Invalid message') if message >= self.group.p: raise Exception('Invalid message') - # Choose an element 1 <= k <= p - 2 - k = BigInt.crypto_random(ONE, self.group.p - TWO) + if randomness is None: + # Choose an element 1 <= k <= p - 2 + k = BigInt.crypto_random(ONE, self.group.p - TWO) + else: + k = randomness gamma = pow(self.group.g, k, self.group.p) delta = (message * pow(self.X, k, self.group.p)) % self.group.p - return EGCiphertext(public_key=self, gamma=gamma, delta=delta) + return EGCiphertext(public_key=self, gamma=gamma, delta=delta, m0=message, randomness=k) # Adida 2008 - def encrypt(self, message): + def message_to_m0(self, message): + m0 = message + ONE + + if pow(m0, self.group.q, self.group.p) == ONE: + # m0 is already in G_q + return m0 + else: + # For the life of me I can't find any reputable references for this aside from Adida 2008... + m0 = (-m0) % self.group.p + return m0 + + def m0_to_message(self, m0): + if m0 < self.group.q: + return m0 - ONE + else: + return ((-m0) % self.group.p) - ONE + + def encrypt(self, message, randomness=None): if message < ZERO: raise Exception('Invalid message') if message >= self.group.q: raise Exception('Invalid message') - m0 = message + ONE - - if pow(m0, self.group.q, self.group.p) == ONE: - # m0 is already in G_q - return self._encrypt(m0) - else: - # For the life of me I can't find any reputable references for this aside from Adida 2008... - m0 = (-m0) % self.group.p - return self._encrypt(m0) + return self._encrypt(self.message_to_m0(message), randomness) class EGPrivateKey(EmbeddedObject): pk_class = EGPublicKey @@ -118,16 +130,16 @@ class EGPrivateKey(EmbeddedObject): pt = (gamma_inv * ciphertext.delta) % self.public_key.group.p # Undo the encryption mapping - if pt < self.public_key.group.q: - return pt - ONE - else: - return ((-pt) % self.public_key.group.p) - ONE + return self.public_key.m0_to_message(pt) class EGCiphertext(EmbeddedObject): public_key = EmbeddedObjectField(EGPublicKey) gamma = EmbeddedObjectField(BigInt) # G^k delta = EmbeddedObjectField(BigInt) # M X^k + randomness = EmbeddedObjectField(BigInt, is_hashed=False) + m0 = EmbeddedObjectField(BigInt, is_hashed=False) + def reencrypt(self, k=None): # Generate an encryption of one if k is None: @@ -136,12 +148,22 @@ class EGCiphertext(EmbeddedObject): delta = pow(self.public_key.X, k, self.public_key.group.p) return EGCiphertext(public_key=self.public_key, gamma=((self.gamma * gamma) % self.public_key.group.p), delta=((self.delta * delta) % self.public_key.group.p)), k + + def deaudit(self): + return EGCiphertext(public_key=self.public_key, gamma=self.gamma, delta=self.delta) + + def is_randomness_valid(self): + ct = self.public_key._encrypt(self.m0, self.randomness) + return ct.gamma == self.gamma and ct.delta == self.delta # Signed ElGamal per Schnorr & Jakobssen class SEGPublicKey(EGPublicKey): - def _encrypt(self, message): - # Choose an element 1 <= k <= p - 2 - r = BigInt.crypto_random(ONE, self.group.p - TWO) + def _encrypt(self, message, randomness=None): + if randomness is None: + # Choose an element 1 <= k <= p - 2 + r = BigInt.crypto_random(ONE, self.group.p - TWO) + else: + r = randomness s = BigInt.crypto_random(ONE, self.group.p - TWO) gamma = pow(self.group.g, r, self.group.p) # h @@ -151,7 +173,7 @@ class SEGPublicKey(EGPublicKey): z = s + c*r - return SEGCiphertext(public_key=self, gamma=gamma, delta=delta, c=c, z=z) + return SEGCiphertext(public_key=self, gamma=gamma, delta=delta, c=c, z=z, m0=message, randomness=r) class SEGPrivateKey(EGPrivateKey): pk_class = SEGPublicKey @@ -166,6 +188,9 @@ class SEGCiphertext(EGCiphertext): c = SHA256().update_bigint(gs, self.gamma, self.delta).hash_as_bigint() return self.c == c + + def deaudit(self): + return SEGCiphertext(public_key=self.public_key, gamma=self.gamma, delta=self.delta, c=self.c, z=self.z) class Polynomial(EmbeddedObject): coefficients = EmbeddedObjectListField(BigInt) # x^0, x^1, ... x^n diff --git a/eos/psr/election.py b/eos/psr/election.py index 87fd432..fc54fee 100644 --- a/eos/psr/election.py +++ b/eos/psr/election.py @@ -45,6 +45,14 @@ class BlockEncryptedAnswer(EncryptedAnswer): obj = EosObject.deserialise_and_unwrap(EosObject.from_json(m)) return obj + + def deaudit(self): + blocks_deaudit = EosList() + + for i in range(len(self.blocks)): + blocks_deaudit.append(self.blocks[i].deaudit()) + + return BlockEncryptedAnswer(blocks=blocks_deaudit) class Trustee(EmbeddedObject): name = StringField() diff --git a/eosweb/core/main.py b/eosweb/core/main.py index 7b26f61..fcce2cd 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -242,6 +242,10 @@ def election_api_cast_vote(election): 'vote': EosObject.serialise_and_wrap(vote, should_protect=True) }), mimetype='application/json') +@app.route('/auditor') +def auditor(): + return flask.render_template('election/auditor.html') + @app.route('/debug') def debug(): assert False diff --git a/eosweb/core/static/css/main.css b/eosweb/core/static/css/main.css index 704922a..dad7e4f 100644 --- a/eosweb/core/static/css/main.css +++ b/eosweb/core/static/css/main.css @@ -23,7 +23,7 @@ .hash { font-family: monospace; - word-wrap: break-word; + word-break: break-all; } .superem { diff --git a/eosweb/core/static/nunjucks/booth/audit.html b/eosweb/core/static/nunjucks/booth/audit.html index a2fd8a7..979b012 100644 --- a/eosweb/core/static/nunjucks/booth/audit.html +++ b/eosweb/core/static/nunjucks/booth/audit.html @@ -19,10 +19,25 @@ #} {% block content %} - TODO +

Ready to audit your ballot

+ +
+

Your vote has not yet been cast. Please follow the instructions to continue.

+
+ +

The following is your ballot with fingerprint {{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}, decrypted and ready for auditing.

+ +
+ {# For some reason nunjucks doesn't like calling this the normal way #} + +
+ +

You may provide the above data to a trusted third-party, or use the Eos auditor to verify that your ballot was prepared correctly.

+ +

Once you are satisfied that your ballot has been prepared correctly, click ‘Continue’. Your ballot will be prepared again, to protect your secrecy, and you may choose to audit your ballot again, or proceed to cast your ballot.

{% endblock %} {% block buttons %} - + {# Dirty trick to go back to the encryption step #} + {% endblock %} diff --git a/eosweb/core/static/nunjucks/booth/cast.html b/eosweb/core/static/nunjucks/booth/cast.html index bc91f0b..c8dd482 100644 --- a/eosweb/core/static/nunjucks/booth/cast.html +++ b/eosweb/core/static/nunjucks/booth/cast.html @@ -83,6 +83,9 @@ $("#cast_prompt").hide(); $("#casting").show(); + // Prepare ballot + booth.ballot = booth.ballot.deaudit(); + $.ajax({ url: "{{ election_base_url }}cast_ballot", type: "POST", diff --git a/eosweb/core/static/nunjucks/booth/encrypt.html b/eosweb/core/static/nunjucks/booth/encrypt.html index 145ded2..74412ea 100644 --- a/eosweb/core/static/nunjucks/booth/encrypt.html +++ b/eosweb/core/static/nunjucks/booth/encrypt.html @@ -21,11 +21,18 @@ + + +{% endblock %}