class vec_plain_multiply(object):
    """Multiply componentwise cipher and plain along cipher's last dimension
    cipher.shape[-1]= plain.shape
    """
    def __init__(self, context):
        self.evaluator = Evaluator(context)

    def __call__(self, cipher, plain):
        """In order to not affect cipher for other calculations, make copy"""
        cipher = dc(cipher)
        if cipher.shape[-1] != plain.shape[0]:
            raise ValueError("The cipher shape and plain shape don't match.")
        for indices in np.ndindex(cipher.shape[:-1]):
            for i in range(plain.shape[0]):
                self.evaluator.multiply_plain(cipher[indices][i], plain[i])
        return cipher
Beispiel #2
0
def dot_product():
    print("Example: Weighted Average")

    # In this example we demonstrate the FractionalEncoder, and use it to compute
    # a weighted average of 10 encrypted rational numbers. In this computation we
    # perform homomorphic multiplications of ciphertexts by plaintexts, which is
    # much faster than regular multiplications of ciphertexts by ciphertexts.
    # Moreover, such `plain multiplications' never increase the ciphertext size,
    # which is why we have no need for evaluation keys in this example.

    # We start by creating encryption parameters, setting up the SEALContext, keys,
    # and other relevant objects. Since our computation has multiplicative depth of
    # only two, it suffices to use a small poly_modulus.
    parms = EncryptionParameters()
    parms.set_poly_modulus("1x^2048 + 1")
    parms.set_coeff_modulus(seal.coeff_modulus_128(2048))
    parms.set_plain_modulus(1 << 8)

    context = SEALContext(parms)
    print_parameters(context)

    keygen = KeyGenerator(context)
    keygen2 = KeyGenerator(context)
    public_key = keygen.public_key()
    secret_key = keygen.secret_key()

    secret_key2 = keygen.secret_key()

    # We also set up an Encryptor, Evaluator, and Decryptor here.
    encryptor = Encryptor(context, public_key)
    evaluator = Evaluator(context)
    decryptor = Decryptor(context, secret_key2)

    # Create a vector of 10 rational numbers (as doubles).
    # rational_numbers = [3.1, 4.159, 2.65, 3.5897, 9.3, 2.3, 8.46, 2.64, 3.383, 2.795]
    rational_numbers = np.random.rand(10)

    # Create a vector of weights.
    # coefficients = [0.1, 0.05, 0.05, 0.2, 0.05, 0.3, 0.1, 0.025, 0.075, 0.05]
    coefficients = np.random.rand(10)

    my_result = np.dot(rational_numbers, coefficients)

    # We need a FractionalEncoder to encode the rational numbers into plaintext
    # polynomials. In this case we decide to reserve 64 coefficients of the
    # polynomial for the integral part (low-degree terms) and expand the fractional
    # part to 32 digits of precision (in base 3) (high-degree terms). These numbers
    # can be changed according to the precision that is needed; note that these
    # choices leave a lot of unused space in the 2048-coefficient polynomials.
    encoder = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 64, 32, 3)

    # We create a vector of ciphertexts for encrypting the rational numbers.
    encrypted_rationals = []
    rational_numbers_string = "Encoding and encrypting: "
    for i in range(10):
        # We create our Ciphertext objects into the vector by passing the
        # encryption parameters as an argument to the constructor. This ensures
        # that enough memory is allocated for a size 2 ciphertext. In this example
        # our ciphertexts never grow in size (plain multiplication does not cause
        # ciphertext growth), so we can expect the ciphertexts to remain in the same
        # location in memory throughout the computation. In more complicated examples
        # one might want to call a constructor that reserves enough memory for the
        # ciphertext to grow to a specified size to avoid costly memory moves when
        # multiplications and relinearizations are performed.
        encrypted_rationals.append(Ciphertext(parms))
        encryptor.encrypt(encoder.encode(rational_numbers[i]), encrypted_rationals[i])
        rational_numbers_string += (str)(rational_numbers[i])[:6]
        if i < 9: rational_numbers_string += ", "
    print(rational_numbers_string)

    # Next we encode the coefficients. There is no reason to encrypt these since they
    # are not private data.
    encoded_coefficients = []
    encoded_coefficients_string = "Encoding plaintext coefficients: "


    encrypted_coefficients =[]

    for i in range(10):
        encoded_coefficients.append(encoder.encode(coefficients[i]))
        encrypted_coefficients.append(Ciphertext(parms))
        encryptor.encrypt(encoded_coefficients[i], encrypted_coefficients[i])
        encoded_coefficients_string += (str)(coefficients[i])[:6]
        if i < 9: encoded_coefficients_string += ", "
    print(encoded_coefficients_string)

    # We also need to encode 0.1. Multiplication by this plaintext will have the
    # effect of dividing by 10. Note that in SEAL it is impossible to divide
    # ciphertext by another ciphertext, but in this way division by a plaintext is
    # possible.
    div_by_ten = encoder.encode(0.1)

    # Now compute each multiplication.

    prod_result = [Ciphertext() for i in range(10)]
    prod_result2 = [Ciphertext() for i in range(10)]

    print("Computing products: ")
    for i in range(10):
        # Note how we use plain multiplication instead of usual multiplication. The
        # result overwrites the first argument in the function call.
        evaluator.multiply_plain(encrypted_rationals[i], encoded_coefficients[i], prod_result[i])
        evaluator.multiply(encrypted_rationals[i], encrypted_coefficients[i], prod_result2[i])
    print("Done")

    # To obtain the linear sum we need to still compute the sum of the ciphertexts
    # in encrypted_rationals. There is an easy way to add together a vector of
    # Ciphertexts.

    encrypted_result = Ciphertext()
    encrypted_result2 = Ciphertext()

    print("Adding up all 10 ciphertexts: ")
    evaluator.add_many(prod_result, encrypted_result)
    evaluator.add_many(prod_result2, encrypted_result2)

    print("Done")

    # Perform division by 10 by plain multiplication with div_by_ten.
    # print("Dividing by 10: ")
    # evaluator.multiply_plain(encrypted_result, div_by_ten)
    # print("Done")

    # How much noise budget do we have left?
    print("Noise budget in result: " + (str)(decryptor.invariant_noise_budget(encrypted_result)) + " bits")

    # Decrypt, decode, and print result.
    plain_result = Plaintext()
    plain_result2 = Plaintext()
    print("Decrypting result: ")
    decryptor.decrypt(encrypted_result, plain_result)
    decryptor.decrypt(encrypted_result2, plain_result2)
    print("Done")

    result = encoder.decode(plain_result)
    print("Weighted average: " + (str)(result)[:8])

    result2 = encoder.decode(plain_result2)
    print("Weighted average: " + (str)(result2)[:8])

    print('\n\n', my_result)
