diff --git a/.gitignore b/.gitignore index a2dca2bee7e168df398b922f596e4e4a6e230a26..8db764f4eadb6e11a9fc766cea5aa1a491b66aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ deploy-latest.sh .DS_Store *~ media/* -venv +venv* celerybeat-* env.sh .cache diff --git a/.travis.yml b/.travis.yml index 1505fccb9e5bd689f6964ac911b32fa86dac2b92..476acaf46b8d58147f43cfecad671310e550741f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,42 +1,46 @@ -sudo: false language: python -python: - - "2.7" - os: linux -before_install: - - export BOTO_CONFIG=/dev/null - -install: - - pip install --upgrade pip - - pip install -r requirements.txt - -before_script: - - psql -c 'create database helios;' -U postgres - -script: "python -Wall manage.py test" - jobs: include: - - dist: trusty - addons: - postgresql: "9.3" - - dist: trusty - addons: - postgresql: "9.4" - - dist: trusty + - python: "3.7" + dist: xenial addons: postgresql: "9.5" - - dist: trusty + - python: "3.7" + dist: xenial addons: postgresql: "9.6" - - dist: xenial + - python: "3.7" + dist: xenial addons: - postgresql: "9.4" - - dist: xenial + postgresql: "10" + - python: "3.8" + dist: bionic addons: postgresql: "9.5" - - dist: xenial + - python: "3.8" + dist: bionic addons: postgresql: "9.6" + - python: "3.8" + dist: bionic + addons: + postgresql: "10" + - python: "3.8" + dist: bionic + addons: + postgresql: "11" + +before_install: + - export BOTO_CONFIG=/dev/null + +install: + - pip3 install --upgrade pip + - pip3 install -r requirements.txt + - pip3 freeze + +before_script: + - psql -c 'create database helios;' -U postgres + +script: "python3 -Wall manage.py test -v 2" diff --git a/INSTALL.md b/INSTALL.md index b76f642a7f50ace2bfc15505418c249e20362060..aff5309427caec9bc59af69dc5041f75e17150c8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,4 +1,4 @@ -* install PostgreSQL 8.3+ +* install PostgreSQL 9.5+ * install Rabbit MQ This is needed for celery to work, which does background processing such as @@ -11,12 +11,22 @@ http://www.virtualenv.org/en/latest/ * cd into the helios-server directory +* install Python3.6 including dev, pip, and venv + +``` +sudo apt install python3.6 python3.6-venv python3.6-pip python3.6-venv +``` + * create a virtualenv -* make sure you use Python2.7 and **not** Python3+ -* The above can be done by typing something similar to ``` -virtualenv --python=/usr/bin/python2.7 $(pwd)/venv +python3.6-m venv $(pwd)/venv +``` + +* you'll also need Postgres dev libraries. For example on Ubuntu: + +``` +sudo apt install libpq-dev ``` * activate virtual environment diff --git a/helios/__init__.py b/helios/__init__.py index 2f598298b565f3a883cf9365699a6911031e0120..21f21bc59da930240e0c88612f1afcc9c0a46178 100644 --- a/helios/__init__.py +++ b/helios/__init__.py @@ -1,7 +1,7 @@ from django.conf import settings # This will make sure the app is always imported when # Django starts so that shared_task will use this app. -from celery_app import app as celery_app +from .celery_app import app as celery_app __all__ = ('celery_app', 'TEMPLATE_BASE', 'ADMIN_ONLY', 'VOTERS_UPLOAD', 'VOTERS_EMAIL',) diff --git a/helios/crypto/algs.py b/helios/crypto/algs.py index acac4f44b4c935a49f4873e8dea92bcb17da0bbc..b7439d9bd3d50cd738b354fcfbfe64495df0db78 100644 --- a/helios/crypto/algs.py +++ b/helios/crypto/algs.py @@ -7,157 +7,61 @@ Ben Adida ben@adida.net """ -import math, hashlib, logging -import randpool, number +import logging -import numtheory +from Crypto.Hash import SHA1 +from Crypto.Util import number -# some utilities -class Utils: - RAND = randpool.RandomPool() - - @classmethod - def random_seed(cls, data): - cls.RAND.add_event(data) - - @classmethod - def random_mpz(cls, n_bits): - low = 2**(n_bits-1) - high = low * 2 - - # increment and find a prime - # return randrange(low, high) - - return number.getRandomNumber(n_bits, cls.RAND.get_bytes) - - @classmethod - def random_mpz_lt(cls, max): - # return randrange(0, max) - n_bits = int(math.floor(math.log(max, 2))) - return (number.getRandomNumber(n_bits, cls.RAND.get_bytes) % max) - - @classmethod - def random_prime(cls, n_bits): - return number.getPrime(n_bits, cls.RAND.get_bytes) - - @classmethod - def is_prime(cls, mpz): - #return numtheory.miller_rabin(mpz) - return number.isPrime(mpz) - - @classmethod - def xgcd(cls, a, b): - """ - Euclid's Extended GCD algorithm - """ - mod = a%b - - if mod == 0: - return 0,1 - else: - x,y = cls.xgcd(b, mod) - return y, x-(y*(a/b)) - - @classmethod - def inverse(cls, mpz, mod): - # return cls.xgcd(mpz,mod)[0] - return number.inverse(mpz, mod) - - @classmethod - def random_safe_prime(cls, n_bits): - p = None - q = None - - while True: - p = cls.random_prime(n_bits) - q = (p-1)/2 - if cls.is_prime(q): - return p - - @classmethod - def random_special_prime(cls, q_n_bits, p_n_bits): - p = None - q = None - - z_n_bits = p_n_bits - q_n_bits - - q = cls.random_prime(q_n_bits) - - while True: - z = cls.random_mpz(z_n_bits) - p = q*z + 1 - if cls.is_prime(p): - return p, q, z +from helios.crypto.utils import random +from helios.utils import to_json class ElGamal: def __init__(self): - self.p = None - self.q = None - self.g = None - - @classmethod - def generate(cls, n_bits): - """ - generate an El-Gamal environment. Returns an instance - of ElGamal(), with prime p, group size q, and generator g - """ - - EG = ElGamal() - - # find a prime p such that (p-1)/2 is prime q - EG.p = Utils.random_safe_prime(n_bits) - - # q is the order of the group - # FIXME: not always p-1/2 - EG.q = (EG.p-1)/2 - - # find g that generates the q-order subgroup - while True: - EG.g = Utils.random_mpz_lt(EG.p) - if pow(EG.g, EG.q, EG.p) == 1: - break - - return EG + self.p = None + self.q = None + self.g = None def generate_keypair(self): - """ - generates a keypair in the setting - """ + """ + generates a keypair in the setting + """ - keypair = EGKeyPair() - keypair.generate(self.p, self.q, self.g) + keypair = EGKeyPair() + keypair.generate(self.p, self.q, self.g) - return keypair + return keypair def toJSONDict(self): - return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)} + return {'p': str(self.p), 'q': str(self.q), 'g': str(self.g)} @classmethod def fromJSONDict(cls, d): - eg = cls() - eg.p = int(d['p']) - eg.q = int(d['q']) - eg.g = int(d['g']) - return eg + eg = cls() + eg.p = int(d['p']) + eg.q = int(d['q']) + eg.g = int(d['g']) + return eg + class EGKeyPair: def __init__(self): - self.pk = EGPublicKey() - self.sk = EGSecretKey() + self.pk = EGPublicKey() + self.sk = EGSecretKey() def generate(self, p, q, g): - """ - Generate an ElGamal keypair - """ - self.pk.g = g - self.pk.p = p - self.pk.q = q + """ + Generate an ElGamal keypair + """ + self.pk.g = g + self.pk.p = p + self.pk.q = q + + self.sk.x = random.mpz_lt(q) + self.pk.y = pow(g, self.sk.x, p) - self.sk.x = Utils.random_mpz_lt(q) - self.pk.y = pow(g, self.sk.x, p) + self.sk.pk = self.pk - self.sk.pk = self.pk class EGPublicKey: def __init__(self): @@ -166,7 +70,7 @@ class EGPublicKey: self.g = None self.q = None - def encrypt_with_r(self, plaintext, r, encode_message= False): + def encrypt_with_r(self, plaintext, r, encode_message=False): """ expecting plaintext.m to be a big integer """ @@ -175,13 +79,13 @@ class EGPublicKey: # make sure m is in the right subgroup if encode_message: - y = plaintext.m + 1 - if pow(y, self.q, self.p) == 1: - m = y - else: - m = -y % self.p + y = plaintext.m + 1 + if pow(y, self.q, self.p) == 1: + m = y + else: + m = -y % self.p else: - m = plaintext.m + m = plaintext.m ciphertext.alpha = pow(self.g, r, self.p) ciphertext.beta = (m * pow(self.y, r, self.p)) % self.p @@ -192,7 +96,7 @@ class EGPublicKey: """ Encrypt a plaintext and return the randomness just generated and used. """ - r = Utils.random_mpz_lt(self.q) + r = random.mpz_lt(self.q) ciphertext = self.encrypt_with_r(plaintext, r) return [ciphertext, r] @@ -207,70 +111,69 @@ class EGPublicKey: """ Serialize to dictionary. """ - return {'y' : str(self.y), 'p' : str(self.p), 'g' : str(self.g) , 'q' : str(self.q)} + return {'y': str(self.y), 'p': str(self.p), 'g': str(self.g), 'q': str(self.q)} toJSONDict = to_dict # quick hack FIXME def toJSON(self): - import utils - return utils.to_json(self.toJSONDict()) + return to_json(self.toJSONDict()) - def __mul__(self,other): - if other == 0 or other == 1: - return self + def __mul__(self, other): + if other == 0 or other == 1: + return self - # check p and q - if self.p != other.p or self.q != other.q or self.g != other.g: - raise Exception("incompatible public keys") + # check p and q + if self.p != other.p or self.q != other.q or self.g != other.g: + raise Exception("incompatible public keys") - result = EGPublicKey() - result.p = self.p - result.q = self.q - result.g = self.g - result.y = (self.y * other.y) % result.p - return result + result = EGPublicKey() + result.p = self.p + result.q = self.q + result.g = self.g + result.y = (self.y * other.y) % result.p + return result - def verify_sk_proof(self, dlog_proof, challenge_generator = None): - """ - verify the proof of knowledge of the secret key - g^response = commitment * y^challenge - """ - left_side = pow(self.g, dlog_proof.response, self.p) - right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p + def verify_sk_proof(self, dlog_proof, challenge_generator=None): + """ + verify the proof of knowledge of the secret key + g^response = commitment * y^challenge + """ + left_side = pow(self.g, dlog_proof.response, self.p) + right_side = (dlog_proof.commitment * pow(self.y, dlog_proof.challenge, self.p)) % self.p - expected_challenge = challenge_generator(dlog_proof.commitment) % self.q + expected_challenge = challenge_generator(dlog_proof.commitment) % self.q - return ((left_side == right_side) and (dlog_proof.challenge == expected_challenge)) + return (left_side == right_side) and (dlog_proof.challenge == expected_challenge) def validate_pk_params(self): - # check primality of p - if not number.isPrime(self.p): - raise Exception("p is not prime.") + # check primality of p + if not number.isPrime(self.p): + raise Exception("p is not prime.") - # check length of p - if not (number.size(self.p) >= 2048): - raise Exception("p of insufficient length. Should be 2048 bits or greater.") + # check length of p + if not (number.size(self.p) >= 2048): + raise Exception("p of insufficient length. Should be 2048 bits or greater.") - # check primality of q - if not number.isPrime(self.q): - raise Exception("q is not prime.") + # check primality of q + if not number.isPrime(self.q): + raise Exception("q is not prime.") - # check length of q - if not (number.size(self.q) >= 256): - raise Exception("q of insufficient length. Should be 256 bits or greater.") + # check length of q + if not (number.size(self.q) >= 256): + raise Exception("q of insufficient length. Should be 256 bits or greater.") - if (pow(self.g,self.q,self.p)!=1): - raise Exception("g does not generate subgroup of order q.") + if pow(self.g, self.q, self.p) != 1: + raise Exception("g does not generate subgroup of order q.") - if not (1 < self.g < self.p-1): - raise Exception("g out of range.") + if not (1 < self.g < self.p - 1): + raise Exception("g out of range.") - if not (1 < self.y < self.p-1): - raise Exception("y out of range.") + if not (1 < self.y < self.p - 1): + raise Exception("y out of range.") - if (pow(self.y,self.q,self.p)!=1): - raise Exception("g does not generate proper group.") + if pow(self.y, self.q, self.p) != 1: + raise Exception("g does not generate proper group.") @classmethod def from_dict(cls, d): @@ -284,14 +187,15 @@ class EGPublicKey: pk.q = int(d['q']) try: - pk.validate_pk_params() + pk.validate_pk_params() except Exception as e: - raise + raise e return pk fromJSONDict = from_dict + class EGSecretKey: def __init__(self): self.x = None @@ -317,25 +221,25 @@ class EGSecretKey: return dec_factor, proof - def decrypt(self, ciphertext, dec_factor = None, decode_m=False): + def decrypt(self, ciphertext, dec_factor=None, decode_m=False): """ Decrypt a ciphertext. Optional parameter decides whether to encode the message into the proper subgroup. """ if not dec_factor: dec_factor = self.decryption_factor(ciphertext) - m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p + m = (number.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p if decode_m: - # get m back from the q-order subgroup - if m < self.pk.q: - y = m - else: - y = -m % self.pk.p + # get m back from the q-order subgroup + if m < self.pk.q: + y = m + else: + y = -m % self.pk.p - return EGPlaintext(y-1, self.pk) + return EGPlaintext(y - 1, self.pk) else: - return EGPlaintext(m, self.pk) + return EGPlaintext(m, self.pk) def prove_decryption(self, ciphertext): """ @@ -350,66 +254,66 @@ class EGSecretKey: and alpha^t = b * beta/m ^ c """ - m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p - beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p + m = (number.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p + beta_over_m = (ciphertext.beta * number.inverse(m, self.pk.p)) % self.pk.p # pick a random w - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) a = pow(self.pk.g, w, self.pk.p) b = pow(ciphertext.alpha, w, self.pk.p) - c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16) + c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(), 16) t = (w + self.x * c) % self.pk.q return m, { - 'commitment' : {'A' : str(a), 'B': str(b)}, - 'challenge' : str(c), - 'response' : str(t) - } + 'commitment': {'A': str(a), 'B': str(b)}, + 'challenge': str(c), + 'response': str(t) + } def to_dict(self): - return {'x' : str(self.x), 'public_key' : self.pk.to_dict()} + return {'x': str(self.x), 'public_key': self.pk.to_dict()} toJSONDict = to_dict def prove_sk(self, challenge_generator): - """ - Generate a PoK of the secret key - Prover generates w, a random integer modulo q, and computes commitment = g^w mod p. - Verifier provides challenge modulo q. - Prover computes response = w + x*challenge mod q, where x is the secret key. - """ - w = Utils.random_mpz_lt(self.pk.q) - commitment = pow(self.pk.g, w, self.pk.p) - challenge = challenge_generator(commitment) % self.pk.q - response = (w + (self.x * challenge)) % self.pk.q - - return DLogProof(commitment, challenge, response) + """ + Generate a PoK of the secret key + Prover generates w, a random integer modulo q, and computes commitment = g^w mod p. + Verifier provides challenge modulo q. + Prover computes response = w + x*challenge mod q, where x is the secret key. + """ + w = random.mpz_lt(self.pk.q) + commitment = pow(self.pk.g, w, self.pk.p) + challenge = challenge_generator(commitment) % self.pk.q + response = (w + (self.x * challenge)) % self.pk.q + return DLogProof(commitment, challenge, response) @classmethod def from_dict(cls, d): if not d: - return None + return None sk = cls() sk.x = int(d['x']) - if d.has_key('public_key'): - sk.pk = EGPublicKey.from_dict(d['public_key']) + if 'public_key' in d: + sk.pk = EGPublicKey.from_dict(d['public_key']) else: - sk.pk = None + sk.pk = None return sk fromJSONDict = from_dict + class EGPlaintext: - def __init__(self, m = None, pk = None): + def __init__(self, m=None, pk=None): self.m = m self.pk = pk def to_dict(self): - return {'m' : self.m} + return {'m': self.m} @classmethod def from_dict(cls, d): @@ -424,17 +328,17 @@ class EGCiphertext: self.alpha = alpha self.beta = beta - def __mul__(self,other): + def __mul__(self, other): """ Homomorphic Multiplication of ciphertexts. """ - if type(other) == int and (other == 0 or other == 1): - return self + if isinstance(other, int) and (other == 0 or other == 1): + return self if self.pk != other.pk: - logging.info(self.pk) - logging.info(other.pk) - raise Exception('different PKs!') + logging.info(self.pk) + logging.info(other.pk) + raise Exception('different PKs!') new = EGCiphertext() @@ -460,7 +364,7 @@ class EGCiphertext: """ Reencryption with fresh randomness, which is returned. """ - r = Utils.random_mpz_lt(self.pk.q) + r = random.mpz_lt(self.pk.q) new_c = self.reenc_with_r(r) return [new_c, r] @@ -471,189 +375,195 @@ class EGCiphertext: return self.reenc_return_r()[0] def __eq__(self, other): - """ - Check for ciphertext equality. - """ - if other == None: - return False + """ + Check for ciphertext equality. + """ + if other is None: + return False - return (self.alpha == other.alpha and self.beta == other.beta) + return self.alpha == other.alpha and self.beta == other.beta def generate_encryption_proof(self, plaintext, randomness, challenge_generator): - """ - Generate the disjunctive encryption proof of encryption - """ - # random W - w = Utils.random_mpz_lt(self.pk.q) + """ + Generate the disjunctive encryption proof of encryption + """ + # random W + w = random.mpz_lt(self.pk.q) - # build the proof - proof = EGZKProof() + # build the proof + proof = EGZKProof() - # compute A=g^w, B=y^w - proof.commitment['A'] = pow(self.pk.g, w, self.pk.p) - proof.commitment['B'] = pow(self.pk.y, w, self.pk.p) + # compute A=g^w, B=y^w + proof.commitment['A'] = pow(self.pk.g, w, self.pk.p) + proof.commitment['B'] = pow(self.pk.y, w, self.pk.p) - # generate challenge - proof.challenge = challenge_generator(proof.commitment); + # generate challenge + proof.challenge = challenge_generator(proof.commitment) - # Compute response = w + randomness * challenge - proof.response = (w + (randomness * proof.challenge)) % self.pk.q; + # Compute response = w + randomness * challenge + proof.response = (w + (randomness * proof.challenge)) % self.pk.q - return proof; + return proof def simulate_encryption_proof(self, plaintext, challenge=None): - # generate a random challenge if not provided - if not challenge: - challenge = Utils.random_mpz_lt(self.pk.q) + # generate a random challenge if not provided + if not challenge: + challenge = random.mpz_lt(self.pk.q) - proof = EGZKProof() - proof.challenge = challenge + proof = EGZKProof() + proof.challenge = challenge - # compute beta/plaintext, the completion of the DH tuple - beta_over_plaintext = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p + # compute beta/plaintext, the completion of the DH tuple + beta_over_plaintext = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p - # random response, does not even need to depend on the challenge - proof.response = Utils.random_mpz_lt(self.pk.q); + # random response, does not even need to depend on the challenge + proof.response = random.mpz_lt(self.pk.q) - # now we compute A and B - proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p - proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p + # now we compute A and B + proof.commitment['A'] = (number.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) + * pow(self.pk.g, proof.response, self.pk.p) + ) % self.pk.p + proof.commitment['B'] = (number.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow( + self.pk.y, proof.response, self.pk.p)) % self.pk.p - return proof + return proof def generate_disjunctive_encryption_proof(self, plaintexts, real_index, randomness, challenge_generator): - # note how the interface is as such so that the result does not reveal which is the real proof. + # note how the interface is as such so that the result does not reveal which is the real proof. - proofs = [None for p in plaintexts] + proofs = [None for _ in plaintexts] - # go through all plaintexts and simulate the ones that must be simulated. - for p_num in range(len(plaintexts)): - if p_num != real_index: - proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num]) + # go through all plaintexts and simulate the ones that must be simulated. + for p_num in range(len(plaintexts)): + if p_num != real_index: + proofs[p_num] = self.simulate_encryption_proof(plaintexts[p_num]) - # the function that generates the challenge - def real_challenge_generator(commitment): - # set up the partial real proof so we're ready to get the hash - proofs[real_index] = EGZKProof() - proofs[real_index].commitment = commitment + # the function that generates the challenge + def real_challenge_generator(commitment): + # set up the partial real proof so we're ready to get the hash + proofs[real_index] = EGZKProof() + proofs[real_index].commitment = commitment - # get the commitments in a list and generate the whole disjunctive challenge - commitments = [p.commitment for p in proofs] - disjunctive_challenge = challenge_generator(commitments); + # get the commitments in a list and generate the whole disjunctive challenge + commitments = [p.commitment for p in proofs] + disjunctive_challenge = challenge_generator(commitments) - # now we must subtract all of the other challenges from this challenge. - real_challenge = disjunctive_challenge - for p_num in range(len(proofs)): - if p_num != real_index: - real_challenge = real_challenge - proofs[p_num].challenge + # now we must subtract all of the other challenges from this challenge. + real_challenge = disjunctive_challenge + for p_num in range(len(proofs)): + if p_num != real_index: + real_challenge = real_challenge - proofs[p_num].challenge - # make sure we mod q, the exponent modulus - return real_challenge % self.pk.q + # make sure we mod q, the exponent modulus + return real_challenge % self.pk.q - # do the real proof - real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator) + # do the real proof + real_proof = self.generate_encryption_proof(plaintexts[real_index], randomness, real_challenge_generator) - # set the real proof - proofs[real_index] = real_proof + # set the real proof + proofs[real_index] = real_proof - return EGZKDisjunctiveProof(proofs) + return EGZKDisjunctiveProof(proofs) def verify_encryption_proof(self, plaintext, proof): - """ - Checks for the DDH tuple g, y, alpha, beta/plaintext. - (PoK of randomness r.) - - Proof contains commitment = {A, B}, challenge, response - """ - # check that A, B are in the correct group - if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1): - return False + """ + Checks for the DDH tuple g, y, alpha, beta/plaintext. + (PoK of randomness r.) + + Proof contains commitment = {A, B}, challenge, response + """ + # check that A, B are in the correct group + if not (pow(proof.commitment['A'], self.pk.q, self.pk.p) == 1 and pow(proof.commitment['B'], self.pk.q, + self.pk.p) == 1): + return False - # check that g^response = A * alpha^challenge - first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p)) + # check that g^response = A * alpha^challenge + first_check = (pow(self.pk.g, proof.response, self.pk.p) == ( + (pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p)) - # check that y^response = B * (beta/m)^challenge - beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p - second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p)) + # check that y^response = B * (beta/m)^challenge + beta_over_m = (self.beta * number.inverse(plaintext.m, self.pk.p)) % self.pk.p + second_check = (pow(self.pk.y, proof.response, self.pk.p) == ( + (pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p)) - # print "1,2: %s %s " % (first_check, second_check) - return (first_check and second_check) + # print "1,2: %s %s " % (first_check, second_check) + return first_check and second_check def verify_disjunctive_encryption_proof(self, plaintexts, proof, challenge_generator): - """ - plaintexts and proofs are all lists of equal length, with matching. + """ + plaintexts and proofs are all lists of equal length, with matching. - overall_challenge is what all of the challenges combined should yield. - """ - if len(plaintexts) != len(proof.proofs): - print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs))) - return False + overall_challenge is what all of the challenges combined should yield. + """ + if len(plaintexts) != len(proof.proofs): + print("bad number of proofs (expected %s, found %s)" % (len(plaintexts), len(proof.proofs))) + return False - for i in range(len(plaintexts)): - # if a proof fails, stop right there - if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]): - print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]) - return False + for i in range(len(plaintexts)): + # if a proof fails, stop right there + if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]): + print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])) + return False - # logging.info("made it past the two encryption proofs") + # logging.info("made it past the two encryption proofs") - # check the overall challenge - return (challenge_generator([p.commitment for p in proof.proofs]) == (sum([p.challenge for p in proof.proofs]) % self.pk.q)) + # check the overall challenge + return (challenge_generator([p.commitment for p in proof.proofs]) == ( + sum([p.challenge for p in proof.proofs]) % self.pk.q)) def verify_decryption_proof(self, plaintext, proof): - """ - Checks for the DDH tuple g, alpha, y, beta/plaintext - (PoK of secret key x.) - """ - return False + """ + Checks for the DDH tuple g, alpha, y, beta/plaintext + (PoK of secret key x.) + """ + return False def verify_decryption_factor(self, dec_factor, dec_proof, public_key): - """ - when a ciphertext is decrypted by a dec factor, the proof needs to be checked - """ - pass + """ + when a ciphertext is decrypted by a dec factor, the proof needs to be checked + """ + pass def decrypt(self, decryption_factors, public_key): - """ - decrypt a ciphertext given a list of decryption factors (from multiple trustees) - For now, no support for threshold - """ - running_decryption = self.beta - for dec_factor in decryption_factors: - running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p + """ + decrypt a ciphertext given a list of decryption factors (from multiple trustees) + For now, no support for threshold + """ + running_decryption = self.beta + for dec_factor in decryption_factors: + running_decryption = (running_decryption * number.inverse(dec_factor, public_key.p)) % public_key.p - return running_decryption + return running_decryption def check_group_membership(self, pk): - """ - checks to see if an ElGamal element belongs to the group in the pk - """ - if not (1 < self.alpha < pk.p-1): - return False - - elif not (1 < self.beta < pk.p-1): - return False + """ + checks to see if an ElGamal element belongs to the group in the pk + """ + if not (1 < self.alpha < pk.p - 1): + return False - elif (pow(self.alpha, pk.q, pk.p)!=1): - return False + elif not (1 < self.beta < pk.p - 1): + return False - elif (pow(self.beta, pk.q, pk.p)!=1): - return False + elif pow(self.alpha, pk.q, pk.p) != 1: + return False - else: - return True + elif pow(self.beta, pk.q, pk.p) != 1: + return False + else: + return True def to_dict(self): return {'alpha': str(self.alpha), 'beta': str(self.beta)} - toJSONDict= to_dict + toJSONDict = to_dict def to_string(self): return "%s,%s" % (self.alpha, self.beta) @classmethod - def from_dict(cls, d, pk = None): + def from_dict(cls, d, pk=None): result = cls() result.alpha = int(d['alpha']) result.beta = int(d['beta']) @@ -668,127 +578,134 @@ class EGCiphertext: expects alpha,beta """ split = str.split(",") - return cls.from_dict({'alpha' : split[0], 'beta' : split[1]}) + return cls.from_dict({'alpha': split[0], 'beta': split[1]}) + class EGZKProof(object): - def __init__(self): - self.commitment = {'A':None, 'B':None} - self.challenge = None - self.response = None + def __init__(self): + self.commitment = {'A': None, 'B': None} + self.challenge = None + self.response = None - @classmethod - def generate(cls, little_g, little_h, x, p, q, challenge_generator): - """ - generate a DDH tuple proof, where challenge generator is - almost certainly EG_fiatshamir_challenge_generator - """ + @classmethod + def generate(cls, little_g, little_h, x, p, q, challenge_generator): + """ + generate a DDH tuple proof, where challenge generator is + almost certainly EG_fiatshamir_challenge_generator + """ - # generate random w - w = Utils.random_mpz_lt(q) + # generate random w + w = random.mpz_lt(q) - # create proof instance - proof = cls() + # create proof instance + proof = cls() - # compute A = little_g^w, B=little_h^w - proof.commitment['A'] = pow(little_g, w, p) - proof.commitment['B'] = pow(little_h, w, p) + # compute A = little_g^w, B=little_h^w + proof.commitment['A'] = pow(little_g, w, p) + proof.commitment['B'] = pow(little_h, w, p) - # get challenge - proof.challenge = challenge_generator(proof.commitment) + # get challenge + proof.challenge = challenge_generator(proof.commitment) - # compute response - proof.response = (w + (x * proof.challenge)) % q + # compute response + proof.response = (w + (x * proof.challenge)) % q - # return proof - return proof + # return proof + return proof - @classmethod - def from_dict(cls, d): - p = cls() - p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])} - p.challenge = int(d['challenge']) - p.response = int(d['response']) - return p + @classmethod + def from_dict(cls, d): + p = cls() + p.commitment = {'A': int(d['commitment']['A']), 'B': int(d['commitment']['B'])} + p.challenge = int(d['challenge']) + p.response = int(d['response']) + return p - fromJSONDict = from_dict + fromJSONDict = from_dict + + def to_dict(self): + return { + 'commitment': {'A': str(self.commitment['A']), 'B': str(self.commitment['B'])}, + 'challenge': str(self.challenge), + 'response': str(self.response) + } - def to_dict(self): - return { - 'commitment' : {'A' : str(self.commitment['A']), 'B' : str(self.commitment['B'])}, - 'challenge': str(self.challenge), - 'response': str(self.response) - } + toJSONDict = to_dict - def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None): - """ - Verify a DH tuple proof - """ - # check that A, B are in the correct group - if not (pow(proof.commitment['A'],self.pk.q,self.pk.p)==1 and pow(proof.commitment['B'],self.pk.q,self.pk.p)==1): - return False + def verify(self, little_g, little_h, big_g, big_h, p, q, challenge_generator=None): + """ + Verify a DH tuple proof + """ + # check that A, B are in the correct group + if not (pow(self.commitment['A'], self.pk.q, self.pk.p) == 1 + and pow(self.commitment['B'], self.pk.q, self.pk.p) == 1): + return False - # check that little_g^response = A * big_g^challenge - first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p)) + # check that little_g^response = A * big_g^challenge + first_check = (pow(little_g, self.response, p) == ((pow(big_g, self.challenge, p) * self.commitment['A']) % p)) - # check that little_h^response = B * big_h^challenge - second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p)) + # check that little_h^response = B * big_h^challenge + second_check = (pow(little_h, self.response, p) == ((pow(big_h, self.challenge, p) * self.commitment['B']) % p)) - # check the challenge? - third_check = True + # check the challenge? + third_check = True - if challenge_generator: - third_check = (self.challenge == challenge_generator(self.commitment)) + if challenge_generator: + third_check = (self.challenge == challenge_generator(self.commitment)) - return (first_check and second_check and third_check) + return first_check and second_check and third_check - toJSONDict = to_dict class EGZKDisjunctiveProof: - def __init__(self, proofs = None): - self.proofs = proofs + def __init__(self, proofs=None): + self.proofs = proofs - @classmethod - def from_dict(cls, d): - dp = cls() - dp.proofs = [EGZKProof.from_dict(p) for p in d] - return dp + @classmethod + def from_dict(cls, d): + dp = cls() + dp.proofs = [EGZKProof.from_dict(p) for p in d] + return dp - def to_dict(self): - return [p.to_dict() for p in self.proofs] + def to_dict(self): + return [p.to_dict() for p in self.proofs] + + toJSONDict = to_dict - toJSONDict = to_dict class DLogProof(object): - def __init__(self, commitment, challenge, response): - self.commitment = commitment - self.challenge = challenge - self.response = response + def __init__(self, commitment, challenge, response): + self.commitment = commitment + self.challenge = challenge + self.response = response - def to_dict(self): - return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response' : str(self.response)} + def to_dict(self): + return {'challenge': str(self.challenge), 'commitment': str(self.commitment), 'response': str(self.response)} - toJSONDict = to_dict + toJSONDict = to_dict - @classmethod - def from_dict(cls, d): - dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response'])) - return dlp + @classmethod + def from_dict(cls, d): + dlp = cls(int(d['commitment']), int(d['challenge']), int(d['response'])) + return dlp + + fromJSONDict = from_dict - fromJSONDict = from_dict def EG_disjunctive_challenge_generator(commitments): - array_to_hash = [] - for commitment in commitments: - array_to_hash.append(str(commitment['A'])) - array_to_hash.append(str(commitment['B'])) + array_to_hash = [] + for commitment in commitments: + array_to_hash.append(str(commitment['A'])) + array_to_hash.append(str(commitment['B'])) + + string_to_hash = ",".join(array_to_hash) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16) - string_to_hash = ",".join(array_to_hash) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) # a challenge generator for Fiat-Shamir with A,B commitment def EG_fiatshamir_challenge_generator(commitment): - return EG_disjunctive_challenge_generator([commitment]) + return EG_disjunctive_challenge_generator([commitment]) + def DLog_challenge_generator(commitment): - string_to_hash = str(commitment) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) + string_to_hash = str(commitment) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(), 16) diff --git a/helios/crypto/electionalgs.py b/helios/crypto/electionalgs.py index a17ff6a2ea53d4f655ce32405893d838d86fc504..de833f8dad05dee916c2a0a2e82e9242e1beb146 100644 --- a/helios/crypto/electionalgs.py +++ b/helios/crypto/electionalgs.py @@ -4,778 +4,807 @@ Election-specific algorithms for Helios Ben Adida 2008-08-30 """ - -import algs -import logging -import utils -import uuid import datetime +import uuid +import logging -class HeliosObject(object): - """ - A base class to ease serialization and de-serialization - crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out - and deserialized from jsonobjects on the way in - """ - FIELDS = [] - JSON_FIELDS = None - - def __init__(self, **kwargs): - self.set_from_args(**kwargs) - - # generate uuid if need be - if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid == None): - self.uuid = str(uuid.uuid4()) - - def set_from_args(self, **kwargs): - for f in self.FIELDS: - if kwargs.has_key(f): - new_val = self.process_value_in(f, kwargs[f]) - setattr(self, f, new_val) - else: - setattr(self, f, None) - - def set_from_other_object(self, o): - for f in self.FIELDS: - if hasattr(o, f): - setattr(self, f, self.process_value_in(f, getattr(o,f))) - else: - setattr(self, f, None) - - def toJSON(self): - return utils.to_json(self.toJSONDict()) - - def toJSONDict(self, alternate_fields=None): - val = {} - for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS): - val[f] = self.process_value_out(f, getattr(self, f)) - return val - - @classmethod - def fromJSONDict(cls, d): - # go through the keys and fix them - new_d = {} - for k in d.keys(): - new_d[str(k)] = d[k] - - return cls(**new_d) - - @classmethod - def fromOtherObject(cls, o): - obj = cls() - obj.set_from_other_object(o) - return obj - - def toOtherObject(self, o): - for f in self.FIELDS: - # FIXME: why isn't this working? - if hasattr(o, f): - # BIG HAMMER - try: - setattr(o, f, self.process_value_out(f, getattr(self,f))) - except: - pass - - @property - def hash(self): - s = utils.to_json(self.toJSONDict()) - return utils.hash_b64(s) - - def process_value_in(self, field_name, field_value): - """ - process some fields on the way into the object - """ - if field_value == None: - return None - - val = self._process_value_in(field_name, field_value) - if val != None: - return val - else: - return field_value +from helios.utils import to_json +from . import algs +from . import utils - def _process_value_in(self, field_name, field_value): - return None - def process_value_out(self, field_name, field_value): +class HeliosObject(object): """ - process some fields on the way out of the object + A base class to ease serialization and de-serialization + crypto objects are kept as full-blown crypto objects, serialized to jsonobjects on the way out + and deserialized from jsonobjects on the way in """ - if field_value == None: - return None - - val = self._process_value_out(field_name, field_value) - if val != None: - return val - else: - return field_value - - def _process_value_out(self, field_name, field_value): - return None + FIELDS = [] + JSON_FIELDS = None + + def __init__(self, **kwargs): + self.set_from_args(**kwargs) + + # generate uuid if need be + if 'uuid' in self.FIELDS and (not hasattr(self, 'uuid') or self.uuid is None): + self.uuid = str(uuid.uuid4()) + + def set_from_args(self, **kwargs): + for f in self.FIELDS: + if f in kwargs: + new_val = self.process_value_in(f, kwargs[f]) + setattr(self, f, new_val) + else: + setattr(self, f, None) + + def set_from_other_object(self, o): + for f in self.FIELDS: + if hasattr(o, f): + setattr(self, f, self.process_value_in(f, getattr(o, f))) + else: + setattr(self, f, None) + + def toJSON(self): + return to_json(self.toJSONDict()) + + def toJSONDict(self, alternate_fields=None): + val = {} + for f in (alternate_fields or self.JSON_FIELDS or self.FIELDS): + val[f] = self.process_value_out(f, getattr(self, f)) + return val + + @classmethod + def fromJSONDict(cls, d): + # go through the keys and fix them + new_d = {} + for k in list(d.keys()): + new_d[str(k)] = d[k] + + return cls(**new_d) + + @classmethod + def fromOtherObject(cls, o): + obj = cls() + obj.set_from_other_object(o) + return obj + + def toOtherObject(self, o): + for f in self.FIELDS: + # FIXME: why isn't this working? + if hasattr(o, f): + # BIG HAMMER + try: + setattr(o, f, self.process_value_out(f, getattr(self, f))) + except: + pass + + @property + def hash(self): + s = to_json(self.toJSONDict()) + return utils.hash_b64(s) + + def process_value_in(self, field_name, field_value): + """ + process some fields on the way into the object + """ + if field_value is None: + return None + + val = self._process_value_in(field_name, field_value) + if val is not None: + return val + else: + return field_value + + def _process_value_in(self, field_name, field_value): + return None + + def process_value_out(self, field_name, field_value): + """ + process some fields on the way out of the object + """ + if field_value is None: + return None + + val = self._process_value_out(field_name, field_value) + if val is not None: + return val + else: + return field_value + + def _process_value_out(self, field_name, field_value): + return None + + def __eq__(self, other): + if not hasattr(self, 'uuid'): + return super(HeliosObject, self) == other + + return other is not None and self.uuid == other.uuid - def __eq__(self, other): - if not hasattr(self, 'uuid'): - return super(HeliosObject,self) == other - - return other != None and self.uuid == other.uuid class EncryptedAnswer(HeliosObject): - """ - An encrypted answer to a single election question - """ - - FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer'] - - # FIXME: remove this constructor and use only named-var constructor from HeliosObject - 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 + An encrypted answer to a single election question """ - 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) + FIELDS = ['choices', 'individual_proofs', 'overall_proof', 'randomness', 'answer'] - return False + # FIXME: remove this constructor and use only named-var constructor from HeliosObject + 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 - def verify(self, pk, min=0, max=1): - possible_plaintexts = self.generate_plaintexts(pk) - homomorphic_sum = 0 + @classmethod + def generate_plaintexts(cls, pk, min=0, max=1): + plaintexts = [] + running_product = 1 - for choice_num in range(len(self.choices)): - choice = self.choices[choice_num] - choice.pk = pk - individual_proof = self.individual_proofs[choice_num] + # 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)) - # verify that elements belong to the proper group - if not choice.check_group_membership(pk): - return False - - # 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 + # next value in running product + running_product = (running_product * pk.g) % pk.p - # compute homomorphic sum if needed - if max != None: - homomorphic_sum = choice * homomorphic_sum + return plaintexts - if max != None: - # determine possible plaintexts for the sum - sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max) + 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 - # 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 + for choice_num in range(len(self.choices)): + choice = self.choices[choice_num] + choice.pk = pk - def toJSONDict(self, with_randomness=False): - value = { - 'choices': [c.to_dict() for c in self.choices], - 'individual_proofs' : [p.to_dict() for p in self.individual_proofs] - } + # redo the encryption + # WORK HERE (paste from below encryption) - if self.overall_proof: - value['overall_proof'] = self.overall_proof.to_dict() - else: - value['overall_proof'] = None - - if with_randomness: - value['randomness'] = [str(r) for r in self.randomness] - value['answer'] = self.answer - - return value - - @classmethod - def fromJSONDict(cls, d, pk=None): - ea = cls() - - ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']] - ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']] - - if d['overall_proof']: - ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof']) - else: - ea.overall_proof = None + return False - if d.has_key('randomness'): - ea.randomness = [int(r) for r in d['randomness']] - ea.answer = d['answer'] + 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 that elements belong to the proper group + if not choice.check_group_membership(pk): + return False + + # 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 is not None: + homomorphic_sum = choice * homomorphic_sum + + if max is not 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 + + def toJSONDict(self, with_randomness=False): + value = { + 'choices': [c.to_dict() for c in self.choices], + 'individual_proofs': [p.to_dict() for p in self.individual_proofs] + } + + if self.overall_proof: + value['overall_proof'] = self.overall_proof.to_dict() + else: + value['overall_proof'] = None + + if with_randomness: + value['randomness'] = [str(r) for r in self.randomness] + value['answer'] = self.answer + + return value + + @classmethod + def fromJSONDict(cls, d, pk=None): + ea = cls() + + ea.choices = [algs.EGCiphertext.from_dict(c, pk) for c in d['choices']] + ea.individual_proofs = [algs.EGZKDisjunctiveProof.from_dict(p) for p in d['individual_proofs']] + + if d['overall_proof']: + ea.overall_proof = algs.EGZKDisjunctiveProof.from_dict(d['overall_proof']) + else: + ea.overall_proof = None + + if 'randomness' in d: + ea.randomness = [int(r) for r in d['randomness']] + ea.answer = d['answer'] + + return ea + + @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 _ in range(len(answers))] + individual_proofs = [None for _ in range(len(answers))] + randomness = [None for _ 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 'min' in question: + 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] = 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 is not 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 is not 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) - return ea - @classmethod - def fromElectionAndAnswer(cls, election, question_num, answer_indexes): +class EncryptedVote(HeliosObject): """ - 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. + An encrypted ballot """ - 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'] + FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid'] + + def verify(self, election): + # correct number of answers + # noinspection PyUnresolvedReferences + n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0 + n_questions = len(election.questions) if election.questions is not None else 0 + if n_answers != n_questions: + logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})") + return False + + # check hash + # noinspection PyUnresolvedReferences + our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode() + actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode() + if our_election_hash != actual_election_hash: + logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ") + return False + + # check ID + # noinspection PyUnresolvedReferences + our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode() + actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode() + if our_election_uuid != actual_election_uuid: + logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_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 'min' in question: + min_answers = question['min'] + + if not ea.verify(election.public_key, min=min_answers, max=question['max']): + return False + + return True + + def get_hash(self): + return utils.hash_b64(to_json(self.toJSONDict())) + + def toJSONDict(self, with_randomness=False): + return { + 'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers], + 'election_hash': self.election_hash, + 'election_uuid': self.election_uuid + } + + @classmethod + def fromJSONDict(cls, d, pk=None): + ev = cls() + + ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']] + ev.election_hash = d['election_hash'] + ev.election_uuid = d['election_uuid'] + + return ev + + @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) - # 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) +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() - # 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 + # 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']]] - return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes) + # 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 [] -class EncryptedVote(HeliosObject): - """ - An encrypted ballot - """ - FIELDS = ['encrypted_answers', 'election_hash', 'election_uuid'] - - 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 + if question['result_type'] == 'relative': + return [counts[0][0]] - return True - - def get_hash(self): - return utils.hash_b64(utils.to_json(self.toJSONDict(), no_whitespace=True)) - - def toJSONDict(self, with_randomness=False): - return { - 'answers': [a.toJSONDict(with_randomness) for a in self.encrypted_answers], - 'election_hash': self.election_hash, - 'election_uuid': self.election_uuid - } + +class Election(HeliosObject): + 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'] - @classmethod - def fromJSONDict(cls, d, pk=None): - ev = cls() + 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'] - ev.encrypted_answers = [EncryptedAnswer.fromJSONDict(ea, pk) for ea in d['answers']] - ev.election_hash = d['election_hash'] - ev.election_uuid = d['election_uuid'] + # need to add in v3.1: use_advanced_audit_features, election_type, and probably more - return ev + def init_tally(self): + return Tally(election=self) - @classmethod - def fromElectionAndAnswers(cls, election, answers): - pk = election.public_key + 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 isinstance(field_value, str): + return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') - # 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) + if field_name == 'public_key': + return algs.EGPublicKey.fromJSONDict(field_value) + if field_name == 'private_key': + return algs.EGSecretKey.fromJSONDict(field_value) -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]] + 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) -class Election(HeliosObject): + if field_name == 'public_key' or field_name == 'private_key': + return field_value.toJSONDict() - 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'] + @property + def registration_status_pretty(self): + if self.openreg: + return "Open" + else: + return "Closed" - 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'] + @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))] - # need to add in v3.1: use_advanced_audit_features, election_type, and probably more + @property + def pretty_result(self): + if not self.result: + return None - def init_tally(self): - return Tally(election=self) + # get the winners + winners = self.winners - 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') + raw_result = self.result + prettified_result = [] - if field_name == 'public_key': - return algs.EGPublicKey.fromJSONDict(field_value) + # loop through questions + for i in range(len(self.questions)): + q = self.questions[i] + pretty_question = [] - if field_name == 'private_key': - return algs.EGSecretKey.fromJSONDict(field_value) + # 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])}) - 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) + prettified_result.append({'question': q['short_name'], 'answers': pretty_question}) - if field_name == 'public_key' or field_name == 'private_key': - return field_value.toJSONDict() + return prettified_result - @property - def registration_status_pretty(self): - if self.openreg: - return "Open" - else: - return "Closed" - @property - def winners(self): +class Voter(HeliosObject): """ - 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. + A voter in an election """ - return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in range(len(self.questions))] + FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias'] + JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name'] - @property - def pretty_result(self): - if not self.result: - return None + # alternative, for when the voter is aliased + ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias'] - # get the winners - winners = self.winners + def toJSONDict(self): + if self.alias is not None: + return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS) + else: + return super(Voter, self).toJSONDict() - raw_result = self.result - prettified_result = [] + @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) - # 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(HeliosObject): - """ - 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(HeliosObject): - """ - 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(HeliosObject): - """ - 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 + a trustee """ - 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 + FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email'] - def increment(self): - self.counter += 1 + def _process_value_in(self, field_name, field_value): + if field_name == 'public_key': + return algs.EGPublicKey.fromJSONDict(field_value) - # new value - new_value = (self.last_dlog_result * self.base) % self.modulus + if field_name == 'pok': + return algs.DLogProof.fromJSONDict(field_value) - # record the discrete log - self.dlogs[new_value] = self.counter + def _process_value_out(self, field_name, field_value): + if field_name == 'public_key' or field_name == 'pok': + return field_value.toJSONDict() - # 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 CastVote(HeliosObject): + """ + A cast vote, which includes an encrypted vote and some cast metadata + """ + FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash'] -class Tally(HeliosObject): - """ - A running homomorphic tally - """ + def __init__(self, *args, **kwargs): + super(CastVote, self).__init__(*args, **kwargs) + self.election = None - FIELDS = ['num_tallied', 'tally'] - JSON_FIELDS = ['num_tallied', 'tally'] + @classmethod + def fromJSONDict(cls, d, election=None): + o = cls() + o.election = election + o.set_from_args(**d) + return o - def __init__(self, *args, **kwargs): - super(Tally, self).__init__(*args, **kwargs) + def toJSONDict(self, include_vote=True): + result = super(CastVote, self).toJSONDict() + if not include_vote: + del result['vote'] + return result - self.election = kwargs.get('election',None) + @classmethod + def fromOtherObject(cls, o, election): + obj = cls() + obj.election = election + obj.set_from_other_object(o) + return obj - if self.election: - self.init_election(self.election) - else: - self.questions = None - self.public_key = None + def _process_value_in(self, field_name, field_value): + if field_name == 'cast_at': + if isinstance(field_value, str): + return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S') - if not self.tally: - self.tally = None + if field_name == 'vote': + return EncryptedVote.fromJSONDict(field_value, self.election.public_key) - # initialize - if self.num_tallied == None: - self.num_tallied = 0 + def _process_value_out(self, field_name, field_value): + # the date + if field_name == 'cast_at': + return str(field_value) - def init_election(self, election): - """ - given the election, initialize some params - """ - self.questions = election.questions - self.public_key = election.public_key + if field_name == 'vote': + return field_value.toJSONDict() - if not self.tally: - self.tally = [[0 for a in q['answers']] for q in self.questions] + def issues(self, election): + """ + Look for consistency problems + """ + issues = [] - 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 = [] + # 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") - for question_num, question in enumerate(self.questions): - answers = question['answers'] - question_factors = [] - question_proof = [] + return issues - 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): +class DLogTable(object): """ - returns an array of tallies and a corresponding array of decryption proofs. + Keeping track of discrete logs """ - # who's keeping track of discrete logs? - if not discrete_logs: - discrete_logs = self.discrete_logs + def __init__(self, base, modulus): + self.dlogs = {1: 0} + self.last_dlog_result = 1 + self.counter = 0 - # for all choices of all questions (double list comprehension) - decrypted_tally = [] - decryption_proof = [] + self.base = base + self.modulus = modulus - for question_num in range(len(self.questions)): - question = self.questions[question_num] - answers = question['answers'] - question_tally = [] - question_proof = [] + def increment(self): + self.counter += 1 - for answer_num in range(len(answers)): - # do decryption and proof of it - plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num]) + # new value + new_value = (self.last_dlog_result * self.base) % self.modulus - # look up appropriate discrete log - question_tally.append(discrete_logs[plaintext]) - question_proof.append(proof) + # record the discrete log + self.dlogs[new_value] = self.counter - decrypted_tally.append(question_tally) - decryption_proof.append(question_proof) + # record the last value + self.last_dlog_result = new_value - return decrypted_tally, decryption_proof + def precompute(self, up_to): + while self.counter < up_to: + self.increment() - 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 - """ + def lookup(self, value): + return self.dlogs.get(value, None) - # 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): +class Tally(HeliosObject): """ - 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). + A running homomorphic tally """ - # pre-compute a dlog table - dlog_table = DLogTable(base = public_key.g, modulus = public_key.p) - dlog_table.precompute(self.num_tallied) - - result = [] + 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 is 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 _ 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 = [] + # 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) + 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)) + q_result.append(dlog_table.lookup(raw_value)) - result.append(q_result) + result.append(q_result) - return 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_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] + 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] diff --git a/helios/crypto/elgamal.py b/helios/crypto/elgamal.py index 88a08c01ba6c7ab7366281faf989d72427c1d4a8..33eb03083319aa0b7f9ab61defbca3a50bd57440 100644 --- a/helios/crypto/elgamal.py +++ b/helios/crypto/elgamal.py @@ -8,12 +8,13 @@ Ben Adida ben@adida.net """ -import math, hashlib, logging -import randpool, number +import logging -import numtheory +from Crypto.Hash import SHA1 +from Crypto.Util.number import inverse + +from helios.crypto.utils import random -from algs import Utils class Cryptosystem(object): def __init__(self): @@ -21,30 +22,6 @@ class Cryptosystem(object): self.q = None self.g = None - @classmethod - def generate(cls, n_bits): - """ - generate an El-Gamal environment. Returns an instance - of ElGamal(), with prime p, group size q, and generator g - """ - - EG = cls() - - # find a prime p such that (p-1)/2 is prime q - EG.p = Utils.random_safe_prime(n_bits) - - # q is the order of the group - # FIXME: not always p-1/2 - EG.q = (EG.p-1)/2 - - # find g that generates the q-order subgroup - while True: - EG.g = Utils.random_mpz_lt(EG.p) - if pow(EG.g, EG.q, EG.p) == 1: - break - - return EG - def generate_keypair(self): """ generates a keypair in the setting @@ -68,7 +45,7 @@ class KeyPair(object): self.pk.p = p self.pk.q = q - self.sk.x = Utils.random_mpz_lt(q) + self.sk.x = random.mpz_lt(q) self.pk.y = pow(g, self.sk.x, p) self.sk.public_key = self.pk @@ -106,7 +83,7 @@ class PublicKey: """ Encrypt a plaintext and return the randomness just generated and used. """ - r = Utils.random_mpz_lt(self.q) + r = random.mpz_lt(self.q) ciphertext = self.encrypt_with_r(plaintext, r) return [ciphertext, r] @@ -181,7 +158,7 @@ class SecretKey: if not dec_factor: dec_factor = self.decryption_factor(ciphertext) - m = (Utils.inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p + m = (inverse(dec_factor, self.pk.p) * ciphertext.beta) % self.pk.p if decode_m: # get m back from the q-order subgroup @@ -207,15 +184,15 @@ class SecretKey: and alpha^t = b * beta/m ^ c """ - m = (Utils.inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p - beta_over_m = (ciphertext.beta * Utils.inverse(m, self.pk.p)) % self.pk.p + m = (inverse(pow(ciphertext.alpha, self.x, self.pk.p), self.pk.p) * ciphertext.beta) % self.pk.p + beta_over_m = (ciphertext.beta * inverse(m, self.pk.p)) % self.pk.p # pick a random w - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) a = pow(self.pk.g, w, self.pk.p) b = pow(ciphertext.alpha, w, self.pk.p) - c = int(hashlib.sha1(str(a) + "," + str(b)).hexdigest(),16) + c = int(SHA1.new(bytes(str(a) + "," + str(b), 'utf-8')).hexdigest(),16) t = (w + self.x * c) % self.pk.q @@ -232,7 +209,7 @@ class SecretKey: Verifier provides challenge modulo q. Prover computes response = w + x*challenge mod q, where x is the secret key. """ - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) commitment = pow(self.pk.g, w, self.pk.p) challenge = challenge_generator(commitment) % self.pk.q response = (w + (self.x * challenge)) % self.pk.q @@ -255,7 +232,7 @@ class Ciphertext: """ Homomorphic Multiplication of ciphertexts. """ - if type(other) == int and (other == 0 or other == 1): + if isinstance(other, int) and (other == 0 or other == 1): return self if self.pk != other.pk: @@ -287,7 +264,7 @@ class Ciphertext: """ Reencryption with fresh randomness, which is returned. """ - r = Utils.random_mpz_lt(self.pk.q) + r = random.mpz_lt(self.pk.q) new_c = self.reenc_with_r(r) return [new_c, r] @@ -301,17 +278,17 @@ class Ciphertext: """ Check for ciphertext equality. """ - if other == None: + if other is None: return False - return (self.alpha == other.alpha and self.beta == other.beta) + return self.alpha == other.alpha and self.beta == other.beta def generate_encryption_proof(self, plaintext, randomness, challenge_generator): """ Generate the disjunctive encryption proof of encryption """ # random W - w = Utils.random_mpz_lt(self.pk.q) + w = random.mpz_lt(self.pk.q) # build the proof proof = ZKProof() @@ -331,20 +308,20 @@ class Ciphertext: def simulate_encryption_proof(self, plaintext, challenge=None): # generate a random challenge if not provided if not challenge: - challenge = Utils.random_mpz_lt(self.pk.q) + challenge = random.mpz_lt(self.pk.q) proof = ZKProof() proof.challenge = challenge # compute beta/plaintext, the completion of the DH tuple - beta_over_plaintext = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p + beta_over_plaintext = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p # random response, does not even need to depend on the challenge - proof.response = Utils.random_mpz_lt(self.pk.q); + proof.response = random.mpz_lt(self.pk.q); # now we compute A and B - proof.commitment['A'] = (Utils.inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p - proof.commitment['B'] = (Utils.inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p + proof.commitment['A'] = (inverse(pow(self.alpha, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.g, proof.response, self.pk.p)) % self.pk.p + proof.commitment['B'] = (inverse(pow(beta_over_plaintext, proof.challenge, self.pk.p), self.pk.p) * pow(self.pk.y, proof.response, self.pk.p)) % self.pk.p return proof @@ -397,7 +374,7 @@ class Ciphertext: first_check = (pow(self.pk.g, proof.response, self.pk.p) == ((pow(self.alpha, proof.challenge, self.pk.p) * proof.commitment['A']) % self.pk.p)) # check that y^response = B * (beta/m)^challenge - beta_over_m = (self.beta * Utils.inverse(plaintext.m, self.pk.p)) % self.pk.p + beta_over_m = (self.beta * inverse(plaintext.m, self.pk.p)) % self.pk.p second_check = (pow(self.pk.y, proof.response, self.pk.p) == ((pow(beta_over_m, proof.challenge, self.pk.p) * proof.commitment['B']) % self.pk.p)) # print "1,2: %s %s " % (first_check, second_check) @@ -416,7 +393,7 @@ class Ciphertext: for i in range(len(plaintexts)): # if a proof fails, stop right there if not self.verify_encryption_proof(plaintexts[i], proof.proofs[i]): - print "bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i]) + print("bad proof %s, %s, %s" % (i, plaintexts[i], proof.proofs[i])) return False # logging.info("made it past the two encryption proofs") @@ -444,7 +421,7 @@ class Ciphertext: """ running_decryption = self.beta for dec_factor in decryption_factors: - running_decryption = (running_decryption * Utils.inverse(dec_factor, public_key.p)) % public_key.p + running_decryption = (running_decryption * inverse(dec_factor, public_key.p)) % public_key.p return running_decryption @@ -473,7 +450,7 @@ class ZKProof(object): """ # generate random w - w = Utils.random_mpz_lt(q) + w = random.mpz_lt(q) # create proof instance proof = cls() @@ -526,7 +503,7 @@ def disjunctive_challenge_generator(commitments): array_to_hash.append(str(commitment['B'])) string_to_hash = ",".join(array_to_hash) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16) # a challenge generator for Fiat-Shamir with A,B commitment def fiatshamir_challenge_generator(commitment): @@ -534,5 +511,5 @@ def fiatshamir_challenge_generator(commitment): def DLog_challenge_generator(commitment): string_to_hash = str(commitment) - return int(hashlib.sha1(string_to_hash).hexdigest(),16) + return int(SHA1.new(bytes(string_to_hash, 'utf-8')).hexdigest(),16) diff --git a/helios/crypto/number.py b/helios/crypto/number.py deleted file mode 100644 index 9d50563e904ab50a5446916953b0091c2f228031..0000000000000000000000000000000000000000 --- a/helios/crypto/number.py +++ /dev/null @@ -1,201 +0,0 @@ -# -# number.py : Number-theoretic functions -# -# Part of the Python Cryptography Toolkit -# -# Distribute and use freely; there are no restrictions on further -# dissemination and usage except those imposed by the laws of your -# country of residence. This software is provided "as is" without -# warranty of fitness for use or suitability for any purpose, express -# or implied. Use at your own risk or not at all. -# - -__revision__ = "$Id: number.py,v 1.13 2003/04/04 18:21:07 akuchling Exp $" - -bignum = long -try: - from Crypto.PublicKey import _fastmath -except ImportError: - _fastmath = None - -# Commented out and replaced with faster versions below -## def long2str(n): -## s='' -## while n>0: -## s=chr(n & 255)+s -## n=n>>8 -## return s - -## import types -## def str2long(s): -## if type(s)!=types.StringType: return s # Integers will be left alone -## return reduce(lambda x,y : x*256+ord(y), s, 0L) - -def size (N): - """size(N:long) : int - Returns the size of the number N in bits. - """ - bits, power = 0,1L - while N >= power: - bits += 1 - power = power << 1 - return bits - -def getRandomNumber(N, randfunc): - """getRandomNumber(N:int, randfunc:callable):long - Return an N-bit random number.""" - - S = randfunc(N/8) - odd_bits = N % 8 - if odd_bits != 0: - char = ord(randfunc(1)) >> (8-odd_bits) - S = chr(char) + S - value = bytes_to_long(S) - value |= 2L ** (N-1) # Ensure high bit is set - assert size(value) >= N - return value - -def GCD(x,y): - """GCD(x:long, y:long): long - Return the GCD of x and y. - """ - x = abs(x) ; y = abs(y) - while x > 0: - x, y = y % x, x - return y - -def inverse(u, v): - """inverse(u:long, u:long):long - Return the inverse of u mod v. - """ - u3, v3 = long(u), long(v) - u1, v1 = 1L, 0L - while v3 > 0: - q=u3 / v3 - u1, v1 = v1, u1 - v1*q - u3, v3 = v3, u3 - v3*q - while u1<0: - u1 = u1 + v - return u1 - -# Given a number of bits to generate and a random generation function, -# find a prime number of the appropriate size. - -def getPrime(N, randfunc): - """getPrime(N:int, randfunc:callable):long - Return a random N-bit prime number. - """ - - number=getRandomNumber(N, randfunc) | 1 - while (not isPrime(number)): - number=number+2 - return number - -def isPrime(N): - """isPrime(N:long):bool - Return true if N is prime. - """ - if N == 1: - return 0 - if N in sieve: - return 1 - for i in sieve: - if (N % i)==0: - return 0 - - # Use the accelerator if available - if _fastmath is not None: - return _fastmath.isPrime(N) - - # Compute the highest bit that's set in N - N1 = N - 1L - n = 1L - while (n<N): - n=n<<1L - n = n >> 1L - - # Rabin-Miller test - for c in sieve[:7]: - a=long(c) ; d=1L ; t=n - while (t): # Iterate over the bits in N1 - x=(d*d) % N - if x==1L and d!=1L and d!=N1: - return 0 # Square root of 1 found - if N1 & t: - d=(x*a) % N - else: - d=x - t = t >> 1L - if d!=1L: - return 0 - return 1 - -# Small primes used for checking primality; these are all the primes -# less than 256. This should be enough to eliminate most of the odd -# numbers before needing to do a Rabin-Miller test at all. - -sieve=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, - 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, - 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, - 197, 199, 211, 223, 227, 229, 233, 239, 241, 251] - -# Improved conversion functions contributed by Barry Warsaw, after -# careful benchmarking - -import struct - -def long_to_bytes(n, blocksize=0): - """long_to_bytes(n:long, blocksize:int) : string - Convert a long integer to a byte string. - - If optional blocksize is given and greater than zero, pad the front of the - byte string with binary zeros so that the length is a multiple of - blocksize. - """ - # after much testing, this algorithm was deemed to be the fastest - s = '' - n = long(n) - pack = struct.pack - while n > 0: - s = pack('>I', n & 0xffffffffL) + s - n = n >> 32 - # strip off leading zeros - for i in range(len(s)): - if s[i] != '\000': - break - else: - # only happens when n == 0 - s = '\000' - i = 0 - s = s[i:] - # add back some pad bytes. this could be done more efficiently w.r.t. the - # de-padding being done above, but sigh... - if blocksize > 0 and len(s) % blocksize: - s = (blocksize - len(s) % blocksize) * '\000' + s - return s - -def bytes_to_long(s): - """bytes_to_long(string) : long - Convert a byte string to a long integer. - - This is (essentially) the inverse of long_to_bytes(). - """ - acc = 0L - unpack = struct.unpack - length = len(s) - if length % 4: - extra = (4 - length % 4) - s = '\000' * extra + s - length = length + extra - for i in range(0, length, 4): - acc = (acc << 32) + unpack('>I', s[i:i+4])[0] - return acc - -# For backwards compatibility... -import warnings -def long2str(n, blocksize=0): - warnings.warn("long2str() has been replaced by long_to_bytes()") - return long_to_bytes(n, blocksize) -def str2long(s): - warnings.warn("str2long() has been replaced by bytes_to_long()") - return bytes_to_long(s) diff --git a/helios/crypto/numtheory.py b/helios/crypto/numtheory.py index 16fcf9aa0c2fe0017471546d0f353c9cb60878bd..e16691745cf7f9dd2713274b67c1a1a0091df12e 100644 --- a/helios/crypto/numtheory.py +++ b/helios/crypto/numtheory.py @@ -103,7 +103,7 @@ def trial_division(n, bound=None): if n == 1: return 1 for p in [2, 3, 5]: if n%p == 0: return p - if bound == None: bound = n + if bound is None: bound = n dif = [6, 4, 2, 4, 2, 4, 6, 2] m = 7; i = 1 while m <= bound and m*m <= n: @@ -207,7 +207,7 @@ def inversemod(a, n): """ g, x, y = xgcd(a, n) if g != 1: - raise ZeroDivisionError, (a,n) + raise ZeroDivisionError(a,n) assert g == 1, "a must be coprime to n." return x%n @@ -225,7 +225,7 @@ def solve_linear(a,b,n): Examples: >>> solve_linear(4, 2, 10) 8 - >>> solve_linear(2, 1, 4) == None + >>> solve_linear(2, 1, 4) is None True """ g, c, _ = xgcd(a,n) # (1) @@ -1014,7 +1014,7 @@ def elliptic_curve_method(N, m, tries=5): E, P = randcurve(N) # (2) try: # (3) Q = ellcurve_mul(E, m, P) # (4) - except ZeroDivisionError, x: # (5) + except ZeroDivisionError as x: # (5) g = gcd(x[0],N) # (6) if g != 1 or g != N: return g # (7) return N @@ -1153,7 +1153,7 @@ class Poly: # (1) return r def __neg__(self): v = {} - for m in self.v.keys(): + for m in list(self.v.keys()): v[m] = -self.v[m] return Poly(v) def __div__(self, other): @@ -1161,7 +1161,7 @@ class Poly: # (1) def __getitem__(self, m): # (6) m = tuple(m) - if not self.v.has_key(m): self.v[m] = 0 + if m not in self.v: self.v[m] = 0 return self.v[m] def __setitem__(self, m, c): self.v[tuple(m)] = c @@ -1169,7 +1169,7 @@ class Poly: # (1) del self.v[tuple(m)] def monomials(self): # (7) - return self.v.keys() + return list(self.v.keys()) def normalize(self): # (8) while True: finished = True @@ -1244,8 +1244,8 @@ def prove_associative(): # (15) - (x3 + x4)*(x3 - x4)*(x3 - x4)) s2 = (x3 - x4)*(x3 - x4)*((y1 - y5)*(y1 - y5) \ - (x1 + x5)*(x1 - x5)*(x1 - x5)) - print "Associative?" - print s1 == s2 # (17) + print("Associative?") + print(s1 == s2) # (17) diff --git a/helios/crypto/randpool.py b/helios/crypto/randpool.py deleted file mode 100644 index 53a8acc035c225b23ea9e6edd2a8b27b39b75082..0000000000000000000000000000000000000000 --- a/helios/crypto/randpool.py +++ /dev/null @@ -1,422 +0,0 @@ -# -# randpool.py : Cryptographically strong random number generation -# -# Part of the Python Cryptography Toolkit -# -# Distribute and use freely; there are no restrictions on further -# dissemination and usage except those imposed by the laws of your -# country of residence. This software is provided "as is" without -# warranty of fitness for use or suitability for any purpose, express -# or implied. Use at your own risk or not at all. -# - -__revision__ = "$Id: randpool.py,v 1.14 2004/05/06 12:56:54 akuchling Exp $" - -import time, array, types, warnings, os.path -from number import long_to_bytes -try: - import Crypto.Util.winrandom as winrandom -except: - winrandom = None - -STIRNUM = 3 - -class RandomPool: - """randpool.py : Cryptographically strong random number generation. - - The implementation here is similar to the one in PGP. To be - cryptographically strong, it must be difficult to determine the RNG's - output, whether in the future or the past. This is done by using - a cryptographic hash function to "stir" the random data. - - Entropy is gathered in the same fashion as PGP; the highest-resolution - clock around is read and the data is added to the random number pool. - A conservative estimate of the entropy is then kept. - - If a cryptographically secure random source is available (/dev/urandom - on many Unixes, Windows CryptGenRandom on most Windows), then use - it. - - Instance Attributes: - bits : int - Maximum size of pool in bits - bytes : int - Maximum size of pool in bytes - entropy : int - Number of bits of entropy in this pool. - - Methods: - add_event([s]) : add some entropy to the pool - get_bytes(int) : get N bytes of random data - randomize([N]) : get N bytes of randomness from external source - """ - - - def __init__(self, numbytes = 160, cipher=None, hash=None): - if hash is None: - from hashlib import sha1 as hash - - # The cipher argument is vestigial; it was removed from - # version 1.1 so RandomPool would work even in the limited - # exportable subset of the code - if cipher is not None: - warnings.warn("'cipher' parameter is no longer used") - - if isinstance(hash, types.StringType): - # ugly hack to force __import__ to give us the end-path module - hash = __import__('Crypto.Hash.'+hash, - None, None, ['new']) - warnings.warn("'hash' parameter should now be a hashing module") - - self.bytes = numbytes - self.bits = self.bytes*8 - self.entropy = 0 - self._hash = hash - - # Construct an array to hold the random pool, - # initializing it to 0. - self._randpool = array.array('B', [0]*self.bytes) - - self._event1 = self._event2 = 0 - self._addPos = 0 - self._getPos = hash().digest_size - self._lastcounter=time.time() - self.__counter = 0 - - self._measureTickSize() # Estimate timer resolution - self._randomize() - - def _updateEntropyEstimate(self, nbits): - self.entropy += nbits - if self.entropy < 0: - self.entropy = 0 - elif self.entropy > self.bits: - self.entropy = self.bits - - def _randomize(self, N = 0, devname = '/dev/urandom'): - """_randomize(N, DEVNAME:device-filepath) - collects N bits of randomness from some entropy source (e.g., - /dev/urandom on Unixes that have it, Windows CryptoAPI - CryptGenRandom, etc) - DEVNAME is optional, defaults to /dev/urandom. You can change it - to /dev/random if you want to block till you get enough - entropy. - """ - data = '' - if N <= 0: - nbytes = int((self.bits - self.entropy)/8+0.5) - else: - nbytes = int(N/8+0.5) - if winrandom: - # Windows CryptGenRandom provides random data. - data = winrandom.new().get_bytes(nbytes) - # GAE fix, benadida - #elif os.path.exists(devname): - # # Many OSes support a /dev/urandom device - # try: - # f=open(devname) - # data=f.read(nbytes) - # f.close() - # except IOError, (num, msg): - # if num!=2: raise IOError, (num, msg) - # # If the file wasn't found, ignore the error - if data: - self._addBytes(data) - # Entropy estimate: The number of bits of - # data obtained from the random source. - self._updateEntropyEstimate(8*len(data)) - self.stir_n() # Wash the random pool - - def randomize(self, N=0): - """randomize(N:int) - use the class entropy source to get some entropy data. - This is overridden by KeyboardRandomize(). - """ - return self._randomize(N) - - def stir_n(self, N = STIRNUM): - """stir_n(N) - stirs the random pool N times - """ - for i in xrange(N): - self.stir() - - def stir (self, s = ''): - """stir(s:string) - Mix up the randomness pool. This will call add_event() twice, - but out of paranoia the entropy attribute will not be - increased. The optional 's' parameter is a string that will - be hashed with the randomness pool. - """ - - entropy=self.entropy # Save inital entropy value - self.add_event() - - # Loop over the randomness pool: hash its contents - # along with a counter, and add the resulting digest - # back into the pool. - for i in range(self.bytes / self._hash().digest_size): - h = self._hash(self._randpool) - h.update(str(self.__counter) + str(i) + str(self._addPos) + s) - self._addBytes( h.digest() ) - self.__counter = (self.__counter + 1) & 0xFFFFffffL - - self._addPos, self._getPos = 0, self._hash().digest_size - self.add_event() - - # Restore the old value of the entropy. - self.entropy=entropy - - - def get_bytes (self, N): - """get_bytes(N:int) : string - Return N bytes of random data. - """ - - s='' - i, pool = self._getPos, self._randpool - h=self._hash() - dsize = self._hash().digest_size - num = N - while num > 0: - h.update( self._randpool[i:i+dsize] ) - s = s + h.digest() - num = num - dsize - i = (i + dsize) % self.bytes - if i<dsize: - self.stir() - i=self._getPos - - self._getPos = i - self._updateEntropyEstimate(- 8*N) - return s[:N] - - - def add_event(self, s=''): - """add_event(s:string) - Add an event to the random pool. The current time is stored - between calls and used to estimate the entropy. The optional - 's' parameter is a string that will also be XORed into the pool. - Returns the estimated number of additional bits of entropy gain. - """ - event = time.time()*1000 - delta = self._noise() - s = (s + long_to_bytes(event) + - 4*chr(0xaa) + long_to_bytes(delta) ) - self._addBytes(s) - if event==self._event1 and event==self._event2: - # If events are coming too closely together, assume there's - # no effective entropy being added. - bits=0 - else: - # Count the number of bits in delta, and assume that's the entropy. - bits=0 - while delta: - delta, bits = delta>>1, bits+1 - if bits>8: bits=8 - - self._event1, self._event2 = event, self._event1 - - self._updateEntropyEstimate(bits) - return bits - - # Private functions - def _noise(self): - # Adds a bit of noise to the random pool, by adding in the - # current time and CPU usage of this process. - # The difference from the previous call to _noise() is taken - # in an effort to estimate the entropy. - t=time.time() - delta = (t - self._lastcounter)/self._ticksize*1e6 - self._lastcounter = t - self._addBytes(long_to_bytes(long(1000*time.time()))) - self._addBytes(long_to_bytes(long(1000*time.clock()))) - self._addBytes(long_to_bytes(long(1000*time.time()))) - self._addBytes(long_to_bytes(long(delta))) - - # Reduce delta to a maximum of 8 bits so we don't add too much - # entropy as a result of this call. - delta=delta % 0xff - return int(delta) - - - def _measureTickSize(self): - # _measureTickSize() tries to estimate a rough average of the - # resolution of time that you can see from Python. It does - # this by measuring the time 100 times, computing the delay - # between measurements, and taking the median of the resulting - # list. (We also hash all the times and add them to the pool) - interval = [None] * 100 - h = self._hash(`(id(self),id(interval))`) - - # Compute 100 differences - t=time.time() - h.update(`t`) - i = 0 - j = 0 - while i < 100: - t2=time.time() - h.update(`(i,j,t2)`) - j += 1 - delta=int((t2-t)*1e6) - if delta: - interval[i] = delta - i += 1 - t=t2 - - # Take the median of the array of intervals - interval.sort() - self._ticksize=interval[len(interval)/2] - h.update(`(interval,self._ticksize)`) - # mix in the measurement times and wash the random pool - self.stir(h.digest()) - - def _addBytes(self, s): - "XOR the contents of the string S into the random pool" - i, pool = self._addPos, self._randpool - for j in range(0, len(s)): - pool[i]=pool[i] ^ ord(s[j]) - i=(i+1) % self.bytes - self._addPos = i - - # Deprecated method names: remove in PCT 2.1 or later. - def getBytes(self, N): - warnings.warn("getBytes() method replaced by get_bytes()", - DeprecationWarning) - return self.get_bytes(N) - - def addEvent (self, event, s=""): - warnings.warn("addEvent() method replaced by add_event()", - DeprecationWarning) - return self.add_event(s + str(event)) - -class PersistentRandomPool (RandomPool): - def __init__ (self, filename=None, *args, **kwargs): - RandomPool.__init__(self, *args, **kwargs) - self.filename = filename - if filename: - try: - # the time taken to open and read the file might have - # a little disk variability, modulo disk/kernel caching... - f=open(filename, 'rb') - self.add_event() - data = f.read() - self.add_event() - # mix in the data from the file and wash the random pool - self.stir(data) - f.close() - except IOError: - # Oh, well; the file doesn't exist or is unreadable, so - # we'll just ignore it. - pass - - def save(self): - if self.filename == "": - raise ValueError, "No filename set for this object" - # wash the random pool before save, provides some forward secrecy for - # old values of the pool. - self.stir_n() - f=open(self.filename, 'wb') - self.add_event() - f.write(self._randpool.tostring()) - f.close() - self.add_event() - # wash the pool again, provide some protection for future values - self.stir() - -# non-echoing Windows keyboard entry -_kb = 0 -if not _kb: - try: - import msvcrt - class KeyboardEntry: - def getch(self): - c = msvcrt.getch() - if c in ('\000', '\xe0'): - # function key - c += msvcrt.getch() - return c - def close(self, delay = 0): - if delay: - time.sleep(delay) - while msvcrt.kbhit(): - msvcrt.getch() - _kb = 1 - except: - pass - -# non-echoing Posix keyboard entry -if not _kb: - try: - import termios - class KeyboardEntry: - def __init__(self, fd = 0): - self._fd = fd - self._old = termios.tcgetattr(fd) - new = termios.tcgetattr(fd) - new[3]=new[3] & ~termios.ICANON & ~termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, new) - def getch(self): - termios.tcflush(0, termios.TCIFLUSH) # XXX Leave this in? - return os.read(self._fd, 1) - def close(self, delay = 0): - if delay: - time.sleep(delay) - termios.tcflush(self._fd, termios.TCIFLUSH) - termios.tcsetattr(self._fd, termios.TCSAFLUSH, self._old) - _kb = 1 - except: - pass - -class KeyboardRandomPool (PersistentRandomPool): - def __init__(self, *args, **kwargs): - PersistentRandomPool.__init__(self, *args, **kwargs) - - def randomize(self, N = 0): - "Adds N bits of entropy to random pool. If N is 0, fill up pool." - import os, string, time - if N <= 0: - bits = self.bits - self.entropy - else: - bits = N*8 - if bits == 0: - return - print bits,'bits of entropy are now required. Please type on the keyboard' - print 'until enough randomness has been accumulated.' - kb = KeyboardEntry() - s='' # We'll save the characters typed and add them to the pool. - hash = self._hash - e = 0 - try: - while e < bits: - temp=str(bits-e).rjust(6) - os.write(1, temp) - s=s+kb.getch() - e += self.add_event(s) - os.write(1, 6*chr(8)) - self.add_event(s+hash.new(s).digest() ) - finally: - kb.close() - print '\n\007 Enough. Please wait a moment.\n' - self.stir_n() # wash the random pool. - kb.close(4) - -if __name__ == '__main__': - pool = RandomPool() - print 'random pool entropy', pool.entropy, 'bits' - pool.add_event('something') - print `pool.get_bytes(100)` - import tempfile, os - fname = tempfile.mktemp() - pool = KeyboardRandomPool(filename=fname) - print 'keyboard random pool entropy', pool.entropy, 'bits' - pool.randomize() - print 'keyboard random pool entropy', pool.entropy, 'bits' - pool.randomize(128) - pool.save() - saved = open(fname, 'rb').read() - print 'saved', `saved` - print 'pool ', `pool._randpool.tostring()` - newpool = PersistentRandomPool(fname) - print 'persistent random pool entropy', pool.entropy, 'bits' - os.remove(fname) diff --git a/helios/crypto/utils.py b/helios/crypto/utils.py index 9953bdc6aee3c80a3c668dbd0b892a490c879fe6..d854c886bd6e6a0128fbdeec232305ee97870a2a 100644 --- a/helios/crypto/utils.py +++ b/helios/crypto/utils.py @@ -1,34 +1,31 @@ """ Crypto Utils """ -import hashlib -import hmac, base64, json +import base64 +import math + +from Crypto.Hash import SHA256 +from Crypto.Random.random import StrongRandom + +random = StrongRandom() + + +def random_mpz_lt(maximum, strong_random=random): + n_bits = int(math.floor(math.log(maximum, 2))) + res = strong_random.getrandbits(n_bits) + while res >= maximum: + res = strong_random.getrandbits(n_bits) + return res + + +random.mpz_lt = random_mpz_lt + -from hashlib import sha256 - def hash_b64(s): - """ - hash the string using sha1 and produce a base64 output - removes the trailing "=" - """ - hasher = sha256(s) - result= base64.b64encode(hasher.digest())[:-1] - return result - -def to_json(d, no_whitespace=False): - if no_whitespace: - return json.dumps(d, sort_keys=True, separators=(',',':')) - else: - return json.dumps(d, sort_keys=True) - -def from_json(json_str): - if not json_str: return None - return json.loads(json_str) - - -def do_hmac(k,s): - """ - HMAC a value with a key, hex output - """ - mac = hmac.new(k, s, hashlib.sha1) - return mac.hexdigest() \ No newline at end of file + """ + hash the string using sha256 and produce a base64 output + removes the trailing "=" + """ + hasher = SHA256.new(s.encode('utf-8')) + result = base64.b64encode(hasher.digest())[:-1] + return result diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py index 93bca2442f0dbe792bf879afd3e3c704c617a28d..574f8eb9355bcec57a4a60e40f9f8c2154a2af25 100644 --- a/helios/datatypes/__init__.py +++ b/helios/datatypes/__init__.py @@ -25,6 +25,7 @@ And when data comes in: # but is not necessary for full JSON-LD objects. LDObject.deserialize(json_string, type=...) """ +import importlib from helios import utils from helios.crypto import utils as cryptoutils @@ -36,21 +37,21 @@ def recursiveToDict(obj): if obj is None: return None - if type(obj) == list: + if isinstance(obj, list): return [recursiveToDict(el) for el in obj] else: return obj.toDict() def get_class(datatype): # already done? - if not isinstance(datatype, basestring): + if not isinstance(datatype, str): 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) + dynamic_module = importlib.import_module("helios.datatypes." + (".".join(parsed_datatype[:-1]))) if not dynamic_module: raise Exception("no module for %s" % datatype) @@ -58,7 +59,7 @@ def get_class(datatype): # go down the attributes to get to the class try: dynamic_ptr = dynamic_module - for attr in parsed_datatype[1:]: + for attr in parsed_datatype[-1:]: dynamic_ptr = getattr(dynamic_ptr, attr) dynamic_cls = dynamic_ptr except AttributeError: @@ -119,7 +120,7 @@ class LDObject(object): @classmethod def instantiate(cls, obj, datatype=None): - "FIXME: should datatype override the object's internal datatype? probably not" + """FIXME: should datatype override the object's internal datatype? probably not""" if isinstance(obj, LDObject): return obj @@ -149,9 +150,11 @@ class LDObject(object): setattr(self.wrapped_obj, attr, val) def loadData(self): - "load data using from the wrapped object" + """ + load data using from the wrapped object + """ # go through the subfields and instantiate them too - for subfield_name, subfield_type in self.STRUCTURED_FIELDS.iteritems(): + for subfield_name, subfield_type in self.STRUCTURED_FIELDS.items(): self.structured_fields[subfield_name] = self.instantiate(self._getattr_wrapped(subfield_name), datatype = subfield_type) def loadDataFromDict(self, d): @@ -160,7 +163,7 @@ class LDObject(object): """ # the structured fields - structured_fields = self.STRUCTURED_FIELDS.keys() + structured_fields = list(self.STRUCTURED_FIELDS.keys()) # go through the fields and set them properly # on the newly instantiated object @@ -195,7 +198,7 @@ class LDObject(object): for f in (alternate_fields or fields): # is it a structured subfield? - if self.structured_fields.has_key(f): + if f in self.structured_fields: val[f] = recursiveToDict(self.structured_fields[f]) else: val[f] = self.process_value_out(f, self._getattr_wrapped(f)) @@ -274,8 +277,10 @@ class LDObject(object): return field_value def _process_value_out(self, field_name, field_value): + if isinstance(field_value, bytes): + return field_value.decode('utf-8') return None - + def __eq__(self, other): if not hasattr(self, 'uuid'): return super(LDObject, self) == other diff --git a/helios/datatypes/djangofield.py b/helios/datatypes/djangofield.py index 05f8cf355845ddf0eed23d4efc93526a4a06e269..a299ace6c6a7b8f55e231a5e645d238aceb7f51c 100644 --- a/helios/datatypes/djangofield.py +++ b/helios/datatypes/djangofield.py @@ -6,9 +6,9 @@ http://www.djangosnippets.org/snippets/377/ and adapted to LDObject """ -import json from django.db import models +from helios import utils from . import LDObject @@ -28,36 +28,26 @@ class LDObjectField(models.TextField): """Convert our string value to LDObject after we load it from the DB""" # did we already convert this? - if not isinstance(value, basestring): + if not isinstance(value, str): return value return self.from_db_value(value) # noinspection PyUnusedLocal def from_db_value(self, value, *args, **kwargs): - if value is None: - return None - # in some cases, we're loading an existing array or dict, - # we skip this part but instantiate the LD object - if isinstance(value, basestring): - try: - parsed_value = json.loads(value) - except: - raise Exception("value is not JSON parseable, that's bad news") - else: - parsed_value = value - - if parsed_value is not None: - # we give the wrapped object back because we're not dealing with serialization types - return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj - return return_val - else: + # from_json takes care of this duality + parsed_value = utils.from_json(value) + if parsed_value is None: return None + # we give the wrapped object back because we're not dealing with serialization types + return_val = LDObject.fromDict(parsed_value, type_hint=self.type_hint).wrapped_obj + return return_val + def get_prep_value(self, value): """Convert our JSON object to a string before we save""" - if isinstance(value, basestring): + if isinstance(value, str): return value if value is None: diff --git a/helios/datetimewidget.py b/helios/datetimewidget.py index 81d81e0172f7538df77a8d8be8f95774c8bd4e99..dfd7ec04a42118460a216988fa2a9f5a323c1093 100644 --- a/helios/datetimewidget.py +++ b/helios/datetimewidget.py @@ -14,7 +14,7 @@ import datetime, time from django.utils.safestring import mark_safe # DATETIMEWIDGET -calbtn = u'''<img src="%smedia/admin/img/admin/icon_calendar.gif" alt="calendar" id="%s_btn" style="cursor: pointer;" title="Select date" /> +calbtn = '''<img src="%smedia/admin/img/admin/icon_calendar.gif" alt="calendar" id="%s_btn" style="cursor: pointer;" title="Select date" /> <script type="text/javascript"> Calendar.setup({ inputField : "%s", @@ -51,13 +51,13 @@ class DateTimeWidget(forms.widgets.TextInput): except: final_attrs['value'] = \ force_unicode(value) - if not final_attrs.has_key('id'): - final_attrs['id'] = u'%s_id' % (name) + if 'id' not in final_attrs: + final_attrs['id'] = '%s_id' % (name) id = final_attrs['id'] jsdformat = self.dformat #.replace('%', '%%') cal = calbtn % (settings.MEDIA_URL, id, id, jsdformat, id) - a = u'<input%s />%s%s' % (forms.util.flatatt(final_attrs), self.media, cal) + a = '<input%s />%s%s' % (forms.util.flatatt(final_attrs), self.media, cal) return mark_safe(a) def value_from_datadict(self, data, files, name): @@ -84,12 +84,12 @@ class DateTimeWidget(forms.widgets.TextInput): Copy of parent's method, but modify value with strftime function before final comparsion """ if data is None: - data_value = u'' + data_value = '' else: data_value = data if initial is None: - initial_value = u'' + initial_value = '' else: initial_value = initial diff --git a/helios/fields.py b/helios/fields.py index cf2ad6c3e7c2c6b506c0080b432f6207f4e37807..8d8e885fec3c13f72a0c24cd6ea0d7a39ab80d5c 100644 --- a/helios/fields.py +++ b/helios/fields.py @@ -1,9 +1,9 @@ -from time import strptime, strftime import datetime -from django import forms -from django.db import models + from django.forms import fields -from widgets import SplitSelectDateTimeWidget + +from .widgets import SplitSelectDateTimeWidget + class SplitDateTimeField(fields.MultiValueField): widget = SplitSelectDateTimeWidget diff --git a/helios/forms.py b/helios/forms.py index 86fc18e5e3a0e88e4a65f32b62ef12b2bc1dea56..d7919675863641cd24abd5ba3082b0e6014d4e84 100644 --- a/helios/forms.py +++ b/helios/forms.py @@ -3,11 +3,12 @@ Forms for Helios """ from django import forms -from models import Election -from widgets import SplitSelectDateTimeWidget -from fields import SplitDateTimeField from django.conf import settings +from .fields import SplitDateTimeField +from .models import Election +from .widgets import SplitSelectDateTimeWidget + class ElectionForm(forms.Form): short_name = forms.SlugField(max_length=40, help_text='no spaces, will be part of the URL for your election, e.g. my-club-2010') diff --git a/helios/management/commands/load_voter_files.py b/helios/management/commands/load_voter_files.py index c34e682aba306f9bd1380a5ed1dafa91cb247b8b..1e3a79beeecf537b24963a5f2b80f1b46f653dfe 100644 --- a/helios/management/commands/load_voter_files.py +++ b/helios/management/commands/load_voter_files.py @@ -28,7 +28,7 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs): dialect=dialect, **kwargs) for row in csv_reader: # decode UTF-8 back to Unicode, cell by cell: - yield [unicode(cell, 'utf-8') for cell in row] + yield [str(cell, 'utf-8') for cell in row] def utf_8_encoder(unicode_csv_data): diff --git a/helios/migrations/0001_initial.py b/helios/migrations/0001_initial.py index 8ba6efbb125a7b1c4e1e3ed6e74b831bedaf25f1..886b370219ab7fd2fc6fb218148761ae5c4d026c 100644 --- a/helios/migrations/0001_initial.py +++ b/helios/migrations/0001_initial.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.db import models, migrations + +import helios.datatypes import helios.datatypes.djangofield import helios_auth.jsonfield -import helios.datatypes class Migration(migrations.Migration): diff --git a/helios/migrations/0002_castvote_cast_ip.py b/helios/migrations/0002_castvote_cast_ip.py index 47db5b169eccee318b2ccb4fc18f125e984a9c83..bb7a422639f66cd81974549a95c000204ebe13da 100644 --- a/helios/migrations/0002_castvote_cast_ip.py +++ b/helios/migrations/0002_castvote_cast_ip.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.db import models, migrations diff --git a/helios/migrations/0003_auto_20160507_1948.py b/helios/migrations/0003_auto_20160507_1948.py index 162d6bb54dcb6c1e74c99e859615d7fe51dc179d..8e6f65266e5c6f04bf3cb07b8927ac94ea2ece95 100644 --- a/helios/migrations/0003_auto_20160507_1948.py +++ b/helios/migrations/0003_auto_20160507_1948.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.db import models, migrations diff --git a/helios/migrations/0004_auto_20170528_2025.py b/helios/migrations/0004_auto_20170528_2025.py index d49bb63753f4438ab79c0ce4101f7e56f7bcf8be..a437c5f11f03a5a0766ecd970a031bda409fe3fd 100644 --- a/helios/migrations/0004_auto_20170528_2025.py +++ b/helios/migrations/0004_auto_20170528_2025.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.db import migrations, models diff --git a/helios/migrations/0005_auto_20210123_0941.py b/helios/migrations/0005_auto_20210123_0941.py new file mode 100644 index 0000000000000000000000000000000000000000..355687c60a0129c2ed948815d938bfb1d67ed581 --- /dev/null +++ b/helios/migrations/0005_auto_20210123_0941.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2021-01-23 09:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('helios', '0004_auto_20170528_2025'), + ] + + operations = [ + migrations.AlterField( + model_name='election', + name='datatype', + field=models.CharField(default='legacy/Election', max_length=250), + ), + migrations.AlterField( + model_name='election', + name='election_type', + field=models.CharField(choices=[('election', 'Election'), ('referendum', 'Referendum')], default='election', max_length=250), + ), + migrations.AlterField( + model_name='voterfile', + name='voter_file', + field=models.FileField(max_length=250, null=True, upload_to='voters/%Y/%m/%d'), + ), + ] diff --git a/helios/models.py b/helios/models.py index 96e83ddc5755b02ff4fe1baf1384f8db03844df7..83344076f7d11068c3d85eec34b5c79613f6154c 100644 --- a/helios/models.py +++ b/helios/models.py @@ -6,25 +6,26 @@ Ben Adida (ben@adida.net) """ -import datetime - -import bleach import copy import csv +import datetime import io -import random -import unicodecsv import uuid + +import bleach +import unicodecsv from django.conf import settings from django.db import models, transaction -from crypto import algs, utils from helios import datatypes -from helios import utils as heliosutils +from helios import utils from helios.datatypes.djangofield import LDObjectField # useful stuff in helios_auth from helios_auth.jsonfield import JSONField from helios_auth.models import User, AUTH_SYSTEMS +from .crypto import algs +from .crypto.elgamal import Cryptosystem +from .crypto.utils import random, hash_b64 class HeliosModel(models.Model, datatypes.LDObjectContainer): @@ -181,22 +182,26 @@ class Election(HeliosModel): if not self.use_voter_aliases: return None - return heliosutils.one_val_raw_sql("select max(cast(substring(alias, 2) as integer)) from " + Voter._meta.db_table + " where election_id = %s", [self.id]) or 0 + return utils.one_val_raw_sql("select max(cast(substring(alias, 2) as integer)) from " + Voter._meta.db_table + " where election_id = %s", [self.id]) or 0 @property def encrypted_tally_hash(self): if not self.encrypted_tally: return None - return utils.hash_b64(self.encrypted_tally.toJSON()) + return hash_b64(self.encrypted_tally.toJSON()) @property def is_archived(self): - return self.archived_at != None + return self.archived_at is not None @property def description_bleached(self): - return bleach.clean(self.description, tags = bleach.ALLOWED_TAGS + ['p', 'h4', 'h5', 'h3', 'h2', 'br', 'u']) + return bleach.clean(self.description, + tags=bleach.ALLOWED_TAGS + ['p', 'h4', 'h5', 'h3', 'h2', 'br', 'u'], + strip=True, + strip_comments=True, + ) @classmethod def get_featured(cls): @@ -209,9 +214,9 @@ class Election(HeliosModel): @classmethod def get_by_user_as_admin(cls, user, archived_p=None, limit=None): query = cls.objects.filter(admin = user) - if archived_p == True: + if archived_p is True: query = query.exclude(archived_at= None) - if archived_p == False: + if archived_p is False: query = query.filter(archived_at= None) query = query.order_by('-created_at') if limit: @@ -222,9 +227,9 @@ class Election(HeliosModel): @classmethod def get_by_user_as_voter(cls, user, archived_p=None, limit=None): query = cls.objects.filter(voter__user = user) - if archived_p == True: + if archived_p is True: query = query.exclude(archived_at= None) - if archived_p == False: + if archived_p is False: query = query.filter(archived_at= None) query = query.order_by('-created_at') if limit: @@ -285,7 +290,7 @@ class Election(HeliosModel): if not self.openreg: return False - if self.eligibility == None: + if self.eligibility is None: return True # is the user eligible for one of these cases? @@ -300,7 +305,7 @@ class Election(HeliosModel): return [] # constraints that are relevant - relevant_constraints = [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type and constraint.has_key('constraint')] + relevant_constraints = [constraint['constraint'] for constraint in self.eligibility if constraint['auth_system'] == user_type and 'constraint' in constraint] if len(relevant_constraints) > 0: return relevant_constraints[0] else: @@ -326,7 +331,7 @@ class Election(HeliosModel): return_val = "<ul>" for constraint in self.eligibility: - if constraint.has_key('constraint'): + if 'constraint' in constraint: for one_constraint in constraint['constraint']: return_val += "<li>%s</li>" % AUTH_SYSTEMS[constraint['auth_system']].pretty_eligibility(one_constraint) else: @@ -340,7 +345,7 @@ class Election(HeliosModel): """ has voting begun? voting begins if the election is frozen, at the prescribed date or at the date that voting was forced to start """ - return self.frozen_at != None and (self.voting_starts_at == None or (datetime.datetime.utcnow() >= (self.voting_started_at or self.voting_starts_at))) + return self.frozen_at is not None and (self.voting_starts_at is None or (datetime.datetime.utcnow() >= (self.voting_started_at or self.voting_starts_at))) def voting_has_stopped(self): """ @@ -348,12 +353,12 @@ class Election(HeliosModel): or failing that the date voting was extended until, or failing that the date voting is scheduled to end at. """ voting_end = self.voting_ended_at or self.voting_extended_until or self.voting_ends_at - return (voting_end != None and datetime.datetime.utcnow() >= voting_end) or self.encrypted_tally + return (voting_end is not None and datetime.datetime.utcnow() >= voting_end) or self.encrypted_tally @property def issues_before_freeze(self): issues = [] - if self.questions == None or len(self.questions) == 0: + if self.questions is None or len(self.questions) == 0: issues.append( {'type': 'questions', 'action': "add questions to the ballot"} @@ -367,7 +372,7 @@ class Election(HeliosModel): }) for t in trustees: - if t.public_key == None: + if t.public_key is None: issues.append({ 'type': 'trustee keypairs', 'action': 'have trustee %s generate a keypair' % t.name @@ -396,8 +401,8 @@ class Election(HeliosModel): self.save() def ready_for_decryption(self): - return self.encrypted_tally != None - + return self.encrypted_tally is not None + def ready_for_decryption_combination(self): """ do we have a tally from all trustees? @@ -445,7 +450,7 @@ class Election(HeliosModel): else: voters = Voter.get_by_election(self) voters_json = utils.to_json([v.toJSONDict() for v in voters]) - self.voters_hash = utils.hash_b64(voters_json) + self.voters_hash = hash_b64(voters_json) def increment_voters(self): ## FIXME @@ -469,15 +474,16 @@ class Election(HeliosModel): """ # don't override existing eligibility - if self.eligibility != None: + if self.eligibility is not None: return # enable this ONLY once the cast_confirm screen makes sense #if self.voter_set.count() == 0: # return - auth_systems = copy.copy(settings.AUTH_ENABLED_AUTH_SYSTEMS) - voter_types = [r['user__user_type'] for r in self.voter_set.values('user__user_type').distinct() if r['user__user_type'] != None] + auth_systems = copy.copy(settings.AUTH_ENABLED_SYSTEMS) + voter_types = [r['user__user_type'] for r in self.voter_set.values('user__user_type').distinct() if + r['user__user_type'] is not None] # password is now separate, not an explicit voter type if self.voter_set.filter(user=None).count() > 0: @@ -526,6 +532,7 @@ class Election(HeliosModel): """ generate a trustee including the secret key, thus a helios-based trustee + :type params: Cryptosystem """ # FIXME: generate the keypair keypair = params.generate_keypair() @@ -553,7 +560,7 @@ class Election(HeliosModel): return None def has_helios_trustee(self): - return self.get_helios_trustee() != None + return self.get_helios_trustee() is not None def helios_trustee_decrypt(self): tally = self.encrypted_tally @@ -597,7 +604,7 @@ class Election(HeliosModel): determining the winner for one question """ # sort the answers , keep track of the index - counts = sorted(enumerate(result), key=lambda(x): x[1]) + counts = sorted(enumerate(result), key=lambda x: x[1]) counts.reverse() the_max = question['max'] or 1 @@ -680,9 +687,9 @@ def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs): for row in csv_reader: # decode UTF-8 back to Unicode, cell by cell: try: - yield [unicode(cell, 'utf-8') for cell in row] + yield [str(cell, 'utf-8') for cell in row] except: - yield [unicode(cell, 'latin-1') for cell in row] + yield [str(cell, 'latin-1') for cell in row] def utf_8_encoder(unicode_csv_data): for line in unicode_csv_data: @@ -713,20 +720,25 @@ class VoterFile(models.Model): def itervoters(self): if self.voter_file_content: - if type(self.voter_file_content) == unicode: - content = self.voter_file_content.encode('utf-8') - else: + if isinstance(self.voter_file_content, str): + content = self.voter_file_content.encode(encoding='utf-8') + elif isinstance(self.voter_file_content, bytes): content = self.voter_file_content + else: + raise TypeError("voter_file_content is of type {0} instead of str or bytes" + .format(str(type(self.voter_file_content)))) # now we have to handle non-universal-newline stuff # we do this in a simple way: replace all \r with \n # then, replace all double \n with single \n # this should leave us with only \n - content = content.replace('\r','\n').replace('\n\n','\n') + content = content.replace(b'\r',b'\n').replace(b'\n\n',b'\n') + close = False voter_stream = io.BytesIO(content) else: - voter_stream = open(self.voter_file.path, "rU") + close = True + voter_stream = open(self.voter_file.path, "rb") #reader = unicode_csv_reader(voter_stream) reader = unicodecsv.reader(voter_stream, encoding='utf-8') @@ -750,6 +762,8 @@ class VoterFile(models.Model): return_dict['name'] = return_dict['email'] yield return_dict + if close: + voter_stream.close() def process(self): self.processing_started_at = datetime.datetime.utcnow() @@ -776,7 +790,7 @@ class VoterFile(models.Model): existing_voter.save() if election.use_voter_aliases: - voter_alias_integers = range(last_alias_num+1, last_alias_num+1+num_voters) + voter_alias_integers = list(range(last_alias_num+1, last_alias_num+1+num_voters)) random.shuffle(voter_alias_integers) for i, voter in enumerate(new_voters): voter.alias = 'V%s' % voter_alias_integers[i] @@ -814,8 +828,7 @@ class Voter(HeliosModel): alias = models.CharField(max_length = 100, null=True) # we keep a copy here for easy tallying - vote = LDObjectField(type_hint = 'legacy/EncryptedVote', - null=True) + vote = LDObjectField(type_hint = 'legacy/EncryptedVote', null=True) vote_hash = models.CharField(max_length = 100, null=True) cast_at = models.DateTimeField(auto_now_add=False, null=True) @@ -838,7 +851,7 @@ class Voter(HeliosModel): # do we need to generate an alias? if election.use_voter_aliases: - heliosutils.lock_row(Election, election.id) + utils.lock_row(Election, election.id) alias_num = election.last_alias_num + 1 voter.alias = "V%s" % alias_num @@ -854,14 +867,14 @@ class Voter(HeliosModel): # the boolean check is not stupid, this is ternary logic # none means don't care if it's cast or not - if cast == True: + if cast is True: query = query.exclude(cast_at = None) - elif cast == False: + elif cast is False: query = query.filter(cast_at = None) # little trick to get around GAE limitation # order by uuid only when no inequality has been added - if cast == None or order_by == 'cast_at' or order_by =='-cast_at': + if cast is None or order_by == 'cast_at' or order_by == '-cast_at': query = query.order_by(order_by) # if we want the list after a certain UUID, add the inequality here @@ -945,12 +958,12 @@ class Voter(HeliosModel): value_to_hash = self.voter_id try: - return utils.hash_b64(value_to_hash) + return hash_b64(value_to_hash) except: try: - return utils.hash_b64(value_to_hash.encode('latin-1')) + return hash_b64(value_to_hash.encode('latin-1')) except: - return utils.hash_b64(value_to_hash.encode('utf-8')) + return hash_b64(value_to_hash.encode('utf-8')) @property def voter_type(self): @@ -970,7 +983,7 @@ class Voter(HeliosModel): if self.voter_password: raise Exception("password already exists") - self.voter_password = heliosutils.random_string(length, alphabet='abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') + self.voter_password = utils.random_string(length, alphabet='abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') def store_vote(self, cast_vote): # only store the vote if it's cast later than the current one @@ -1035,9 +1048,9 @@ class CastVote(HeliosModel): """ find a tiny version of the hash for a URL slug. """ - safe_hash = self.vote_hash - for c in ['/', '+']: - safe_hash = safe_hash.replace(c,'') + safe_hash = self.vote_hash.decode() if isinstance(self.vote_hash, bytes) else self.vote_hash + for c in ['/', '+', '#']: + safe_hash = safe_hash.replace(c, '') length = 8 while True: @@ -1164,7 +1177,7 @@ class Trustee(HeliosModel): """ # not saved yet? if not self.secret: - self.secret = heliosutils.random_string(12) + self.secret = utils.random_string(12) self.election.append_log("Trustee %s added" % self.name) super(Trustee, self).save(*args, **kwargs) diff --git a/helios/security.py b/helios/security.py index 88dc6754d0bea4e046b6a3e2bb3f8e7c13bb63b5..2f0852d95eb92485c155f359463162b665c1e00a 100644 --- a/helios/security.py +++ b/helios/security.py @@ -4,21 +4,19 @@ Helios Security -- mostly access control Ben Adida (ben@adida.net) """ +import urllib.parse # nicely update the wrapper function from functools import update_wrapper -from django.urls import reverse +from django.conf import settings from django.core.exceptions import PermissionDenied from django.http import Http404 -from django.conf import settings - -from models import Voter, Trustee, Election -from helios_auth.security import get_user - from django.http import HttpResponseRedirect -import urllib +from django.urls import reverse import helios +from helios_auth.security import get_user +from .models import Voter, Trustee, Election class HSTSMiddleware: @@ -45,7 +43,7 @@ def get_voter(request, user, election): return the current voter """ voter = None - if request.session.has_key('CURRENT_VOTER_ID'): + if 'CURRENT_VOTER_ID' in request.session: voter = Voter.objects.get(id=request.session['CURRENT_VOTER_ID']) if voter.election != election: voter = None @@ -59,7 +57,7 @@ def get_voter(request, user, election): # a function to check if the current user is a trustee HELIOS_TRUSTEE_UUID = 'helios_trustee_uuid' def get_logged_in_trustee(request): - if request.session.has_key(HELIOS_TRUSTEE_UUID): + if HELIOS_TRUSTEE_UUID in request.session: return Trustee.get_by_uuid(request.session[HELIOS_TRUSTEE_UUID]) else: return None @@ -72,26 +70,26 @@ def set_logged_in_trustee(request, trustee): # def do_election_checks(election, props): # frozen - if props.has_key('frozen'): + if 'frozen' in props: frozen = props['frozen'] else: frozen = None # newvoters (open for registration) - if props.has_key('newvoters'): + if 'newvoters' in props: newvoters = props['newvoters'] else: newvoters = None # frozen check - if frozen != None: + if frozen is not None: if frozen and not election.frozen_at: raise PermissionDenied() if not frozen and election.frozen_at: raise PermissionDenied() # open for new voters check - if newvoters != None: + if newvoters is not None: if election.can_add_voters() != newvoters: raise PermissionDenied() @@ -120,10 +118,10 @@ def election_view(**checks): # if private election, only logged in voters if election.private_p and not checks.get('allow_logins',False): - from views import password_voter_login + from .views import password_voter_login if not user_can_see_election(request, election): return_url = request.get_full_path() - return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.urlencode({ + return HttpResponseRedirect("%s?%s" % (reverse(password_voter_login, args=[election.uuid]), urllib.parse.urlencode({ 'return_url' : return_url }))) @@ -158,11 +156,13 @@ def user_can_see_election(request, election): return True # then this user has to be a voter - return (get_voter(request, user, election) != None) + return get_voter(request, user, election) is not None + def api_client_can_admin_election(api_client, election): - return election.api_client == api_client and api_client != None - + return election.api_client == api_client and api_client is not None + + # decorator for checking election admin access, and some properties of the election # frozen - is the election frozen # newvoters - does the election accept new voters diff --git a/helios/stats_views.py b/helios/stats_views.py index 5e71a32e8dca7cf20a191aeffe9cc8dc5bda3492..e506637fa7b8ba224bf96ce6a544758dbdaacd70 100644 --- a/helios/stats_views.py +++ b/helios/stats_views.py @@ -12,8 +12,8 @@ from django.http import HttpResponseRedirect from helios import tasks, url_names from helios.models import CastVote, Election from helios_auth.security import get_user -from security import PermissionDenied -from view_utils import render_template +from .security import PermissionDenied +from .view_utils import render_template def require_admin(request): diff --git a/helios/tasks.py b/helios/tasks.py index 8610097182707b2aa40abc68e79c148fa664b19d..a1079c1d33c321747d05d5d8b317015f7c928ba1 100644 --- a/helios/tasks.py +++ b/helios/tasks.py @@ -8,9 +8,9 @@ import copy from celery import shared_task from celery.utils.log import get_logger -import signals -from models import CastVote, Election, Voter, VoterFile -from view_utils import render_template_raw +from . import signals +from .models import CastVote, Election, Voter, VoterFile +from .view_utils import render_template_raw @shared_task diff --git a/helios/tests.py b/helios/tests.py index 157ce2823da9cc332790cea5561cd11d8704730d..60103c8d3c9b9aa4193d71ea9156d804f313da64 100644 --- a/helios/tests.py +++ b/helios/tests.py @@ -3,11 +3,12 @@ Unit Tests for Helios """ import datetime +import logging import re -import urllib +import uuid +from urllib.parse import urlencode import django_webtest -import uuid from django.conf import settings from django.core import mail from django.core.files import File @@ -53,45 +54,46 @@ class ElectionModelTests(TestCase): self.assertTrue(self.created_p) # should have a creation time - self.assertNotEquals(self.election.created_at, None) + self.assertNotEqual(self.election.created_at, None) self.assertTrue(self.election.created_at < datetime.datetime.utcnow()) def test_find_election(self): election = models.Election.get_by_user_as_admin(self.user)[0] - self.assertEquals(self.election, election) + self.assertEqual(self.election, election) election = models.Election.get_by_uuid(self.election.uuid) - self.assertEquals(self.election, election) + self.assertEqual(self.election, election) election = models.Election.get_by_short_name(self.election.short_name) - self.assertEquals(self.election, election) + self.assertEqual(self.election, election) def test_setup_trustee(self): self.setup_trustee() - self.assertEquals(self.election.num_trustees, 1) + self.assertEqual(self.election.num_trustees, 1) def test_add_voters_file(self): election = self.election FILE = "helios/fixtures/voter-file.csv" - vf = models.VoterFile.objects.create(election = election, voter_file = File(open(FILE), "voter_file.css")) - vf.process() + with open(FILE, 'r', encoding='utf-8') as f: + vf = models.VoterFile.objects.create(election = election, voter_file = File(f, "voter_file.css")) + vf.process() # make sure that we stripped things correctly voter = election.voter_set.get(voter_login_id = 'benadida5') - self.assertEquals(voter.voter_email, 'ben5@adida.net') - self.assertEquals(voter.voter_name, 'Ben5 Adida') + self.assertEqual(voter.voter_email, 'ben5@adida.net') + self.assertEqual(voter.voter_name, 'Ben5 Adida') def test_check_issues_before_freeze(self): # should be three issues: no trustees, and no questions, and no voters issues = self.election.issues_before_freeze - self.assertEquals(len(issues), 3) + self.assertEqual(len(issues), 3) self.setup_questions() # should be two issues: no trustees, and no voters issues = self.election.issues_before_freeze - self.assertEquals(len(issues), 2) + self.assertEqual(len(issues), 2) self.election.questions = None @@ -99,7 +101,7 @@ class ElectionModelTests(TestCase): # should be two issues: no questions, and no voters issues = self.election.issues_before_freeze - self.assertEquals(len(issues), 2) + self.assertEqual(len(issues), 2) self.setup_questions() @@ -107,7 +109,7 @@ class ElectionModelTests(TestCase): self.setup_openreg() issues = self.election.issues_before_freeze - self.assertEquals(len(issues), 0) + self.assertEqual(len(issues), 0) def test_helios_trustee(self): self.election.generate_trustee(views.ELGAMAL_PARAMS) @@ -115,7 +117,7 @@ class ElectionModelTests(TestCase): self.assertTrue(self.election.has_helios_trustee()) trustee = self.election.get_helios_trustee() - self.assertNotEquals(trustee, None) + self.assertNotEqual(trustee, None) def test_log(self): LOGS = ["testing 1", "testing 2", "testing 3"] @@ -126,7 +128,7 @@ class ElectionModelTests(TestCase): pulled_logs = [l.log for l in self.election.get_log().all()] pulled_logs.reverse() - self.assertEquals(LOGS,pulled_logs) + self.assertEqual(LOGS,pulled_logs) def test_eligibility(self): self.election.eligibility = [{'auth_system': self.user.user_type}] @@ -137,7 +139,7 @@ class ElectionModelTests(TestCase): # what about after saving? self.election.save() e = models.Election.objects.get(uuid = self.election.uuid) - self.assertEquals(e.eligibility, [{'auth_system': self.user.user_type}]) + self.assertEqual(e.eligibility, [{'auth_system': self.user.user_type}]) self.election.openreg = True @@ -150,6 +152,13 @@ class ElectionModelTests(TestCase): def test_facebook_eligibility(self): self.election.eligibility = [{'auth_system': 'facebook', 'constraint':[{'group': {'id': '123', 'name':'Fake Group'}}]}] + import settings + fb_enabled = 'facebook' in settings.AUTH_ENABLED_SYSTEMS + if not fb_enabled: + logging.error("'facebook' not enabled for auth, cannot its constraints.") + self.assertFalse(self.election.user_eligible_p(self.fb_user)) + return + # without openreg, this should be false self.assertFalse(self.election.user_eligible_p(self.fb_user)) @@ -166,7 +175,7 @@ class ElectionModelTests(TestCase): self.assertTrue(self.election.user_eligible_p(self.fb_user)) # also check that eligibility_category_id does the right thing - self.assertEquals(self.election.eligibility_category_id('facebook'), '123') + self.assertEqual(self.election.eligibility_category_id('facebook'), '123') def test_freeze(self): # freezing without trustees and questions, no good @@ -194,7 +203,7 @@ class ElectionModelTests(TestCase): def test_voter_registration(self): # before adding a voter voters = models.Voter.get_by_election(self.election) - self.assertEquals(0, len(voters)) + self.assertEqual(0, len(voters)) # make sure no voter yet voter = models.Voter.get_by_election_and_user(self.election, self.user) @@ -202,7 +211,7 @@ class ElectionModelTests(TestCase): # make sure no voter at all across all elections voters = models.Voter.get_by_user(self.user) - self.assertEquals(0, len(voters)) + self.assertEqual(0, len(voters)) # register the voter voter = models.Voter.register_user_in_election(self.user, self.election) @@ -212,17 +221,17 @@ class ElectionModelTests(TestCase): self.assertIsNotNone(voter) self.assertIsNotNone(voter_2) - self.assertEquals(voter, voter_2) + self.assertEqual(voter, voter_2) # make sure voter is there in this call too voters = models.Voter.get_by_user(self.user) - self.assertEquals(1, len(voters)) - self.assertEquals(voter, voters[0]) + self.assertEqual(1, len(voters)) + self.assertEqual(voter, voters[0]) voter_2 = models.Voter.get_by_election_and_uuid(self.election, voter.uuid) - self.assertEquals(voter, voter_2) + self.assertEqual(voter, voter_2) - self.assertEquals(voter.user, self.user) + self.assertEqual(voter.user, self.user) @@ -241,13 +250,13 @@ class VoterModelTests(TestCase): v.save() # password has been generated! - self.assertFalse(v.voter_password == None) + self.assertFalse(v.voter_password is None) # can't generate passwords twice self.assertRaises(Exception, lambda: v.generate_password()) # check that you can get at the voter user structure - self.assertEquals(v.get_user().user_id, v.voter_email) + self.assertEqual(v.get_user().user_id, v.voter_email) class CastVoteModelTests(TestCase): @@ -289,7 +298,7 @@ class DatatypeTests(TestCase): 'B' : '234324243'} ld_obj = datatypes.LDObject.fromDict(original_dict, type_hint = 'legacy/EGZKProofCommitment') - self.assertEquals(original_dict, ld_obj.toDict()) + self.assertEqual(original_dict, ld_obj.toDict()) @@ -303,9 +312,8 @@ class DataFormatBlackboxTests(object): self.election = models.Election.objects.all()[0] def assertEqualsToFile(self, response, file_path): - expected = open(file_path) - self.assertEquals(response.content, expected.read()) - expected.close() + with open(file_path) as expected: + self.assertEqual(response.content, expected.read().encode('utf-8')) def test_election(self): response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False) @@ -349,23 +357,27 @@ class LegacyElectionBlackboxTests(DataFormatBlackboxTests, TestCase): class WebTest(django_webtest.WebTest): def assertStatusCode(self, response, status_code): - if hasattr(response, 'status_code'): - assert response.status_code == status_code, response.status_code + actual_code = response.status_code if hasattr(response, 'status_code') else response.status_int + if isinstance(status_code, (list, tuple)): + assert actual_code in status_code, "%s instad of %s" % (actual_code, status_code) else: - assert response.status_int == status_code, response.status_int + assert actual_code == status_code, "%s instad of %s" % (actual_code, status_code) - def assertRedirects(self, response, url): + def assertRedirects(self, response, url=None): """ reimplement this in case it's a WebOp response and it seems to be screwing up in a few places too thus the localhost exception """ + self.assertStatusCode(response, (301, 302)) + location = None if hasattr(response, 'location'): - assert url in response.location, response.location + location = response.location else: - assert url in response['location'], response['location'] - self.assertStatusCode(response, 302) + location = response['location'] + if url is not None: + assert url in location, location #return super(django_webtest.WebTest, self).assertRedirects(response, url) #assert url in response.location, "redirected to %s instead of %s" % (response.location, url) @@ -374,11 +386,17 @@ class WebTest(django_webtest.WebTest): self.assertStatusCode(response, 200) if hasattr(response, "testbody"): - assert text in response.testbody, "missing text %s" % text + t = response.testbody elif hasattr(response, "body"): - assert text in response.body, "missing text %s" % text + t = response.body else: - assert text in response.content, "missing text %s" % text + t = response.content + + if isinstance(text, bytes): + text = text.decode() + if isinstance(t, bytes): + t = t.decode() + assert text in t, "missing text %s" % text ## @@ -418,7 +436,7 @@ class ElectionBlackboxTests(WebTest): def test_election_params(self): response = self.client.get("/helios/elections/params") - self.assertEquals(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize()) + self.assertEqual(response.content, views.ELGAMAL_PARAMS_LD_OBJECT.serialize().encode('utf-8')) def test_election_404(self): response = self.client.get("/helios/elections/foobar") @@ -430,15 +448,15 @@ class ElectionBlackboxTests(WebTest): def test_get_election_shortcut(self): response = self.client.get("/helios/e/%s" % self.election.short_name, follow=True) - self.assertContains(response, self.election.description) + self.assertContains(response, self.election.description_bleached) def test_get_election_raw(self): response = self.client.get("/helios/elections/%s" % self.election.uuid, follow=False) - self.assertEquals(response.content, self.election.toJSON()) + self.assertEqual(response.content, self.election.toJSON().encode('utf-8')) def test_get_election(self): response = self.client.get("/helios/elections/%s/view" % self.election.uuid, follow=False) - self.assertContains(response, self.election.description) + self.assertContains(response, self.election.description_bleached) def test_get_election_questions(self): response = self.client.get("/helios/elections/%s/questions" % self.election.uuid, follow=False) @@ -460,7 +478,7 @@ class ElectionBlackboxTests(WebTest): def test_get_election_voters_raw(self): response = self.client.get("/helios/elections/%s/voters/" % self.election.uuid, follow=False) - self.assertEquals(len(utils.from_json(response.content)), self.election.num_voters) + self.assertEqual(len(response.json()), self.election.num_voters) def test_election_creation_not_logged_in(self): response = self.client.post("/helios/elections/new", { @@ -489,7 +507,7 @@ class ElectionBlackboxTests(WebTest): self.assertRedirects(response, "/helios/elections/%s/view" % self.election.uuid) new_election = models.Election.objects.get(uuid = self.election.uuid) - self.assertEquals(new_election.short_name, self.election.short_name + "-2") + self.assertEqual(new_election.short_name, self.election.short_name + "-2") def test_get_election_stats(self): self.setup_login(from_scratch=True, user_id='mccio@github.com', user_type='google') @@ -533,10 +551,11 @@ class ElectionBlackboxTests(WebTest): full_election_params.update(election_params or {}) response = self.client.post("/helios/elections/new", full_election_params) + self.assertRedirects(response) # we are redirected to the election, let's extract the ID out of the URL - election_id = re.search('/elections/([^/]+)/', str(response['Location'])) - self.assertIsNotNone(election_id, "Election id not found in redirect: %s" % str(response['Location'])) + election_id = re.search('/elections/([^/]+)/', str(response['location'])) + self.assertIsNotNone(election_id, "Election id not found in redirect: %s" % str(response['location'])) election_id = election_id.group(1) # helios is automatically added as a trustee @@ -569,7 +588,7 @@ class ElectionBlackboxTests(WebTest): # and we want to check that there are now voters response = self.client.get("/helios/elections/%s/voters/" % election_id) NUM_VOTERS = 4 - self.assertEquals(len(utils.from_json(response.content)), NUM_VOTERS) + self.assertEqual(len(response.json()), NUM_VOTERS) # let's get a single voter single_voter = models.Election.objects.get(uuid = election_id).voter_set.all()[0] @@ -602,7 +621,7 @@ class ElectionBlackboxTests(WebTest): }) self.assertRedirects(response, "/helios/elections/%s/view" % election_id) num_messages_after = len(mail.outbox) - self.assertEquals(num_messages_after - num_messages_before, NUM_VOTERS) + self.assertEqual(num_messages_after - num_messages_before, NUM_VOTERS) email_message = mail.outbox[num_messages_before] assert "your password" in email_message.subject, "bad subject in email" @@ -613,7 +632,7 @@ class ElectionBlackboxTests(WebTest): # now log out as administrator self.clear_login() - self.assertEquals(self.client.session.has_key('user'), False) + self.assertEqual('user' in self.client.session, False) # return the voter username and password to vote return election_id, username, password @@ -629,11 +648,11 @@ class ElectionBlackboxTests(WebTest): # parse it as an encrypted vote with randomness, and make sure randomness is there the_ballot = utils.from_json(response.testbody) - assert the_ballot['answers'][0].has_key('randomness'), "no randomness" + assert 'randomness' in the_ballot['answers'][0], "no randomness" assert len(the_ballot['answers'][0]['randomness']) == 2, "not enough randomness" # parse it as an encrypted vote, and re-serialize it - ballot = datatypes.LDObject.fromDict(utils.from_json(response.testbody), type_hint='legacy/EncryptedVote') + ballot = datatypes.LDObject.fromDict(the_ballot, type_hint='legacy/EncryptedVote') encrypted_vote = ballot.serialize() # cast the ballot @@ -661,7 +680,7 @@ class ElectionBlackboxTests(WebTest): # confirm the vote, now with the actual form cast_form = cast_confirm_page.form - if 'status_update' in cast_form.fields.keys(): + if 'status_update' in list(cast_form.fields.keys()): cast_form['status_update'] = False response = cast_form.submit() @@ -711,7 +730,7 @@ class ElectionBlackboxTests(WebTest): self.assertRedirects(response, "/helios/elections/%s/view" % election_id) # should trigger helios decryption automatically - self.assertNotEquals(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None) + self.assertNotEqual(models.Election.objects.get(uuid=election_id).get_helios_trustee().decryption_proofs, None) # combine decryptions response = self.client.post("/helios/elections/%s/combine_decryptions" % election_id, { @@ -732,7 +751,7 @@ class ElectionBlackboxTests(WebTest): # check that tally matches response = self.client.get("/helios/elections/%s/result" % election_id) - self.assertEquals(utils.from_json(response.content), [[0,1]]) + self.assertEqual(response.json(), [[0,1]]) def test_do_complete_election(self): election_id, username, password = self._setup_complete_election() @@ -761,7 +780,7 @@ class ElectionBlackboxTests(WebTest): response = self.app.get("/helios/elections/%s/view" % election_id) # ensure it redirects - self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urllib.urlencode({"return_url": "/helios/elections/%s/view" % election_id}))) + self.assertRedirects(response, "/helios/elections/%s/password_voter_login?%s" % (election_id, urlencode({"return_url": "/helios/elections/%s/view" % election_id}))) login_form = response.follow().form diff --git a/helios/urls.py b/helios/urls.py index 6d278a9d195f17b169f4d7f126ef32ed3c012c61..9183eedd1fdc4e2c7fd89535b557d972106fc216 100644 --- a/helios/urls.py +++ b/helios/urls.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- from django.conf.urls import url, include -import url_names as names -import views +from . import views, url_names as names urlpatterns = [ url(r'^autologin$', views.admin_autologin), diff --git a/helios/utils.py b/helios/utils.py index 03095a75074b55123571991a8e35cf0924248431..0080c5f0a3d979d0efd6854f9e7304ec62bd43d1 100644 --- a/helios/utils.py +++ b/helios/utils.py @@ -5,15 +5,17 @@ Ben Adida - ben@adida.net 2005-04-11 """ -import urllib, re, datetime, string +import datetime +import re +import string +import urllib.parse +from django.conf import settings + +from helios.crypto.utils import random # utils from helios_auth, too from helios_auth.utils import * -from django.conf import settings - -import random, logging - def split_by_length(str, length, rejoin_with=None): """ @@ -38,7 +40,7 @@ def urlencode(str): if not str: return "" - return urllib.quote(str) + return urllib.parse.quote(str) def urlencodeall(str): """ @@ -53,11 +55,11 @@ def urldecode(str): if not str: return "" - return urllib.unquote(str) + return urllib.parse.unquote(str) def dictToURLParams(d): if d: - return '&'.join([i + '=' + urlencode(v) for i,v in d.items()]) + return '&'.join([i + '=' + urlencode(v) for i,v in list(d.items())]) else: return None ## @@ -87,31 +89,28 @@ def xss_strip_all_tags(s): if text[:2] == "&#": try: if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)) + return chr(int(text[3:-1], 16)) else: - return unichr(int(text[2:-1])) + return chr(int(text[2:-1])) except ValueError: pass elif text[:1] == "&": - import htmlentitydefs - entity = htmlentitydefs.entitydefs.get(text[1:-1]) + import html.entities + entity = html.entities.entitydefs.get(text[1:-1]) if entity: if entity[:2] == "&#": try: - return unichr(int(entity[2:-1])) + return chr(int(entity[2:-1])) except ValueError: pass else: - return unicode(entity, "iso-8859-1") + return str(entity, "iso-8859-1") return text # leave as is return re.sub("(?s)<[^>]*>|&#?\w+;", fixup, s) - -random.seed() def random_string(length=20, alphabet=None): - random.seed() ALPHABET = alphabet or 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' r_string = '' for i in range(length): @@ -131,7 +130,7 @@ def get_prefix(): ## def string_to_datetime(str, fmt="%Y-%m-%d %H:%M"): - if str == None: + if str is None: return None return datetime.datetime.strptime(str, fmt) diff --git a/helios/view_utils.py b/helios/view_utils.py index 26da7046c50d1f6c8d62e256cd67175f76cf141f..1c8c9a9011e44404633dcfd17e5706ce86bdb2b3 100644 --- a/helios/view_utils.py +++ b/helios/view_utils.py @@ -12,7 +12,7 @@ from django.template import loader from functools import update_wrapper import helios -import utils +from . import utils from helios_auth.security import get_user ## @@ -32,7 +32,7 @@ def prepare_vars(request, values): vars_with_user['user'] = get_user(request) # csrf protection - if request.session.has_key('csrf_token'): + if 'csrf_token' in request.session: vars_with_user['csrf_token'] = request.session['csrf_token'] vars_with_user['utils'] = utils @@ -67,7 +67,7 @@ def render_template_raw(request, template_name, values=None): def render_json(json_txt): - return HttpResponse(json_txt, "application/json") + return HttpResponse(utils.to_json(json_txt), content_type="application/json") # decorator @@ -79,8 +79,8 @@ def return_json(func): def convert_to_json(self, *args, **kwargs): return_val = func(self, *args, **kwargs) try: - return render_json(utils.to_json(return_val)) - except Exception, e: + return render_json(return_val) + except Exception as e: import logging logging.error("problem with serialization: " + str(return_val) + " / " + str(e)) raise e diff --git a/helios/views.py b/helios/views.py index 6d049a8c732daabb74e438dfad6286f9f6a62b29..26a58519d5d803b4b6cb0e8edc957d4f968e81de 100644 --- a/helios/views.py +++ b/helios/views.py @@ -5,51 +5,46 @@ Helios Django Views Ben Adida (ben@adida.net) """ -from django.urls import reverse -from django.core.paginator import Paginator +import base64 +import datetime +import logging +import os +import uuid +from urllib.parse import urlencode + from django.core.exceptions import PermissionDenied -from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden +from django.core.paginator import Paginator from django.db import transaction, IntegrityError - +from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseForbidden +from django.urls import reverse from validate_email import validate_email -import urllib, os, base64 - -from crypto import algs, electionalgs, elgamal -from crypto import utils as cryptoutils -from workflows import homomorphic +import helios_auth.url_names as helios_auth_urls from helios import utils, VOTERS_EMAIL, VOTERS_UPLOAD, url_names -from view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw - -from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts +from helios_auth import views as auth_views from helios_auth.auth_systems import AUTH_SYSTEMS, can_list_categories from helios_auth.models import AuthenticationExpired -import helios_auth.url_names as helios_auth_urls - -from helios_auth import views as auth_views - -import tasks - -from security import (election_view, election_admin, - trustee_check, set_logged_in_trustee, - can_create_election, user_can_see_election, get_voter, - user_can_admin_election, user_can_feature_election) - -import uuid, datetime -import logging - -from models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot -import datatypes - -import forms +from helios_auth.security import check_csrf, login_required, get_user, save_in_session_across_logouts +from . import datatypes +from . import forms +from . import tasks +from .crypto import algs, electionalgs, elgamal +from .crypto import utils as cryptoutils +from .models import User, Election, CastVote, Voter, VoterFile, Trustee, AuditedBallot +from .security import (election_view, election_admin, + trustee_check, set_logged_in_trustee, + can_create_election, user_can_see_election, get_voter, + user_can_admin_election, user_can_feature_election) +from .view_utils import SUCCESS, FAILURE, return_json, render_template, render_template_raw +from .workflows import homomorphic # Parameters for everything ELGAMAL_PARAMS = elgamal.Cryptosystem() # trying new ones from OlivierP -ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071L -ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917L -ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533L +ELGAMAL_PARAMS.p = 16328632084933010002384055033805457329601614771185955389739167309086214800406465799038583634953752941675645562182498120750264980492381375579367675648771293800310370964745767014243638518442553823973482995267304044326777047662957480269391322789378384619428596446446984694306187644767462460965622580087564339212631775817895958409016676398975671266179637898557687317076177218843233150695157881061257053019133078545928983562221396313169622475509818442661047018436264806901023966236718367204710755935899013750306107738002364137917426595737403871114187750804346564731250609196846638183903982387884578266136503697493474682071 +ELGAMAL_PARAMS.q = 61329566248342901292543872769978950870633559608669337131139375508370458778917 +ELGAMAL_PARAMS.g = 14887492224963187634282421537186040801304008017743492304481737382571933937568724473847106029915040150784031882206090286938661464458896494215273989547889201144857352611058572236578734319505128042602372864570426550855201448111746579871811249114781674309062693442442368697449970648232621880001709535143047913661432883287150003429802392229361583608686643243349727791976247247948618930423866180410558458272606627111270040091203073580238905303994472202930783207472394578498507764703191288249547659899997131166130259700604433891232298182348403175947450284433411265966789131024573629546048637848902243503970966798589660808533 # object ready for serialization ELGAMAL_PARAMS_LD_OBJECT = datatypes.LDObject.instantiate(ELGAMAL_PARAMS, datatype='legacy/EGParams') @@ -78,7 +73,7 @@ def user_reauth(request, user): # add a parameter to prevent it? Maybe. login_url = "%s%s?%s" % (settings.SECURE_URL_HOST, reverse(helios_auth_urls.AUTH_START, args=[user.user_type]), - urllib.urlencode({'return_url': + urlencode({'return_url': request.get_full_path()})) return HttpResponseRedirect(login_url) @@ -124,9 +119,9 @@ def election_shortcut(request, election_short_name): # a hidden view behind the shortcut that performs the actual perm check @election_view() def _election_vote_shortcut(request, election): - vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])})) + vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])})) - test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : vote_url})) + test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url})) return HttpResponseRedirect(test_cookie_url) @@ -304,9 +299,9 @@ def one_election_view(request, election): election_badge_url = get_election_badge_url(election) status_update_message = None - vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urllib.urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])})) + vote_url = "%s/booth/vote.html?%s" % (settings.SECURE_URL_HOST, urlencode({'election_url' : reverse(url_names.election.ELECTION_HOME, args=[election.uuid])})) - test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : vote_url})) + test_cookie_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : vote_url})) if user: voter = Voter.get_by_election_and_user(election, user) @@ -329,13 +324,13 @@ def one_election_view(request, election): # status update message? if election.openreg: if election.voting_has_started: - status_update_message = u"Vote in %s" % election.name + status_update_message = "Vote in %s" % election.name else: - status_update_message = u"Register to vote in %s" % election.name + status_update_message = "Register to vote in %s" % election.name # result! if election.result: - status_update_message = u"Results are in for %s" % election.name + status_update_message = "Results are in for %s" % election.name trustees = Trustee.get_by_election(election) @@ -353,20 +348,20 @@ def one_election_view(request, election): def test_cookie(request): continue_url = request.GET['continue_url'] request.session.set_test_cookie() - next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urllib.urlencode({'continue_url': continue_url})) + next_url = "%s?%s" % (reverse(url_names.COOKIE_TEST_2), urlencode({'continue_url': continue_url})) return HttpResponseRedirect(settings.SECURE_URL_HOST + next_url) def test_cookie_2(request): continue_url = request.GET['continue_url'] if not request.session.test_cookie_worked(): - return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urllib.urlencode({'continue_url': continue_url})))) + return HttpResponseRedirect(settings.SECURE_URL_HOST + ("%s?%s" % (reverse(url_names.COOKIE_NO), urlencode({'continue_url': continue_url})))) request.session.delete_test_cookie() return HttpResponseRedirect(continue_url) def nocookies(request): - retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urllib.urlencode({'continue_url' : request.GET['continue_url']})) + retest_url = "%s?%s" % (reverse(url_names.COOKIE_TEST), urlencode({'continue_url' : request.GET['continue_url']})) return render_template(request, 'nocookies', {'retest_url': retest_url}) ## @@ -496,10 +491,8 @@ def get_randomness(request, election): get some randomness to sprinkle into the sjcl entropy pool """ return { - # back to urandom, it's fine - "randomness" : base64.b64encode(os.urandom(32)) - #"randomness" : base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes) - } + "randomness" : base64.b64encode(os.urandom(32)).decode('utf-8') + } @election_view(frozen=True) @return_json @@ -593,7 +586,7 @@ def password_voter_login(request, election): return one_election_cast_confirm(request, election.uuid) except Voter.DoesNotExist: - redirect_url = login_url + "?" + urllib.urlencode({ + redirect_url = login_url + "?" + urlencode({ 'bad_voter_login' : '1', 'return_url' : return_url }) @@ -601,7 +594,7 @@ def password_voter_login(request, election): return HttpResponseRedirect(settings.SECURE_URL_HOST + redirect_url) else: # bad form, bad voter login - redirect_url = login_url + "?" + urllib.urlencode({ + redirect_url = login_url + "?" + urlencode({ 'bad_voter_login' : '1', 'return_url' : return_url }) @@ -615,7 +608,7 @@ def one_election_cast_confirm(request, election): user = get_user(request) # if no encrypted vote, the user is reloading this page or otherwise getting here in a bad way - if (not request.session.has_key('encrypted_vote')) or request.session['encrypted_vote'] == None: + if ('encrypted_vote' not in request.session) or request.session['encrypted_vote'] is None: return HttpResponseRedirect(settings.URL_HOST) # election not frozen or started @@ -693,7 +686,7 @@ def one_election_cast_confirm(request, election): password_only = False - if auth_systems == None or 'password' in auth_systems: + if auth_systems is None or 'password' in auth_systems: show_password = True password_login_form = forms.VoterPasswordForm() @@ -763,7 +756,7 @@ def one_election_cast_done(request, election): # only log out if the setting says so *and* we're dealing # with a site-wide voter. Definitely remove current_voter # checking that voter.user != None is needed because voter.user may now be None if voter is password only - if voter.user == user and voter.user != None: + if voter.user == user and voter.user is not None: logout = settings.LOGOUT_ON_CONFIRMATION else: logout = False @@ -819,7 +812,7 @@ def one_election_bboard(request, election): order_by = 'alias' # if there's a specific voter - if request.GET.has_key('q'): + if 'q' in request.GET: # FIXME: figure out the voter by voter_id voters = [] else: @@ -843,7 +836,7 @@ def one_election_audited_ballots(request, election): UI to show election audited ballots """ - if request.GET.has_key('vote_hash'): + if 'vote_hash' in request.GET: b = AuditedBallot.get(election, request.GET['vote_hash']) return HttpResponse(b.raw_vote, content_type="text/plain") @@ -964,7 +957,7 @@ def one_election_copy(request, election): name = "Copy of " + election.name, election_type = election.election_type, private_p = election.private_p, - description = election.description, + description = election.description_bleached, questions = election.questions, eligibility = election.eligibility, openreg = election.openreg, @@ -1082,14 +1075,14 @@ def one_election_compute_tally(request, election): @trustee_check def trustee_decrypt_and_prove(request, election, trustee): - if not _check_election_tally_type(election) or election.encrypted_tally == None: + if not _check_election_tally_type(election) or election.encrypted_tally is None: return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW,args=[election.uuid])) return render_template(request, 'trustee_decrypt_and_prove', {'election': election, 'trustee': trustee}) @election_view(frozen=True) def trustee_upload_decryption(request, election, trustee_uuid): - if not _check_election_tally_type(election) or election.encrypted_tally == None: + if not _check_election_tally_type(election) or election.encrypted_tally is None: return FAILURE trustee = Trustee.get_by_election_and_uuid(election, trustee_uuid) @@ -1130,7 +1123,7 @@ def release_result(request, election): election.save() if request.POST.get('send_email', ''): - return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urllib.urlencode({'template': 'result'}))) + return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_email, args=[election.uuid]),urlencode({'template': 'result'}))) else: return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(url_names.election.ELECTION_VIEW, args=[election.uuid])) @@ -1293,7 +1286,7 @@ def voters_upload(request, election): return HttpResponseRedirect(settings.SECURE_URL_HOST + reverse(voters_list_pretty, args=[election.uuid])) else: # we need to confirm - if request.FILES.has_key('voters_file'): + if 'voters_file' in request.FILES: voters_file = request.FILES['voters_file'] voter_file_obj = election.add_voters_file(voters_file) @@ -1314,7 +1307,7 @@ def voters_upload(request, election): return render_template(request, 'voters_upload_confirm', {'election': election, 'voters': voters, 'problems': problems}) else: - return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urllib.urlencode({'e':'no voter file specified, try again'}))) + return HttpResponseRedirect("%s?%s" % (settings.SECURE_URL_HOST + reverse(voters_upload, args=[election.uuid]), urlencode({'e':'no voter file specified, try again'}))) @election_admin() def voters_upload_cancel(request, election): @@ -1471,9 +1464,9 @@ def ballot_list(request, election): and optionally take a after parameter. """ limit = after = None - if request.GET.has_key('limit'): + if 'limit' in request.GET: limit = int(request.GET['limit']) - if request.GET.has_key('after'): + if 'after' in request.GET: after = datetime.datetime.strptime(request.GET['after'], '%Y-%m-%d %H:%M:%S') voters = Voter.get_by_election(election, cast=True, order_by='cast_at', limit=limit, after=after) diff --git a/helios/widgets.py b/helios/widgets.py index 9eff2f435bc1bf7e37cb8174a5bc1a6db12272f1..8e3f3e87d26a2798510593c74002e6b869a009dc 100644 --- a/helios/widgets.py +++ b/helios/widgets.py @@ -54,18 +54,18 @@ class SelectTimeWidget(Widget): self.meridiem_val = 'a.m.' # Default to Morning (A.M.) if hour_step and twelve_hr: - self.hours = range(1,13,hour_step) + self.hours = list(range(1,13,hour_step)) elif hour_step: # 24hr, with stepping. - self.hours = range(0,24,hour_step) + self.hours = list(range(0,24,hour_step)) elif twelve_hr: # 12hr, no stepping - self.hours = range(1,13) + self.hours = list(range(1,13)) else: # 24hr, no stepping - self.hours = range(0,24) + self.hours = list(range(0,24)) if minute_step: - self.minutes = range(0,60,minute_step) + self.minutes = list(range(0,60,minute_step)) else: - self.minutes = range(0,60) + self.minutes = list(range(0,60)) def render(self, name, value, attrs=None, renderer=None): try: # try to get time values from a datetime.time object (value) @@ -77,7 +77,7 @@ class SelectTimeWidget(Widget): self.meridiem_val = 'a.m.' except AttributeError: hour_val = minute_val = 0 - if isinstance(value, basestring): + if isinstance(value, str): match = RE_TIME.match(value) if match: time_groups = match.groups() @@ -113,8 +113,8 @@ class SelectTimeWidget(Widget): # For times to get displayed correctly, the values MUST be converted to unicode # When Select builds a list of options, it checks against Unicode values - hour_val = u"%.2d" % hour_val - minute_val = u"%.2d" % minute_val + hour_val = "%.2d" % hour_val + minute_val = "%.2d" % minute_val hour_choices = [("%.2d"%i, "%.2d"%i) for i in self.hours] local_attrs = self.build_attrs({'id': self.hour_field % id_}) @@ -137,7 +137,7 @@ class SelectTimeWidget(Widget): select_html = Select(choices=meridiem_choices).render(self.meridiem_field % name, self.meridiem_val, local_attrs) output.append(select_html) - return mark_safe(u'\n'.join(output)) + return mark_safe('\n'.join(output)) def id_for_label(self, id_): return '%s_hour' % id_ @@ -179,7 +179,7 @@ class SplitSelectDateTimeWidget(MultiWidget): # See https://stackoverflow.com/questions/4324676/django-multiwidget-subclass-not-calling-decompress def value_from_datadict(self, data, files, name): if data.get(name, None) is None: - return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)] + return [widget.value_from_datadict(data, files, name ) for widget in self.widgets] return self.decompress(data.get(name, None)) def decompress(self, value): @@ -187,6 +187,23 @@ class SplitSelectDateTimeWidget(MultiWidget): return [value.date(), value.time().replace(microsecond=0)] return [None, None] + def compress(self, data_list): + """ + Takes the values from the MultiWidget and passes them as a + list to this function. This function needs to compress the + list into a single object in order to be correctly rendered by the widget. + For instace, django.forms.widgets.SelectDateWidget.format_value(value) + expects a date object or a string, not a list. + This method was taken from helios/fields.py + """ + if data_list: + import datetime + if not (data_list[0] and data_list[1]): + return None + return datetime.datetime.combine(*data_list) + return None + def render(self, name, value, attrs=None, renderer=None): + value = self.compress(value) rendered_widgets = list(widget.render(name, value, attrs=attrs, renderer=renderer) for widget in self.widgets) - return u'<br/>'.join(rendered_widgets) + return '<br/>'.join(rendered_widgets) diff --git a/helios/workflows/homomorphic.py b/helios/workflows/homomorphic.py index c3c98eb715482af7421a90e42f3c23c74c71e12f..9c8039679bff10f9b0178fc16f1b27bafdbc16be 100644 --- a/helios/workflows/homomorphic.py +++ b/helios/workflows/homomorphic.py @@ -6,6 +6,7 @@ Ben Adida reworked 2011-01-09 """ +import logging from helios.crypto import algs from . import WorkflowObject @@ -68,10 +69,10 @@ class EncryptedAnswer(WorkflowObject): return False # compute homomorphic sum if needed - if max != None: + if max is not None: homomorphic_sum = choice * homomorphic_sum - if max != None: + if max is not None: # determine possible plaintexts for the sum sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max) @@ -110,7 +111,7 @@ class EncryptedAnswer(WorkflowObject): # min and max for number of answers, useful later min_answers = 0 - if question.has_key('min'): + if 'min' in question: min_answers = question['min'] max_answers = question['max'] @@ -124,7 +125,7 @@ class EncryptedAnswer(WorkflowObject): num_selected_answers += 1 # randomness and encryption - randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q) + randomness[answer_num] = algs.random.mpz_lt(pk.q) choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num]) # generate proof @@ -132,7 +133,7 @@ class EncryptedAnswer(WorkflowObject): randomness[answer_num], algs.EG_disjunctive_challenge_generator) # sum things up homomorphically if needed - if max_answers != None: + if max_answers is not None: homomorphic_sum = choices[answer_num] * homomorphic_sum randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q @@ -142,7 +143,7 @@ class EncryptedAnswer(WorkflowObject): if num_selected_answers < min_answers: raise Exception("Need to select at least %s answer(s)" % min_answers) - if max_answers != None: + if max_answers is not None: sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers) # need to subtract the min from the offset @@ -160,7 +161,7 @@ class EncryptedVote(WorkflowObject): An encrypted ballot """ def __init__(self): - self.encrypted_answers = None + self.encrypted_answers = [] @property def datatype(self): @@ -176,26 +177,37 @@ class EncryptedVote(WorkflowObject): answers = property(_answers_get, _answers_set) def verify(self, election): - # right number of answers - if len(self.encrypted_answers) != len(election.questions): + # correct number of answers + # noinspection PyUnresolvedReferences + n_answers = len(self.encrypted_answers) if self.encrypted_answers is not None else 0 + n_questions = len(election.questions) if election.questions is not None else 0 + if n_answers != n_questions: + logging.error(f"Incorrect number of answers ({n_answers}) vs questions ({n_questions})") return False - + # check hash - if self.election_hash != election.hash: - # print "%s / %s " % (self.election_hash, election.hash) + # noinspection PyUnresolvedReferences + our_election_hash = self.election_hash if isinstance(self.election_hash, str) else self.election_hash.decode() + actual_election_hash = election.hash if isinstance(election.hash, str) else election.hash.decode() + if our_election_hash != actual_election_hash: + logging.error(f"Incorrect election_hash {our_election_hash} vs {actual_election_hash} ") return False - + # check ID - if self.election_uuid != election.uuid: + # noinspection PyUnresolvedReferences + our_election_uuid = self.election_uuid if isinstance(self.election_uuid, str) else self.election_uuid.decode() + actual_election_uuid = election.uuid if isinstance(election.uuid, str) else election.uuid.decode() + if our_election_uuid != actual_election_uuid: + logging.error(f"Incorrect election_uuid {our_election_uuid} vs {actual_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'): + if 'min' in question: min_answers = question['min'] if not ea.verify(election.public_key, min=min_answers, max=question['max']): diff --git a/helios_auth/__init__.py b/helios_auth/__init__.py index ca245ff38f820db528afbed3a43d597bc8642b44..d5e23fa983fe0874f005481d1cddae929efde928 100644 --- a/helios_auth/__init__.py +++ b/helios_auth/__init__.py @@ -4,7 +4,7 @@ from django.conf import settings TEMPLATE_BASE = settings.AUTH_TEMPLATE_BASE or "helios_auth/templates/base.html" # enabled auth systems -import auth_systems -ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_AUTH_SYSTEMS or auth_systems.AUTH_SYSTEMS.keys() -DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_AUTH_SYSTEM or None +from . import auth_systems +ENABLED_AUTH_SYSTEMS = settings.AUTH_ENABLED_SYSTEMS or list(auth_systems.AUTH_SYSTEMS.keys()) +DEFAULT_AUTH_SYSTEM = settings.AUTH_DEFAULT_SYSTEM or None diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py index 5a0e9233ba4df89067688283e90649aef4f1ae70..e9dc9763a371790a2574f199d4b341674135b8a2 100644 --- a/helios_auth/auth_systems/__init__.py +++ b/helios_auth/auth_systems/__init__.py @@ -1,22 +1,49 @@ +from django.conf import settings + +_enabled = settings.AUTH_ENABLED_SYSTEMS or None +def _is_enabled(system): + return _enabled is None or system in _enabled AUTH_SYSTEMS = {} -import twitter, password, cas, facebook, google, yahoo, linkedin, clever -AUTH_SYSTEMS['twitter'] = twitter -AUTH_SYSTEMS['linkedin'] = linkedin -AUTH_SYSTEMS['password'] = password -AUTH_SYSTEMS['cas'] = cas -AUTH_SYSTEMS['facebook'] = facebook -AUTH_SYSTEMS['google'] = google -AUTH_SYSTEMS['yahoo'] = yahoo -AUTH_SYSTEMS['clever'] = clever +if _is_enabled('twitter'): + from . import twitter + AUTH_SYSTEMS['twitter'] = twitter + +if _is_enabled('linkedin'): + from . import linkedin + AUTH_SYSTEMS['linkedin'] = linkedin + +if _is_enabled('password'): + from . import password + AUTH_SYSTEMS['password'] = password + +if _is_enabled('cas'): + from . import cas + AUTH_SYSTEMS['cas'] = cas + +if _is_enabled('facebook'): + from . import facebook + AUTH_SYSTEMS['facebook'] = facebook + +if _is_enabled('google'): + from . import google + AUTH_SYSTEMS['google'] = google + +if _is_enabled('yahoo'): + from . import yahoo + AUTH_SYSTEMS['yahoo'] = yahoo + +if _is_enabled('clever'): + from . import clever + AUTH_SYSTEMS['clever'] = clever # not ready #import live #AUTH_SYSTEMS['live'] = live def can_check_constraint(auth_system): - return hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint') + return auth_system in AUTH_SYSTEMS and hasattr(AUTH_SYSTEMS[auth_system], 'check_constraint') def can_list_categories(auth_system): - return hasattr(AUTH_SYSTEMS[auth_system], 'list_categories') + return auth_system in AUTH_SYSTEMS and hasattr(AUTH_SYSTEMS[auth_system], 'list_categories') diff --git a/helios_auth/auth_systems/cas.py b/helios_auth/auth_systems/cas.py index dd4216b5ecc2c3953892332fa49ae8a51b13eeff..21c86cfe73577021491bacfd1c20f58f36c94fe2 100644 --- a/helios_auth/auth_systems/cas.py +++ b/helios_auth/auth_systems/cas.py @@ -7,13 +7,14 @@ https://sp.princeton.edu/oit/sdp/CAS/Wiki%20Pages/Python.aspx import datetime import re -import urllib -import urllib2 +import urllib.parse +import urllib.request import uuid +from xml.etree import ElementTree + from django.conf import settings from django.core.mail import send_mail from django.http import HttpResponseRedirect -from xml.etree import ElementTree CAS_EMAIL_DOMAIN = "princeton.edu" CAS_URL= 'https://fed.princeton.edu/cas/' @@ -42,17 +43,17 @@ def _get_service_url(): def get_auth_url(request, redirect_url): request.session['cas_redirect_url'] = redirect_url - return CAS_URL + 'login?service=' + urllib.quote(_get_service_url()) + return CAS_URL + 'login?service=' + urllib.parse.quote(_get_service_url()) def get_user_category(user_id): theurl = CAS_ELIGIBILITY_URL % user_id - auth_handler = urllib2.HTTPBasicAuthHandler() + auth_handler = urllib.request.HTTPBasicAuthHandler() auth_handler.add_password(realm=CAS_ELIGIBILITY_REALM, uri= theurl, user= CAS_USERNAME, passwd = CAS_PASSWORD) - opener = urllib2.build_opener(auth_handler) - urllib2.install_opener(opener) + opener = urllib.request.build_opener(auth_handler) + urllib.request.install_opener(opener) - result = urllib2.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip() + result = urllib.request.urlopen(CAS_ELIGIBILITY_URL % user_id).read().strip() parsed_result = ElementTree.fromstring(result) return parsed_result.text @@ -78,11 +79,11 @@ def get_saml_info(ticket): </soap-env:Envelope> """ % (uuid.uuid1(), datetime.datetime.utcnow().isoformat(), ticket) - url = CAS_SAML_VALIDATE_URL % urllib.quote(_get_service_url()) + url = CAS_SAML_VALIDATE_URL % urllib.parse.quote(_get_service_url()) # by virtue of having a body, this is a POST - req = urllib2.Request(url, saml_request) - raw_response = urllib2.urlopen(req).read() + req = urllib.request.Request(url, saml_request) + raw_response = urllib.request.urlopen(req).read() logging.info("RESP:\n%s\n\n" % raw_response) @@ -130,8 +131,8 @@ def get_user_info(user_id): </soap-env:Envelope> """ % user_id - req = urllib2.Request(url, request_body, headers) - response = urllib2.urlopen(req).read() + req = urllib.request.Request(url, request_body, headers) + response = urllib.request.urlopen(req).read() # parse the result from xml.dom.minidom import parseString @@ -149,12 +150,12 @@ def get_user_info(user_id): def get_user_info_special(ticket): # fetch the information from the CAS server val_url = CAS_URL + "validate" + \ - '?service=' + urllib.quote(_get_service_url()) + \ - '&ticket=' + urllib.quote(ticket) - r = urllib.urlopen(val_url).readlines() # returns 2 lines + '?service=' + urllib.parse.quote(_get_service_url()) + \ + '&ticket=' + urllib.parse.quote(ticket) + r = urllib.request.urlopen(val_url).readlines() # returns 2 lines # success - if len(r) == 2 and re.match("yes", r[0]) != None: + if len(r) == 2 and re.match("yes", r[0]) is not None: netid = r[1].strip() category = get_user_category(netid) @@ -212,7 +213,7 @@ def send_message(user_id, name, user_info, subject, body): else: email = "%s@%s" % (user_id, CAS_EMAIL_DOMAIN) - if user_info.has_key('name'): + if 'name' in user_info: name = user_info["name"] else: name = email @@ -224,7 +225,7 @@ def send_message(user_id, name, user_info, subject, body): # def check_constraint(constraint, user): - if not user.info.has_key('category'): + if 'category' not in user.info: return False return constraint['year'] == user.info['category'] diff --git a/helios_auth/auth_systems/clever.py b/helios_auth/auth_systems/clever.py index 498951f8dbf0e3d60f2460c83d580be0b703bb64..48648a0b07a55add763512be3beb457ed08bb298 100644 --- a/helios_auth/auth_systems/clever.py +++ b/helios_auth/auth_systems/clever.py @@ -4,12 +4,14 @@ Clever Authentication """ import base64 +import urllib.parse + import httplib2 -import json -import urllib from django.conf import settings from oauth2client.client import OAuth2WebServerFlow, OAuth2Credentials +from helios_auth import utils + # some parameters to indicate that status updating is not possible STATUS_UPDATES = False @@ -42,7 +44,7 @@ def get_user_info_after_auth(request): # do the POST manually, because OAuth2WebFlow can't do auth header for token exchange http = httplib2.Http(".cache") auth_header = "Basic %s" % base64.b64encode(settings.CLEVER_CLIENT_ID + ":" + settings.CLEVER_CLIENT_SECRET) - resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.urlencode({ + resp_headers, content = http.request("https://clever.com/oauth/tokens", "POST", urllib.parse.urlencode({ "code" : code, "grant_type": "authorization_code", "redirect_uri": redirect_uri @@ -51,7 +53,7 @@ def get_user_info_after_auth(request): 'Content-Type': "application/x-www-form-urlencoded" }) - token_response = json.loads(content) + token_response = utils.from_json(content) access_token = token_response['access_token'] # package the credentials @@ -62,7 +64,7 @@ def get_user_info_after_auth(request): (resp_headers, content) = http.request("https://api.clever.com/me", "GET") # {"type":"student","data":{"id":"563395179f7408755c0006b7","district":"5633941748c07c0100000aac","type":"student","created":"2015-10-30T16:04:39.262Z","credentials":{"district_password":"eel7Thohd","district_username":"dianes10"},"dob":"1998-11-01T00:00:00.000Z","ell_status":"Y","email":"diane.s@example.org","gender":"F","grade":"9","hispanic_ethnicity":"Y","last_modified":"2015-10-30T16:04:39.274Z","location":{"zip":"11433"},"name":{"first":"Diane","last":"Schmeler","middle":"J"},"race":"Asian","school":"5633950c62fc41c041000005","sis_id":"738733110","state_id":"114327752","student_number":"738733110"},"links":[{"rel":"self","uri":"/me"},{"rel":"canonical","uri":"/v1.1/students/563395179f7408755c0006b7"},{"rel":"district","uri":"/v1.1/districts/5633941748c07c0100000aac"}]} - response = json.loads(content) + response = utils.from_json(content) user_id = response['data']['id'] user_name = "%s %s" % (response['data']['name']['first'], response['data']['name']['last']) @@ -70,7 +72,7 @@ def get_user_info_after_auth(request): user_district = response['data']['district'] user_grade = response['data'].get('grade', None) - print content + print(content) # watch out, response also contains email addresses, but not sure whether thsoe are verified or not # so for email address we will only look at the id_token @@ -100,7 +102,7 @@ def send_message(user_id, name, user_info, subject, body): # def check_constraint(constraint, user): - if not user.info.has_key('grade'): + if 'grade' not in user.info: return False return constraint['grade'] == user.info['grade'] diff --git a/helios_auth/auth_systems/facebook.py b/helios_auth/auth_systems/facebook.py index 4c50c6594336f60c2999303c57088e6feeb77b4c..4810ec20494fef34aba4e5017d092c88e185c39c 100644 --- a/helios_auth/auth_systems/facebook.py +++ b/helios_auth/auth_systems/facebook.py @@ -2,8 +2,6 @@ Facebook Authentication """ -import logging - from django.conf import settings from django.core.mail import send_mail @@ -12,7 +10,7 @@ API_KEY = settings.FACEBOOK_API_KEY API_SECRET = settings.FACEBOOK_API_SECRET #from facebookclient import Facebook -import urllib, urllib2, cgi +import urllib.request, urllib.error, urllib.parse # some parameters to indicate that status updating is possible STATUS_UPDATES = True @@ -22,21 +20,21 @@ from helios_auth import utils def facebook_url(url, params): if params: - return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params)) + return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params)) else: return "https://graph.facebook.com%s" % url def facebook_get(url, params): full_url = facebook_url(url,params) try: - return urllib2.urlopen(full_url).read() - except urllib2.HTTPError: + return urllib.request.urlopen(full_url).read() + except urllib.error.HTTPError: from helios_auth.models import AuthenticationExpired raise AuthenticationExpired() def facebook_post(url, params): full_url = facebook_url(url, None) - return urllib2.urlopen(full_url, urllib.urlencode(params)).read() + return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read() def get_auth_url(request, redirect_url): request.session['fb_redirect_uri'] = redirect_url @@ -69,7 +67,7 @@ def update_status(user_id, user_info, token, message): }) def send_message(user_id, user_name, user_info, subject, body): - if user_info.has_key('email'): + if 'email' in user_info: send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (user_name, user_info['email'])], fail_silently=False) diff --git a/helios_auth/auth_systems/facebookclient/__init__.py b/helios_auth/auth_systems/facebookclient/__init__.py deleted file mode 100644 index 03264730c9ab284b3500793fc232ad15175a945e..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/__init__.py +++ /dev/null @@ -1,1431 +0,0 @@ -#! /usr/bin/env python -# -# pyfacebook - Python bindings for the Facebook API -# -# Copyright (c) 2008, Samuel Cormier-Iijima -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may -# be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -Python bindings for the Facebook API (pyfacebook - http://code.google.com/p/pyfacebook) - -PyFacebook is a client library that wraps the Facebook API. - -For more information, see - -Home Page: http://code.google.com/p/pyfacebook -Developer Wiki: http://wiki.developers.facebook.com/index.php/Python -Facebook IRC Channel: #facebook on irc.freenode.net - -PyFacebook can use simplejson if it is installed, which -is much faster than XML and also uses less bandwith. Go to -http://undefined.org/python/#simplejson to download it, or do -apt-get install python-simplejson on a Debian-like system. -""" - -import sys -import time -import struct -import urllib -import urllib2 -import httplib -import hashlib -import binascii -import urlparse -import mimetypes - -# try to use simplejson first, otherwise fallback to XML -RESPONSE_FORMAT = 'JSON' - -import json - -# try: -# import json as simplejson -# except ImportError: -# try: -# import simplejson -# except ImportError: -# try: -# from django.utils import simplejson -# except ImportError: -# try: -# import jsonlib as simplejson -# simplejson.loads -# except (ImportError, AttributeError): -# from xml.dom import minidom -# RESPONSE_FORMAT = 'XML' - -# support Google App Engine. GAE does not have a working urllib.urlopen. -try: - from google.appengine.api import urlfetch - - def urlread(url, data=None, headers=None): - if data is not None: - if headers is None: - headers = {"Content-type": "application/x-www-form-urlencoded"} - method = urlfetch.POST - else: - if headers is None: - headers = {} - method = urlfetch.GET - - result = urlfetch.fetch(url, method=method, - payload=data, headers=headers) - - if result.status_code == 200: - return result.content - else: - raise urllib2.URLError("fetch error url=%s, code=%d" % (url, result.status_code)) - -except ImportError: - def urlread(url, data=None): - res = urllib2.urlopen(url, data=data) - return res.read() - -__all__ = ['Facebook'] - -VERSION = '0.1' - -FACEBOOK_URL = 'http://api.facebook.com/restserver.php' -FACEBOOK_SECURE_URL = 'https://api.facebook.com/restserver.php' - -class json(object): pass - -# simple IDL for the Facebook API -METHODS = { - 'application': { - 'getPublicInfo': [ - ('application_id', int, ['optional']), - ('application_api_key', str, ['optional']), - ('application_canvas_name', str,['optional']), - ], - }, - - # admin methods - 'admin': { - 'getAllocation': [ - ('integration_point_name', str, []), - ], - }, - - # auth methods - 'auth': { - 'revokeAuthorization': [ - ('uid', int, ['optional']), - ], - }, - - # feed methods - 'feed': { - 'publishStoryToUser': [ - ('title', str, []), - ('body', str, ['optional']), - ('image_1', str, ['optional']), - ('image_1_link', str, ['optional']), - ('image_2', str, ['optional']), - ('image_2_link', str, ['optional']), - ('image_3', str, ['optional']), - ('image_3_link', str, ['optional']), - ('image_4', str, ['optional']), - ('image_4_link', str, ['optional']), - ('priority', int, ['optional']), - ], - - 'publishActionOfUser': [ - ('title', str, []), - ('body', str, ['optional']), - ('image_1', str, ['optional']), - ('image_1_link', str, ['optional']), - ('image_2', str, ['optional']), - ('image_2_link', str, ['optional']), - ('image_3', str, ['optional']), - ('image_3_link', str, ['optional']), - ('image_4', str, ['optional']), - ('image_4_link', str, ['optional']), - ('priority', int, ['optional']), - ], - - 'publishTemplatizedAction': [ - ('title_template', str, []), - ('page_actor_id', int, ['optional']), - ('title_data', json, ['optional']), - ('body_template', str, ['optional']), - ('body_data', json, ['optional']), - ('body_general', str, ['optional']), - ('image_1', str, ['optional']), - ('image_1_link', str, ['optional']), - ('image_2', str, ['optional']), - ('image_2_link', str, ['optional']), - ('image_3', str, ['optional']), - ('image_3_link', str, ['optional']), - ('image_4', str, ['optional']), - ('image_4_link', str, ['optional']), - ('target_ids', list, ['optional']), - ], - - 'registerTemplateBundle': [ - ('one_line_story_templates', json, []), - ('short_story_templates', json, ['optional']), - ('full_story_template', json, ['optional']), - ('action_links', json, ['optional']), - ], - - 'deactivateTemplateBundleByID': [ - ('template_bundle_id', int, []), - ], - - 'getRegisteredTemplateBundles': [], - - 'getRegisteredTemplateBundleByID': [ - ('template_bundle_id', str, []), - ], - - 'publishUserAction': [ - ('template_bundle_id', int, []), - ('template_data', json, ['optional']), - ('target_ids', list, ['optional']), - ('body_general', str, ['optional']), - ('story_size', int, ['optional']), - ], - }, - - # fql methods - 'fql': { - 'query': [ - ('query', str, []), - ], - }, - - # friends methods - 'friends': { - 'areFriends': [ - ('uids1', list, []), - ('uids2', list, []), - ], - - 'get': [ - ('flid', int, ['optional']), - ], - - 'getLists': [], - - 'getAppUsers': [], - }, - - # notifications methods - 'notifications': { - 'get': [], - - 'send': [ - ('to_ids', list, []), - ('notification', str, []), - ('email', str, ['optional']), - ('type', str, ['optional']), - ], - - 'sendRequest': [ - ('to_ids', list, []), - ('type', str, []), - ('content', str, []), - ('image', str, []), - ('invite', bool, []), - ], - - 'sendEmail': [ - ('recipients', list, []), - ('subject', str, []), - ('text', str, ['optional']), - ('fbml', str, ['optional']), - ] - }, - - # profile methods - 'profile': { - 'setFBML': [ - ('markup', str, ['optional']), - ('uid', int, ['optional']), - ('profile', str, ['optional']), - ('profile_action', str, ['optional']), - ('mobile_fbml', str, ['optional']), - ('profile_main', str, ['optional']), - ], - - 'getFBML': [ - ('uid', int, ['optional']), - ('type', int, ['optional']), - ], - - 'setInfo': [ - ('title', str, []), - ('type', int, []), - ('info_fields', json, []), - ('uid', int, []), - ], - - 'getInfo': [ - ('uid', int, []), - ], - - 'setInfoOptions': [ - ('field', str, []), - ('options', json, []), - ], - - 'getInfoOptions': [ - ('field', str, []), - ], - }, - - # users methods - 'users': { - 'getInfo': [ - ('uids', list, []), - ('fields', list, [('default', ['name'])]), - ], - - 'getStandardInfo': [ - ('uids', list, []), - ('fields', list, [('default', ['uid'])]), - ], - - 'getLoggedInUser': [], - - 'isAppAdded': [], - - 'hasAppPermission': [ - ('ext_perm', str, []), - ('uid', int, ['optional']), - ], - - 'setStatus': [ - ('status', str, []), - ('clear', bool, []), - ('status_includes_verb', bool, ['optional']), - ('uid', int, ['optional']), - ], - }, - - # events methods - 'events': { - 'get': [ - ('uid', int, ['optional']), - ('eids', list, ['optional']), - ('start_time', int, ['optional']), - ('end_time', int, ['optional']), - ('rsvp_status', str, ['optional']), - ], - - 'getMembers': [ - ('eid', int, []), - ], - - 'create': [ - ('event_info', json, []), - ], - }, - - # update methods - 'update': { - 'decodeIDs': [ - ('ids', list, []), - ], - }, - - # groups methods - 'groups': { - 'get': [ - ('uid', int, ['optional']), - ('gids', list, ['optional']), - ], - - 'getMembers': [ - ('gid', int, []), - ], - }, - - # marketplace methods - 'marketplace': { - 'createListing': [ - ('listing_id', int, []), - ('show_on_profile', bool, []), - ('listing_attrs', str, []), - ], - - 'getCategories': [], - - 'getListings': [ - ('listing_ids', list, []), - ('uids', list, []), - ], - - 'getSubCategories': [ - ('category', str, []), - ], - - 'removeListing': [ - ('listing_id', int, []), - ('status', str, []), - ], - - 'search': [ - ('category', str, ['optional']), - ('subcategory', str, ['optional']), - ('query', str, ['optional']), - ], - }, - - # pages methods - 'pages': { - 'getInfo': [ - ('fields', list, [('default', ['page_id', 'name'])]), - ('page_ids', list, ['optional']), - ('uid', int, ['optional']), - ], - - 'isAdmin': [ - ('page_id', int, []), - ], - - 'isAppAdded': [ - ('page_id', int, []), - ], - - 'isFan': [ - ('page_id', int, []), - ('uid', int, []), - ], - }, - - # photos methods - 'photos': { - 'addTag': [ - ('pid', int, []), - ('tag_uid', int, [('default', 0)]), - ('tag_text', str, [('default', '')]), - ('x', float, [('default', 50)]), - ('y', float, [('default', 50)]), - ('tags', str, ['optional']), - ], - - 'createAlbum': [ - ('name', str, []), - ('location', str, ['optional']), - ('description', str, ['optional']), - ], - - 'get': [ - ('subj_id', int, ['optional']), - ('aid', int, ['optional']), - ('pids', list, ['optional']), - ], - - 'getAlbums': [ - ('uid', int, ['optional']), - ('aids', list, ['optional']), - ], - - 'getTags': [ - ('pids', list, []), - ], - }, - - # status methods - 'status': { - 'get': [ - ('uid', int, ['optional']), - ('limit', int, ['optional']), - ], - 'set': [ - ('status', str, ['optional']), - ('uid', int, ['optional']), - ], - }, - - # fbml methods - 'fbml': { - 'refreshImgSrc': [ - ('url', str, []), - ], - - 'refreshRefUrl': [ - ('url', str, []), - ], - - 'setRefHandle': [ - ('handle', str, []), - ('fbml', str, []), - ], - }, - - # SMS Methods - 'sms' : { - 'canSend' : [ - ('uid', int, []), - ], - - 'send' : [ - ('uid', int, []), - ('message', str, []), - ('session_id', int, []), - ('req_session', bool, []), - ], - }, - - 'data': { - 'getCookies': [ - ('uid', int, []), - ('string', str, ['optional']), - ], - - 'setCookie': [ - ('uid', int, []), - ('name', str, []), - ('value', str, []), - ('expires', int, ['optional']), - ('path', str, ['optional']), - ], - }, - - # connect methods - 'connect': { - 'registerUsers': [ - ('accounts', json, []), - ], - - 'unregisterUsers': [ - ('email_hashes', json, []), - ], - - 'getUnconnectedFriendsCount': [ - ], - }, - - #stream methods (beta) - 'stream' : { - 'addComment' : [ - ('post_id', int, []), - ('comment', str, []), - ('uid', int, ['optional']), - ], - - 'addLike': [ - ('uid', int, ['optional']), - ('post_id', int, ['optional']), - ], - - 'get' : [ - ('viewer_id', int, ['optional']), - ('source_ids', list, ['optional']), - ('start_time', int, ['optional']), - ('end_time', int, ['optional']), - ('limit', int, ['optional']), - ('filter_key', str, ['optional']), - ], - - 'getComments' : [ - ('post_id', int, []), - ], - - 'getFilters' : [ - ('uid', int, ['optional']), - ], - - 'publish' : [ - ('message', str, ['optional']), - ('attachment', json, ['optional']), - ('action_links', json, ['optional']), - ('target_id', str, ['optional']), - ('uid', str, ['optional']), - ], - - 'remove' : [ - ('post_id', int, []), - ('uid', int, ['optional']), - ], - - 'removeComment' : [ - ('comment_id', int, []), - ('uid', int, ['optional']), - ], - - 'removeLike' : [ - ('uid', int, ['optional']), - ('post_id', int, ['optional']), - ], - } -} - -class Proxy(object): - """Represents a "namespace" of Facebook API calls.""" - - def __init__(self, client, name): - self._client = client - self._name = name - - def __call__(self, method=None, args=None, add_session_args=True): - # for Django templates - if method is None: - return self - - if add_session_args: - self._client._add_session_args(args) - - return self._client('%s.%s' % (self._name, method), args) - - -# generate the Facebook proxies -def __generate_proxies(): - for namespace in METHODS: - methods = {} - - for method in METHODS[namespace]: - params = ['self'] - body = ['args = {}'] - - for param_name, param_type, param_options in METHODS[namespace][method]: - param = param_name - - for option in param_options: - if isinstance(option, tuple) and option[0] == 'default': - if param_type == list: - param = '%s=None' % param_name - body.append('if %s is None: %s = %s' % (param_name, param_name, repr(option[1]))) - else: - param = '%s=%s' % (param_name, repr(option[1])) - - if param_type == json: - # we only jsonify the argument if it's a list or a dict, for compatibility - body.append('if isinstance(%s, list) or isinstance(%s, dict): %s = simplejson.dumps(%s)' % ((param_name,) * 4)) - - if 'optional' in param_options: - param = '%s=None' % param_name - body.append('if %s is not None: args[\'%s\'] = %s' % (param_name, param_name, param_name)) - else: - body.append('args[\'%s\'] = %s' % (param_name, param_name)) - - params.append(param) - - # simple docstring to refer them to Facebook API docs - body.insert(0, '"""Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=%s.%s"""' % (namespace, method)) - - body.insert(0, 'def %s(%s):' % (method, ', '.join(params))) - - body.append('return self(\'%s\', args)' % method) - - exec('\n '.join(body)) - - methods[method] = eval(method) - - proxy = type('%sProxy' % namespace.title(), (Proxy, ), methods) - - globals()[proxy.__name__] = proxy - - -__generate_proxies() - - -class FacebookError(Exception): - """Exception class for errors received from Facebook.""" - - def __init__(self, code, msg, args=None): - self.code = code - self.msg = msg - self.args = args - - def __str__(self): - return 'Error %s: %s' % (self.code, self.msg) - - -class AuthProxy(AuthProxy): - """Special proxy for facebook.auth.""" - - def getSession(self): - """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.getSession""" - args = {} - try: - args['auth_token'] = self._client.auth_token - except AttributeError: - raise RuntimeError('Client does not have auth_token set.') - result = self._client('%s.getSession' % self._name, args) - self._client.session_key = result['session_key'] - self._client.uid = result['uid'] - self._client.secret = result.get('secret') - self._client.session_key_expires = result['expires'] - return result - - def createToken(self): - """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=auth.createToken""" - token = self._client('%s.createToken' % self._name) - self._client.auth_token = token - return token - - -class FriendsProxy(FriendsProxy): - """Special proxy for facebook.friends.""" - - def get(self, **kwargs): - """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=friends.get""" - if not kwargs.get('flid') and self._client._friends: - return self._client._friends - return super(FriendsProxy, self).get(**kwargs) - - -class PhotosProxy(PhotosProxy): - """Special proxy for facebook.photos.""" - - def upload(self, image, aid=None, caption=None, size=(604, 1024), filename=None, callback=None): - """Facebook API call. See http://developers.facebook.com/documentation.php?v=1.0&method=photos.upload - - size -- an optional size (width, height) to resize the image to before uploading. Resizes by default - to Facebook's maximum display width of 604. - """ - args = {} - - if aid is not None: - args['aid'] = aid - - if caption is not None: - args['caption'] = caption - - args = self._client._build_post_args('facebook.photos.upload', self._client._add_session_args(args)) - - try: - import cStringIO as StringIO - except ImportError: - import StringIO - - # check for a filename specified...if the user is passing binary data in - # image then a filename will be specified - if filename is None: - try: - import Image - except ImportError: - data = StringIO.StringIO(open(image, 'rb').read()) - else: - img = Image.open(image) - if size: - img.thumbnail(size, Image.ANTIALIAS) - data = StringIO.StringIO() - img.save(data, img.format) - else: - # there was a filename specified, which indicates that image was not - # the path to an image file but rather the binary data of a file - data = StringIO.StringIO(image) - image = filename - - content_type, body = self.__encode_multipart_formdata(list(args.iteritems()), [(image, data)]) - urlinfo = urlparse.urlsplit(self._client.facebook_url) - try: - content_length = len(body) - chunk_size = 4096 - - h = httplib.HTTPConnection(urlinfo[1]) - h.putrequest('POST', urlinfo[2]) - h.putheader('Content-Type', content_type) - h.putheader('Content-Length', str(content_length)) - h.putheader('MIME-Version', '1.0') - h.putheader('User-Agent', 'PyFacebook Client Library') - h.endheaders() - - if callback: - count = 0 - while len(body) > 0: - if len(body) < chunk_size: - data = body - body = '' - else: - data = body[0:chunk_size] - body = body[chunk_size:] - - h.send(data) - count += 1 - callback(count, chunk_size, content_length) - else: - h.send(body) - - response = h.getresponse() - - if response.status != 200: - raise Exception('Error uploading photo: Facebook returned HTTP %s (%s)' % (response.status, response.reason)) - response = response.read() - except: - # sending the photo failed, perhaps we are using GAE - try: - from google.appengine.api import urlfetch - - try: - response = urlread(url=self._client.facebook_url,data=body,headers={'POST':urlinfo[2],'Content-Type':content_type,'MIME-Version':'1.0'}) - except urllib2.URLError: - raise Exception('Error uploading photo: Facebook returned %s' % (response)) - except ImportError: - # could not import from google.appengine.api, so we are not running in GAE - raise Exception('Error uploading photo.') - - return self._client._parse_response(response, 'facebook.photos.upload') - - - def __encode_multipart_formdata(self, fields, files): - """Encodes a multipart/form-data message to upload an image.""" - boundary = '-------tHISiStheMulTIFoRMbOUNDaRY' - crlf = '\r\n' - l = [] - - for (key, value) in fields: - l.append('--' + boundary) - l.append('Content-Disposition: form-data; name="%s"' % str(key)) - l.append('') - l.append(str(value)) - for (filename, value) in files: - l.append('--' + boundary) - l.append('Content-Disposition: form-data; filename="%s"' % (str(filename), )) - l.append('Content-Type: %s' % self.__get_content_type(filename)) - l.append('') - l.append(value.getvalue()) - l.append('--' + boundary + '--') - l.append('') - body = crlf.join(l) - content_type = 'multipart/form-data; boundary=%s' % boundary - return content_type, body - - - def __get_content_type(self, filename): - """Returns a guess at the MIME type of the file from the filename.""" - return str(mimetypes.guess_type(filename)[0]) or 'application/octet-stream' - - -class Facebook(object): - """ - Provides access to the Facebook API. - - Instance Variables: - - added - True if the user has added this application. - - api_key - Your API key, as set in the constructor. - - app_name - Your application's name, i.e. the APP_NAME in http://apps.facebook.com/APP_NAME/ if - this is for an internal web application. Optional, but useful for automatic redirects - to canvas pages. - - auth_token - The auth token that Facebook gives you, either with facebook.auth.createToken, - or through a GET parameter. - - callback_path - The path of the callback set in the Facebook app settings. If your callback is set - to http://www.example.com/facebook/callback/, this should be '/facebook/callback/'. - Optional, but useful for automatic redirects back to the same page after login. - - desktop - True if this is a desktop app, False otherwise. Used for determining how to - authenticate. - - ext_perms - Any extended permissions that the user has granted to your application. - This parameter is set only if the user has granted any. - - facebook_url - The url to use for Facebook requests. - - facebook_secure_url - The url to use for secure Facebook requests. - - in_canvas - True if the current request is for a canvas page. - - in_iframe - True if the current request is for an HTML page to embed in Facebook inside an iframe. - - is_session_from_cookie - True if the current request session comes from a session cookie. - - in_profile_tab - True if the current request is for a user's tab for your application. - - internal - True if this Facebook object is for an internal application (one that can be added on Facebook) - - locale - The user's locale. Default: 'en_US' - - page_id - Set to the page_id of the current page (if any) - - profile_update_time - The time when this user's profile was last updated. This is a UNIX timestamp. Default: None if unknown. - - secret - Secret that is used after getSession for desktop apps. - - secret_key - Your application's secret key, as set in the constructor. - - session_key - The current session key. Set automatically by auth.getSession, but can be set - manually for doing infinite sessions. - - session_key_expires - The UNIX time of when this session key expires, or 0 if it never expires. - - uid - After a session is created, you can get the user's UID with this variable. Set - automatically by auth.getSession. - - ---------------------------------------------------------------------- - - """ - - def __init__(self, api_key, secret_key, auth_token=None, app_name=None, callback_path=None, internal=None, proxy=None, facebook_url=None, facebook_secure_url=None): - """ - Initializes a new Facebook object which provides wrappers for the Facebook API. - - If this is a desktop application, the next couple of steps you might want to take are: - - facebook.auth.createToken() # create an auth token - facebook.login() # show a browser window - wait_login() # somehow wait for the user to log in - facebook.auth.getSession() # get a session key - - For web apps, if you are passed an auth_token from Facebook, pass that in as a named parameter. - Then call: - - facebook.auth.getSession() - - """ - self.api_key = api_key - self.secret_key = secret_key - self.session_key = None - self.session_key_expires = None - self.auth_token = auth_token - self.secret = None - self.uid = None - self.page_id = None - self.in_canvas = False - self.in_iframe = False - self.is_session_from_cookie = False - self.in_profile_tab = False - self.added = False - self.app_name = app_name - self.callback_path = callback_path - self.internal = internal - self._friends = None - self.locale = 'en_US' - self.profile_update_time = None - self.ext_perms = None - self.proxy = proxy - if facebook_url is None: - self.facebook_url = FACEBOOK_URL - else: - self.facebook_url = facebook_url - if facebook_secure_url is None: - self.facebook_secure_url = FACEBOOK_SECURE_URL - else: - self.facebook_secure_url = facebook_secure_url - - for namespace in METHODS: - self.__dict__[namespace] = eval('%sProxy(self, \'%s\')' % (namespace.title(), 'facebook.%s' % namespace)) - - - def _hash_args(self, args, secret=None): - """Hashes arguments by joining key=value pairs, appending a secret, and then taking the MD5 hex digest.""" - # @author: houyr - # fix for UnicodeEncodeError - hasher = hashlib.md5(''.join(['%s=%s' % (isinstance(x, unicode) and x.encode("utf-8") or x, isinstance(args[x], unicode) and args[x].encode("utf-8") or args[x]) for x in sorted(args.keys())])) - if secret: - hasher.update(secret) - elif self.secret: - hasher.update(self.secret) - else: - hasher.update(self.secret_key) - return hasher.hexdigest() - - - def _parse_response_item(self, node): - """Parses an XML response node from Facebook.""" - if node.nodeType == node.DOCUMENT_NODE and \ - node.childNodes[0].hasAttributes() and \ - node.childNodes[0].hasAttribute('list') and \ - node.childNodes[0].getAttribute('list') == "true": - return {node.childNodes[0].nodeName: self._parse_response_list(node.childNodes[0])} - elif node.nodeType == node.ELEMENT_NODE and \ - node.hasAttributes() and \ - node.hasAttribute('list') and \ - node.getAttribute('list')=="true": - return self._parse_response_list(node) - elif len(filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes)) > 0: - return self._parse_response_dict(node) - else: - return ''.join(node.data for node in node.childNodes if node.nodeType == node.TEXT_NODE) - - - def _parse_response_dict(self, node): - """Parses an XML dictionary response node from Facebook.""" - result = {} - for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): - result[item.nodeName] = self._parse_response_item(item) - if node.nodeType == node.ELEMENT_NODE and node.hasAttributes(): - if node.hasAttribute('id'): - result['id'] = node.getAttribute('id') - return result - - - def _parse_response_list(self, node): - """Parses an XML list response node from Facebook.""" - result = [] - for item in filter(lambda x: x.nodeType == x.ELEMENT_NODE, node.childNodes): - result.append(self._parse_response_item(item)) - return result - - - def _check_error(self, response): - """Checks if the given Facebook response is an error, and then raises the appropriate exception.""" - if type(response) is dict and response.has_key('error_code'): - raise FacebookError(response['error_code'], response['error_msg'], response['request_args']) - - - def _build_post_args(self, method, args=None): - """Adds to args parameters that are necessary for every call to the API.""" - if args is None: - args = {} - - for arg in args.items(): - if type(arg[1]) == list: - args[arg[0]] = ','.join(str(a) for a in arg[1]) - elif type(arg[1]) == unicode: - args[arg[0]] = arg[1].encode("UTF-8") - elif type(arg[1]) == bool: - args[arg[0]] = str(arg[1]).lower() - - args['method'] = method - args['api_key'] = self.api_key - args['v'] = '1.0' - args['format'] = RESPONSE_FORMAT - args['sig'] = self._hash_args(args) - - return args - - - def _add_session_args(self, args=None): - """Adds 'session_key' and 'call_id' to args, which are used for API calls that need sessions.""" - if args is None: - args = {} - - if not self.session_key: - return args - #some calls don't need a session anymore. this might be better done in the markup - #raise RuntimeError('Session key not set. Make sure auth.getSession has been called.') - - args['session_key'] = self.session_key - args['call_id'] = str(int(time.time() * 1000)) - - return args - - - def _parse_response(self, response, method, format=None): - """Parses the response according to the given (optional) format, which should be either 'JSON' or 'XML'.""" - if not format: - format = RESPONSE_FORMAT - - if format == 'JSON': - result = simplejson.loads(response) - - self._check_error(result) - elif format == 'XML': - dom = minidom.parseString(response) - result = self._parse_response_item(dom) - dom.unlink() - - if 'error_response' in result: - self._check_error(result['error_response']) - - result = result[method[9:].replace('.', '_') + '_response'] - else: - raise RuntimeError('Invalid format specified.') - - return result - - - def hash_email(self, email): - """ - Hash an email address in a format suitable for Facebook Connect. - - """ - email = email.lower().strip() - return "%s_%s" % ( - struct.unpack("I", struct.pack("i", binascii.crc32(email)))[0], - hashlib.md5(email).hexdigest(), - ) - - - def unicode_urlencode(self, params): - """ - @author: houyr - A unicode aware version of urllib.urlencode. - """ - if isinstance(params, dict): - params = params.items() - return urllib.urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) - for k, v in params]) - - - def __call__(self, method=None, args=None, secure=False): - """Make a call to Facebook's REST server.""" - # for Django templates, if this object is called without any arguments - # return the object itself - if method is None: - return self - - # __init__ hard-codes into en_US - if args is not None and not args.has_key('locale'): - args['locale'] = self.locale - - # @author: houyr - # fix for bug of UnicodeEncodeError - post_data = self.unicode_urlencode(self._build_post_args(method, args)) - - if self.proxy: - proxy_handler = urllib2.ProxyHandler(self.proxy) - opener = urllib2.build_opener(proxy_handler) - if secure: - response = opener.open(self.facebook_secure_url, post_data).read() - else: - response = opener.open(self.facebook_url, post_data).read() - else: - if secure: - response = urlread(self.facebook_secure_url, post_data) - else: - response = urlread(self.facebook_url, post_data) - - return self._parse_response(response, method) - - - # URL helpers - def get_url(self, page, **args): - """ - Returns one of the Facebook URLs (www.facebook.com/SOMEPAGE.php). - Named arguments are passed as GET query string parameters. - - """ - return 'http://www.facebook.com/%s.php?%s' % (page, urllib.urlencode(args)) - - - def get_app_url(self, path=''): - """ - Returns the URL for this app's canvas page, according to app_name. - - """ - return 'http://apps.facebook.com/%s/%s' % (self.app_name, path) - - - def get_add_url(self, next=None): - """ - Returns the URL that the user should be redirected to in order to add the application. - - """ - args = {'api_key': self.api_key, 'v': '1.0'} - - if next is not None: - args['next'] = next - - return self.get_url('install', **args) - - - def get_authorize_url(self, next=None, next_cancel=None): - """ - Returns the URL that the user should be redirected to in order to - authorize certain actions for application. - - """ - args = {'api_key': self.api_key, 'v': '1.0'} - - if next is not None: - args['next'] = next - - if next_cancel is not None: - args['next_cancel'] = next_cancel - - return self.get_url('authorize', **args) - - - def get_login_url(self, next=None, popup=False, canvas=True): - """ - Returns the URL that the user should be redirected to in order to login. - - next -- the URL that Facebook should redirect to after login - - """ - args = {'api_key': self.api_key, 'v': '1.0'} - - if next is not None: - args['next'] = next - - if canvas is True: - args['canvas'] = 1 - - if popup is True: - args['popup'] = 1 - - if self.auth_token is not None: - args['auth_token'] = self.auth_token - - return self.get_url('login', **args) - - - def login(self, popup=False): - """Open a web browser telling the user to login to Facebook.""" - import webbrowser - webbrowser.open(self.get_login_url(popup=popup)) - - - def get_ext_perm_url(self, ext_perm, next=None, popup=False): - """ - Returns the URL that the user should be redirected to in order to grant an extended permission. - - ext_perm -- the name of the extended permission to request - next -- the URL that Facebook should redirect to after login - - """ - args = {'ext_perm': ext_perm, 'api_key': self.api_key, 'v': '1.0'} - - if next is not None: - args['next'] = next - - if popup is True: - args['popup'] = 1 - - return self.get_url('authorize', **args) - - - def request_extended_permission(self, ext_perm, popup=False): - """Open a web browser telling the user to grant an extended permission.""" - import webbrowser - webbrowser.open(self.get_ext_perm_url(ext_perm, popup=popup)) - - - def check_session(self, request): - """ - Checks the given Django HttpRequest for Facebook parameters such as - POST variables or an auth token. If the session is valid, returns True - and this object can now be used to access the Facebook API. Otherwise, - it returns False, and the application should take the appropriate action - (either log the user in or have him add the application). - - """ - self.in_canvas = (request.POST.get('fb_sig_in_canvas') == '1') - - if self.session_key and (self.uid or self.page_id): - return True - - - if request.method == 'POST': - params = self.validate_signature(request.POST) - else: - if 'installed' in request.GET: - self.added = True - - if 'fb_page_id' in request.GET: - self.page_id = request.GET['fb_page_id'] - - if 'auth_token' in request.GET: - self.auth_token = request.GET['auth_token'] - - try: - self.auth.getSession() - except FacebookError, e: - self.auth_token = None - return False - - return True - - params = self.validate_signature(request.GET) - - if not params: - # first check if we are in django - to check cookies - if hasattr(request, 'COOKIES'): - params = self.validate_cookie_signature(request.COOKIES) - self.is_session_from_cookie = True - else: - # if not, then we might be on GoogleAppEngine, check their request object cookies - if hasattr(request,'cookies'): - params = self.validate_cookie_signature(request.cookies) - self.is_session_from_cookie = True - - if not params: - return False - - if params.get('in_canvas') == '1': - self.in_canvas = True - - if params.get('in_iframe') == '1': - self.in_iframe = True - - if params.get('in_profile_tab') == '1': - self.in_profile_tab = True - - if params.get('added') == '1': - self.added = True - - if params.get('expires'): - self.session_key_expires = int(params['expires']) - - if 'locale' in params: - self.locale = params['locale'] - - if 'profile_update_time' in params: - try: - self.profile_update_time = int(params['profile_update_time']) - except ValueError: - pass - - if 'ext_perms' in params: - self.ext_perms = params['ext_perms'] - - if 'friends' in params: - if params['friends']: - self._friends = params['friends'].split(',') - else: - self._friends = [] - - if 'session_key' in params: - self.session_key = params['session_key'] - if 'user' in params: - self.uid = params['user'] - elif 'page_id' in params: - self.page_id = params['page_id'] - else: - return False - elif 'profile_session_key' in params: - self.session_key = params['profile_session_key'] - if 'profile_user' in params: - self.uid = params['profile_user'] - else: - return False - elif 'canvas_user' in params: - self.uid = params['canvas_user'] - elif 'uninstall' in params: - self.uid = params['user'] - else: - return False - - return True - - - def validate_signature(self, post, prefix='fb_sig', timeout=None): - """ - Validate parameters passed to an internal Facebook app from Facebook. - - """ - args = post.copy() - - if prefix not in args: - return None - - del args[prefix] - - if timeout and '%s_time' % prefix in post and time.time() - float(post['%s_time' % prefix]) > timeout: - return None - - args = dict([(key[len(prefix + '_'):], value) for key, value in args.items() if key.startswith(prefix)]) - - hash = self._hash_args(args) - - if hash == post[prefix]: - return args - else: - return None - - def validate_cookie_signature(self, cookies): - """ - Validate parameters passed by cookies, namely facebookconnect or js api. - """ - - api_key = self.api_key - if api_key not in cookies: - return None - - prefix = api_key + "_" - - params = {} - vals = '' - for k in sorted(cookies): - if k.startswith(prefix): - key = k.replace(prefix,"") - value = cookies[k] - params[key] = value - vals += '%s=%s' % (key, value) - - hasher = hashlib.md5(vals) - - hasher.update(self.secret_key) - digest = hasher.hexdigest() - if digest == cookies[api_key]: - params['is_session_from_cookie'] = True - return params - else: - return False - - - - -if __name__ == '__main__': - # sample desktop application - - api_key = '' - secret_key = '' - - facebook = Facebook(api_key, secret_key) - - facebook.auth.createToken() - - # Show login window - # Set popup=True if you want login without navigational elements - facebook.login() - - # Login to the window, then press enter - print 'After logging in, press enter...' - raw_input() - - facebook.auth.getSession() - print 'Session Key: ', facebook.session_key - print 'Your UID: ', facebook.uid - - info = facebook.users.getInfo([facebook.uid], ['name', 'birthday', 'affiliations', 'sex'])[0] - - print 'Your Name: ', info['name'] - print 'Your Birthday: ', info['birthday'] - print 'Your Gender: ', info['sex'] - - friends = facebook.friends.get() - friends = facebook.users.getInfo(friends[0:5], ['name', 'birthday', 'relationship_status']) - - for friend in friends: - print friend['name'], 'has a birthday on', friend['birthday'], 'and is', friend['relationship_status'] - - arefriends = facebook.friends.areFriends([friends[0]['uid']], [friends[1]['uid']]) - - photos = facebook.photos.getAlbums(facebook.uid) - diff --git a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/__init__.py deleted file mode 100644 index 68b1b27c37d19e1372369837006947b4f60ef7c5..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/__init__.py +++ /dev/null @@ -1,248 +0,0 @@ -import re -import datetime -import facebook - -from django.http import HttpResponse, HttpResponseRedirect -from django.core.exceptions import ImproperlyConfigured -from django.conf import settings -from datetime import datetime - -try: - from threading import local -except ImportError: - from django.utils._threading_local import local - -__all__ = ['Facebook', 'FacebookMiddleware', 'get_facebook_client', 'require_login', 'require_add'] - -_thread_locals = local() - -class Facebook(facebook.Facebook): - def redirect(self, url): - """ - Helper for Django which redirects to another page. If inside a - canvas page, writes a <fb:redirect> instead to achieve the same effect. - - """ - if self.in_canvas: - return HttpResponse('<fb:redirect url="%s" />' % (url, )) - elif re.search("^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?", url.lower()): - return HttpResponse('<script type="text/javascript">\ntop.location.href = "%s";\n</script>' % url) - else: - return HttpResponseRedirect(url) - - -def get_facebook_client(): - """ - Get the current Facebook object for the calling thread. - - """ - try: - return _thread_locals.facebook - except AttributeError: - raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') - - -def require_login(next=None, internal=None): - """ - Decorator for Django views that requires the user to be logged in. - The FacebookMiddleware must be installed. - - Standard usage: - @require_login() - def some_view(request): - ... - - Redirecting after login: - To use the 'next' parameter to redirect to a specific page after login, a callable should - return a path relative to the Post-add URL. 'next' can also be an integer specifying how many - parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, - settings.callback_path and settings.app_name are checked to redirect to the same page after logging - in. (This is the default behavior.) - @require_login(next=some_callable) - def some_view(request): - ... - """ - def decorator(view): - def newview(request, *args, **kwargs): - next = newview.next - internal = newview.internal - - try: - fb = request.facebook - except: - raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') - - if internal is None: - internal = request.facebook.internal - - if callable(next): - next = next(request.path) - elif isinstance(next, int): - next = '/'.join(request.path.split('/')[next + 1:]) - elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): - next = request.path[len(fb.callback_path):] - elif not isinstance(next, str): - next = '' - - if not fb.check_session(request): - #If user has never logged in before, the get_login_url will redirect to the TOS page - return fb.redirect(fb.get_login_url(next=next)) - - if internal and request.method == 'GET' and fb.app_name: - return fb.redirect('%s%s' % (fb.get_app_url(), next)) - - return view(request, *args, **kwargs) - newview.next = next - newview.internal = internal - return newview - return decorator - - -def require_add(next=None, internal=None, on_install=None): - """ - Decorator for Django views that requires application installation. - The FacebookMiddleware must be installed. - - Standard usage: - @require_add() - def some_view(request): - ... - - Redirecting after installation: - To use the 'next' parameter to redirect to a specific page after login, a callable should - return a path relative to the Post-add URL. 'next' can also be an integer specifying how many - parts of request.path to strip to find the relative URL of the canvas page. If 'next' is None, - settings.callback_path and settings.app_name are checked to redirect to the same page after logging - in. (This is the default behavior.) - @require_add(next=some_callable) - def some_view(request): - ... - - Post-install processing: - Set the on_install parameter to a callable in order to handle special post-install processing. - The callable should take a request object as the parameter. - @require_add(on_install=some_callable) - def some_view(request): - ... - """ - def decorator(view): - def newview(request, *args, **kwargs): - next = newview.next - internal = newview.internal - - try: - fb = request.facebook - except: - raise ImproperlyConfigured('Make sure you have the Facebook middleware installed.') - - if internal is None: - internal = request.facebook.internal - - if callable(next): - next = next(request.path) - elif isinstance(next, int): - next = '/'.join(request.path.split('/')[next + 1:]) - elif next is None and fb.callback_path and request.path.startswith(fb.callback_path): - next = request.path[len(fb.callback_path):] - else: - next = '' - - if not fb.check_session(request): - if fb.added: - if request.method == 'GET' and fb.app_name: - return fb.redirect('%s%s' % (fb.get_app_url(), next)) - return fb.redirect(fb.get_login_url(next=next)) - else: - return fb.redirect(fb.get_add_url(next=next)) - - if not fb.added: - return fb.redirect(fb.get_add_url(next=next)) - - if 'installed' in request.GET and callable(on_install): - on_install(request) - - if internal and request.method == 'GET' and fb.app_name: - return fb.redirect('%s%s' % (fb.get_app_url(), next)) - - return view(request, *args, **kwargs) - newview.next = next - newview.internal = internal - return newview - return decorator - -# try to preserve the argspecs -try: - import decorator -except ImportError: - pass -else: - def updater(f): - def updated(*args, **kwargs): - original = f(*args, **kwargs) - def newdecorator(view): - return decorator.new_wrapper(original(view), view) - return decorator.new_wrapper(newdecorator, original) - return decorator.new_wrapper(updated, f) - require_login = updater(require_login) - require_add = updater(require_add) - -class FacebookMiddleware(object): - """ - Middleware that attaches a Facebook object to every incoming request. - The Facebook object created can also be accessed from models for the - current thread by using get_facebook_client(). - - """ - - def __init__(self, api_key=None, secret_key=None, app_name=None, callback_path=None, internal=None): - self.api_key = api_key or settings.FACEBOOK_API_KEY - self.secret_key = secret_key or settings.FACEBOOK_SECRET_KEY - self.app_name = app_name or getattr(settings, 'FACEBOOK_APP_NAME', None) - self.callback_path = callback_path or getattr(settings, 'FACEBOOK_CALLBACK_PATH', None) - self.internal = internal or getattr(settings, 'FACEBOOK_INTERNAL', True) - self.proxy = None - if getattr(settings, 'USE_HTTP_PROXY', False): - self.proxy = settings.HTTP_PROXY - - def process_request(self, request): - _thread_locals.facebook = request.facebook = Facebook(self.api_key, self.secret_key, app_name=self.app_name, callback_path=self.callback_path, internal=self.internal, proxy=self.proxy) - if not self.internal: - if 'fb_sig_session_key' in request.GET and 'fb_sig_user' in request.GET: - request.facebook.session_key = request.session['facebook_session_key'] = request.GET['fb_sig_session_key'] - request.facebook.uid = request.session['fb_sig_user'] = request.GET['fb_sig_user'] - elif request.session.get('facebook_session_key', None) and request.session.get('facebook_user_id', None): - request.facebook.session_key = request.session['facebook_session_key'] - request.facebook.uid = request.session['facebook_user_id'] - - def process_response(self, request, response): - if not self.internal and request.facebook.session_key and request.facebook.uid: - request.session['facebook_session_key'] = request.facebook.session_key - request.session['facebook_user_id'] = request.facebook.uid - - if request.facebook.session_key_expires: - expiry = datetime.datetime.fromtimestamp(request.facebook.session_key_expires) - request.session.set_expiry(expiry) - - try: - fb = request.facebook - except: - return response - - if not fb.is_session_from_cookie: - # Make sure the browser accepts our session cookies inside an Iframe - response['P3P'] = 'CP="NOI DSP COR NID ADMa OPTa OUR NOR"' - fb_cookies = { - 'expires': fb.session_key_expires, - 'session_key': fb.session_key, - 'user': fb.uid, - } - - expire_time = None - if fb.session_key_expires: - expire_time = datetime.utcfromtimestamp(fb.session_key_expires) - - for k in fb_cookies: - response.set_cookie(self.api_key + '_' + k, fb_cookies[k], expires=expire_time) - response.set_cookie(self.api_key , fb._hash_args(fb_cookies), expires=expire_time) - - return response diff --git a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py b/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py deleted file mode 100644 index 6f954397308f7af525d9fa600fb29e8cf6902c33..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/context_processors.py +++ /dev/null @@ -1,6 +0,0 @@ -def messages(request): - """Returns messages similar to ``django.core.context_processors.auth``.""" - if hasattr(request, 'facebook') and request.facebook.uid is not None: - from models import Message - messages = Message.objects.get_and_delete_all(uid=request.facebook.uid) - return {'messages': messages} \ No newline at end of file diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/__init__.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py deleted file mode 100644 index 666ccd3f39403df207fac99cee88c6ca00789b8f..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import models - -# get_facebook_client lets us get the current Facebook object -# from outside of a view, which lets us have cleaner code -from facebook.djangofb import get_facebook_client - -class UserManager(models.Manager): - """Custom manager for a Facebook User.""" - - def get_current(self): - """Gets a User object for the logged-in Facebook user.""" - facebook = get_facebook_client() - user, created = self.get_or_create(id=int(facebook.uid)) - if created: - # we could do some custom actions for new users here... - pass - return user - -class User(models.Model): - """A simple User model for Facebook users.""" - - # We use the user's UID as the primary key in our database. - id = models.IntegerField(primary_key=True) - - # TODO: The data that you want to store for each user would go here. - # For this sample, we let users let people know their favorite progamming - # language, in the spirit of Extended Info. - language = models.CharField(maxlength=64, default='Python') - - # Add the custom manager - objects = UserManager() diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml b/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml deleted file mode 100644 index 6734dd17caa138540fb10d0fcb750d70c8600d33..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/templates/canvas.fbml +++ /dev/null @@ -1,22 +0,0 @@ -<fb:header> - {% comment %} - We can use {{ fbuser }} to get at the current user. - {{ fbuser.id }} will be the user's UID, and {{ fbuser.language }} - is his/her favorite language (Python :-). - {% endcomment %} - Welcome, <fb:name uid="{{ fbuser.id }}" firstnameonly="true" useyou="false" />! -</fb:header> - -<div class="clearfix" style="float: left; border: 1px #d8dfea solid; padding: 10px 10px 10px 10px; margin-left: 30px; margin-bottom: 30px; width: 500px;"> - Your favorite language is {{ fbuser.language|escape }}. - <br /><br /> - - <div class="grayheader clearfix"> - <br /><br /> - - <form action="." method="POST"> - <input type="text" name="language" value="{{ fbuser.language|escape }}" /> - <input type="submit" value="Change" /> - </form> - </div> -</div> diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py deleted file mode 100644 index f75d8d258360fc43e12b2343182d89f14a01ef8c..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.conf.urls import url - -from views import canvas - -urlpatterns = [ - url(r'^$', canvas), -] diff --git a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py b/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py deleted file mode 100644 index 609314fe01b3bf546984841b9dded39756bfa0cb..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/default_app/views.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.http import HttpResponse -# from django.views.generic.simple import direct_to_template -#uncomment the following two lines and the one below -#if you dont want to use a decorator instead of the middleware -#from django.utils.decorators import decorator_from_middleware -#from facebook.djangofb import FacebookMiddleware - -# Import the Django helpers -import facebook.djangofb as facebook - -# The User model defined in models.py -from models import User - -# We'll require login for our canvas page. This -# isn't necessarily a good idea, as we might want -# to let users see the page without granting our app -# access to their info. See the wiki for details on how -# to do this. -#@decorator_from_middleware(FacebookMiddleware) -@facebook.require_login() -def canvas(request): - # Get the User object for the currently logged in user - user = User.objects.get_current() - - # Check if we were POSTed the user's new language of choice - if 'language' in request.POST: - user.language = request.POST['language'][:64] - user.save() - - # User is guaranteed to be logged in, so pass canvas.fbml - # an extra 'fbuser' parameter that is the User object for - # the currently logged in user. - #return direct_to_template(request, 'canvas.fbml', extra_context={'fbuser': user}) - return None - -@facebook.require_login() -def ajax(request): - return HttpResponse('hello world') diff --git a/helios_auth/auth_systems/facebookclient/djangofb/models.py b/helios_auth/auth_systems/facebookclient/djangofb/models.py deleted file mode 100644 index b5d2c62221e9926f7ab4b57cb95fb71ab22be2da..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/djangofb/models.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.db import models -from django.utils.html import escape -from django.utils.safestring import mark_safe - -FB_MESSAGE_STATUS = ( - (0, 'Explanation'), - (1, 'Error'), - (2, 'Success'), -) - -class MessageManager(models.Manager): - def get_and_delete_all(self, uid): - messages = [] - for m in self.filter(uid=uid): - messages.append(m) - m.delete() - return messages - -class Message(models.Model): - """Represents a message for a Facebook user.""" - uid = models.CharField(max_length=25) - status = models.IntegerField(choices=FB_MESSAGE_STATUS) - message = models.CharField(max_length=300) - objects = MessageManager() - - def __unicode__(self): - return self.message - - def _fb_tag(self): - return self.get_status_display().lower() - - def as_fbml(self): - return mark_safe(u'<fb:%s message="%s" />' % ( - self._fb_tag(), - escape(self.message), - )) diff --git a/helios_auth/auth_systems/facebookclient/webappfb.py b/helios_auth/auth_systems/facebookclient/webappfb.py deleted file mode 100644 index 5fdf77af5c05ce29ccd56b6326ca4b8f64a08294..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/webappfb.py +++ /dev/null @@ -1,170 +0,0 @@ -# -# webappfb - Facebook tools for Google's AppEngine "webapp" Framework -# -# Copyright (c) 2009, Max Battcher -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may -# be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from google.appengine.api import memcache -from google.appengine.ext.webapp import RequestHandler -from facebook import Facebook -import yaml - -""" -Facebook tools for Google AppEngine's object-oriented "webapp" framework. -""" - -# This global configuration dictionary is for configuration variables -# for Facebook requests such as the application's API key and secret -# key. Defaults to loading a 'facebook.yaml' YAML file. This should be -# useful and familiar for most AppEngine development. -FACEBOOK_CONFIG = yaml.load(file('facebook.yaml', 'r')) - -class FacebookRequestHandler(RequestHandler): - """ - Base class for request handlers for Facebook apps, providing useful - Facebook-related tools: a local - """ - - def _fbconfig_value(self, name, default=None): - """ - Checks the global config dictionary and then for a class/instance - variable, using a provided default if no value is found. - """ - if name in FACEBOOK_CONFIG: - default = FACEBOOK_CONFIG[name] - - return getattr(self, name, default) - - def initialize(self, request, response): - """ - Initialize's this request's Facebook client. - """ - super(FacebookRequestHandler, self).initialize(request, response) - - app_name = self._fbconfig_value('app_name', '') - api_key = self._fbconfig_value('api_key', None) - secret_key = self._fbconfig_value('secret_key', None) - - self.facebook = Facebook(api_key, secret_key, - app_name=app_name) - - require_app = self._fbconfig_value('require_app', False) - require_login = self._fbconfig_value('require_login', False) - need_session = self._fbconfig_value('need_session', False) - check_session = self._fbconfig_value('check_session', True) - - self._messages = None - self.redirecting = False - - if require_app or require_login: - if not self.facebook.check_session(request): - self.redirect(self.facebook.get_login_url(next=request.path)) - self.redirecting = True - return - elif check_session: - self.facebook.check_session(request) # ignore response - - # NOTE: require_app is deprecated according to modern Facebook login - # policies. Included for completeness, but unnecessary. - if require_app and not self.facebook.added: - self.redirect(self.facebook.get_add_url(next=request.path)) - self.redirecting = True - return - - if not (require_app or require_login) and need_session: - self.facebook.auth.getSession() - - def redirect(self, url, **kwargs): - """ - For Facebook canvas pages we should use <fb:redirect /> instead of - a normal redirect. - """ - if self.facebook.in_canvas: - self.response.clear() - self.response.out.write('<fb:redirect url="%s" />' % (url, )) - else: - super(FacebookRequestHandler, self).redirect(url, **kwargs) - - def add_user_message(self, kind, msg, detail='', time=15 * 60): - """ - Add a message to the current user to memcache. - """ - if self.facebook.uid: - key = 'messages:%s' % self.facebook.uid - self._messages = memcache.get(key) - message = { - 'kind': kind, - 'message': msg, - 'detail': detail, - } - if self._messages is not None: - self._messages.append(message) - else: - self._messages = [message] - memcache.set(key, self._messages, time=time) - - def get_and_delete_user_messages(self): - """ - Get all of the messages for the current user; removing them. - """ - if self.facebook.uid: - key = 'messages:%s' % self.facebook.uid - if not hasattr(self, '_messages') or self._messages is None: - self._messages = memcache.get(key) - memcache.delete(key) - return self._messages - return None - -class FacebookCanvasHandler(FacebookRequestHandler): - """ - Request handler for Facebook canvas (FBML application) requests. - """ - - def canvas(self, *args, **kwargs): - """ - This will be your handler to deal with Canvas requests. - """ - raise NotImplementedError() - - def get(self, *args): - """ - All valid canvas views are POSTS. - """ - # TODO: Attempt to auto-redirect to Facebook canvas? - self.error(404) - - def post(self, *args, **kwargs): - """ - Check a couple of simple safety checks and then call the canvas - handler. - """ - if self.redirecting: return - - if not self.facebook.in_canvas: - self.error(404) - return - - self.canvas(*args, **kwargs) - -# vim: ai et ts=4 sts=4 sw=4 diff --git a/helios_auth/auth_systems/facebookclient/wsgi.py b/helios_auth/auth_systems/facebookclient/wsgi.py deleted file mode 100644 index f6a790db14858d762159478a4aa51e7323144b5d..0000000000000000000000000000000000000000 --- a/helios_auth/auth_systems/facebookclient/wsgi.py +++ /dev/null @@ -1,129 +0,0 @@ -"""This is some simple helper code to bridge the Pylons / PyFacebook gap. - -There's some generic WSGI middleware, some Paste stuff, and some Pylons -stuff. Once you put FacebookWSGIMiddleware into your middleware stack, -you'll have access to ``environ["pyfacebook.facebook"]``, which is a -``facebook.Facebook`` object. If you're using Paste (which includes -Pylons users), you can also access this directly using the facebook -global in this module. - -""" - -# Be careful what you import. Don't expect everyone to have Pylons, -# Paste, etc. installed. Degrade gracefully. - -from facebook import Facebook - -__docformat__ = "restructuredtext" - - -# Setup Paste, if available. This needs to stay in the same module as -# FacebookWSGIMiddleware below. - -try: - from paste.registry import StackedObjectProxy - from webob.exc import _HTTPMove - from paste.util.quoting import strip_html, html_quote, no_quote -except ImportError: - pass -else: - facebook = StackedObjectProxy(name="PyFacebook Facebook Connection") - - - class CanvasRedirect(_HTTPMove): - - """This is for canvas redirects.""" - - title = "See Other" - code = 200 - template = '<fb:redirect url="%(location)s" />' - - def html(self, environ): - """ text/html representation of the exception """ - body = self.make_body(environ, self.template, html_quote, no_quote) - return body - -class FacebookWSGIMiddleware(object): - - """This is WSGI middleware for Facebook.""" - - def __init__(self, app, config, facebook_class=Facebook): - """Initialize the Facebook middleware. - - ``app`` - This is the WSGI application being wrapped. - - ``config`` - This is a dict containing the keys "pyfacebook.apikey" and - "pyfacebook.secret". - - ``facebook_class`` - If you want to subclass the Facebook class, you can pass in - your replacement here. Pylons users will want to use - PylonsFacebook. - - """ - self.app = app - self.config = config - self.facebook_class = facebook_class - - def __call__(self, environ, start_response): - config = self.config - real_facebook = self.facebook_class(config["pyfacebook.apikey"], - config["pyfacebook.secret"]) - registry = environ.get('paste.registry') - if registry: - registry.register(facebook, real_facebook) - environ['pyfacebook.facebook'] = real_facebook - return self.app(environ, start_response) - - -# The remainder is Pylons specific. - -try: - import pylons - from pylons.controllers.util import redirect_to as pylons_redirect_to - from routes import url_for -except ImportError: - pass -else: - - - class PylonsFacebook(Facebook): - - """Subclass Facebook to add Pylons goodies.""" - - def check_session(self, request=None): - """The request parameter is now optional.""" - if request is None: - request = pylons.request - return Facebook.check_session(self, request) - - # The Django request object is similar enough to the Paste - # request object that check_session and validate_signature - # should *just work*. - - def redirect_to(self, url): - """Wrap Pylons' redirect_to function so that it works in_canvas. - - By the way, this won't work until after you call - check_session(). - - """ - if self.in_canvas: - raise CanvasRedirect(url) - pylons_redirect_to(url) - - def apps_url_for(self, *args, **kargs): - """Like url_for, but starts with "http://apps.facebook.com".""" - return "http://apps.facebook.com" + url_for(*args, **kargs) - - - def create_pylons_facebook_middleware(app, config): - """This is a simple wrapper for FacebookWSGIMiddleware. - - It passes the correct facebook_class. - - """ - return FacebookWSGIMiddleware(app, config, - facebook_class=PylonsFacebook) diff --git a/helios_auth/auth_systems/google.py b/helios_auth/auth_systems/google.py index 03419915600fe58636ce9e2098a0219075fa804d..0b08b04c17c01225eaa6c227e74c55e46f168055 100644 --- a/helios_auth/auth_systems/google.py +++ b/helios_auth/auth_systems/google.py @@ -4,11 +4,12 @@ Google Authentication """ import httplib2 -import json from django.conf import settings from django.core.mail import send_mail from oauth2client.client import OAuth2WebServerFlow +from helios_auth import utils + # some parameters to indicate that status updating is not possible STATUS_UPDATES = False @@ -30,7 +31,7 @@ def get_auth_url(request, redirect_url): def get_user_info_after_auth(request): flow = get_flow(request.session['google-redirect-url']) - if not request.GET.has_key('code'): + if 'code' not in request.GET: return None code = request.GET['code'] @@ -48,7 +49,7 @@ def get_user_info_after_auth(request): http = credentials.authorize(http) (resp_headers, content) = http.request("https://people.googleapis.com/v1/people/me?personFields=names", "GET") - response = json.loads(content) + response = utils.from_json(content.decode('utf-8')) name = response['names'][0]['displayName'] diff --git a/helios_auth/auth_systems/linkedin.py b/helios_auth/auth_systems/linkedin.py index 696eda9836f0c141360d24f00739d8ed66fca95e..e75e0786d46a508f9846bd6b9d1491be1bdbcbc5 100644 --- a/helios_auth/auth_systems/linkedin.py +++ b/helios_auth/auth_systems/linkedin.py @@ -2,18 +2,12 @@ LinkedIn Authentication """ -from oauthclient import client - -from django.urls import reverse -from django.http import HttpResponseRedirect - -from helios_auth import utils - from xml.etree import ElementTree -import logging - from django.conf import settings + +from .oauthclient import client + API_KEY = settings.LINKEDIN_API_KEY API_SECRET = settings.LINKEDIN_API_SECRET diff --git a/helios_auth/auth_systems/live.py b/helios_auth/auth_systems/live.py index 9f34a2783198003f09a74abb1671d9630cf3895a..348d02ff34a22d685b3d6dbe46aa5885ad162b4d 100644 --- a/helios_auth/auth_systems/live.py +++ b/helios_auth/auth_systems/live.py @@ -5,14 +5,14 @@ so much like Facebook # NOT WORKING YET because Windows Live documentation and status is unclear. Is it in beta? I think it is. """ -import logging +import urllib.parse +import urllib.request from django.conf import settings + APP_ID = settings.LIVE_APP_ID APP_SECRET = settings.LIVE_APP_SECRET -import urllib, urllib2, cgi - # some parameters to indicate that status updating is possible STATUS_UPDATES = False # STATUS_UPDATE_WORDING_TEMPLATE = "Send %s to your facebook status" @@ -21,17 +21,17 @@ from helios_auth import utils def live_url(url, params): if params: - return "https://graph.facebook.com%s?%s" % (url, urllib.urlencode(params)) + return "https://graph.facebook.com%s?%s" % (url, urllib.parse.urlencode(params)) else: return "https://graph.facebook.com%s" % url def live_get(url, params): full_url = live_url(url,params) - return urllib2.urlopen(full_url).read() + return urllib.request.urlopen(full_url).read() def live_post(url, params): full_url = live_url(url, None) - return urllib2.urlopen(full_url, urllib.urlencode(params)).read() + return urllib.request.urlopen(full_url, urllib.parse.urlencode(params)).read() def get_auth_url(request, redirect_url): request.session['live_redirect_uri'] = redirect_url @@ -41,16 +41,16 @@ def get_auth_url(request, redirect_url): 'scope': 'publish_stream'}) def get_user_info_after_auth(request): - args = facebook_get('/oauth/access_token', { + args = live_get('/oauth/access_token', { 'client_id' : APP_ID, 'redirect_uri' : request.session['fb_redirect_uri'], - 'client_secret' : API_SECRET, + 'client_secret' : APP_SECRET, 'code' : request.GET['code'] }) - access_token = cgi.parse_qs(args)['access_token'][0] + access_token = urllib.parse.parse_qs(args)['access_token'][0] - info = utils.from_json(facebook_get('/me', {'access_token':access_token})) + info = utils.from_json(live_get('/me', {'access_token':access_token})) return {'type': 'facebook', 'user_id' : info['id'], 'name': info['name'], 'info': info, 'token': {'access_token': access_token}} @@ -58,7 +58,7 @@ def update_status(user_id, user_info, token, message): """ post a message to the auth system's update stream, e.g. twitter stream """ - result = facebook_post('/me/feed', { + result = live_post('/me/feed', { 'access_token': token['access_token'], 'message': message }) diff --git a/helios_auth/auth_systems/oauthclient/client.py b/helios_auth/auth_systems/oauthclient/client.py index c9e6b829bb946694130cabcc6b336939864bcb3a..4980535e45ab58942cc632c00ecc50c0dd33d578 100644 --- a/helios_auth/auth_systems/oauthclient/client.py +++ b/helios_auth/auth_systems/oauthclient/client.py @@ -7,12 +7,11 @@ Used the SampleClient from the OAUTH.org example python client as basis. props to leahculver for making a very hard to use but in the end usable oauth lib. ''' -import httplib -import urllib, urllib2 -import time +import urllib.request import webbrowser -import oauth as oauth -from urlparse import urlparse + +from . import oauth as oauth + class LoginOAuthClient(oauth.OAuthClient): @@ -36,31 +35,35 @@ class LoginOAuthClient(oauth.OAuthClient): self.sha1_method = oauth.OAuthSignatureMethod_HMAC_SHA1() self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) - if ((oauth_token != None) and (oauth_token_secret!=None)): + if (oauth_token is not None) and (oauth_token_secret is not None): self.token = oauth.OAuthConsumer(oauth_token, oauth_token_secret) else: self.token = None - def oauth_request(self,url, args = {}, method=None): - if (method==None): - if args=={}: + def oauth_request(self, url, args=None, method=None): + if args is None: + args = {} + if method is None: + if args == {}: method = "GET" else: method = "POST" req = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, method, url, args) req.sign_request(self.sha1_method, self.consumer,self.token) - if (method=="GET"): + if method== "GET": return self.http_wrapper(req.to_url()) - elif (method == "POST"): + elif method == "POST": return self.http_wrapper(req.get_normalized_http_url(),req.to_postdata()) #this is barely working. (i think. mostly it is that everyone else is using httplib) - def http_wrapper(self, url, postdata={}): + def http_wrapper(self, url, postdata=None): + if postdata is None: + postdata = {} try: - if (postdata != {}): - f = urllib.urlopen(url, postdata) + if postdata != {}: + f = urllib.request.urlopen(url, postdata) else: - f = urllib.urlopen(url) + f = urllib.request.urlopen(url) response = f.read() except: import traceback @@ -133,26 +136,26 @@ if __name__ == '__main__': consumer_key = '' consumer_secret = '' while not consumer_key: - consumer_key = raw_input('Please enter consumer key: ') + consumer_key = input('Please enter consumer key: ') while not consumer_secret: - consumer_secret = raw_input('Please enter consumer secret: ') + consumer_secret = input('Please enter consumer secret: ') auth_client = LoginOAuthClient(consumer_key,consumer_secret) tok = auth_client.get_request_token() token = tok['oauth_token'] token_secret = tok['oauth_token_secret'] url = auth_client.get_authorize_url(token) webbrowser.open(url) - print "Visit this URL to authorize your app: " + url - response_token = raw_input('What is the oauth_token from twitter: ') + print("Visit this URL to authorize your app: " + url) + response_token = input('What is the oauth_token from twitter: ') response_client = LoginOAuthClient(consumer_key, consumer_secret,token, token_secret, server_params={}) tok = response_client.get_access_token() - print "Making signed request" + print("Making signed request") #verify user access content = response_client.oauth_request('https://twitter.com/account/verify_credentials.json', method='POST') #make an update #content = response_client.oauth_request('https://twitter.com/statuses/update.xml', {'status':'Updated from a python oauth client. awesome.'}, method='POST') - print content + print(content) - print 'Done.' + print('Done.') diff --git a/helios_auth/auth_systems/oauthclient/oauth/__init__.py b/helios_auth/auth_systems/oauthclient/oauth/__init__.py index baf543ed4a8db09d92ee91b925d18bc6f5d09f93..f318a8af4421f92c9a5276d619fd8ef52dbe9644 100755 --- a/helios_auth/auth_systems/oauthclient/oauth/__init__.py +++ b/helios_auth/auth_systems/oauthclient/oauth/__init__.py @@ -1,10 +1,8 @@ -import cgi -import urllib -import time -import random -import urlparse -import hmac import binascii +import hmac +import random +import time +import urllib.parse VERSION = '1.0' # Hi Blaine! HTTP_METHOD = 'GET' @@ -22,7 +20,7 @@ def build_authenticate_header(realm=''): # url escape def escape(s): # escape '/' too - return urllib.quote(s, safe='~') + return urllib.parse.quote(s, safe='~') # util function: current timestamp # seconds since epoch (UTC) @@ -60,12 +58,12 @@ class OAuthToken(object): self.secret = secret def to_string(self): - return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) # return a token from something like: # oauth_token_secret=digg&oauth_token=digg def from_string(s): - params = cgi.parse_qs(s, keep_blank_values=False) + params = urllib.parse.parse_qs(s, keep_blank_values=False) key = params['oauth_token'][0] secret = params['oauth_token_secret'][0] return OAuthToken(key, secret) @@ -112,7 +110,7 @@ class OAuthRequest(object): # get any non-oauth parameters def get_nonoauth_parameters(self): parameters = {} - for k, v in self.parameters.iteritems(): + for k, v in self.parameters.items(): # ignore oauth parameters if k.find('oauth_') < 0: parameters[k] = v @@ -123,14 +121,14 @@ class OAuthRequest(object): auth_header = 'OAuth realm="%s"' % realm # add the oauth parameters if self.parameters: - for k, v in self.parameters.iteritems(): + for k, v in self.parameters.items(): if k[:6] == 'oauth_': auth_header += ', %s="%s"' % (k, escape(str(v))) return {'Authorization': auth_header} # serialize as post data for a POST request def to_postdata(self): - return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items()]) # serialize as a url for a GET request def to_url(self): @@ -144,7 +142,7 @@ class OAuthRequest(object): del params['oauth_signature'] except: pass - key_values = params.items() + key_values = list(params.items()) # sort lexicographically, first after key, then after value key_values.sort() # combine key value pairs in string and escape @@ -156,7 +154,7 @@ class OAuthRequest(object): # parses the url and rebuilds it to be scheme://host/path def get_normalized_http_url(self): - parts = urlparse.urlparse(self.http_url) + parts = urllib.parse.urlparse(self.http_url) url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path return url_string @@ -194,7 +192,7 @@ class OAuthRequest(object): parameters.update(query_params) # URL parameters - param_str = urlparse.urlparse(http_url)[4] # query + param_str = urllib.parse.urlparse(http_url)[4] # query url_params = OAuthRequest._split_url_string(param_str) parameters.update(url_params) @@ -249,15 +247,15 @@ class OAuthRequest(object): # split key-value param_parts = param.split('=', 1) # remove quotes and unescape the value - params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"')) return params _split_header = staticmethod(_split_header) # util function: turn url string into parameters, has to do some unescaping def _split_url_string(param_str): - parameters = cgi.parse_qs(param_str, keep_blank_values=False) - for k, v in parameters.iteritems(): - parameters[k] = urllib.unquote(v[0]) + parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.items(): + parameters[k] = urllib.parse.unquote(v[0]) return parameters _split_url_string = staticmethod(_split_url_string) @@ -273,7 +271,7 @@ class OAuthServer(object): self.signature_methods = signature_methods or {} def set_data_store(self, oauth_data_store): - self.data_store = data_store + self.data_store = oauth_data_store def get_data_store(self): return self.data_store @@ -351,7 +349,7 @@ class OAuthServer(object): # get the signature method object signature_method = self.signature_methods[signature_method] except: - signature_method_names = ', '.join(self.signature_methods.keys()) + signature_method_names = ', '.join(list(self.signature_methods.keys())) raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) return signature_method @@ -499,11 +497,11 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): # hmac object try: - import hashlib # 2.5 - hashed = hmac.new(key, raw, hashlib.sha1) + from Crypto.Hash import SHA1 + hashed = hmac.new(key, raw, SHA1) except: - import sha # deprecated - hashed = hmac.new(key, raw, sha) + import hashlib + hashed = hmac.new(key, raw, hashlib.sha1) # calculate the digest base 64 return binascii.b2a_base64(hashed.digest())[:-1] diff --git a/helios_auth/auth_systems/openid/util.py b/helios_auth/auth_systems/openid/util.py index 1ed33f3be0f82de0f2f9d545a79ccf8b972bd878..2a2e12a429b0206f6700acbb929b5e01ffdfe11f 100644 --- a/helios_auth/auth_systems/openid/util.py +++ b/helios_auth/auth_systems/openid/util.py @@ -3,20 +3,15 @@ Utility code for the Django example consumer and server. """ -from urlparse import urljoin +from urllib.parse import urljoin -from django.db import connection -from django.template.context import RequestContext -from django.template import loader -from django import http +from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.db import connection from django.urls import reverse as reverseURL - -from django.conf import settings - -from openid.store.filestore import FileOpenIDStore from openid.store import sqlstore -from openid.yadis.constants import YADIS_CONTENT_TYPE +from openid.store.filestore import FileOpenIDStore + def getOpenIDStore(filestore_path, table_prefix): """ @@ -69,14 +64,13 @@ def getOpenIDStore(filestore_path, table_prefix): s = types[db_engine](connection.connection, **tablenames) except KeyError: - raise ImproperlyConfigured, \ - "Database engine %s not supported by OpenID library" % \ - (db_engine,) + raise ImproperlyConfigured("Database engine %s not supported by OpenID library" % \ + (db_engine,)) try: s.createTables() - except (SystemExit, KeyboardInterrupt, MemoryError), e: - raise + except (SystemExit, KeyboardInterrupt, MemoryError) as e: + raise e except: # XXX This is not the Right Way to do this, but because the # underlying database implementation might differ in behavior @@ -138,5 +132,5 @@ def normalDict(request_data): values are lists, because in OpenID, each key in the query arg set can have at most one value. """ - return dict((k, v[0]) for k, v in request_data.iteritems()) + return dict((k, v[0]) for k, v in request_data.items()) diff --git a/helios_auth/auth_systems/openid/view_helpers.py b/helios_auth/auth_systems/openid/view_helpers.py index 06eef8a287bbbf0d5a05d32d8ed0e13939761eac..de79b451a037800f974e33535e52569af754c1c6 100644 --- a/helios_auth/auth_systems/openid/view_helpers.py +++ b/helios_auth/auth_systems/openid/view_helpers.py @@ -1,14 +1,8 @@ - -from django import http -from django.http import HttpResponseRedirect - from openid.consumer import consumer from openid.consumer.discover import DiscoveryFailure from openid.extensions import ax, pape, sreg -from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE -from openid.server.trustroot import RP_RETURN_TO_URL_TYPE -import util +from . import util PAPE_POLICIES = [ 'AUTH_PHISHING_RESISTANT', @@ -56,16 +50,12 @@ def start_openid(session, openid_url, trust_root, return_to): # Start OpenID authentication. c = get_consumer(session) - error = None try: auth_request = c.begin(openid_url) - except DiscoveryFailure, e: + except DiscoveryFailure as e: # Some other protocol-level failure occurred. - error = "OpenID discovery error: %s" % (str(e),) - - if error: - raise Exception("error in openid") + raise Exception("error in openid: OpenID discovery error") from e # Add Simple Registration request information. Some fields # are optional, some are required. It's possible that the @@ -80,7 +70,7 @@ def start_openid(session, openid_url, trust_root, return_to): # XXX - uses myOpenID-compatible schema values, which are # not those listed at axschema.org. - for k, v in AX_REQUIRED_FIELDS.iteritems(): + for k, v in AX_REQUIRED_FIELDS.items(): ax_request.add(ax.AttrInfo(v, required=True)) auth_request.addExtension(ax_request) @@ -123,12 +113,12 @@ def finish_openid(session, request_args, return_to): ax_response = ax.FetchResponse.fromSuccessResponse(response) if ax_response: - for k, v in AX_REQUIRED_FIELDS.iteritems(): + for k, v in AX_REQUIRED_FIELDS.items(): """ the values are the URIs, they are the key into the data the key is the shortname """ - if ax_response.data.has_key(v): + if v in ax_response.data: ax_items[k] = ax_response.get(v) # Map different consumer status codes to template contexts. @@ -141,7 +131,7 @@ def finish_openid(session, request_args, return_to): consumer.SUCCESS: {'url': response.getDisplayIdentifier(), - 'sreg': sreg_response and sreg_response.items(), + 'sreg': sreg_response and list(sreg_response.items()), 'ax': ax_items} } diff --git a/helios_auth/auth_systems/password.py b/helios_auth/auth_systems/password.py index aadb03fb05e52d9bbe4e9b2d5e8e02933b89590f..36f42ecffcc109f283d157e1ffe2a37fadb8b706 100644 --- a/helios_auth/auth_systems/password.py +++ b/helios_auth/auth_systems/password.py @@ -52,7 +52,7 @@ def password_login_view(request): # set this in case we came here straight from the multi-login chooser # and thus did not have a chance to hit the "start/password" URL request.session['auth_system_name'] = 'password' - if request.POST.has_key('return_url'): + if 'return_url' in request.POST: request.session['auth_return_url'] = request.POST.get('return_url') if form.is_valid(): diff --git a/helios_auth/auth_systems/twitter.py b/helios_auth/auth_systems/twitter.py index 8739d607ebcb1d85b0b539c8f85eb34cceaa4ce8..541ac4df7cd874259a71be3566246fa3b2a48b57 100644 --- a/helios_auth/auth_systems/twitter.py +++ b/helios_auth/auth_systems/twitter.py @@ -2,7 +2,7 @@ Twitter Authentication """ -from oauthclient import client +from .oauthclient import client from django.conf.urls import url from django.urls import reverse diff --git a/helios_auth/auth_systems/yahoo.py b/helios_auth/auth_systems/yahoo.py index 5131a19be520301ab4ea6643e88d97015d186d0f..ccfdc12ffd8850dc17feb54bee08b4fa784a5c1e 100644 --- a/helios_auth/auth_systems/yahoo.py +++ b/helios_auth/auth_systems/yahoo.py @@ -6,7 +6,7 @@ Yahoo Authentication from django.conf import settings from django.core.mail import send_mail -from openid import view_helpers +from .openid import view_helpers # some parameters to indicate that status updating is not possible STATUS_UPDATES = False diff --git a/helios_auth/jsonfield.py b/helios_auth/jsonfield.py index 34cecf7894487c1141388abe13d5a0934a4ab9dc..ab66d5d5d95f5e48860e36f88a4f70124cfee558 100644 --- a/helios_auth/jsonfield.py +++ b/helios_auth/jsonfield.py @@ -5,10 +5,12 @@ http://www.djangosnippets.org/snippets/377/ """ import json -from django.core.exceptions import ValidationError + from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from . import utils + class JSONField(models.TextField): """ @@ -37,14 +39,10 @@ class JSONField(models.TextField): # noinspection PyUnusedLocal def from_db_value(self, value, *args, **kwargs): - if value == "" or value is None: + parsed_value = utils.from_json(value) + if parsed_value is None: return None - try: - parsed_value = json.loads(value) - except Exception as e: - raise ValidationError("Received value is not JSON", e) - if self.json_type and parsed_value: parsed_value = self.json_type.fromJSONDict(parsed_value, **self.deserialization_params) @@ -55,7 +53,7 @@ class JSONField(models.TextField): def get_prep_value(self, value): """Convert our JSON object to a string before we save""" - if isinstance(value, basestring): + if isinstance(value, str): return value if value is None: diff --git a/helios_auth/migrations/0001_initial.py b/helios_auth/migrations/0001_initial.py index fc54ff2aa58d29872fdd0453e87ffb9123974af8..002068a63be3355f5eb285660f1b0eb5b7514816 100644 --- a/helios_auth/migrations/0001_initial.py +++ b/helios_auth/migrations/0001_initial.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.db import models, migrations + import helios_auth.jsonfield diff --git a/helios_auth/models.py b/helios_auth/models.py index fb050d225808599e95459e057f7ff0489e5c1ecf..15fa0253c7e16acc1200f61d0d527f2d01aa76fb 100644 --- a/helios_auth/models.py +++ b/helios_auth/models.py @@ -8,8 +8,8 @@ Ben Adida """ from django.db import models -from auth_systems import AUTH_SYSTEMS -from jsonfield import JSONField +from .auth_systems import can_check_constraint, AUTH_SYSTEMS +from .jsonfield import JSONField # an exception to catch when a user is no longer authenticated @@ -53,7 +53,7 @@ class User(models.Model): if not created_p: # special case the password: don't replace it if it exists - if obj.info.has_key('password'): + if 'password' in obj.info: info['password'] = obj.info['password'] obj.info = info @@ -64,7 +64,7 @@ class User(models.Model): return obj def can_update_status(self): - if not AUTH_SYSTEMS.has_key(self.user_type): + if self.user_type not in AUTH_SYSTEMS: return False return AUTH_SYSTEMS[self.user_type].STATUS_UPDATES @@ -74,7 +74,7 @@ class User(models.Model): Certain auth systems can choose to limit election creation to certain users. """ - if not AUTH_SYSTEMS.has_key(self.user_type): + if self.user_type not in AUTH_SYSTEMS: return False return AUTH_SYSTEMS[self.user_type].can_create_election(self.user_id, self.info) @@ -86,16 +86,16 @@ class User(models.Model): return AUTH_SYSTEMS[self.user_type].STATUS_UPDATE_WORDING_TEMPLATE def update_status(self, status): - if AUTH_SYSTEMS.has_key(self.user_type): + if self.user_type in AUTH_SYSTEMS: AUTH_SYSTEMS[self.user_type].update_status(self.user_id, self.info, self.token, status) def send_message(self, subject, body): - if AUTH_SYSTEMS.has_key(self.user_type): + if self.user_type in AUTH_SYSTEMS: subject = subject.split("\n")[0] AUTH_SYSTEMS[self.user_type].send_message(self.user_id, self.name, self.info, subject, body) def send_notification(self, message): - if AUTH_SYSTEMS.has_key(self.user_type): + if self.user_type in AUTH_SYSTEMS: if hasattr(AUTH_SYSTEMS[self.user_type], 'send_notification'): AUTH_SYSTEMS[self.user_type].send_notification(self.user_id, self.info, message) @@ -110,17 +110,17 @@ class User(models.Model): return False # no constraint? Then eligible! - if not eligibility_case.has_key('constraint'): + if 'constraint' not in eligibility_case: return True # from here on we know we match the auth system, but do we match one of the constraints? - auth_system = AUTH_SYSTEMS[self.user_type] - # does the auth system allow for checking a constraint? - if not hasattr(auth_system, 'check_constraint'): + if not can_check_constraint(self.user_type): return False - + + auth_system = AUTH_SYSTEMS[self.user_type] + for constraint in eligibility_case['constraint']: # do we match on this constraint? if auth_system.check_constraint(constraint=constraint, user = self): @@ -141,14 +141,14 @@ class User(models.Model): if self.name: return self.name - if self.info.has_key('name'): + if 'name' in self.info: return self.info['name'] return self.user_id @property def public_url(self): - if AUTH_SYSTEMS.has_key(self.user_type): + if self.user_type in AUTH_SYSTEMS: if hasattr(AUTH_SYSTEMS[self.user_type], 'public_url'): return AUTH_SYSTEMS[self.user_type].public_url(self.user_id) diff --git a/helios_auth/security/__init__.py b/helios_auth/security/__init__.py index d3c5ac184a160d6790b1f04e8584341919dd3ef5..1fa30e1cf3d5edef11b7a445f4527235f0f567d8 100644 --- a/helios_auth/security/__init__.py +++ b/helios_auth/security/__init__.py @@ -12,7 +12,7 @@ from django.http import HttpResponseRedirect # nicely update the wrapper function from functools import update_wrapper -import oauth +from . import oauth from helios_auth.models import User FIELDS_TO_SAVE = 'FIELDS_TO_SAVE' @@ -93,10 +93,10 @@ def get_user(request): # request.session.set_expiry(settings.SESSION_COOKIE_AGE) # set up CSRF protection if needed - if not request.session.has_key('csrf_token') or (type(request.session['csrf_token']) != str and type(request.session['csrf_token']) != unicode): + if 'csrf_token' not in request.session or not isinstance(request.session['csrf_token'], str): request.session['csrf_token'] = str(uuid.uuid4()) - if request.session.has_key('user'): + if 'user' in request.session: user = request.session['user'] # find the user @@ -109,7 +109,7 @@ def check_csrf(request): if request.method != "POST": return HttpResponseNotAllowed("only a POST for this URL") - if (not request.POST.has_key('csrf_token')) or (request.POST['csrf_token'] != request.session['csrf_token']): + if ('csrf_token' not in request.POST) or (request.POST['csrf_token'] != request.session['csrf_token']): raise Exception("A CSRF problem was detected") def save_in_session_across_logouts(request, field_name, field_value): diff --git a/helios_auth/security/oauth.py b/helios_auth/security/oauth.py index 71676c89f039eebadbb1129081e8e96d2f638ee7..568272bd835eaae3a3f9238b821fab8b870222e8 100644 --- a/helios_auth/security/oauth.py +++ b/helios_auth/security/oauth.py @@ -6,14 +6,12 @@ Hacked a bit by Ben Adida (ben@adida.net) so that: - access tokens are looked up with an extra param of consumer """ -import urllib -import time -import random -import urlparse -import hmac import base64 +import hmac import logging -import hashlib +import random +import time +import urllib.parse VERSION = '1.0' # Hi Blaine! HTTP_METHOD = 'GET' @@ -31,7 +29,7 @@ def build_authenticate_header(realm=''): # url escape def escape(s): # escape '/' too - return urllib.quote(s, safe='~') + return urllib.parse.quote(s, safe='~') # util function: current timestamp # seconds since epoch (UTC) @@ -69,13 +67,13 @@ class OAuthToken(object): self.secret = secret def to_string(self): - return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + return urllib.parse.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) # return a token from something like: # oauth_token_secret=digg&oauth_token=digg @staticmethod def from_string(s): - params = urlparse.parse_qs(s, keep_blank_values=False) + params = urllib.parse.parse_qs(s, keep_blank_values=False) key = params['oauth_token'][0] secret = params['oauth_token_secret'][0] return OAuthToken(key, secret) @@ -124,7 +122,7 @@ class OAuthRequest(object): # get any non-oauth parameters def get_nonoauth_parameters(self): parameters = {} - for k, v in self.parameters.iteritems(): + for k, v in self.parameters.items(): # ignore oauth parameters if k.find('oauth_') < 0: parameters[k] = v @@ -135,7 +133,7 @@ class OAuthRequest(object): auth_header = 'OAuth realm="%s"' % realm # add the oauth parameters if self.parameters: - for k, v in self.parameters.iteritems(): + for k, v in self.parameters.items(): # only if it's a standard OAUTH param (Ben) if k in self.OAUTH_PARAMS: auth_header += ', %s="%s"' % (k, escape(str(v))) @@ -143,7 +141,7 @@ class OAuthRequest(object): # serialize as post data for a POST request def to_postdata(self): - return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) + return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.items()) # serialize as a url for a GET request def to_url(self): @@ -157,7 +155,7 @@ class OAuthRequest(object): del params['oauth_signature'] except: pass - key_values = params.items() + key_values = list(params.items()) # sort lexicographically, first after key, then after value key_values.sort() # combine key value pairs in string and escape @@ -169,7 +167,7 @@ class OAuthRequest(object): # parses the url and rebuilds it to be scheme://host/path def get_normalized_http_url(self): - parts = urlparse.urlparse(self.http_url) + parts = urllib.parse.urlparse(self.http_url) url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path return url_string @@ -208,7 +206,7 @@ class OAuthRequest(object): parameters.update(query_params) # URL parameters - param_str = urlparse.urlparse(http_url)[4] # query + param_str = urllib.parse.urlparse(http_url)[4] # query url_params = OAuthRequest._split_url_string(param_str) parameters.update(url_params) @@ -263,15 +261,15 @@ class OAuthRequest(object): # split key-value param_parts = param.split('=', 1) # remove quotes and unescape the value - params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"')) return params # util function: turn url string into parameters, has to do some unescaping @staticmethod def _split_url_string(param_str): - parameters = urlparse.parse_qs(param_str, keep_blank_values=False) - for k, v in parameters.iteritems(): - parameters[k] = urllib.unquote(v[0]) + parameters = urllib.parse.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.items(): + parameters[k] = urllib.parse.unquote(v[0]) return parameters # OAuthServer is a worker to check a requests validity against a data store @@ -364,7 +362,7 @@ class OAuthServer(object): # get the signature method object signature_method = self.signature_methods[signature_method] except: - signature_method_names = ', '.join(self.signature_methods.keys()) + signature_method_names = ', '.join(list(self.signature_methods.keys())) raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) return signature_method @@ -513,7 +511,12 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): key, raw = self.build_signature_base_string(oauth_request, consumer, token) # hmac object - hashed = hmac.new(key, raw, hashlib.sha1) + try: + from Crypto.Hash import SHA1 + hashed = hmac.new(key, raw, SHA1) + except: + import hashlib + hashed = hmac.new(key, raw, hashlib.sha1) # calculate the digest base 64 return base64.b64encode(hashed.digest()) diff --git a/helios_auth/tests.py b/helios_auth/tests.py index de21e3b09c600084194a47bd5cd0f73522a74daa..934933258f49fb8a64752fc241340a1ad4489789 100644 --- a/helios_auth/tests.py +++ b/helios_auth/tests.py @@ -3,16 +3,15 @@ Unit Tests for Auth Systems """ import unittest -import models +from django.core import mail from django.db import IntegrityError, transaction - -from django.test.client import Client from django.test import TestCase +from django.urls import reverse -from django.core import mail +from . import models, views +from .auth_systems import AUTH_SYSTEMS, password as password_views -from auth_systems import AUTH_SYSTEMS class UserModelTests(unittest.TestCase): @@ -23,7 +22,7 @@ class UserModelTests(unittest.TestCase): """ there should not be two users with the same user_type and user_id """ - for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + for auth_system, auth_system_module in AUTH_SYSTEMS.items(): models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name':'Foo Bar'}) def double_insert(): @@ -36,22 +35,22 @@ class UserModelTests(unittest.TestCase): """ shouldn't create two users, and should reset the password """ - for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + for auth_system, auth_system_module in AUTH_SYSTEMS.items(): u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name':'Foo Bar'}) def double_update_or_create(): new_name = 'Foo2 Bar' u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_cou', info={'name': new_name}) - self.assertEquals(u.id, u2.id) - self.assertEquals(u2.info['name'], new_name) + self.assertEqual(u.id, u2.id) + self.assertEqual(u2.info['name'], new_name) def test_can_create_election(self): """ check that auth systems have the can_create_election call and that it's true for the common ones """ - for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + for auth_system, auth_system_module in AUTH_SYSTEMS.items(): assert(hasattr(auth_system_module, 'can_create_election')) if auth_system != 'clever': assert(auth_system_module.can_create_election('foobar', {})) @@ -62,13 +61,13 @@ class UserModelTests(unittest.TestCase): check that a user set up with status update ability reports it as such, and otherwise does not report it """ - for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + for auth_system, auth_system_module in AUTH_SYSTEMS.items(): u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'}) if hasattr(auth_system_module, 'send_message'): - self.assertNotEquals(u.update_status_template, None) + self.assertNotEqual(u.update_status_template, None) else: - self.assertEquals(u.update_status_template, None) + self.assertEqual(u.update_status_template, None) def test_eligibility(self): """ @@ -76,22 +75,18 @@ class UserModelTests(unittest.TestCase): FIXME: also test constraints on eligibility """ - for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + for auth_system, auth_system_module in AUTH_SYSTEMS.items(): u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_status_update', info={'name':'Foo Bar Status Update'}) self.assertTrue(u.is_eligible_for({'auth_system': auth_system})) def test_eq(self): - for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems(): + for auth_system, auth_system_module in AUTH_SYSTEMS.items(): u = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'}) u2 = models.User.update_or_create(user_type = auth_system, user_id = 'foobar_eq', info={'name':'Foo Bar Status Update'}) - self.assertEquals(u, u2) - + self.assertEqual(u, u2) -import views -import auth_systems.password as password_views -from django.urls import reverse # FIXME: login CSRF should make these tests more complicated # and should be tested for @@ -125,6 +120,6 @@ class UserBlackboxTests(TestCase): """using the test email backend""" self.test_user.send_message("testing subject", "testing body") - self.assertEquals(len(mail.outbox), 1) - self.assertEquals(mail.outbox[0].subject, "testing subject") - self.assertEquals(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>") + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, "testing subject") + self.assertEqual(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>") diff --git a/helios_auth/urls.py b/helios_auth/urls.py index 5244e10b8a1e2f07f8ab712c3b8f3997e58bedfa..9fbc158f499f0df32cd3f31830990b537ed00f73 100644 --- a/helios_auth/urls.py +++ b/helios_auth/urls.py @@ -7,9 +7,8 @@ Ben Adida (ben@adida.net) from django.conf.urls import url -import url_names -import views -from settings import AUTH_ENABLED_AUTH_SYSTEMS +from settings import AUTH_ENABLED_SYSTEMS +from . import views, url_names urlpatterns = [ # basic static stuff @@ -23,11 +22,11 @@ urlpatterns = [ ] # password auth -if 'password' in AUTH_ENABLED_AUTH_SYSTEMS: - from auth_systems.password import urlpatterns as password_patterns +if 'password' in AUTH_ENABLED_SYSTEMS: + from .auth_systems.password import urlpatterns as password_patterns urlpatterns.extend(password_patterns) # twitter -if 'twitter' in AUTH_ENABLED_AUTH_SYSTEMS: - from auth_systems.twitter import urlpatterns as twitter_patterns +if 'twitter' in AUTH_ENABLED_SYSTEMS: + from .auth_systems.twitter import urlpatterns as twitter_patterns urlpatterns.extend(twitter_patterns) diff --git a/helios_auth/utils.py b/helios_auth/utils.py index f57dedf6e961e8598963049dca55f6cdaf211f00..dc723adce77a3f08c19e1a0438a8ae20f611e882 100644 --- a/helios_auth/utils.py +++ b/helios_auth/utils.py @@ -7,23 +7,30 @@ Some basic utils import json + ## JSON def to_json(d): - return json.dumps(d, sort_keys=True) - -def from_json(json_str): - if not json_str: return None - return json.loads(json_str) - -def JSONtoDict(json): - x=json.loads(json) - return x - -def JSONFiletoDict(filename): - f = open(filename, 'r') - content = f.read() - f.close() - return JSONtoDict(content) - + return json.dumps(d, sort_keys=True) + + +def from_json(value): + if value == "" or value is None: + return None + if isinstance(value, str): + try: + return json.loads(value) + except Exception as e: + # import ast + # try: + # parsed_value = ast.literal_eval(parsed_value) + # except Exception as e1: + raise Exception("value is not JSON parseable, that's bad news") from e + return value + + +def JSONFiletoDict(filename): + with open(filename, 'r') as f: + content = f.read() + return from_json(content) diff --git a/helios_auth/view_utils.py b/helios_auth/view_utils.py index 6699ab28ab5030b4b8703ea4868f5ef1edd22a14..48b90c7806167ee9031a3c5e55a82f03adf9af37 100644 --- a/helios_auth/view_utils.py +++ b/helios_auth/view_utils.py @@ -52,7 +52,3 @@ def render_template_raw(request, template_name, values=None): vars_with_user = prepare_vars(request, values) return t.render(context=vars_with_user, request=request) - - -def render_json(json_txt): - return HttpResponse(json_txt) diff --git a/helios_auth/views.py b/helios_auth/views.py index f246dafd582ef696d6ba2351fad93d507f7924eb..73f23050ba2f9767a94da17a2e5b180578c4c6be 100644 --- a/helios_auth/views.py +++ b/helios_auth/views.py @@ -5,19 +5,19 @@ Ben Adida 2009-07-05 """ -import urllib -from django.urls import reverse +from urllib.parse import urlencode + from django.http import HttpResponseRedirect, HttpResponse +from django.urls import reverse -import helios_auth import settings -from auth_systems import AUTH_SYSTEMS -from auth_systems import password +from helios_auth import DEFAULT_AUTH_SYSTEM, ENABLED_AUTH_SYSTEMS from helios_auth.security import get_user from helios_auth.url_names import AUTH_INDEX, AUTH_START, AUTH_AFTER, AUTH_WHY, AUTH_AFTER_INTERVENTION -from models import User -from security import FIELDS_TO_SAVE -from view_utils import render_template, render_template_raw +from .auth_systems import AUTH_SYSTEMS, password +from .models import User +from .security import FIELDS_TO_SAVE +from .view_utils import render_template, render_template_raw def index(request): @@ -28,21 +28,21 @@ def index(request): user = get_user(request) # single auth system? - if len(helios_auth.ENABLED_AUTH_SYSTEMS) == 1 and not user: - return HttpResponseRedirect(reverse(AUTH_START, args=[helios_auth.ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', '')) + if len(ENABLED_AUTH_SYSTEMS) == 1 and not user: + return HttpResponseRedirect(reverse(AUTH_START, args=[ENABLED_AUTH_SYSTEMS[0]])+ '?return_url=' + request.GET.get('return_url', '')) - #if helios_auth.DEFAULT_AUTH_SYSTEM and not user: - # return HttpResponseRedirect(reverse(start, args=[helios_auth.DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', '')) + #if DEFAULT_AUTH_SYSTEM and not user: + # return HttpResponseRedirect(reverse(start, args=[DEFAULT_AUTH_SYSTEM])+ '?return_url=' + request.GET.get('return_url', '')) default_auth_system_obj = None - if helios_auth.DEFAULT_AUTH_SYSTEM: - default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM] + if DEFAULT_AUTH_SYSTEM: + default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM] #form = password.LoginForm() return render_template(request, 'index', {'return_url' : request.GET.get('return_url', '/'), - 'enabled_auth_systems' : helios_auth.ENABLED_AUTH_SYSTEMS, - 'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM, + 'enabled_auth_systems' : ENABLED_AUTH_SYSTEMS, + 'default_auth_system': DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj}) def login_box_raw(request, return_url='/', auth_systems = None): @@ -50,20 +50,20 @@ def login_box_raw(request, return_url='/', auth_systems = None): a chunk of HTML that shows the various login options """ default_auth_system_obj = None - if helios_auth.DEFAULT_AUTH_SYSTEM: - default_auth_system_obj = AUTH_SYSTEMS[helios_auth.DEFAULT_AUTH_SYSTEM] + if DEFAULT_AUTH_SYSTEM: + default_auth_system_obj = AUTH_SYSTEMS[DEFAULT_AUTH_SYSTEM] # make sure that auth_systems includes only available and enabled auth systems if auth_systems is not None: - enabled_auth_systems = set(auth_systems).intersection(set(helios_auth.ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys())) + enabled_auth_systems = set(auth_systems).intersection(set(ENABLED_AUTH_SYSTEMS)).intersection(set(AUTH_SYSTEMS.keys())) else: - enabled_auth_systems = set(helios_auth.ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys())) + enabled_auth_systems = set(ENABLED_AUTH_SYSTEMS).intersection(set(AUTH_SYSTEMS.keys())) form = password.LoginForm() return render_template_raw(request, 'login_box', { 'enabled_auth_systems': enabled_auth_systems, 'return_url': return_url, - 'default_auth_system': helios_auth.DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj, + 'default_auth_system': DEFAULT_AUTH_SYSTEM, 'default_auth_system_obj': default_auth_system_obj, 'form' : form}) def do_local_logout(request): @@ -74,7 +74,7 @@ def do_local_logout(request): user = None - if request.session.has_key('user'): + if 'user' in request.session: user = request.session['user'] # 2010-08-14 be much more aggressive here @@ -151,7 +151,7 @@ def _do_auth(request): return HttpResponse("an error occurred trying to contact " + system_name +", try again later") def start(request, system_name): - if not (system_name in helios_auth.ENABLED_AUTH_SYSTEMS): + if not (system_name in ENABLED_AUTH_SYSTEMS): return HttpResponseRedirect(reverse(AUTH_INDEX)) # why is this here? Let's try without it @@ -173,7 +173,7 @@ def perms_why(request): def after(request): # which auth system were we using? - if not request.session.has_key('auth_system_name'): + if 'auth_system_name' not in request.session: do_local_logout(request) return HttpResponseRedirect("/") @@ -188,7 +188,7 @@ def after(request): request.session['user'] = user else: - return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urllib.urlencode({'system_name' : request.session['auth_system_name']}))) + return HttpResponseRedirect("%s?%s" % (reverse(AUTH_WHY), urlencode({'system_name' : request.session['auth_system_name']}))) # does the auth system want to present an additional view? # this is, for example, to prompt the user to follow @heliosvoting @@ -203,7 +203,7 @@ def after(request): def after_intervention(request): return_url = "/" - if request.session.has_key('auth_return_url'): + if 'auth_return_url' in request.session: return_url = request.session['auth_return_url'] del request.session['auth_return_url'] return HttpResponseRedirect(settings.URL_HOST + return_url) diff --git a/heliosverifier/js/jscrypto/helios.js b/heliosverifier/js/jscrypto/helios.js index 93c0ed48c317d21fee609d30a4108157b1fddbfd..f46a31bbb2db866ddf6acd59b120213d770b1fb8 100644 --- a/heliosverifier/js/jscrypto/helios.js +++ b/heliosverifier/js/jscrypto/helios.js @@ -609,13 +609,14 @@ HELIOS.dejsonify_list_of_lists = function(lol, item_dejsonifier) { } HELIOS.Trustee = Class.extend({ - init: function(uuid, public_key, public_key_hash, pok, decryption_factors, decryption_proofs) { + init: function(uuid, public_key, public_key_hash, pok, decryption_factors, decryption_proofs, email) { this.uuid = uuid; this.public_key = public_key; this.public_key_hash = public_key_hash; this.pok = pok; this.decryption_factors = decryption_factors; this.decryption_proofs = decryption_proofs; + this.email = email; }, toJSONObject: function() { @@ -631,5 +632,7 @@ HELIOS.Trustee.fromJSONObject = function(d) { return new HELIOS.Trustee(d.uuid, ElGamal.PublicKey.fromJSONObject(d.public_key), d.public_key_hash, ElGamal.DLogProof.fromJSONObject(d.pok), HELIOS.dejsonify_list_of_lists(d.decryption_factors, BigInt.fromJSONObject), - HELIOS.dejsonify_list_of_lists(d.decryption_proofs, ElGamal.Proof.fromJSONObject)); -}; \ No newline at end of file + HELIOS.dejsonify_list_of_lists(d.decryption_proofs, ElGamal.Proof.fromJSONObject), + d.email + ); +}; diff --git a/requirements.txt b/requirements.txt index 8e69fb261c4417d9c3c41cdd8a10c8911ecb4713..0b88e889d1dfaa36250db009fc41e40e1bea00c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,22 @@ Django==1.11.28 -anyjson==0.3.3 +django_webtest>=1.9.7 + +gunicorn==20.0.4 + +dj_database_url==0.5.0 +psycopg2==2.8.4 + celery==4.2.1 -django-picklefield==0.3.0 -kombu==4.2.0 -html5lib==0.999 -psycopg2==2.7.3.2 -pyparsing==1.5.7 -python-dateutil>=1.5 -python-openid==2.2.5 -wsgiref==0.1.2 -gunicorn==19.9 -requests==2.21.0 -unicodecsv==0.9.0 -dj_database_url==0.3.0 -django_webtest>=1.9 -webtest==2.0.18 -bleach==1.4.1 -boto==2.27.0 -django-ses==0.6.0 -validate_email==1.2 -oauth2client==1.2 -rollbar==0.12.1 +django-picklefield==1.1.0 + +python-dateutil>=2.8 +unicodecsv==0.14.1 +bleach==3.1.1 +validate_email==1.3 +pycryptodome==3.8.2 + +python3-openid==3.0.10 +boto==2.49.0 +django-ses==0.8.14 +oauth2client==4.1.3 +rollbar==0.14.7 diff --git a/runtime.txt b/runtime.txt index f27f1cc5ca4d2e750411e5ee682f6eb6baa674c2..4252f10667b7c2fce89b70b74097960c86c7cf6c 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-2.7.15 +python-3.6.12 diff --git a/server_ui/urls.py b/server_ui/urls.py index e691c07afe7b29c0b347e5bde5ea75e9addd65c7..aad77654242dcfa268cc23b7103de800b68b9dfa 100644 --- a/server_ui/urls.py +++ b/server_ui/urls.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from django.conf.urls import url -from views import home, about, docs, faq, privacy +from .views import home, about, docs, faq, privacy urlpatterns = [ url(r'^$', home), diff --git a/server_ui/view_utils.py b/server_ui/view_utils.py index df947214620fc0169b9569767a84e633cd0e7615..f499603e108fdbb3db9a352df2e0fb6efb4ab2e9 100644 --- a/server_ui/view_utils.py +++ b/server_ui/view_utils.py @@ -20,7 +20,7 @@ def render_template(request, template_name, values = None): vars_with_user['CURRENT_URL'] = request.path # csrf protection - if request.session.has_key('csrf_token'): + if 'csrf_token' in request.session: vars_with_user['csrf_token'] = request.session['csrf_token'] return render_to_response('server_ui/templates/%s.html' % template_name, vars_with_user) diff --git a/server_ui/views.py b/server_ui/views.py index 190d902aa71f4c0722ed68a787055a4f978ba7c8..5010dbc34f6b270af8ceb1aad8faab14686ebaaa 100644 --- a/server_ui/views.py +++ b/server_ui/views.py @@ -3,15 +3,15 @@ server_ui specific views """ import copy + from django.conf import settings import helios_auth.views as auth_views from helios.models import Election from helios.security import can_create_election from helios_auth.security import get_user -from view_utils import render_template -import glue - +from . import glue +from .view_utils import render_template glue.glue() # actually apply glue helios.view <-> helios.signals @@ -36,7 +36,7 @@ def home(request): else: elections_voted = None - auth_systems = copy.copy(settings.AUTH_ENABLED_AUTH_SYSTEMS) + auth_systems = copy.copy(settings.AUTH_ENABLED_SYSTEMS) try: auth_systems.remove('password') except: pass diff --git a/settings.py b/settings.py index b7266d2cf66f9d3c0cf34d4fd05285f88eb67459..f4dddc3bd18e1466f77c5ccb6b1eddaace743d6b 100644 --- a/settings.py +++ b/settings.py @@ -9,7 +9,7 @@ TESTING = 'test' in sys.argv # go through environment variables and override them def get_from_env(var, default): - if not TESTING and os.environ.has_key(var): + if not TESTING and var in os.environ: return os.environ[var] else: return default @@ -38,19 +38,16 @@ SHOW_USER_INFO = (get_from_env('SHOW_USER_INFO', '1') == '1') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'helios' - } + 'NAME': 'helios', + 'CONN_MAX_AGE': 600, + }, } # override if we have an env variable if get_from_env('DATABASE_URL', None): import dj_database_url - DATABASES['default'] = dj_database_url.config() + DATABASES['default'] = dj_database_url.config(conn_max_age=600, ssl_require=True) DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2' - DATABASES['default']['CONN_MAX_AGE'] = 600 - - # require SSL - DATABASES['default']['OPTIONS'] = {'sslmode': 'require'} # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -211,9 +208,11 @@ HELIOS_VOTERS_EMAIL = True HELIOS_PRIVATE_DEFAULT = False # authentication systems enabled -#AUTH_ENABLED_AUTH_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo'] -AUTH_ENABLED_AUTH_SYSTEMS = get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'google').split(",") -AUTH_DEFAULT_AUTH_SYSTEM = get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None) +# AUTH_ENABLED_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo'] +AUTH_ENABLED_SYSTEMS = get_from_env('AUTH_ENABLED_SYSTEMS', + get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'password,google,facebook') + ).split(",") +AUTH_DEFAULT_SYSTEM = get_from_env('AUTH_DEFAULT_SYSTEM', get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None)) # google GOOGLE_CLIENT_ID = get_from_env('GOOGLE_CLIENT_ID', '') @@ -262,12 +261,12 @@ if get_from_env('EMAIL_USE_AWS', '0') == '1': # set up logging import logging + logging.basicConfig( - level = logging.DEBUG, - format = '%(asctime)s %(levelname)s %(message)s' + level=logging.DEBUG if TESTING else logging.INFO, + format='%(asctime)s %(levelname)s %(message)s' ) - # set up celery CELERY_BROKER_URL = get_from_env('CELERY_BROKER_URL', 'amqp://localhost') if TESTING: @@ -277,7 +276,7 @@ if TESTING: # Rollbar Error Logging ROLLBAR_ACCESS_TOKEN = get_from_env('ROLLBAR_ACCESS_TOKEN', None) if ROLLBAR_ACCESS_TOKEN: - print "setting up rollbar" + print("setting up rollbar") MIDDLEWARE += ['rollbar.contrib.django.middleware.RollbarNotifierMiddleware',] ROLLBAR = { 'access_token': ROLLBAR_ACCESS_TOKEN,