def compute_partial_re_encryption_key( old_share: KeyShare, old_lc: LagrangeCoefficient, new_share: KeyShare, new_lc: LagrangeCoefficient) -> PartialReEncryptionKey: """ Compute a partial re-encryption key from a participants old and new share. :param old_share: the participants "old" key share :param old_lc: "old" lagrange coefficient provided by the coordinating party :param new_share: the participants "new" key share :param new_lc: "new" lagrange coefficient provided by the coordinating party :return: the partial re-encryption key """ curve_params = old_share.curve_params if curve_params != new_share.curve_params: raise ThresholdCryptoError( 'Differing curves not supported for re-encryption!') if old_share.x != old_lc.participant_index: raise ThresholdCryptoError( 'Lagrange coefficient for OLD share was computed for other participant index' ) if new_share.x != new_lc.participant_index: raise ThresholdCryptoError( 'Lagrange coefficient for NEW share was computed for other participant index' ) partial_re_key = (new_share.y * new_lc.coefficient - old_share.y * old_lc.coefficient) % curve_params.order return PartialReEncryptionKey(partial_re_key, curve_params)
def receive_F_ij_value(self, F_ij: DkgFijValue): """ Receive the F_ij value of another participant. :param F_ij: the received F_ij value """ # implicit check for successful receival of all commitments self.compute_public_key() source_id = F_ij.source_participant_id len_F_ij = len(F_ij.F_ij) if source_id not in self.all_participant_ids: raise ThresholdCryptoError( "Received F_ij values from unknown participant id {}".format( source_id)) if source_id == self.id: raise ThresholdCryptoError( "Received own F_ij values - don't do this") if len_F_ij != self.threshold_params.t: raise ThresholdCryptoError( "List of F_ij values from participant {} has length {} != {} = t" .format(source_id, len_F_ij, self.threshold_params.t)) if source_id not in self._received_F: self._received_F[source_id] = F_ij else: raise ThresholdCryptoError( "F_ij values from participant {} already received".format( source_id))
def _decrypt_message( partial_decryptions: List[PartialDecryption], encrypted_message: EncryptedMessage, ) -> str: # this method does not contain the check for given number of partial decryptions to allow testing the failing decryption curve_params = partial_decryptions[0].curve_params for partial_key in partial_decryptions: if partial_key.curve_params != curve_params: raise ThresholdCryptoError( "Varying curve parameters found in partial re-encryption keys") key_point = _combine_shares( partial_decryptions, encrypted_message, curve_params, ) point_bytes = _key_bytes_from_point(key_point) try: key = nacl.hash.blake2b(point_bytes, digest_size=nacl.secret.SecretBox.KEY_SIZE, encoder=nacl.encoding.RawEncoder) box = nacl.secret.SecretBox(key) encoded_plaintext = box.decrypt(encrypted_message.ciphertext) except nacl.exceptions.CryptoError as e: raise ThresholdCryptoError('Message decryption failed. Internal: ' + str(e)) return str(encoded_plaintext, 'utf-8')
def lagrange_coefficient_for_key_share_indices( key_share_indices: List[int], p_idx: int, curve_params: CurveParameters) -> LagrangeCoefficient: """ Create the ith Lagrange coefficient for a list of key shares. :param key_share_indices: the used indices for the participants key shares :param curve_params: the used curve parameters :param p_idx: the participant index (= the shares x value), the Lagrange coefficient belongs to :return: """ if p_idx not in key_share_indices: raise ThresholdCryptoError( "Participant index {} not found in used indices {} for computation of Lagrange coefficient" .format(p_idx, key_share_indices)) idx_len = len(key_share_indices) i = key_share_indices.index(p_idx) def x(idx): return key_share_indices[idx] tmp = [(-x(j) * number.prime_mod_inv(x(i) - x(j), curve_params.order)) for j in range(0, idx_len) if not j == i] coefficient = number.prod(tmp) % curve_params.order # lambda_i return LagrangeCoefficient(p_idx, key_share_indices, coefficient)
def _unchecked_s_ij_value_for_participant( self, target_participant_id: ParticipantId) -> DkgSijValue: # This method is provided so that it can be used for the own s_ij value computation in the __init__ # method without performing the check in s_ij_value_for_participant. if target_participant_id not in self.all_participant_ids: raise ThresholdCryptoError( "Participant id {} not present in known participant ids". format(target_participant_id)) else: return DkgSijValue(self.id, target_participant_id, self._local_sij[target_participant_id])
def open_commitment(self) -> DkgOpenCommitment: """ The participants open commitment to the "public key share" h_i, which can be evaluated. :return: the open commitment """ if len(self._received_closed_commitments) != self.threshold_params.n: raise ThresholdCryptoError( "Open commitment is just accessible when all other closed commitments were received" ) return self._unchecked_open_commitment()
def receive_sij(self, received_sij: DkgSijValue): """ Receive the s_ij value of another participant. :param received_sij: the received s_ij value """ source_id = received_sij.source_participant_id target_id = received_sij.target_participant_id sij = received_sij.s_ij if source_id not in self.all_participant_ids: raise ThresholdCryptoError( "Received s_ij value from unknown participant id {}".format( source_id)) if source_id == self.id: raise ThresholdCryptoError( "Received own s_ij value - don't do this") if target_id != self.id: raise ThresholdCryptoError( "Received s_ij value for foreign participant (own id={}, target id={})" .format(self.id, target_id)) if source_id not in self._received_sij: self._received_sij[source_id] = received_sij else: raise ThresholdCryptoError( "s_ij value for participant {} already received".format( source_id)) # verify received F values s_ijP = sij * self.curve_params.P F_list = [(self.id**l) * F_jl for l, F_jl in enumerate(self._received_F[source_id].F_ij)] F_sum = number.ecc_sum(F_list) if s_ijP != F_sum: raise ThresholdCryptoError( "F verification failed for participant {}".format(source_id))
def combine_partial_re_encryption_keys( partial_keys: List[PartialReEncryptionKey], old_public_key: PublicKey, new_public_key: PublicKey, old_threshold_params: ThresholdParameters, new_threshold_params: ThresholdParameters) -> ReEncryptionKey: """ Combine a number of partial re-encryption keys yielding the re-encryption key. :param partial_keys: The partial keys as provided by participants :param old_public_key: the public key of the old access structure :param new_public_key: the public key of the new access structure :param old_threshold_params: the threshold parameters of the old access structure :param new_threshold_params: the threshold parameters of the new access structure :return: the re-encryption key """ # TODO check threshold parameters max_t = max(old_threshold_params.t, new_threshold_params.t) if len(partial_keys) < max_t: raise ThresholdCryptoError( "Not enough partial re-encryption keys given") curve_params = partial_keys[0].curve_params for partial_key in partial_keys: if partial_key.curve_params != curve_params: raise ThresholdCryptoError( "Varying curve parameters found in partial re-encryption keys") re_key = sum([k.partial_key for k in partial_keys]) % curve_params.order # check that the proxy key is valid using the given public keys # proxy_key * P =?= new_pub - old_pub checkval1 = curve_params.P * re_key checkval2 = new_public_key.Q + (-old_public_key.Q) if checkval1 != checkval2: raise ThresholdCryptoError( "The combined proxy key is invalid for given public keys.") return ReEncryptionKey(re_key, curve_params)
def receive_closed_commitment(self, commitment: DkgClosedCommitment): """ Receive a closed commitment to the "public key share" h_i of another participant. :param commitment: the received commitment """ source_id = commitment.participant_id if source_id not in self.all_participant_ids: raise ThresholdCryptoError( "Received closed commitment from unknown participant id {}". format(source_id)) if source_id == self.id: raise ThresholdCryptoError( "Received own closed commitment - don't do this") if source_id not in self._received_closed_commitments: self._received_closed_commitments[source_id] = commitment else: raise ThresholdCryptoError( "Closed commitment from participant {} already received". format(source_id))
def s_ij_value_for_participant( self, target_participant_id: ParticipantId) -> DkgSijValue: """ The s_ij value from Pedersens DKG protocol for ONE other particular participant. This value has to be sent SECRETLY to the target participant, which is not covered by this library for now. :param target_participant_id: the id of the target participant """ if len(self._received_F) != self.threshold_params.n: raise ThresholdCryptoError( "s_ij values are just accessible when all other F_ij values were received" ) return self._unchecked_s_ij_value_for_participant( target_participant_id)
def compute_public_key(self) -> PublicKey: """ Compute the public key from received commitment values. :return: the public key """ if len(self._received_open_commitments) != self.threshold_params.n: raise ThresholdCryptoError("Not all commitments were received") participants_h_i = [ c.h_i for c in self._received_open_commitments.values() ] h = number.ecc_sum(participants_h_i) return PublicKey(h, self.curve_params)
def decrypt_message(partial_decryptions: List[PartialDecryption], encrypted_message: EncryptedMessage, threshold_params: ThresholdParameters) -> str: """ Decrypt a message using the combination of at least t partial decryptions. Similar to the encryption process the hybrid approach is used for decryption. :param partial_decryptions: at least t partial decryptions :param encrypted_message: the encrapted message to be decrypted :param threshold_params: the used threshold parameters :return: the decrypted message """ if len(partial_decryptions) < threshold_params.t: raise ThresholdCryptoError('less than t partial decryptions given') return _decrypt_message(partial_decryptions, encrypted_message)
def compute_share(self) -> KeyShare: """ Compute the participants key share from values obtained during the DKG protocol. :return: the final key share after the DKG protocol """ if len(self._received_sij) != self.threshold_params.n: raise ThresholdCryptoError( "Received less s_ij values than necessary: {} != {} = n". format(len(self._received_sij), self.threshold_params.n)) self._s_i = sum( rs.s_ij for rs in self._received_sij.values()) % self.curve_params.order self.key_share = KeyShare(self.id, self._s_i, self.curve_params) return self.key_share
def encrypt_message(message: str, public_key: PublicKey) -> EncryptedMessage: """ Encrypt a message using a public key. A hybrid encryption approach is used to include advantages of symmetric encryption (fast, independent of message-length, integrity-preserving by using AE-scheme). Internally a combination of Salsa20 and Poly1305 from the cryptographic library NaCl is used. :param message: the message to be encrypted :param public_key: the public key :return: the encrypted message """ curve_params = public_key.curve_params encoded_message = bytes(message, 'utf-8') # Create random subgroup element and use its hash as symmetric key to prevent # attacks described in "Why Textbook ElGamal and RSA Encryption Are Insecure" # by Boneh et. al. r = number.random_in_range(1, curve_params.order) key_point = r * curve_params.P point_bytes = _key_bytes_from_point(key_point) try: symmetric_key = nacl.hash.blake2b( point_bytes, digest_size=nacl.secret.SecretBox.KEY_SIZE, encoder=nacl.encoding.RawEncoder) # Use derived symmetric key to encrypt the message box = nacl.secret.SecretBox(symmetric_key) encrypted = box.encrypt(encoded_message) except nacl.exceptions.CryptoError as e: print('Encryption failed: ' + str(e)) raise ThresholdCryptoError('Message encryption failed.') # Use threshold scheme to encrypt the curve point used as hash input to derive the symmetric key C1, C2 = _encrypt_key_point(key_point, public_key.Q, curve_params) return EncryptedMessage(C1, C2, encrypted)
def receive_open_commitment(self, open_commitment: DkgOpenCommitment): """ Receive an open (evaluatable) commitment to the "public key share" h_i of another participant. :param open_commitment: the received commitment """ source_id = open_commitment.participant_id if source_id not in self.all_participant_ids: raise ThresholdCryptoError( "Received open commitment from unknown participant id {}". format(source_id)) if source_id == self.id: raise ThresholdCryptoError( "Received own open commitment - don't do this") if source_id not in self._received_closed_commitments: raise ThresholdCryptoError( "Received open commitment from participant id {} withput received closed commitment" .format(source_id)) closed_commitment = self._received_closed_commitments[source_id] if closed_commitment.commitment != open_commitment.commitment: raise ThresholdCryptoError( "Open and close commitment values differ for participant {}". format(source_id)) if self._compute_commitment( open_commitment.r, open_commitment.h_i) != closed_commitment.commitment: raise ThresholdCryptoError( "Invalid commitment for participant {}".format(source_id)) if source_id not in self._received_open_commitments: self._received_open_commitments[source_id] = open_commitment else: raise ThresholdCryptoError( "Open commitment from participant {} already received".format( source_id))
def __init__(self, own_id: ParticipantId, all_participant_ids: List[ParticipantId], curve_params: CurveParameters, threshold_params: ThresholdParameters): """ Initialize a participant. :param own_id: the id of this participant. As this id is used as the final shares x value, it's important that participants use distinct ids. :param all_participant_ids: a list of all :param curve_params: the curve parameters used :param threshold_params: the required threshold parameters """ if len(set(all_participant_ids)) != threshold_params.n: raise ThresholdCryptoError( "List of distinct participant ids has length {} != {} = n". format(len(all_participant_ids), threshold_params.n)) if own_id not in all_participant_ids: raise ThresholdCryptoError( "Own id must be contained in all participant ids") self.all_participant_ids: List[ParticipantId] = all_participant_ids self.id: ParticipantId = own_id self.curve_params: CurveParameters = curve_params self.threshold_params: ThresholdParameters = threshold_params self._x_i: int = number.random_in_range(0, curve_params.order) self._h_i: ECC.EccPoint = self._x_i * curve_params.P self._polynom: number.PolynomMod = number.PolynomMod.create_random_polynom( self._x_i, self.threshold_params.t - 1, curve_params.order) # calculate own F_ij values self._local_F_ij: List[ECC.EccPoint] = [] for coeff in self._polynom.coefficients: self._local_F_ij.append(coeff * curve_params.P) # calculate own s_ij values self._local_sij: Dict[ParticipantId, int] = {} for p_id in self.all_participant_ids: s_ij = self._polynom.evaluate(p_id) self._local_sij[p_id] = s_ij # random value for commitment of h_i rand_int = random.getrandbits(self._COMMITMENT_RANDOM_BITS) self._commitment_random: bytes = number.int_to_bytes(rand_int) self._commitment: bytes = self._compute_commitment( self._commitment_random, self._h_i) self._received_closed_commitments: Dict[ParticipantId, DkgClosedCommitment] = { self.id: self.closed_commmitment() } self._received_open_commitments: Dict[ ParticipantId, DkgOpenCommitment] = { self.id: self._unchecked_open_commitment() } self._received_F: Dict[ParticipantId, DkgFijValue] = { self.id: self.F_ij_value() } self._received_sij: Dict[ParticipantId, DkgSijValue] = { self.id: self._unchecked_s_ij_value_for_participant(self.id) } self._s_i: Optional[int] = None self.key_share: Optional[KeyShare] = None