Beispiel #3
0
def example_ckks_basics():
    print("Example: CKKS Basics");

    #In this example we demonstrate evaluating a polynomial function
    #
    #    PI*x^3 + 0.4*x + 1
    #
    #on encrypted floating-point input data x for a set of 4096 equidistant points
    #in the interval [0, 1]. This example demonstrates many of the main features
    #of the CKKS scheme, but also the challenges in using it.
    #
    # We start by setting up the CKKS scheme.

    parms = EncryptionParameters(scheme_type.CKKS)

    #We saw in `2_encoders.cpp' that multiplication in CKKS causes scales
    #in ciphertexts to grow. The scale of any ciphertext must not get too close
    #to the total size of coeff_modulus, or else the ciphertext simply runs out of
    #room to store the scaled-up plaintext. The CKKS scheme provides a `rescale'
    #functionality that can reduce the scale, and stabilize the scale expansion.
    #
    #Rescaling is a kind of modulus switch operation (recall `3_levels.cpp').
    #As modulus switching, it removes the last of the primes from coeff_modulus,
    #but as a side-effect it scales down the ciphertext by the removed prime.
    #Usually we want to have perfect control over how the scales are changed,
    #which is why for the CKKS scheme it is more common to use carefully selected
    #primes for the coeff_modulus.
    #
    #More precisely, suppose that the scale in a CKKS ciphertext is S, and the
    #last prime in the current coeff_modulus (for the ciphertext) is P. Rescaling
    #to the next level changes the scale to S/P, and removes the prime P from the
    #coeff_modulus, as usual in modulus switching. The number of primes limits
    #how many rescalings can be done, and thus limits the multiplicative depth of
    #the computation.
    #
    #It is possible to choose the initial scale freely. One good strategy can be
    #to is to set the initial scale S and primes P_i in the coeff_modulus to be
    #very close to each other. If ciphertexts have scale S before multiplication,
    #they have scale S^2 after multiplication, and S^2/P_i after rescaling. If all
    #P_i are close to S, then S^2/P_i is close to S again. This way we stabilize the
    #scales to be close to S throughout the computation. Generally, for a circuit
    #of depth D, we need to rescale D times, i.e., we need to be able to remove D
    #primes from the coefficient modulus. Once we have only one prime left in the
    #coeff_modulus, the remaining prime must be larger than S by a few bits to
    #preserve the pre-decimal-point value of the plaintext.
    #
    #Therefore, a generally good strategy is to choose parameters for the CKKS
    #scheme as follows: 
    #
    #    (1) Choose a 60-bit prime as the first prime in coeff_modulus. This will
    #        give the highest precision when decrypting;
    #    (2) Choose another 60-bit prime as the last element of coeff_modulus, as
    #        this will be used as the special prime and should be as large as the
    #        largest of the other primes;
    #    (3) Choose the intermediate primes to be close to each other.
    #
    #We use CoeffModulus::Create to generate primes of the appropriate size. Note
    #that our coeff_modulus is 200 bits total, which is below the bound for our
    #poly_modulus_degree: CoeffModulus::MaxBitCount(8192) returns 218.

    poly_modulus_degree = 8192
    parms.set_poly_modulus_degree(poly_modulus_degree)
    parms.set_coeff_modulus(CoeffModulus.Create(
        poly_modulus_degree, IntVector([60, 40, 40, 60])))

    #We choose the initial scale to be 2^40. At the last level, this leaves us
    #60-40=20 bits of precision before the decimal point, and enough (roughly
    #10-20 bits) of precision after the decimal point. Since our intermediate
    #primes are 40 bits (in fact, they are very close to 2^40), we can achieve
    #scale stabilization as described above.

    scale = 2.0**40

    context = SEALContext.Create(parms)
    print_parameters(context)

    keygen = KeyGenerator(context)
    public_key = keygen.public_key()
    secret_key = keygen.secret_key()
    relin_keys = keygen.relin_keys()
    encryptor = Encryptor(context, public_key)
    evaluator = Evaluator(context)
    decryptor = Decryptor(context, secret_key)

    encoder = CKKSEncoder(context)
    slot_count = encoder.slot_count()
    print("Number of slots: {}".format(slot_count))

    step_size = 1.0 / (slot_count - 1)
    input = DoubleVector(list(map(lambda x: x*step_size, range(0, slot_count))))

    print("Input vector: ")
    print_vector(input)

    print("Evaluating polynomial PI*x^3 + 0.4x + 1 ...")

    #We create plaintexts for PI, 0.4, and 1 using an overload of CKKSEncoder::encode
    #that encodes the given floating-point value to every slot in the vector.

    plain_coeff3 = Plaintext()
    plain_coeff1 = Plaintext()
    plain_coeff0 = Plaintext()
    encoder.encode(3.14159265, scale, plain_coeff3)
    encoder.encode(0.4, scale, plain_coeff1)
    encoder.encode(1.0, scale, plain_coeff0)

    x_plain = Plaintext()
    print("Encode input vectors.")
    encoder.encode(input, scale, x_plain)
    x1_encrypted = Ciphertext() 
    encryptor.encrypt(x_plain, x1_encrypted)

    #To compute x^3 we first compute x^2 and relinearize. However, the scale has
    #now grown to 2^80.

    x3_encrypted = Ciphertext() 
    print("Compute x^2 and relinearize:")
    evaluator.square(x1_encrypted, x3_encrypted)
    evaluator.relinearize_inplace(x3_encrypted, relin_keys)
    print("    + Scale of x^2 before rescale: {} bits".format(log2(x3_encrypted.scale())))

    #Now rescale; in addition to a modulus switch, the scale is reduced down by
    #a factor equal to the prime that was switched away (40-bit prime). Hence, the
    #new scale should be close to 2^40. Note, however, that the scale is not equal
    #to 2^40: this is because the 40-bit prime is only close to 2^40.
    print("Rescale x^2.")
    evaluator.rescale_to_next_inplace(x3_encrypted)
    print("    + Scale of x^2 after rescale: {} bits".format(log2(x3_encrypted.scale())))

    #Now x3_encrypted is at a different level than x1_encrypted, which prevents us
    #from multiplying them to compute x^3. We could simply switch x1_encrypted to
    #the next parameters in the modulus switching chain. However, since we still
    #need to multiply the x^3 term with PI (plain_coeff3), we instead compute PI*x
    #first and multiply that with x^2 to obtain PI*x^3. To this end, we compute
    #PI*x and rescale it back from scale 2^80 to something close to 2^40.

    print("Compute and rescale PI*x.")
    x1_encrypted_coeff3 = Ciphertext() 
    evaluator.multiply_plain(x1_encrypted, plain_coeff3, x1_encrypted_coeff3)
    print("    + Scale of PI*x before rescale: {} bits".format(log2(x1_encrypted_coeff3.scale())))
    evaluator.rescale_to_next_inplace(x1_encrypted_coeff3)
    print("    + Scale of PI*x after rescale: {} bits".format(log2(x1_encrypted_coeff3.scale())))

    #Since x3_encrypted and x1_encrypted_coeff3 have the same exact scale and use
    #the same encryption parameters, we can multiply them together. We write the
    #result to x3_encrypted, relinearize, and rescale. Note that again the scale
    #is something close to 2^40, but not exactly 2^40 due to yet another scaling
    #by a prime. We are down to the last level in the modulus switching chain.

    print("Compute, relinearize, and rescale (PI*x)*x^2.")
    evaluator.multiply_inplace(x3_encrypted, x1_encrypted_coeff3)
    evaluator.relinearize_inplace(x3_encrypted, relin_keys)
    print("    + Scale of PI*x^3 before rescale: {} bits".format(log2(x3_encrypted.scale())))

    evaluator.rescale_to_next_inplace(x3_encrypted)
    print("    + Scale of PI*x^3 after rescale: {} bits".format(log2(x3_encrypted.scale())))

    #Next we compute the degree one term. All this requires is one multiply_plain
    #with plain_coeff1. We overwrite x1_encrypted with the result.

    print("Compute and rescale 0.4*x.")
    evaluator.multiply_plain_inplace(x1_encrypted, plain_coeff1)
    print("    + Scale of 0.4*x before rescale: {} bits".format(log2(x1_encrypted.scale())))
    evaluator.rescale_to_next_inplace(x1_encrypted)
    print("    + Scale of 0.4*x after rescale: {} bits".format(log2(x1_encrypted.scale())))

    #Now we would hope to compute the sum of all three terms. However, there is
    #a serious problem: the encryption parameters used by all three terms are
    #different due to modulus switching from rescaling.
    #
    #Encrypted addition and subtraction require that the scales of the inputs are
    #the same, and also that the encryption parameters (parms_id) match. If there
    #is a mismatch, Evaluator will throw an exception.

    print("Parameters used by all three terms are different.")
    print("    + Modulus chain index for x3_encrypted: {}".format(
        context.get_context_data(x3_encrypted.parms_id()).chain_index()))
    print("    + Modulus chain index for x1_encrypted: {}".format(
        context.get_context_data(x1_encrypted.parms_id()).chain_index()))
    print("    + Modulus chain index for plain_coeff0: {}".format(
        context.get_context_data(plain_coeff0.parms_id()).chain_index()))

    #Let us carefully consider what the scales are at this point. We denote the
    #primes in coeff_modulus as P_0, P_1, P_2, P_3, in this order. P_3 is used as
    #the special modulus and is not involved in rescalings. After the computations
    #above the scales in ciphertexts are:
    #
    #    - Product x^2 has scale 2^80 and is at level 2;
    #    - Product PI*x has scale 2^80 and is at level 2;
    #    - We rescaled both down to scale 2^80/P_2 and level 1;
    #    - Product PI*x^3 has scale (2^80/P_2)^2;
    #    - We rescaled it down to scale (2^80/P_2)^2/P_1 and level 0;
    #    - Product 0.4*x has scale 2^80;
    #    - We rescaled it down to scale 2^80/P_2 and level 1;
    #    - The contant term 1 has scale 2^40 and is at level 2.
    #
    #Although the scales of all three terms are approximately 2^40, their exact
    #values are different, hence they cannot be added together.

    print("The exact scales of all three terms are different:")
    print("    + Exact scale in PI*x^3: {0:0.10f}".format(x3_encrypted.scale()))
    print("    + Exact scale in  0.4*x: {0:0.10f}".format(x1_encrypted.scale()))
    print("    + Exact scale in      1: {0:0.10f}".format(plain_coeff0.scale()))

    #There are many ways to fix this problem. Since P_2 and P_1 are really close
    #to 2^40, we can simply "lie" to Microsoft SEAL and set the scales to be the
    #same. For example, changing the scale of PI*x^3 to 2^40 simply means that we
    #scale the value of PI*x^3 by 2^120/(P_2^2*P_1), which is very close to 1.
    #This should not result in any noticeable error.
    #
    #Another option would be to encode 1 with scale 2^80/P_2, do a multiply_plain
    #with 0.4*x, and finally rescale. In this case we would need to additionally
    #make sure to encode 1 with appropriate encryption parameters (parms_id).
    #
    #In this example we will use the first (simplest) approach and simply change
    #the scale of PI*x^3 and 0.4*x to 2^40.
    print("Normalize scales to 2^40.")
    x3_encrypted.set_scale(2.0**40)
    x1_encrypted.set_scale(2.0**40)

    #We still have a problem with mismatching encryption parameters. This is easy
    #to fix by using traditional modulus switching (no rescaling). CKKS supports
    #modulus switching just like the BFV scheme, allowing us to switch away parts
    #of the coefficient modulus when it is simply not needed.

    print("Normalize encryption parameters to the lowest level.")
    last_parms_id = x3_encrypted.parms_id()
    evaluator.mod_switch_to_inplace(x1_encrypted, last_parms_id)
    evaluator.mod_switch_to_inplace(plain_coeff0, last_parms_id)

    #All three ciphertexts are now compatible and can be added.

    print("Compute PI*x^3 + 0.4*x + 1.")
    encrypted_result = Ciphertext()
    evaluator.add(x3_encrypted, x1_encrypted, encrypted_result)
    evaluator.add_plain_inplace(encrypted_result, plain_coeff0)

    #First print the true result.

    plain_result = Plaintext() 
    print("Decrypt and decode PI*x^3 + 0.4x + 1.")
    print("    + Expected result:")
    true_result = DoubleVector(list(map(lambda x: (3.14159265 * x * x + 0.4)* x + 1, input)))
    print_vector(true_result)

    #Decrypt, decode, and print the result.
    decryptor.decrypt(encrypted_result, plain_result)
    result = DoubleVector()
    encoder.decode(plain_result, result)
    print("    + Computed result ...... Correct.")
    print_vector(result)
