def _raw_mul(self, plaintext): """Returns the integer E(a * plaintext), where E(a) = ciphertext Args: plaintext (int): number by which to multiply the `EncryptedNumber`. *plaintext* is typically an encoding. 0 <= *plaintext* < :attr:`~PaillierPublicKey.n` Returns: int: Encryption of the product of `self` and the scalar encoded in *plaintext*. Raises: TypeError: if *plaintext* is not an int. ValueError: if *plaintext* is not between 0 and :attr:`PaillierPublicKey.n`. """ if not isinstance(plaintext, int): raise TypeError('Expected ciphertext to be int, not %s' % type(plaintext)) if plaintext < 0 or plaintext >= self.public_key.n: raise ValueError('Scalar out of bounds: %i' % plaintext) if self.public_key.n - self.public_key.max_int <= plaintext: # Very large plaintext, play a sneaky trick using inverses neg_c = invert(self.ciphertext(False), self.public_key.nsquare) neg_scalar = self.public_key.n - plaintext return powmod(neg_c, neg_scalar, self.public_key.nsquare) else: return powmod(self.ciphertext(False), plaintext, self.public_key.nsquare)
def prepare_ballot(pk, n_choices, n_candidates): n2 = pk.nsquare ballot = [] for _ in range(n_candidates): # values provided (ciphertexts and corresponding proofs of validity) ciphertexts = [] proofs = [] # product of ciphertexts of the raw (ciphertext and randomization) row_c = 1 row_r = 1 plaintexts = [1] + [0] * (n_choices - 1) for plaintext in plaintexts: ciphertext, r, proof = encrypt_among(pk, plaintext, [0, 1]) ciphertexts.append(ciphertext) proofs.append(proof) row_c = row_c * ciphertext.raw_value % n2 row_r = row_r * r % pk.n # prove that row_c is an encryption of 1 omega = random.SystemRandom().randrange(pk.n) a = powmod(omega, pk.n, n2) e = H(a) z = omega * powmod(row_r, e, pk.n) row_proof = a, z row_proof = None row = (ciphertexts, proofs, row_proof) ballot.append(row) return ballot
def raw_multiply(self, a, b, randomization=None): """Multiply a raw ciphertext with a plaintext Arguments: a (int): a ciphertext for this Paillier public key b (int): a plaintext randomization (int, optional): the randomization factor; if not provided, a secure-random value is chosen Returns: tuple: a pair of integers, corresponding to the raw ciphertext encrypting the product of the value encrypted by `a` and the value `b`, and the randomization used """ n2 = self.nsquare # explicitely reduces to avoid large exponents b %= self.n if b > self.n // 2: b -= self.n # if a is of the form (1+n)^q, then we can avoid the exponentiation q, r = divmod(a - 1, self.n) if r == 0: raw_value = (1 + self.n * q * b) % n2 else: raw_value = util.powmod(a, b, n2) # apply randomization if randomization is None: randomization = random.SystemRandom().randrange(self.n) raw_value = raw_value * util.powmod(randomization, self.n, n2) % n2 return raw_value, randomization
def verify_decrypt(self, ciphertext, partial_decryption, proof): """Check the proof of decryption of the corresponding secret key Arguments: ciphertext (PaillierCiphertext): the ciphertext to be decrypted in a verifiable manner partial_decryption (int): the corresponding partial decryption proof (int): a proof that `partial_decryption` is indeed a partial decryption of `ciphertext` under the corresponding secret key """ pk = self.public_key # run Chaum-Pedersen protocol in the Fiat-Shamir heuristic t1, t2, w = proof h = util.H([ ciphertext.raw_value, partial_decryption, t1, self.verification_base, self.verification, t2, ]) # verify proof # check that v^s = t_1 * v_i^c if util.powmod(self.verification_base, w, pk.n) != \ t1 * util.powmod(self.verification, h, pk.n) % pk.n: raise InvalidProof # check that (x^2)^s = t_2 * (m^2)^c if util.powmod(ciphertext.raw_value, _CP*_QR*w, pk.n) != \ t2 * util.powmod(partial_decryption, _CP*h, pk.n) % pk.n: raise InvalidProof
def decrypt(self, ciphertext, relative=True): """Decrypt a ciphertext Arguments: ciphertext (int or PaillierCiphertext): the ciphertext relative (bool): whether the result should be interpreted as a relative integer (i.e. in [-n/2, n/2] rather than in [0, n]) Returns: int: the message represented in the ciphertext If no transformation other than (re)randomization has been performed on the ciphertext, then the original message should be returned. If homomorphic operations have been performed, then the result of these operations on the original messages should be returned. If relative is set to `True`, then the returned value is a relative integer between `-n/2` and `n/2`. Otherwise, it is a non-negative integer lower than `n`. """ pk = self.public_key p, q = self.p, self.q if isinstance(ciphertext, PaillierCiphertext): ciphertext = ciphertext.raw_value m_mod_p = pk.L(util.powmod(ciphertext, p - 1, p * p), p) * self.hp % p m_mod_q = pk.L(util.powmod(ciphertext, q - 1, q * q), q) * self.hq % q plaintext = util.crt([m_mod_p, m_mod_q], [p, q]) if relative and plaintext >= pk.n // 2: plaintext -= pk.n return plaintext
def raw_decrypt(self, ciphertext): """Decrypt raw ciphertext and return raw plaintext. Args: ciphertext (int): (usually from :meth:`EncryptedNumber.ciphertext()`) that is to be Paillier decrypted. Returns: int: Paillier decryption of ciphertext. This is a positive integer < :attr:`public_key.n`. Raises: TypeError: if ciphertext is not an int. """ if not isinstance(ciphertext, int): raise TypeError('Expected ciphertext to be an int, not: %s' % type(ciphertext)) decrypt_to_p = self.l_function( powmod(ciphertext, self.p - 1, self.psquare), self.p) * self.hp % self.p decrypt_to_q = self.l_function( powmod(ciphertext, self.q - 1, self.qsquare), self.q) * self.hq % self.q return self.crt(decrypt_to_p, decrypt_to_q)
def prove_decrypt_batched(self, ciphertext_batch): """Batched version of `prove_decrypt()` Arguments: ciphertext_batch (list): the ciphertexts (PaillierCiphertext) to be decrypted in a verifiable manner Returns: tuple: `partial_decryption_batch`, `proof` where `partial_decryption_batch` is a list of the partial decryptions (int) of the ciphertexts, and `proof` is a proof (int) that they are indeed so """ pk = self.public_key partial_decryption_batch = [ self.decrypt(ciphertext) for ciphertext in ciphertext_batch ] # run protocol in the Fiat-Shamir heuristic # to aggregate ZKPs, the verifier provides λ_i *after* the plaintexts # have been provided; then combined_ciphertext = ∏ ciphertext^{λ_i} # and combined_plaintext = ∏ m^{λ_i} (not needed for prover) lambda_batch = [ util.H([ciphertext.raw_value, partial_decryption]) for ciphertext, partial_decryption in zip( ciphertext_batch, partial_decryption_batch) ] combined_plaintext = util.prod( util.powmod(plaintext, lambda_, pk.n) for plaintext, lambda_ in zip(partial_decryption_batch, lambda_batch)) combined_ciphertext = util.prod( util.powmod(ciphertext.raw_value, lambda_, pk.n) for ciphertext, lambda_ in zip(ciphertext_batch, lambda_batch)) try: # raises IndexError if not enough precomputations were forecast r, t1 = self.precomputed_values.pop() except AttributeError: # no pre-computations r = random.SystemRandom().randrange(pk.n << (2 * pk.security_parameter)) t1 = util.powmod(self.verification_base, r, pk.n) # prove knowledge of key_share such that: # * v_i = v**key_share # * combined_plaintext**2 = (combined_ciphertext**2)**(2*key_share) t2 = util.powmod(combined_ciphertext, _CP * _QR * r, pk.n) h = util.H([ combined_ciphertext, combined_plaintext, t1, self.verification_base, self.verification, t2, ]) w = r + h * self.key_share proof = t1, t2, w return partial_decryption_batch, proof
def verify_knowledge(self, proof): """Check the proof of knowledge of the corresponding secret key Arguments: proof (int): a proof of knowledge of the secret key """ pk = self.public_key # run Schnorr protocol in the Fiat-Shamir heuristic t, w = proof h = util.H([self.verification_base, self.verification, t]) # verify proof if util.powmod(self.verification_base, w, pk.n) != \ t * util.powmod(self.verification, h, pk.n) % pk.n: raise InvalidProof
def __init__(self, p, q, g): """Constructor Arguments: p (int): parameter from the Paillier cryptosystem q (int): parameter from the Paillier cryptosystem g (int): parameter from the Paillier cryptosystem """ self.p = p self.q = q self.public_key = pk = PaillierPublicKey(p * q, g) # pre-computations self.hp = util.invert(pk.L(util.powmod(pk.g, p - 1, p * p), p), p) self.hq = util.invert(pk.L(util.powmod(pk.g, q - 1, q * q), q), q)
def prove_private_multiply(self, x, cy): """Multiply a ciphertext with a plaintext in a verifiable manner Arguments: x (int): the clear operand cy (PaillierCiphertext): the encrypted operand Returns: tuple: `cx`, `cz`, `proof` where `cx` is an encryption (PaillierCiphertext) of x, `cz` is an encryption (PaillierCiphertext) of x*y and `proof` is a proof (int) that z = x * y """ n2 = self.nsquare # precomputable values try: # raises IndexError if not enough precomputations were forecast x_, (cx, rx), u, (cu, ru) = self.precomputed_values.pop() except AttributeError: # no pre-computations u = random.SystemRandom().randrange(self.n) cx, rx = self.raw_multiply(self.g, x) # ⟦x⟧ cu, ru = self.raw_multiply(self.g, u) # ⟦u⟧ else: # ensure consistency with arguments if x is None: x = x_ elif x != x_: raise ValueError # other encrypted values cz, rz = self.raw_multiply(cy, x) # ⟦z⟧ = ⟦xy⟧ cyu, ryu = self.raw_multiply(cy, u) # ⟦yu⟧ # run protocol in the Fiat-Shamir heuristic h = util.H([cx, cy, cz, cu, cyu]) rs = ru * util.powmod(rx, h, self.n) % self.n rys = ryu * util.powmod(rz, h, self.n) % self.n w = u + x * h proof = cu, cyu, w, rs, rys return cx, cz, proof
def __init__(self, public_key, verification_base, key_share): """Constructor Arguments: public_key (PaillierPublicKey): the non-shared Paillier public key key_share (int): parameter s_i in shared Paillier """ self.public_key = public_key self.verification_base = verification_base self.key_share = key_share n2 = public_key.nsquare self.verification = util.powmod(verification_base, key_share, n2)
def encrypt_among(pk, plaintext, values): n2 = pk.nsquare raw_ciphertext, r = pk.raw_multiply(pk.g, plaintext) ciphertext = paillier.PaillierCiphertext(pk, raw_ciphertext) omega = random.SystemRandom().randrange(pk.n) e = [random.SystemRandom().randrange(2 << (2 * 1023)) for _ in values] z = [random.SystemRandom().randrange(pk.n) for _ in values] a = [ powmod(omega, pk.n, n2) if plaintext == value else powmod(z[i], pk.n, n2) * powmod(raw_ciphertext * powmod(pk.g, value, n2), -e[i], n2) % n2 for i, value in enumerate(values) ] e_challenge = H(a) i = values.index(plaintext) e[i] = e_challenge - (sum(e) + e[i]) z[i] = omega * powmod(r, e[i], pk.n) % pk.n proof = a, e, z return ciphertext, r, proof
def decrypt(self, ciphertext): """(Partially) decrypt a ciphertext Arguments: ciphertext (PaillierCiphertext): the ciphertext to be decrypted Returns: int: the decryption share of the ciphertext corresponding to this secret key share """ pk = self.public_key return util.powmod(ciphertext.raw_value, _QR * self.key_share, pk.n)
def prove_decrypt(self, ciphertext): """(Partially) decrypt a ciphertext in a verifiable manner Arguments: ciphertext (PaillierCiphertext): the ciphertext to be decrypted Returns: tuple: `partial_decryption`, `proof` where `partial_decryption` is the partial decryption (int) of the ciphertext, and `proof` is a proof (int) that it is indeed so """ pk = self.public_key partial_decryption = self.decrypt(ciphertext) try: # raises IndexError if not enough precomputations were forecast r, t1 = self.precomputed_values.pop() except AttributeError: # no pre-computations r = random.SystemRandom().randrange(pk.n << (2 * pk.security_parameter)) t1 = util.powmod(self.verification_base, r, pk.n) # prove knowledge of key_share such that: # * v_i = v**key_share # * (partial_decryption**2) = (ciphertext**2)**(2*key_share) t2 = util.powmod(ciphertext.raw_value, _CP * _QR * r, pk.n) # run Chaum-Pedersen protocol in the Fiat-Shamir heuristic h = util.H([ ciphertext.raw_value, partial_decryption, t1, self.verification_base, self.verification, t2, ]) w = r + h * self.key_share proof = t1, t2, w return partial_decryption, proof
def share_paillier_keypair(pk, sk, n_shares): """Share an existing keypair for the Paillier cryptosystem Arguments: pk (PaillierPublicKey): public part of the keypair to be shared sk (PaillierSecretKey): secret part of the keypair to be shared n_shares (int): the number of shares into which to split the keypair Returns: tuple: pair of two elements, usually named respectively `pk_shares` (`list` of `PaillierPublicKeyShare`) and `sk_shares` (`list` of `PaillierSecretKeyShare`). When used together, the secret key shares (`sk_shares`) allow to decrypt ciphertexts generated using the given public key (but not using another), using the method `assemble_decryption_shares()` from `PaillierPublicKeyShare`. The public key shares (`pk_shares`) can be used to verify that each secret key share was used correctly (usually one share would be given to each party, and decryption would imply that each party correctly processes the ciphertext using their secret key share). """ m = (sk.p - 1) * (sk.q - 1) exponent = util.invert(pk.n, m) # the verification base must generate the quadratic residues; which happens # with overwhelming probability for a random square verification_base = random.SystemRandom().randrange(pk.nsquare)**2 % pk.n # split the secret exponent into required number of shares key_shares = [ random.SystemRandom().randrange(m) for _ in range(n_shares - 1) ] key_shares.append((exponent - sum(key_shares)) % (m)) # compute corresponding verification elements verifications = [ util.powmod(verification_base, key_share, pk.nsquare) for key_share in key_shares ] # create public and private key shares pk_shares = [ PaillierPublicKeyShare(pk, verification_base, verification) for verification in verifications ] sk_shares = [ PaillierSecretKeyShare(pk, verification_base, key_share) for key_share in key_shares ] return pk_shares, sk_shares
def verify_private_multiply_batched(self, cx, cy_list, cz_list, proof): """Check the proof of multiplication of a ciphertext with a plaintext Arguments: cx (PaillierCiphertext): the encrypted left operand cy_list (list): the encrypted right operands (PaillierCiphertext) cz_list (list): the encrypted products (PaillierCiphertext) of x*y for each y in `cy_list` proof (int): a proof that z = x*y for each y, z in cy_list, cz_list """ n2 = self.nsquare # generate random λ_i *after* ciphertexts have been provided cu, cyu, w, rs, rys = proof lambda_list = [ util.H([cx, cy, cz]) for cy, cz in zip(cy_list, cz_list) ] # compute combined ciphertexts cy = util.prod( util.powmod(cy, lambda_, self.nsquare) for cy, lambda_ in zip(cy_list, lambda_list)) cz = util.prod( util.powmod(cz, lambda_, self.nsquare) for cz, lambda_ in zip(cz_list, lambda_list)) # run private multiply protocol in the Fiat-Shamir heuristic h = util.H([cx, cy, cz, cu, cyu]) # verify proofs cs, _ = self.raw_multiply(self.g, w, rs) # ⟦s⟧ = ⟦u + xe⟧ cys, _ = self.raw_multiply(cy, w, rys) # ⟦ys⟧ = ⟦y(u + xe)⟧ # ⟦u⟧ * ⟦x⟧**e = ⟦u + xe⟧ = ⟦s⟧ if cs != cu * util.powmod(cx, h, n2) % n2: raise InvalidProof # ⟦yu⟧ * ⟦z⟧**e = ⟦yu + yxe⟧ = ⟦ys⟧ if cys != cyu * util.powmod(cz, h, n2) % n2: raise InvalidProof
def verify_private_multiply(self, cx, cy, cz, proof): """Check the proof of multiplication of a ciphertext with a plaintext Arguments: cx (int): the encrypted left operand cy (int): the encrypted right operand cz (int): the encrypted result proof (int): a proof that z = x*y """ n2 = self.nsquare # run protocol in the Fiat-Shamir heuristic cu, cyu, w, rs, rys = proof h = util.H([cx, cy, cz, cu, cyu]) # verify proofs cs, _ = self.raw_multiply(self.g, w, rs) # ⟦s⟧ = ⟦u + xe⟧ cys, _ = self.raw_multiply(cy, w, rys) # ⟦ys⟧ = ⟦y(u + xe)⟧ # ⟦u⟧ * ⟦x⟧**e = ⟦u + xe⟧ = ⟦s⟧ if cs != cu * util.powmod(cx, h, n2) % n2: raise InvalidProof # ⟦yu⟧ * ⟦z⟧**e = ⟦yu + yxe⟧ = ⟦ys⟧ if cys != cyu * util.powmod(cz, h, n2) % n2: raise InvalidProof
def verify_decrypt_batched(self, ciphertext_batch, partial_decryption_batch, proof): """Batched version of `verify_decrypt()` Arguments: ciphertext_batch (list): the ciphertexts (PaillierCiphertext) to be decrypted in a verifiable manner partial_decryption_batch (list): the corresponding partial decryptions (int) proof: a proof that each element of `partial_decryption_batch` is indeed a partial decryption of the corresponding element in `ciphertext_batch` under the secret key corresponding to self """ pk = self.public_key # run protocol in the Fiat-Shamir heuristic t1, t2, w = proof # generate random λ_i *after* decryption shares have been provided lambda_batch = [ util.H([ciphertext.raw_value, partial_decryption]) for ciphertext, partial_decryption in zip( ciphertext_batch, partial_decryption_batch) ] # compute combined plaintext and ciphertext for verification combined_plaintext = util.prod( util.powmod(plaintext, lambda_, pk.n) for plaintext, lambda_ in zip(partial_decryption_batch, lambda_batch)) combined_ciphertext = util.prod( util.powmod(ciphertext.raw_value, lambda_, pk.n) for ciphertext, lambda_ in zip(ciphertext_batch, lambda_batch)) h = util.H([ combined_ciphertext, combined_plaintext, t1, self.verification_base, self.verification, t2, ]) # verify proof # check that v^s = t_1 * v_i^c if util.powmod(self.verification_base, w, pk.n) != \ t1 * util.powmod(self.verification, h, pk.n) % pk.n: raise InvalidProof # check that (x^2)^s = t_2 * (m^2)^c if util.powmod(combined_ciphertext, _CP*_QR*w, pk.n) != \ t2 * util.powmod(combined_plaintext, _CP*h, pk.n) % pk.n: raise InvalidProof
def raw_encrypt(self, plaintext, r_value=None): """Paillier encryption of a positive integer plaintext < :attr:`n`. You probably should be using :meth:`encrypt` instead, because it handles positive and negative ints and floats. Args: plaintext (int): a positive integer < :attr:`n` to be Paillier encrypted. Typically this is an encoding of the actual number you want to encrypt. r_value (int): obfuscator for the ciphertext; by default (i.e. r_value is None), a random value is used. Returns: int: Paillier encryption of plaintext. Raises: TypeError: if plaintext is not an int or mpz. """ if not isinstance(plaintext, int) and not isinstance( plaintext, type(mpz(1))) and not isinstance( plaintext, numpy.int64): raise TypeError('Expected int type plaintext but got: %s' % type(plaintext)) if self.n - self.max_int <= plaintext < self.n: # Very large plaintext, take a sneaky shortcut using inverses neg_plaintext = self.n - plaintext # = abs(plaintext - nsquare) neg_ciphertext = (self.n * neg_plaintext + 1) % self.nsquare nude_ciphertext = invert(neg_ciphertext, self.nsquare) else: # we chose g = n + 1, so that we can exploit the fact that # (n+1)^plaintext = n*plaintext + 1 mod n^2 nude_ciphertext = (self.n * plaintext + 1) % self.nsquare # r = r_value or self.get_random_lt_n() # obfuscator = powmod(r, self.n, self.nsquare) r = r_value or powmod(self.get_random_lt_n(), self.n, self.nsquare) # Pass the precomputed obfuscator obfuscator = r return (nude_ciphertext * obfuscator) % self.nsquare
def obfuscate(self): """Disguise ciphertext by multiplying by r ** n with random r. This operation must be performed for every `EncryptedNumber` that is sent to an untrusted party, otherwise eavesdroppers might deduce relationships between this and an antecedent `EncryptedNumber`. For example:: enc = public_key.encrypt(1337) send_to_nsa(enc) # NSA can't decrypt (we hope!) product = enc * 3.14 send_to_nsa(product) # NSA can deduce 3.14 by bruteforce attack product2 = enc * 2.718 product2.obfuscate() send_to_nsa(product) # NSA can't deduce 2.718 by bruteforce attack """ r = self.public_key.get_random_lt_n() r_pow_n = powmod(r, self.public_key.n, self.public_key.nsquare) self.__ciphertext = self.__ciphertext * r_pow_n % self.public_key.nsquare self.__is_obfuscated = True
def precompute_proofs(self, n_uses): """Precompute and cache some values used in the proofs Using this function does not decreases (nor increases, assuming the exact value for `n_uses` is provided) the total execution time. However, it can be useful to perform some computation in advance, so that the actual use of the secret shares does not take as much time (i.e. when the inputs to the protocol are known). Arguments: n_uses (int): upper-bound on the number of proofs that will be performed (the larger the value, the longer this step takes) """ pk = self.public_key randoms = [ random.SystemRandom().randrange(pk.n << (2 * pk.security_parameter)) for _ in range(n_uses) ] self.precomputed_values = [ (r, util.powmod(self.verification_base, r, pk.n)) for r in randoms ]
def prove_knowledge(self): """Proves knowldege of the secret key share Returns: int: the proof """ pk = self.public_key try: # raises IndexError if not enough precomputations were forecast r, t = self.precomputed_values.pop() except AttributeError: # no pre-computations r = random.SystemRandom().randrange(pk.n << (2 * pk.security_parameter)) t = util.powmod(self.verification_base, r, pk.n) # run Schnorr protocol in the Fiat-Shamir heuristic h = util.H([self.verification_base, self.verification, t]) w = r + h * self.key_share proof = t, w return proof
def assemble_decryption_shares(ciphertext, shares, decryption_shares, relative=True): """Assemble decryptions share in a complete decryption Arguments: shares (list): the public key shares (PaillierPublicKeyShare) corresponding to the secret key shares used to decrypt the ciphertext decryption_shares (list): the decryption shares (int) resulting from the (partial) decryptions by the secret key shares relative (bool): whether the result should be interpreted as a relative integer (i.e. in [-n/2, n/2] rather than in [0, n]) Returns: int: the message represented in the ciphertext If no transformation other than (re)randomization has been performed on the ciphertext, then the original message should be returned. If homomorphic operations have been performed, then the result of these operations on the original messages should be returned. If relative is set to `True`, then the returned value is a relative integer between `-n/2` and `n/2`. Otherwise, it is a non-negative integer lower than `n`. """ pk = shares[0].public_key C = ciphertext.raw_value R = util.prod(decryption_shares, pk.n) plaintext = ( (C * util.powmod(R, -pk.n, pk.nsquare) % pk.nsquare) - 1) // pk.n # TODO: _QR if relative and plaintext >= pk.n // 2: plaintext -= pk.n return plaintext
def h_function(self, x, xsquare): """Computes the h-function as defined in Paillier's paper page 12, 'Decryption using Chinese-remaindering'. """ return invert( self.l_function(powmod(self.public_key.g, x - 1, xsquare), x), x)
def prove_private_multiply_batched(self, x, cy_list): """Multiply a secret with several ciphertexts in a verifiable manner Arguments: x (int): the secret operand (plaintext) cy_list (list): the list of encrypted operands (PaillierCiphertext) Returns: tuple: `cx`, `cz_list`, `proof` where `cx` is an encryption (PaillierCiphertext) of x, `cz_list` is a list of encryptions (PaillierCiphertext) of x*y for each y in `cy_list`, and `proof` (int) is a proof that z = x*y for each y, z in cy_list, cz_list """ n2 = self.nsquare # precomputable values try: # raises IndexError if not enough precomputations were forecast x_, (cx, rx), u, (cu, ru) = self.precomputed_values.pop() except AttributeError: # no pre-computations u = random.SystemRandom().randrange(self.n) cx, rx = self.raw_multiply(self.g, x) # ⟦x⟧ cu, ru = self.raw_multiply(self.g, u) # ⟦u⟧ else: # ensure consistency with arguments if x is None: x = x_ elif x != x_: raise ValueError # encrypted result cz_rz_list = [self.raw_multiply(cy, x) for cy in cy_list] # ⟦z⟧ = ⟦xy⟧ cz_list = [cz for cz, rz in cz_rz_list] rz_list = [rz for cz, rz in cz_rz_list] lambda_list = [ util.H([cx, cy, cz]) for cy, cz in zip(cy_list, cz_list) ] # compute combined ciphertexts cy = util.prod( util.powmod(cy, lambda_, self.nsquare) for cy, lambda_ in zip(cy_list, lambda_list)) cz = util.prod( util.powmod(cz, lambda_, self.nsquare) for cz, lambda_ in zip(cz_list, lambda_list)) rz = util.prod( util.powmod(rz, lambda_, self.n) for rz, lambda_ in zip(rz_list, lambda_list)) # other encrypted values cyu, ryu = self.raw_multiply(cy, u) # ⟦yu⟧ # run private multiply protocol in the Fiat-Shamir heuristic h = util.H([cx, cy, cz, cu, cyu]) rs = ru * util.powmod(rx, h, self.n) % self.n rys = ryu * util.powmod(rz, h, self.n) % self.n w = u + x * h proof = cu, cyu, w, rs, rys return cx, cz_list, proof