diff --git a/auth/jsonfield.py b/auth/jsonfield.py index ff63893e3a44cf841d1dd32350c36970cdc5c6f6..44574a14d973e88b0b12fde42df2c5b33d6fcfcd 100644 --- a/auth/jsonfield.py +++ b/auth/jsonfield.py @@ -30,18 +30,18 @@ class JSONField(models.TextField): def to_python(self, value): """Convert our string value to JSON after we load it from the DB""" - # must handle the case where the value is already ready if self.json_type: if isinstance(value, self.json_type): return value - else: - if isinstance(value, dict) or isinstance(value, list): - return value + + if isinstance(value, dict) or isinstance(value, list): + return value if value == "" or value == None: return None parsed_value = json.loads(value) + if self.json_type and parsed_value: parsed_value = self.json_type.fromJSONDict(parsed_value, **self.deserialization_params) @@ -58,7 +58,7 @@ class JSONField(models.TextField): if value == None: return None - if self.json_type or hasattr(value,'toJSONDict'): + if self.json_type and isinstance(value, self.json_type): the_dict = value.toJSONDict() else: the_dict = value diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py index 4735ba97a3b2cc55f191fc0597135e4e87af6bfe..8674a54462773108e41a654fe37ec526a82f5e24 100644 --- a/helios/datatypes/__init__.py +++ b/helios/datatypes/__init__.py @@ -41,6 +41,33 @@ def recursiveToDict(obj): else: return obj.toDict() +def get_class(datatype): + # already done? + if not isinstance(datatype, basestring): + return datatype + + # parse datatype string "v31/Election" --> from v31 import Election + parsed_datatype = datatype.split("/") + + # get the module + dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1) + + if not dynamic_module: + raise Exception("no module for %s" % datatpye) + + # go down the attributes to get to the class + try: + dynamic_ptr = dynamic_module + for attr in parsed_datatype[1:]: + dynamic_ptr = getattr(dynamic_ptr, attr) + dynamic_cls = dynamic_ptr + except AttributeError: + raise Exception ("no module for %s" % datatype) + + + return dynamic_cls + + class LDObjectContainer(object): """ a simple container for an LD Object. @@ -54,7 +81,7 @@ class LDObjectContainer(object): return self._ld_object def toJSONDict(self): - return self.ld_object.toDict() + return self.ld_object.toJSONDict() def toJSON(self): return self.ld_object.serialize() @@ -89,22 +116,6 @@ class LDObject(object): self.wrapped_obj = wrapped_obj self.structured_fields = {} - @classmethod - def get_class(cls, datatype): - # parse datatype string "v31/Election" --> from v31 import Election - parsed_datatype = datatype.split("/") - - # get the module - dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1) - - # go down the attributes to get to the class - dynamic_ptr = dynamic_module - for attr in parsed_datatype[1:]: - dynamic_ptr = getattr(dynamic_ptr, attr) - dynamic_cls = dynamic_ptr - - return dynamic_cls - @classmethod def instantiate(cls, obj, datatype=None): if hasattr(obj, 'datatype') and not datatype: @@ -117,12 +128,8 @@ class LDObject(object): if not obj: return None - # array - if isArray(datatype): - return [cls.instantiate(el, datatype = datatype.element_type) for el in obj] - # the class - dynamic_cls = cls.get_class(datatype) + dynamic_cls = get_class(datatype) # instantiate it return_obj = dynamic_cls(obj) @@ -154,6 +161,8 @@ class LDObject(object): val[f] = self.process_value_out(f, getattr(self.wrapped_obj, f)) return val + toJSONDict = toDict + def loadDataFromDict(self, d): """ load data from a dictionary @@ -167,11 +176,18 @@ class LDObject(object): for f in self.FIELDS: if f in structured_fields: # a structured ld field, recur - sub_ld_object = self.fromDict(d[f], type_hint = self.STRUCTURED_FIELDS[f]) + try: + sub_ld_object = self.fromDict(d[f], type_hint = self.STRUCTURED_FIELDS[f]) + except TypeError: + import pdb; pdb.set_trace() + self.structured_fields[f] = sub_ld_object - + # set the field on the wrapped object too - setattr(self.wrapped_obj, f, sub_ld_object.wrapped_obj) + try: + setattr(self.wrapped_obj, f, sub_ld_object.wrapped_obj) + except AttributeError: + import pdb; pdb.set_trace() else: # a simple type new_val = self.process_value_in(f, d[f]) @@ -184,9 +200,13 @@ class LDObject(object): ld_type = type_hint # get the LD class so we know what wrapped object to instantiate - ld_cls = cls.get_class(ld_type) + ld_cls = get_class(ld_type) wrapped_obj_cls = ld_cls.WRAPPED_OBJ_CLASS + + if not wrapped_obj_cls: + raise Exception("cannot instantiate wrapped object for %s" % ld_type) + wrapped_obj = wrapped_obj_cls() # then instantiate the LD object and load the data @@ -241,25 +261,33 @@ class LDObject(object): return other != None and self.uuid == other.uuid -class ArrayOfObjects(LDObject): +class BaseArrayOfObjects(LDObject): """ If one type has, as a subtype, an array of things, then this is the structured field used """ + ELEMENT_TYPE = None + WRAPPED_OBJ_CLASS = list - def __init__(self, wrapped_array, item_type): - self.item_type = item_type - self.items = [LDObject.instantiate(wrapped_item, item_type) for wrapped_item in wrapped_array] + def __init__(self, wrapped_obj): + self.items = [] + super(BaseArrayOfObjects, self).__init__(wrapped_obj) def toDict(self): - return [item.serialize() for item in self.items] + return [item.toDict() for item in self.items] -class arrayOf(object): + def loadDataFromDict(self, d): + "assumes that d is a list" + # TODO: should we be using ELEMENT_TYPE_CLASS here instead of LDObject? + self.items = [LDObject.fromDict(element, type_hint = self.ELEMENT_TYPE) for element in d] + + +def arrayOf(element_type): """ a wrapper for the construtor of the array returns the constructor """ - def __init__(self, element_type): - self.element_type = element_type + class ArrayOfTypedObjects(BaseArrayOfObjects): + ELEMENT_TYPE = element_type + + return ArrayOfTypedObjects -def isArray(field_type): - return type(field_type) == arrayOf diff --git a/helios/datatypes/legacy.py b/helios/datatypes/legacy.py index 3f6902ab983bd8fcd376ca69498a4603752bcfd0..e646eb41e32daefa256b2b86f1b0d000d9b2143c 100644 --- a/helios/datatypes/legacy.py +++ b/helios/datatypes/legacy.py @@ -3,13 +3,17 @@ Legacy datatypes for Helios (v3.0) """ from helios.datatypes import LDObject, arrayOf +from helios.crypto import elgamal as crypto_elgamal +from helios.workflows import homomorphic ## ## utilities class DictObject(object): - def __init__(self, d): + def __init__(self, d=None): self.d = d + if not self.d: + self.d = {} def __getattr__(self, k): return self.d[k] @@ -31,8 +35,9 @@ class Election(LegacyObject): 'voting_ends_at': 'core/Timestamp', 'frozen_at': 'core/Timestamp' } - + class EncryptedAnswer(LegacyObject): + WRAPPED_OBJ_CLASS = homomorphic.EncryptedAnswer FIELDS = ['choices', 'individual_proofs', 'overall_proof'] STRUCTURED_FIELDS = { 'choices': arrayOf('legacy/EGCiphertext'), @@ -53,6 +58,7 @@ class EncryptedVote(LegacyObject): """ An encrypted ballot """ + WRAPPED_OBJ_CLASS = homomorphic.EncryptedVote FIELDS = ['answers', 'election_hash', 'election_uuid'] STRUCTURED_FIELDS = { 'answers' : arrayOf('legacy/EncryptedAnswer') @@ -93,6 +99,7 @@ class Trustee(LegacyObject): 'decryption_proofs' : arrayOf(arrayOf('legacy/DLogProof'))} class EGPublicKey(LegacyObject): + WRAPPED_OBJ_CLASS = crypto_elgamal.PublicKey FIELDS = ['y', 'p', 'g', 'q'] STRUCTURED_FIELDS = { 'y': 'core/BigInteger', @@ -100,7 +107,15 @@ class EGPublicKey(LegacyObject): 'q': 'core/BigInteger', 'g': 'core/BigInteger'} +class EGSecretKey(LegacyObject): + WRAPPED_OBJ_CLASS = crypto_elgamal.SecretKey + FIELDS = ['x','pk'] + STRUCTURED_FIELDS = { + 'x': 'core/BigInteger', + 'pk': 'legacy/EGPublicKey'} + class EGCiphertext(LegacyObject): + WRAPPED_OBJ_CLASS = crypto_elgamal.Ciphertext FIELDS = ['alpha','beta'] STRUCTURED_FIELDS = { 'alpha': 'core/BigInteger', @@ -116,6 +131,7 @@ class EGZKProofCommitment(LegacyObject): super(EGZKProofCommitment, self).__init__(DictObject(wrapped_obj)) class EGZKProof(LegacyObject): + WRAPPED_OBJ_CLASS = crypto_elgamal.ZKProof FIELDS = ['commitment', 'challenge', 'response'] STRUCTURED_FIELDS = { 'commitment': 'legacy/EGZKProofCommitment', @@ -123,10 +139,15 @@ class EGZKProof(LegacyObject): 'response' : 'core/BigInteger'} class EGZKDisjunctiveProof(LegacyObject): + WRAPPED_OBJ_CLASS = crypto_elgamal.ZKDisjunctiveProof FIELDS = ['proofs'] STRUCTURED_FIELDS = { 'proofs': arrayOf('legacy/EGZKProof')} + def loadDataFromDict(self, d): + "hijack and make sure we add the proofs name back on" + return super(EGZKDisjunctiveProof, self).loadDataFromDict({'proofs': d}) + def toDict(self): "hijack toDict and make it return the proofs array only, since that's the spec for legacy" return super(EGZKDisjunctiveProof, self).toDict()['proofs'] diff --git a/helios/datatypes/pkc/elgamal.py b/helios/datatypes/pkc/elgamal.py index 21f7cc922f5ce62388b4c4781eb4a14f251f8233..0f0688dc45af8213c6b351873bde97216132422d 100644 --- a/helios/datatypes/pkc/elgamal.py +++ b/helios/datatypes/pkc/elgamal.py @@ -3,7 +3,7 @@ data types for 2011/01 Helios """ from helios.datatypes import LDObject -from helios.crypto import elgamal +from helios.crypto import elgamal as crypto_elgamal class DiscreteLogProof(LDObject): FIELDS = ['challenge', 'commitment', 'response'] @@ -13,7 +13,7 @@ class DiscreteLogProof(LDObject): 'response' : 'core/BigInteger'} class PublicKey(LDObject): - WRAPPED_OBJ_CLASS = elgamal.PublicKey + WRAPPED_OBJ_CLASS = crypto_elgamal.PublicKey FIELDS = ['y', 'p', 'g', 'q'] STRUCTURED_FIELDS = { diff --git a/helios/models.py b/helios/models.py index 3ea75ed9f0cb1062bec0b410256e4ac461a70c26..516c7af466d34338cfb2acb1d8559fa413fd223e 100644 --- a/helios/models.py +++ b/helios/models.py @@ -906,7 +906,7 @@ class Trustee(HeliosModel): null=True) decryption_proofs = JSONField(datatypes.LDObject, - deserialization_params = {'type_hint' : datatypes.arrayOf(datatypes.arrayOf('legacy/DLogProof'))}, + deserialization_params = {'type_hint' : datatypes.arrayOf(datatypes.arrayOf('legacy/EGZKProof'))}, null=True) def save(self, *args, **kwargs): diff --git a/helios/workflows/__init__.py b/helios/workflows/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/helios/workflows/homomorphic.py b/helios/workflows/homomorphic.py new file mode 100644 index 0000000000000000000000000000000000000000..39d9bc6a7bee75c9be379a4416cc772d32d73a84 --- /dev/null +++ b/helios/workflows/homomorphic.py @@ -0,0 +1,613 @@ +""" +homomorphic workflow and algorithms for Helios + +Ben Adida +2008-08-30 +reworked 2011-01-09 +""" + +from helios.crypto import algs, utils +import logging +import uuid +import datetime + +class EncryptedAnswer(object): + """ + An encrypted answer to a single election question + """ + + def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None): + self.choices = choices + self.individual_proofs = individual_proofs + self.overall_proof = overall_proof + self.randomness = randomness + self.answer = answer + + @classmethod + def generate_plaintexts(cls, pk, min=0, max=1): + plaintexts = [] + running_product = 1 + + # run the product up to the min + for i in range(max+1): + # if we're in the range, add it to the array + if i >= min: + plaintexts.append(algs.EGPlaintext(running_product, pk)) + + # next value in running product + running_product = (running_product * pk.g) % pk.p + + return plaintexts + + def verify_plaintexts_and_randomness(self, pk): + """ + this applies only if the explicit answers and randomness factors are given + we do not verify the proofs here, that is the verify() method + """ + if not hasattr(self, 'answer'): + return False + + for choice_num in range(len(self.choices)): + choice = self.choices[choice_num] + choice.pk = pk + + # redo the encryption + # WORK HERE (paste from below encryption) + + return False + + def verify(self, pk, min=0, max=1): + possible_plaintexts = self.generate_plaintexts(pk) + homomorphic_sum = 0 + + for choice_num in range(len(self.choices)): + choice = self.choices[choice_num] + choice.pk = pk + individual_proof = self.individual_proofs[choice_num] + + # verify the proof on the encryption of that choice + if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof, algs.EG_disjunctive_challenge_generator): + return False + + # compute homomorphic sum if needed + if max != None: + homomorphic_sum = choice * homomorphic_sum + + if max != None: + # determine possible plaintexts for the sum + sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max) + + # verify the sum + return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof, algs.EG_disjunctive_challenge_generator) + else: + # approval voting, no need for overall proof verification + return True + + @classmethod + def fromElectionAndAnswer(cls, election, question_num, answer_indexes): + """ + Given an election, a question number, and a list of answers to that question + in the form of an array of 0-based indexes into the answer array, + produce an EncryptedAnswer that works. + """ + question = election.questions[question_num] + answers = question['answers'] + pk = election.public_key + + # initialize choices, individual proofs, randomness and overall proof + choices = [None for a in range(len(answers))] + individual_proofs = [None for a in range(len(answers))] + overall_proof = None + randomness = [None for a in range(len(answers))] + + # possible plaintexts [0, 1] + plaintexts = cls.generate_plaintexts(pk) + + # keep track of number of options selected. + num_selected_answers = 0; + + # homomorphic sum of all + homomorphic_sum = 0 + randomness_sum = 0 + + # min and max for number of answers, useful later + min_answers = 0 + if question.has_key('min'): + min_answers = question['min'] + max_answers = question['max'] + + # go through each possible answer and encrypt either a g^0 or a g^1. + for answer_num in range(len(answers)): + plaintext_index = 0 + + # assuming a list of answers + if answer_num in answer_indexes: + plaintext_index = 1 + num_selected_answers += 1 + + # randomness and encryption + randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q) + choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num]) + + # generate proof + individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts, plaintext_index, + randomness[answer_num], algs.EG_disjunctive_challenge_generator) + + # sum things up homomorphically if needed + if max_answers != None: + homomorphic_sum = choices[answer_num] * homomorphic_sum + randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q + + # prove that the sum is 0 or 1 (can be "blank vote" for this answer) + # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded + + if num_selected_answers < min_answers: + raise Exception("Need to select at least %s answer(s)" % min_answers) + + if max_answers != None: + sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers) + + # need to subtract the min from the offset + overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts, num_selected_answers - min_answers, randomness_sum, algs.EG_disjunctive_challenge_generator); + else: + # approval voting + overall_proof = None + + return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes) + +# WORK HERE + +class EncryptedVote(object): + """ + An encrypted ballot + """ + + def verify(self, election): + # right number of answers + if len(self.encrypted_answers) != len(election.questions): + return False + + # check hash + if self.election_hash != election.hash: + # print "%s / %s " % (self.election_hash, election.hash) + return False + + # check ID + if self.election_uuid != election.uuid: + return False + + # check proofs on all of answers + for question_num in range(len(election.questions)): + ea = self.encrypted_answers[question_num] + + question = election.questions[question_num] + min_answers = 0 + if question.has_key('min'): + min_answers = question['min'] + + if not ea.verify(election.public_key, min=min_answers, max=question['max']): + return False + + return True + + @classmethod + def fromElectionAndAnswers(cls, election, answers): + pk = election.public_key + + # each answer is an index into the answer array + encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for answer_num in range(len(answers))] + return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid = election.uuid) + + +def one_question_winner(question, result, num_cast_votes): + """ + determining the winner for one question + """ + # sort the answers , keep track of the index + counts = sorted(enumerate(result), key=lambda(x): x[1]) + counts.reverse() + + # if there's a max > 1, we assume that the top MAX win + if question['max'] > 1: + return [c[0] for c in counts[:question['max']]] + + # if max = 1, then depends on absolute or relative + if question['result_type'] == 'absolute': + if counts[0][1] >= (num_cast_votes/2 + 1): + return [counts[0][0]] + else: + return [] + + if question['result_type'] == 'relative': + return [counts[0][0]] + +class Election(object): + + FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg', + 'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at', 'election_type'] + + JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg', + 'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at'] + + # need to add in v3.1: use_advanced_audit_features, election_type, and probably more + + def init_tally(self): + return Tally(election=self) + + def _process_value_in(self, field_name, field_value): + if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at': + if type(field_value) == str or type(field_value) == unicode: + return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') + + if field_name == 'public_key': + return algs.EGPublicKey.fromJSONDict(field_value) + + if field_name == 'private_key': + return algs.EGSecretKey.fromJSONDict(field_value) + + def _process_value_out(self, field_name, field_value): + # the date + if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at': + return str(field_value) + + if field_name == 'public_key' or field_name == 'private_key': + return field_value.toJSONDict() + + @property + def registration_status_pretty(self): + if self.openreg: + return "Open" + else: + return "Closed" + + @property + def winners(self): + """ + Depending on the type of each question, determine the winners + returns an array of winners for each question, aka an array of arrays. + assumes that if there is a max to the question, that's how many winners there are. + """ + return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in range(len(self.questions))] + + @property + def pretty_result(self): + if not self.result: + return None + + # get the winners + winners = self.winners + + raw_result = self.result + prettified_result = [] + + # loop through questions + for i in range(len(self.questions)): + q = self.questions[i] + pretty_question = [] + + # go through answers + for j in range(len(q['answers'])): + a = q['answers'][j] + count = raw_result[i][j] + pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])}) + + prettified_result.append({'question': q['short_name'], 'answers': pretty_question}) + + return prettified_result + + +class Voter(object): + """ + A voter in an election + """ + FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias'] + JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name'] + + # alternative, for when the voter is aliased + ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias'] + + def toJSONDict(self): + fields = None + if self.alias != None: + return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS) + else: + return super(Voter,self).toJSONDict() + + @property + def voter_id_hash(self): + if self.voter_login_id: + # for backwards compatibility with v3.0, and since it doesn't matter + # too much if we hash the email or the unique login ID here. + return utils.hash_b64(self.voter_login_id) + else: + return utils.hash_b64(self.voter_id) + +class Trustee(object): + """ + a trustee + """ + FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email'] + + def _process_value_in(self, field_name, field_value): + if field_name == 'public_key': + return algs.EGPublicKey.fromJSONDict(field_value) + + if field_name == 'pok': + return algs.DLogProof.fromJSONDict(field_value) + + def _process_value_out(self, field_name, field_value): + if field_name == 'public_key' or field_name == 'pok': + return field_value.toJSONDict() + +class CastVote(object): + """ + A cast vote, which includes an encrypted vote and some cast metadata + """ + FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash'] + + def __init__(self, *args, **kwargs): + super(CastVote, self).__init__(*args, **kwargs) + self.election = None + + @classmethod + def fromJSONDict(cls, d, election=None): + o = cls() + o.election = election + o.set_from_args(**d) + return o + + def toJSONDict(self, include_vote=True): + result = super(CastVote,self).toJSONDict() + if not include_vote: + del result['vote'] + return result + + @classmethod + def fromOtherObject(cls, o, election): + obj = cls() + obj.election = election + obj.set_from_other_object(o) + return obj + + def _process_value_in(self, field_name, field_value): + if field_name == 'cast_at': + if type(field_value) == str: + return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') + + if field_name == 'vote': + return EncryptedVote.fromJSONDict(field_value, self.election.public_key) + + def _process_value_out(self, field_name, field_value): + # the date + if field_name == 'cast_at': + return str(field_value) + + if field_name == 'vote': + return field_value.toJSONDict() + + def issues(self, election): + """ + Look for consistency problems + """ + issues = [] + + # check the election + if self.vote.election_uuid != election.uuid: + issues.append("the vote's election UUID does not match the election for which this vote is being cast") + + return issues + +class DLogTable(object): + """ + Keeping track of discrete logs + """ + + def __init__(self, base, modulus): + self.dlogs = {} + self.dlogs[1] = 0 + self.last_dlog_result = 1 + self.counter = 0 + + self.base = base + self.modulus = modulus + + def increment(self): + self.counter += 1 + + # new value + new_value = (self.last_dlog_result * self.base) % self.modulus + + # record the discrete log + self.dlogs[new_value] = self.counter + + # record the last value + self.last_dlog_result = new_value + + def precompute(self, up_to): + while self.counter < up_to: + self.increment() + + def lookup(self, value): + return self.dlogs.get(value, None) + + +class Tally(object): + """ + A running homomorphic tally + """ + + FIELDS = ['num_tallied', 'tally'] + JSON_FIELDS = ['num_tallied', 'tally'] + + def __init__(self, *args, **kwargs): + super(Tally, self).__init__(*args, **kwargs) + + self.election = kwargs.get('election',None) + + if self.election: + self.init_election(self.election) + else: + self.questions = None + self.public_key = None + + if not self.tally: + self.tally = None + + # initialize + if self.num_tallied == None: + self.num_tallied = 0 + + def init_election(self, election): + """ + given the election, initialize some params + """ + self.questions = election.questions + self.public_key = election.public_key + + if not self.tally: + self.tally = [[0 for a in q['answers']] for q in self.questions] + + def add_vote_batch(self, encrypted_votes, verify_p=True): + """ + Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification + rather than a whole proof verif for each vote. + """ + for vote in encrypted_votes: + self.add_vote(vote, verify_p) + + def add_vote(self, encrypted_vote, verify_p=True): + # do we verify? + if verify_p: + if not encrypted_vote.verify(self.election): + raise Exception('Bad Vote') + + # for each question + for question_num in range(len(self.questions)): + question = self.questions[question_num] + answers = question['answers'] + + # for each possible answer to each question + for answer_num in range(len(answers)): + # do the homomorphic addition into the tally + enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num] + enc_vote_choice.pk = self.public_key + self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[answer_num] * self.tally[question_num][answer_num] + + self.num_tallied += 1 + + def decryption_factors_and_proofs(self, sk): + """ + returns an array of decryption factors and a corresponding array of decryption proofs. + makes the decryption factors into strings, for general Helios / JS compatibility. + """ + # for all choices of all questions (double list comprehension) + decryption_factors = [] + decryption_proof = [] + + for question_num, question in enumerate(self.questions): + answers = question['answers'] + question_factors = [] + question_proof = [] + + for answer_num, answer in enumerate(answers): + # do decryption and proof of it + dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num]) + + # look up appropriate discrete log + # this is the string conversion + question_factors.append(str(dec_factor)) + question_proof.append(proof.toJSONDict()) + + decryption_factors.append(question_factors) + decryption_proof.append(question_proof) + + return decryption_factors, decryption_proof + + def decrypt_and_prove(self, sk, discrete_logs=None): + """ + returns an array of tallies and a corresponding array of decryption proofs. + """ + + # who's keeping track of discrete logs? + if not discrete_logs: + discrete_logs = self.discrete_logs + + # for all choices of all questions (double list comprehension) + decrypted_tally = [] + decryption_proof = [] + + for question_num in range(len(self.questions)): + question = self.questions[question_num] + answers = question['answers'] + question_tally = [] + question_proof = [] + + for answer_num in range(len(answers)): + # do decryption and proof of it + plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num]) + + # look up appropriate discrete log + question_tally.append(discrete_logs[plaintext]) + question_proof.append(proof) + + decrypted_tally.append(question_tally) + decryption_proof.append(question_proof) + + return decrypted_tally, decryption_proof + + def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator): + """ + decryption_factors is a list of lists of dec factors + decryption_proofs are the corresponding proofs + public_key is, of course, the public key of the trustee + """ + + # go through each one + for q_num, q in enumerate(self.tally): + for a_num, answer_tally in enumerate(q): + # parse the proof + proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num]) + + # check that g, alpha, y, dec_factor is a DH tuple + if not proof.verify(public_key.g, answer_tally.alpha, public_key.y, int(decryption_factors[q_num][a_num]), public_key.p, public_key.q, challenge_generator): + return False + + return True + + def decrypt_from_factors(self, decryption_factors, public_key): + """ + decrypt a tally given decryption factors + + The decryption factors are a list of decryption factor sets, for each trustee. + Each decryption factor set is a list of lists of decryption factors (questions/answers). + """ + + # pre-compute a dlog table + dlog_table = DLogTable(base = public_key.g, modulus = public_key.p) + dlog_table.precompute(self.num_tallied) + + result = [] + + # go through each one + for q_num, q in enumerate(self.tally): + q_result = [] + + for a_num, a in enumerate(q): + # coalesce the decryption factors into one list + dec_factor_list = [df[q_num][a_num] for df in decryption_factors] + raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key) + + q_result.append(dlog_table.lookup(raw_value)) + + result.append(q_result) + + return result + + def _process_value_in(self, field_name, field_value): + if field_name == 'tally': + return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value] + + def _process_value_out(self, field_name, field_value): + if field_name == 'tally': + return [[a.toJSONDict() for a in q] for q in field_value] +