class CipherMatrix:
    """

    """
    def __init__(self, matrix=None):
        """

        :param matrix: numpy.ndarray to be encrypted.
        """

        self.parms = EncryptionParameters()
        self.parms.set_poly_modulus("1x^2048 + 1")
        self.parms.set_coeff_modulus(seal.coeff_modulus_128(2048))
        self.parms.set_plain_modulus(1 << 8)

        self.context = SEALContext(self.parms)

        # self.encoder = IntegerEncoder(self.context.plain_modulus())
        self.encoder = FractionalEncoder(self.context.plain_modulus(),
                                         self.context.poly_modulus(), 64, 32,
                                         3)

        self.keygen = KeyGenerator(self.context)
        self.public_key = self.keygen.public_key()
        self.secret_key = self.keygen.secret_key()

        self.encryptor = Encryptor(self.context, self.public_key)
        self.decryptor = Decryptor(self.context, self.secret_key)

        self.evaluator = Evaluator(self.context)

        self._saved = False
        self._encrypted = False
        self._id = '{0:04d}'.format(np.random.randint(1000))

        if matrix is not None:
            assert len(
                matrix.shape) == 2, "Only 2D numpy matrices accepted currently"
            self.matrix = np.copy(matrix)
            self.encrypted_matrix = np.empty(self.matrix.shape, dtype=object)
            for i in range(self.matrix.shape[0]):
                for j in range(self.matrix.shape[1]):
                    self.encrypted_matrix[i, j] = Ciphertext()

        else:
            self.matrix = None
            self.encrypted_matrix = None

        print(self._id, "Created")

    def __repr__(self):
        """

        :return:
        """
        print("Encrypted:", self._encrypted)
        if not self._encrypted:
            print(self.matrix)
            return ""

        else:
            return '[]'

    def __str__(self):
        """

        :return:
        """
        print("| Encryption parameters:")
        print("| poly_modulus: " + self.context.poly_modulus().to_string())

        # Print the size of the true (product) coefficient modulus
        print("| coeff_modulus_size: " + (
            str)(self.context.total_coeff_modulus().significant_bit_count()) +
              " bits")

        print("| plain_modulus: " +
              (str)(self.context.plain_modulus().value()))
        print("| noise_standard_deviation: " +
              (str)(self.context.noise_standard_deviation()))

        if self.matrix is not None:
            print(self.matrix.shape)

        return str(type(self))

    def __add__(self, other):
        """

        :param other:
        :return:
        """
        assert isinstance(
            other, CipherMatrix), "Can only be added with a CipherMatrix"

        A_enc = self._encrypted
        B_enc = other._encrypted

        if A_enc:
            A = self.encrypted_matrix
        else:
            A = self.matrix

        if B_enc:
            B = other.encrypted_matrix
        else:
            B = other.matrix

        assert A.shape == B.shape, "Dimension mismatch, Matrices must be of same shape. Got {} and {}".format(
            A.shape, B.shape)

        shape = A.shape

        result = CipherMatrix(np.zeros(shape, dtype=np.int32))
        result._update_cryptors(self.get_keygen())

        if A_enc:
            if B_enc:

                res_mat = result.encrypted_matrix
                for i in range(shape[0]):
                    for j in range(shape[1]):
                        self.evaluator.add(A[i, j], B[i, j], res_mat[i, j])

                result._encrypted = True

            else:
                res_mat = result.encrypted_matrix
                for i in range(shape[0]):
                    for j in range(shape[1]):
                        self.evaluator.add_plain(A[i, j],
                                                 self.encoder.encode(B[i, j]),
                                                 res_mat[i, j])

                result._encrypted = True

        else:
            if B_enc:

                res_mat = result.encrypted_matrix
                for i in range(shape[0]):
                    for j in range(shape[1]):
                        self.evaluator.add_plain(B[i, j],
                                                 self.encoder.encode(A[i, j]),
                                                 res_mat[i, j])

                result._encrypted = True

            else:

                result.matrix = A + B
                result._encrypted = False

        return result

    def __sub__(self, other):
        """

        :param other:
        :return:
        """
        assert isinstance(other, CipherMatrix)
        if other._encrypted:
            shape = other.encrypted_matrix.shape

            for i in range(shape[0]):
                for j in range(shape[1]):
                    self.evaluator.negate(other.encrypted_matrix[i, j])

        else:
            other.matrix = -1 * other.matrix

        return self + other

    def __mul__(self, other):
        """

        :param other:
        :return:
        """

        assert isinstance(
            other, CipherMatrix), "Can only be multiplied with a CipherMatrix"

        # print("LHS", self._id, "RHS", other._id)
        A_enc = self._encrypted
        B_enc = other._encrypted

        if A_enc:
            A = self.encrypted_matrix
        else:
            A = self.matrix

        if B_enc:
            B = other.encrypted_matrix
        else:
            B = other.matrix

        Ashape = A.shape
        Bshape = B.shape

        assert Ashape[1] == Bshape[0], "Dimensionality mismatch"
        result_shape = [Ashape[0], Bshape[1]]

        result = CipherMatrix(np.zeros(shape=result_shape))

        if A_enc:
            if B_enc:

                for i in range(Ashape[0]):
                    for j in range(Bshape[1]):

                        result_array = []
                        for k in range(Ashape[1]):

                            res = Ciphertext()
                            self.evaluator.multiply(A[i, k], B[k, j], res)

                            result_array.append(res)

                        self.evaluator.add_many(result_array,
                                                result.encrypted_matrix[i, j])

                result._encrypted = True

            else:

                for i in range(Ashape[0]):
                    for j in range(Bshape[1]):

                        result_array = []
                        for k in range(Ashape[1]):
                            res = Ciphertext()
                            self.evaluator.multiply_plain(
                                A[i, k], self.encoder.encode(B[k, j]), res)

                            result_array.append(res)

                        self.evaluator.add_many(result_array,
                                                result.encrypted_matrix[i, j])

                result._encrypted = True

        else:
            if B_enc:

                for i in range(Ashape[0]):
                    for j in range(Bshape[1]):

                        result_array = []
                        for k in range(Ashape[1]):
                            res = Ciphertext()
                            self.evaluator.multiply_plain(
                                B[i, k], self.encoder.encode(A[k, j]), res)

                            result_array.append(res)

                        self.evaluator.add_many(result_array,
                                                result.encrypted_matrix[i, j])

                result._encrypted = True

            else:

                result.matrix = np.matmul(A, B)
                result._encrypted = False

        return result

    def save(self, path):
        """

        :param path:
        :return:
        """

        save_dir = os.path.join(path, self._id)

        if self._saved:
            print("CipherMatrix already saved")

        else:
            assert not os.path.isdir(save_dir), "Directory already exists"
            os.mkdir(save_dir)

        if not self._encrypted:
            self.encrypt()

        shape = self.encrypted_matrix.shape

        for i in range(shape[0]):
            for j in range(shape[1]):

                element_name = str(i) + '-' + str(j) + '.ahem'
                self.encrypted_matrix[i, j].save(
                    os.path.join(save_dir, element_name))

        self.secret_key.save("/keys/" + "." + self._id + '.wheskey')

        self._saved = True
        return save_dir

    def load(self, path, load_secret_key=False):
        """

        :param path:
        :param load_secret_key:
        :return:
        """

        self._id = path.split('/')[-1]
        print("Loading Matrix:", self._id)

        file_list = os.listdir(path)
        index_list = [[file.split('.')[0].split('-'), file]
                      for file in file_list]

        M = int(max([int(ind[0][0]) for ind in index_list])) + 1
        N = int(max([int(ind[0][1]) for ind in index_list])) + 1
        del self.encrypted_matrix
        self.encrypted_matrix = np.empty([M, N], dtype=object)

        for index in index_list:
            i = int(index[0][0])
            j = int(index[0][1])

            self.encrypted_matrix[i, j] = Ciphertext()
            self.encrypted_matrix[i, j].load(os.path.join(path, index[1]))

        if load_secret_key:
            self.secret_key.load("/keys/" + "." + self._id + '.wheskey')

        self.matrix = np.empty(self.encrypted_matrix.shape)
        self._encrypted = True

    def encrypt(self, matrix=None, keygen=None):
        """

        :param matrix:
        :return:
        """

        assert not self._encrypted, "Matrix already encrypted"

        if matrix is not None:
            assert self.matrix is None, "matrix already exists"
            self.matrix = np.copy(matrix)

        shape = self.matrix.shape

        self.encrypted_matrix = np.empty(shape, dtype=object)

        if keygen is not None:
            self._update_cryptors(keygen)

        for i in range(shape[0]):
            for j in range(shape[1]):
                val = self.encoder.encode(self.matrix[i, j])
                self.encrypted_matrix[i, j] = Ciphertext()
                self.encryptor.encrypt(val, self.encrypted_matrix[i, j])

        self._encrypted = True

    def decrypt(self, encrypted_matrix=None, keygen=None):
        """

        :return:
        """

        if encrypted_matrix is not None:
            self.encrypted_matrix = encrypted_matrix

        assert self._encrypted, "No encrypted matrix"

        del self.matrix
        shape = self.encrypted_matrix.shape

        self.matrix = np.empty(shape)

        if keygen is not None:
            self._update_cryptors(keygen)

        for i in range(shape[0]):
            for j in range(shape[1]):
                plain_text = Plaintext()
                self.decryptor.decrypt(self.encrypted_matrix[i, j], plain_text)
                self.matrix[i, j] = self.encoder.decode(plain_text)

        self._encrypted = False
        return np.copy(self.matrix)

    def get_keygen(self):
        """

        :return:
        """
        return self.keygen

    def _update_cryptors(self, keygen):
        """

        :param keygen:
        :return:
        """

        self.keygen = keygen
        self.public_key = keygen.public_key()
        self.secret_key = keygen.secret_key()

        self.encryptor = Encryptor(self.context, self.public_key)
        self.decryptor = Decryptor(self.context, self.secret_key)

        return
    trace_vector.append(trace(matrixPower_vector[i]))

# Vector c is defined as coefficint vector for the charactersitic equation of the matrix
c = [Ciphertext(trace_vector[0])]
evaluator.negate(c[0])

# The following is the implementation of Newton-identities to calculate the value of coeffecients
for i in range(1, n):
    c_new = Ciphertext(trace_vector[i])
    for j in range(i):
        tc = Ciphertext()
        evaluator.multiply(trace_vector[i - 1 - j], c[j], tc)
        evaluator.add(c_new, tc)
    evaluator.negate(c_new)
    frac = encoderF.encode(1 / (i + 1))
    evaluator.multiply_plain(c_new, frac)
    c.append(c_new)

matrixPower_vector = [iden_matrix(n)] + matrixPower_vector
c0 = Ciphertext()
encryptor.encrypt(encoderF.encode(1), c0)
c = [c0] + c

# Adding the matrices multiplie by their coefficients
for i in range(len(matrixPower_vector) - 1):
    for j in range(len(c)):
        if (i + j == n - 1):
            mult(c[j], matrixPower_vector[i])
            for t in range(n):
                for s in range(n):
                    evaluator.add(A_inv[t][s], matrixPower_vector[i][t][s])
Beispiel #6
0
class SealOps:
    @classmethod
    def with_env(cls):
        parms = EncryptionParameters(scheme_type.CKKS)
        parms.set_poly_modulus_degree(POLY_MODULUS_DEGREE)
        parms.set_coeff_modulus(
            CoeffModulus.Create(POLY_MODULUS_DEGREE, PRIME_SIZE_LIST))

        context = SEALContext.Create(parms)

        keygen = KeyGenerator(context)
        public_key = keygen.public_key()
        secret_key = keygen.secret_key()
        relin_keys = keygen.relin_keys()
        galois_keys = keygen.galois_keys()

        return cls(context=context,
                   public_key=public_key,
                   secret_key=secret_key,
                   relin_keys=relin_keys,
                   galois_keys=galois_keys,
                   poly_modulus_degree=POLY_MODULUS_DEGREE,
                   scale=SCALE)

    def __init__(self,
                 context: SEALContext,
                 scale: float,
                 poly_modulus_degree: int,
                 public_key: PublicKey = None,
                 secret_key: SecretKey = None,
                 relin_keys: RelinKeys = None,
                 galois_keys: GaloisKeys = None):
        self.scale = scale
        self.context = context
        self.encoder = CKKSEncoder(context)
        self.evaluator = Evaluator(context)
        self.encryptor = Encryptor(context, public_key)
        self.decryptor = Decryptor(context, secret_key)
        self.relin_keys = relin_keys
        self.galois_keys = galois_keys
        self.poly_modulus_degree_log = np.log2(poly_modulus_degree)

    def encrypt(self, matrix: np.array):
        matrix = Matrix.from_numpy_array(array=matrix)
        cipher_matrix = CipherMatrix(rows=matrix.rows, cols=matrix.cols)

        for i in range(matrix.rows):
            encoded_row = Plaintext()
            self.encoder.encode(matrix[i], self.scale, encoded_row)
            self.encryptor.encrypt(encoded_row, cipher_matrix[i])

        return cipher_matrix

    def decrypt(self, cipher_matrix: CipherMatrix) -> Matrix:
        matrix = Matrix(rows=cipher_matrix.rows, cols=cipher_matrix.cols)

        for i in range(matrix.rows):
            row = Vector()
            encoded_row = Plaintext()
            self.decryptor.decrypt(cipher_matrix[i], encoded_row)
            self.encoder.decode(encoded_row, row)
            matrix[i] = row

        return matrix

    def add(self, matrix_a: CipherMatrix,
            matrix_b: CipherMatrix) -> CipherMatrix:
        self.validate_same_dimension(matrix_a, matrix_b)

        result_matrix = CipherMatrix(rows=matrix_a.rows, cols=matrix_a.cols)
        for i in range(matrix_a.rows):
            a_tag, b_tag = self.get_matched_scale_vectors(
                matrix_a[i], matrix_b[i])
            self.evaluator.add(a_tag, b_tag, result_matrix[i])

        return result_matrix

    def add_plain(self, matrix_a: CipherMatrix,
                  matrix_b: np.array) -> CipherMatrix:
        matrix_b = Matrix.from_numpy_array(matrix_b)
        self.validate_same_dimension(matrix_a, matrix_b)

        result_matrix = CipherMatrix(rows=matrix_a.rows, cols=matrix_a.cols)

        for i in range(matrix_a.rows):
            row = matrix_b[i]
            encoded_row = Plaintext()
            self.encoder.encode(row, self.scale, encoded_row)
            self.evaluator.mod_switch_to_inplace(encoded_row,
                                                 matrix_a[i].parms_id())
            self.evaluator.add_plain(matrix_a[i], encoded_row,
                                     result_matrix[i])

        return result_matrix

    def multiply_plain(self, matrix_a: CipherMatrix,
                       matrix_b: np.array) -> CipherMatrix:
        matrix_b = Matrix.from_numpy_array(matrix_b)
        self.validate_same_dimension(matrix_a, matrix_b)

        result_matrix = CipherMatrix(rows=matrix_a.rows, cols=matrix_a.cols)

        for i in range(matrix_a.rows):
            row = matrix_b[i]
            encoded_row = Plaintext()
            self.encoder.encode(row, self.scale, encoded_row)
            self.evaluator.mod_switch_to_inplace(encoded_row,
                                                 matrix_a[i].parms_id())
            self.evaluator.multiply_plain(matrix_a[i], encoded_row,
                                          result_matrix[i])

        return result_matrix

    def dot_vector(self, a: Ciphertext, b: Ciphertext) -> Ciphertext:
        result = Ciphertext()

        self.evaluator.multiply(a, b, result)
        self.evaluator.relinearize_inplace(result, self.relin_keys)
        self.vector_sum_inplace(result)
        self.get_vector_first_element(result)
        self.evaluator.rescale_to_next_inplace(result)

        return result

    def dot_vector_with_plain(self, a: Ciphertext,
                              b: DoubleVector) -> Ciphertext:
        result = Ciphertext()

        b_plain = Plaintext()
        self.encoder.encode(b, self.scale, b_plain)

        self.evaluator.multiply_plain(a, b_plain, result)
        self.vector_sum_inplace(result)
        self.get_vector_first_element(result)

        self.evaluator.rescale_to_next_inplace(result)

        return result

    def get_vector_range(self, vector_a: Ciphertext, i: int,
                         j: int) -> Ciphertext:
        cipher_range = Ciphertext()

        one_and_zeros = DoubleVector([0.0 if x < i else 1.0 for x in range(j)])
        plain = Plaintext()
        self.encoder.encode(one_and_zeros, self.scale, plain)
        self.evaluator.mod_switch_to_inplace(plain, vector_a.parms_id())
        self.evaluator.multiply_plain(vector_a, plain, cipher_range)

        return cipher_range

    def dot_matrix_with_matrix_transpose(self, matrix_a: CipherMatrix,
                                         matrix_b: CipherMatrix):
        result_matrix = CipherMatrix(rows=matrix_a.rows, cols=matrix_a.cols)

        rows_a = matrix_a.rows
        cols_b = matrix_b.rows

        for i in range(rows_a):
            vector_dot_products = []
            zeros = Plaintext()

            for j in range(cols_b):
                vector_dot_products += [
                    self.dot_vector(matrix_a[i], matrix_b[j])
                ]

                if j == 0:
                    zero = DoubleVector()
                    self.encoder.encode(zero, vector_dot_products[j].scale(),
                                        zeros)
                    self.evaluator.mod_switch_to_inplace(
                        zeros, vector_dot_products[j].parms_id())
                    self.evaluator.add_plain(vector_dot_products[j], zeros,
                                             result_matrix[i])
                else:
                    self.evaluator.rotate_vector_inplace(
                        vector_dot_products[j], -j, self.galois_keys)
                    self.evaluator.add_inplace(result_matrix[i],
                                               vector_dot_products[j])

        for vec in result_matrix:
            self.evaluator.relinearize_inplace(vec, self.relin_keys)
            self.evaluator.rescale_to_next_inplace(vec)

        return result_matrix

    def dot_matrix_with_plain_matrix_transpose(self, matrix_a: CipherMatrix,
                                               matrix_b: np.array):
        matrix_b = Matrix.from_numpy_array(matrix_b)
        result_matrix = CipherMatrix(rows=matrix_a.rows, cols=matrix_a.cols)

        rows_a = matrix_a.rows
        cols_b = matrix_b.rows

        for i in range(rows_a):
            vector_dot_products = []
            zeros = Plaintext()

            for j in range(cols_b):
                vector_dot_products += [
                    self.dot_vector_with_plain(matrix_a[i], matrix_b[j])
                ]

                if j == 0:
                    zero = DoubleVector()
                    self.encoder.encode(zero, vector_dot_products[j].scale(),
                                        zeros)
                    self.evaluator.mod_switch_to_inplace(
                        zeros, vector_dot_products[j].parms_id())
                    self.evaluator.add_plain(vector_dot_products[j], zeros,
                                             result_matrix[i])
                else:
                    self.evaluator.rotate_vector_inplace(
                        vector_dot_products[j], -j, self.galois_keys)
                    self.evaluator.add_inplace(result_matrix[i],
                                               vector_dot_products[j])

        for vec in result_matrix:
            self.evaluator.relinearize_inplace(vec, self.relin_keys)
            self.evaluator.rescale_to_next_inplace(vec)

        return result_matrix

    @staticmethod
    def validate_same_dimension(matrix_a, matrix_b):
        if matrix_a.rows != matrix_b.rows or matrix_a.cols != matrix_b.cols:
            raise ArithmeticError("Matrices aren't of the same dimension")

    def vector_sum_inplace(self, cipher: Ciphertext):
        rotated = Ciphertext()

        for i in range(int(self.poly_modulus_degree_log - 1)):
            self.evaluator.rotate_vector(cipher, pow(2, i), self.galois_keys,
                                         rotated)
            self.evaluator.add_inplace(cipher, rotated)

    def get_vector_first_element(self, cipher: Ciphertext):
        one_and_zeros = DoubleVector([1.0])
        plain = Plaintext()
        self.encoder.encode(one_and_zeros, self.scale, plain)
        self.evaluator.multiply_plain_inplace(cipher, plain)

    def get_matched_scale_vectors(self, a: Ciphertext,
                                  b: Ciphertext) -> (Ciphertext, Ciphertext):
        a_tag = Ciphertext(a)
        b_tag = Ciphertext(b)

        a_index = self.context.get_context_data(a.parms_id()).chain_index()
        b_index = self.context.get_context_data(b.parms_id()).chain_index()

        # Changing the mod if required, else just setting the scale
        if a_index < b_index:
            self.evaluator.mod_switch_to_inplace(b_tag, a.parms_id())

        elif a_index > b_index:
            self.evaluator.mod_switch_to_inplace(a_tag, b.parms_id())

        a_tag.set_scale(self.scale)
        b_tag.set_scale(self.scale)

        return a_tag, b_tag
Beispiel #7
0
class FHECryptoEngine(CryptoEngine):
    def __init__(self):
        CryptoEngine.__init__(self, defs.ENC_MODE_FHE)
        self.log_id = 'FHECryptoEngine'
        self.encrypt_params = None
        self.context = None
        self.encryptor = None
        self.evaluator = None
        self.decryptor = None

        return

    def load_keys(self):
        self.private_key = SecretKey()
        self.private_key.load(defs.FN_FHE_PRIVATE_KEY)

        self.public_key = PublicKey()
        self.public_key.load(defs.FN_FHE_PUBLIC_KEY)
        
        return True

    def generate_keys(self):
        if self.encrypt_params == None or \
           self.context == None:
            self.init_encrypt_params()

        keygen = KeyGenerator(self.context)

        # Generate the private key
        self.private_key = keygen.secret_key()
        self.private_key.save(defs.FN_FHE_PRIVATE_KEY)

        # Generate the public key
        self.public_key = keygen.public_key()
        self.public_key.save(defs.FN_FHE_PUBLIC_KEY)

        return True

    def init_encrypt_params(self):
        self.encrypt_params = EncryptionParameters()
        self.encrypt_params.set_poly_modulus("1x^2048 + 1")
        self.encrypt_params.set_coeff_modulus(seal.coeff_modulus_128(2048))
        self.encrypt_params.set_plain_modulus(1 << 8)

        self.context = SEALContext(self.encrypt_params)

        return

    def initialize(self, use_old_keys=False):
        # Initialize encryption params
        self.init_encrypt_params()

        # Check if the public & private key files exist
        if os.path.isfile(defs.FN_FHE_PUBLIC_KEY) and \
           os.path.isfile(defs.FN_FHE_PRIVATE_KEY) and \
           use_old_keys == True:

            self.log("Keys already exist. Reusing them instead.")
            if not self.load_keys():
                self.log("Failed to load keys")
                return False

        else:
            # If not, then attempt to generate new ones
            if not self.generate_keys():
                self.log("Failed to generate keys")
                return False

        # Setup the rest of the crypto engine
        self.encryptor = Encryptor(self.context, self.public_key)
        self.evaluator = Evaluator(self.context)
        self.decryptor = Decryptor(self.context, self.private_key)

        # Set the initialized flag
        self.initialized = True

        return True

    def encrypt(self, data):
        if not self.initialized:
            self.log("Not initialized")
            return False

        # Setup the encoder
        encoder = FractionalEncoder(self.context.plain_modulus(), self.context.poly_modulus(), 64, 32, 3)

        # Create the array of encrypted data objects
        encrypted_data = []
        for raw_data in data:
            encrypted_data.append(Ciphertext(self.encrypt_params))
            self.encryptor.encrypt( encoder.encode(raw_data), encrypted_data[-1] )

        # Pickle each Ciphertext, base64 encode it, and store it in the array
        for i in range(0, len(encrypted_data)):
            encrypted_data[i].save("fhe_enc.bin")
            encrypted_data[i] = base64.b64encode( pickle.dumps(encrypted_data[i]) )

        return encrypted_data

    def evaluate(self, encrypted_data, lower_idx=0, higher_idx=-1):
        result = Ciphertext()

        # Setup the encoder
        encoder = FractionalEncoder(self.context.plain_modulus(), self.context.poly_modulus(), 64, 32, 3)

        # Unpack the data first
        unpacked_data = []
        for d in encrypted_data[lower_idx:higher_idx]:
            unpacked_data.append(pickle.loads(base64.b64decode(d)))

        # Perform operations
        self.evaluator.add_many(unpacked_data, result)
        div = encoder.encode(1/len(unpacked_data))
        self.evaluator.multiply_plain(result, div)

        # Pack the result
        result = base64.b64encode( pickle.dumps(result) )

        return result

    def decrypt(self, raw_data):
        if not self.initialized:
            self.log("Not initialized")
            return False

        # Setup the encoder
        encoder = FractionalEncoder(self.context.plain_modulus(), self.context.poly_modulus(), 64, 32, 3)
        
        # Unpickle, base64 decode, and decrypt each ciphertext result
        decrypted_data = []
        for d in raw_data:
            encrypted_data = pickle.loads( base64.b64decode(d) )
            plain_data = Plaintext()
            self.decryptor.decrypt(encrypted_data, plain_data)
            decrypted_data.append( str(encoder.decode(plain_data)) )

        return decrypted_data