def processMessage(self): ''' Fetches a message from the queue and computes encrypted operations to get the generation / new grid state and pushes new encrypted grid state in the queue to the client :return: ''' print("[SERVER SIMULATION] Start fetching message") # fetch Message from queue, message is a dictionary with structure # {"encrypted_old_grid": encrypted_old_grid, "encrypted_live_neighbours_grid": encrypted_live_neighbours_grid} message_from_client = self.client_to_server_queue.get(0) print("[SERVER SIMULATION] In Queue", message_from_client) encrypted_old_grid = message_from_client["encrypted_old_grid"] encrypted_live_neighbours_grid = message_from_client[ "encrypted_live_neighbours_grid"] # Pyseal Evaluator evaluator = Evaluator(self.encryptionContext) # Computation of new grid encrypted_new_grid = [] for i in range(self.N * self.N): encrypted_result = Ciphertext() evaluator.add(encrypted_old_grid[i], encrypted_live_neighbours_grid[i], encrypted_result) encrypted_new_grid.append(encrypted_result) print("[SERVER SIMULATION] New Grid Computed") print("[SERVER SIMULATION] Message to client ", encrypted_new_grid) self.server_to_client_queue.put(encrypted_new_grid) print( "[SERVER SIMULATION] Put computed grid into server to client queue" )
keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) value=7 plain1 = encoder.encode(value1) print("Encoded " + (str)(value) + " as polynomial " + plain1.to_string() + " (plain1)") encrypted _data= Ciphertext() encryptor.encrypt(plain, encrypted_data) print("Noise budget in encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted_data)) + " bits") # operations that can be performed ---> # result stored in encrypted1 data evaluator.negate(encrypted1_data) # result stored in encrypted1 data, encrpyted1 is modified evaluator.add(encrypted1_data, encrypted2_data) # result stored in encrypted1 data, encrpyted1 is modified evaluator.multiply(encrypted1_data, encrypted2_data) plain_result = Plaintext() decryptor.decrypt(encrypted_data, plain_result) print("Plaintext polynomial: " + plain_result.to_string()) print("Decoded integer: " + (str)(encoder.decode_int32(plain_result)))
def pickle_ciphertext(): 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 the parameters that we have chosen print_parameters(context); encoder = IntegerEncoder(context.plain_modulus()) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() # To be able to encrypt, we need to construct an instance of Encryptor. Note that # the Encryptor only requires the public key. encryptor = Encryptor(context, public_key) # Computations on the ciphertexts are performed with the Evaluator class. evaluator = Evaluator(context) # We will of course want to decrypt our results to verify that everything worked, # so we need to also construct an instance of Decryptor. Note that the Decryptor # requires the secret key. decryptor = Decryptor(context, secret_key) # We start by encoding two integers as plaintext polynomials. value1 = 5; plain1 = encoder.encode(value1); print("Encoded " + (str)(value1) + " as polynomial " + plain1.to_string() + " (plain1)") value2 = -7; plain2 = encoder.encode(value2); print("Encoded " + (str)(value2) + " as polynomial " + plain2.to_string() + " (plain2)") # Encrypting the values is easy. encrypted1 = Ciphertext() encrypted2 = Ciphertext() print("Encrypting plain1: ", encrypted1) encryptor.encrypt(plain1, encrypted1) print("Done (encrypted1)", encrypted1) print("Encrypting plain2: ") encryptor.encrypt(plain2, encrypted2) print("Done (encrypted2)") # output = open('ciphertest.pkl', 'wb') # dill.dumps(encrypted_save, output) # output.close() # encrypted1 = dill.load(open('ciphertest.pkl', 'rb')) output = open('session.pkl', 'wb') dill.dump_session('session.pkl') del encrypted1 sill.load_session('session.pkl') # To illustrate the concept of noise budget, we print the budgets in the fresh # encryptions. print("Noise budget in encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") print("Noise budget in encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted2)) + " bits") # As a simple example, we compute (-encrypted1 + encrypted2) * encrypted2. # Negation is a unary operation. evaluator.negate(encrypted1) # Negation does not consume any noise budget. print("Noise budget in -encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") # Addition can be done in-place (overwriting the first argument with the result, # or alternatively a three-argument overload with a separate destination variable # can be used. The in-place variants are always more efficient. Here we overwrite # encrypted1 with the sum. evaluator.add(encrypted1, encrypted2) # It is instructive to think that addition sets the noise budget to the minimum # of the input noise budgets. In this case both inputs had roughly the same # budget going on, and the output (in encrypted1) has just slightly lower budget. # Depending on probabilistic effects, the noise growth consumption may or may # not be visible when measured in whole bits. print("Noise budget in -encrypted1 + encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") # Finally multiply with encrypted2. Again, we use the in-place version of the # function, overwriting encrypted1 with the product. evaluator.multiply(encrypted1, encrypted2) # Multiplication consumes a lot of noise budget. This is clearly seen in the # print-out. The user can change the plain_modulus to see its effect on the # rate of noise budget consumption. print("Noise budget in (-encrypted1 + encrypted2) * encrypted2: " + (str)( decryptor.invariant_noise_budget(encrypted1)) + " bits") # Now we decrypt and decode our result. plain_result = Plaintext() print("Decrypting result: ") decryptor.decrypt(encrypted1, plain_result) print("Done") # Print the result plaintext polynomial. print("Plaintext polynomial: " + plain_result.to_string()) # Decode to obtain an integer result. print("Decoded integer: " + (str)(encoder.decode_int32(plain_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
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)
decryptor = Decryptor(context, secret_key) for i in range(len(A)): A_plain.append(encoder.encode(A[i])) A_cipherObject.append(Ciphertext()) encryptor.encrypt(A_plain[i],A_cipherObject[i]) print("Noise budget of "+ str(i)+" "+str((decryptor.invariant_noise_budget(A_cipherObject[i]))) + " bits") A_cipherObject=chunk(A_cipherObject) C=A_cipherObject #shallow copy # partial pivoting for i in range(3,-1,-1): evaluator.negate(C[i][0]) evaluator.add(C[i][0], C[i+1][0]) plain_result = Plaintext() decryptor.decrypt(C[i][0], plain_result) if (int(encoder.decode_int32(plain_result))>0): for j in range(8): # add code to combine appended matrix and normal matrix together D=A_cipherObject #shallow copy # reducing to diagnol matrix for i in range(4): for j in range (8): if (j!=i): plain_result = Plaintext() X=D[i][i]
# Also creates a vector trace_vector which contains trace of matrix A, A^2 ... A^(n-1) for i in range(1, n): matrixPower_vector.append(raise_power(matrixPower_vector[i - 1])) 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])
class PySeal: evaluator: Evaluator parms: EncryptionParameters def __init__(self, seal_server_params, cipher_save_path): self.parms = EncryptionParameters(scheme_type.CKKS) self.cipher_save_path = cipher_save_path # Set Coeff modulus coeff_modulus = [ SmallModulus(i) for i in seal_server_params["coeff_modulus"] ] self.parms.set_coeff_modulus(coeff_modulus) # Set Plain modulus self.parms.set_plain_modulus(seal_server_params["plain_modulus"]) # Set Poly modulus degree self.parms.set_poly_modulus_degree( seal_server_params["poly_modulus_degree"]) self.context = SEALContext.Create(self.parms) self.evaluator = Evaluator(self.context) # Store the encrypted here self._encrypts = [] def get_param_info(self): res = { "scheme": self.get_scheme_name(self.parms.scheme()), "poly_modulus_degree": self.parms.poly_modulus_degree(), "coeff_modulus": [i.value() for i in self.parms.coeff_modulus()], "coeff_modulus_size": [i.bit_count() for i in self.parms.coeff_modulus()], "plain_modulus": self.parms.plain_modulus().value(), } return res @staticmethod def get_scheme_name(scheme): if scheme == scheme_type.BFV: scheme_name = "BFV" elif scheme == scheme_type.CKKS: scheme_name = "CKKS" else: scheme_name = "unsupported scheme" return scheme_name @classmethod def print_parameters(cls, context): context_data = context.key_context_data() scheme_name = cls.get_scheme_name(context_data.parms().scheme()) print("/") print("| Encryption parameters:") print("| scheme: " + scheme_name) print("| poly_modulus_degree: " + str(context_data.parms().poly_modulus_degree())) print("| coeff_modulus size: ", end="") coeff_modulus = context_data.parms().coeff_modulus() coeff_modulus_sum = 0 for j in coeff_modulus: coeff_modulus_sum += j.bit_count() print(str(coeff_modulus_sum) + "(", end="") for i in range(len(coeff_modulus) - 1): print(str(coeff_modulus[i].bit_count()) + " + ", end="") print(str(coeff_modulus[-1].bit_count()) + ") bits") if context_data.parms().scheme() == scheme_type.BFV: print("| plain_modulus: " + str(context_data.parms().plain_modulus().value())) print("\\") def save_cipher_and_encode64(self, encrypted_object: Ciphertext): encrypted_object.save(self.cipher_save_path) with open(self.cipher_save_path, 'rb') as f: encrypted_text = f.read() # print(encrypted_text) res = base64.b64encode(encrypted_text) os.remove(self.cipher_save_path) # logging.debug("Resulting call type: {}".format(type(res))) # logging.debug(res[-10:]) return res.decode('utf-8') def decode64_and_get_cipher_object(self, ins: str): decoded = base64.b64decode(ins.encode('utf-8')) with open(self.cipher_save_path, 'wb') as f: f.write(decoded) cipher_object = Ciphertext() cipher_object.load(self.context, self.cipher_save_path) os.remove(self.cipher_save_path) return cipher_object def save_encrypted_weight(self, weight): self._encrypts.append(weight) def aggregate_encrypted_weights(self): if len(self._encrypts) == 0: return [], 0 weight = self._encrypts[0] num_party = len(self._encrypts) if num_party > 1: # Iterate through layers for layer_idx in range(len(weight)): # Iterate through layer partitions for layer_partition_idx in range(len(weight[layer_idx])): agg_layer_partition_weights = [] # Iterate through results from workers for worker_result in range(num_party): cipher_object = self.decode64_and_get_cipher_object( self._encrypts[worker_result][layer_idx] [layer_partition_idx]["encrypted_content"]) agg_layer_partition_weights.append(cipher_object) # print(agg_layer_weights) res = self.add_encrypted_matrix( *agg_layer_partition_weights) weight[layer_idx][layer_partition_idx][ "encrypted_content"] = self.save_cipher_and_encode64( res) # print("weight agg:", weight[layer_idx]) del self._encrypts[:num_party] return weight, num_party def add_encrypted_matrix(self, *argv): inputs = [] for arg in argv: inputs.append(arg) if len(inputs) <= 1: # Case 1: If there is only one input matrix return inputs[0] # Case 2: If there are at least 2 inputs encrypted_result = Ciphertext() self.evaluator.add(inputs[0], inputs[1], encrypted_result) for i in range(2, len(inputs)): self.evaluator.add_inplace(encrypted_result, inputs[i]) res = encrypted_result return res
# encryptions. print("Noise budget in encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") print("Noise budget in encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted2)) + " bits") # As a simple example, we compute (-encrypted1 + encrypted2) * encrypted2. # Negation is a unary operation. evaluator.negate(encrypted1) # Negation does not consume any noise budget. print("Noise budget in -encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") evaluator.add(encrypted1, encrypted2) print("Noise budget in -encrypted1 + encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") #evaluator.multiply(encrypted1, encrypted2) #print("Noise budget in (-encrypted1 + encrypted2) * encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") # Now we decrypt and decode our result. plain_result = Plaintext() print("Decrypting result: ") decryptor.decrypt(encrypted1, plain_result) print("Done") # Print the result plaintext polynomial.
def example_basics_i(): print_example_banner("Example: Basics I") # In this example we demonstrate setting up encryption parameters and other # relevant objects for performing simple computations on encrypted integers. # SEAL uses the Fan-Vercauteren (FV) homomorphic encryption scheme. We refer to # https://eprint.iacr.org/2012/144 for full details on how the FV scheme works. # For better performance, SEAL implements the "FullRNS" optimization of FV, as # described in https://eprint.iacr.org/2016/510. # The first task is to set up an instance of the EncryptionParameters class. # It is critical to understand how these different parameters behave, how they # affect the encryption scheme, performance, and the security level. There are # three encryption parameters that are necessary to set: # - poly_modulus (polynomial modulus); # - coeff_modulus ([ciphertext] coefficient modulus); # - plain_modulus (plaintext modulus). # A fourth parameter -- noise_standard_deviation -- has a default value of 3.19 # and should not be necessary to modify unless the user has a specific reason # to and knows what they are doing. # The encryption scheme implemented in SEAL cannot perform arbitrary computations # on encrypted data. Instead, each ciphertext has a specific quantity called the # `invariant noise budget' -- or `noise budget' for short -- measured in bits. # The noise budget of a freshly encrypted ciphertext (initial noise budget) is # determined by the encryption parameters. Homomorphic operations consume the # noise budget at a rate also determined by the encryption parameters. In SEAL # the two basic homomorphic operations are additions and multiplications, of # which additions can generally be thought of as being nearly free in terms of # noise budget consumption compared to multiplications. Since noise budget # consumption is compounding in sequential multiplications, the most significant # factor in choosing appropriate encryption parameters is the multiplicative # depth of the arithmetic circuit that needs to be evaluated. Once the noise # budget in a ciphertext reaches zero, it becomes too corrupted to be decrypted. # Thus, it is essential to choose the parameters to be large enough to support # the desired computation; otherwise the result is impossible to make sense of # even with the secret key. parms = EncryptionParameters() # We first set the polynomial modulus. This must be a power-of-2 cyclotomic # polynomial, i.e. a polynomial of the form "1x^(power-of-2) + 1". The polynomial # modulus should be thought of mainly affecting the security level of the scheme; # larger polynomial modulus makes the scheme more secure. At the same time, it # makes ciphertext sizes larger, and consequently all operations slower. # Recommended degrees for poly_modulus are 1024, 2048, 4096, 8192, 16384, 32768, # but it is also possible to go beyond this. Since we perform only a very small # computation in this example, it suffices to use a very small polynomial modulus parms.set_poly_modulus("1x^2048 + 1") # Next we choose the [ciphertext] coefficient modulus (coeff_modulus). The size # of the coefficient modulus should be thought of as the most significant factor # in determining the noise budget in a freshly encrypted ciphertext: bigger means # more noise budget. Unfortunately, a larger coefficient modulus also lowers the # security level of the scheme. Thus, if a large noise budget is required for # complicated computations, a large coefficient modulus needs to be used, and the # reduction in the security level must be countered by simultaneously increasing # the polynomial modulus. # To make parameter selection easier for the user, we have constructed sets of # largest allowed coefficient moduli for 128-bit and 192-bit security levels # for different choices of the polynomial modulus. These recommended parameters # follow the Security white paper at http://HomomorphicEncryption.org. However, # due to the complexity of this topic, we highly recommend the user to directly # consult an expert in homomorphic encryption and RLWE-based encryption schemes # to determine the security of their parameter choices. # Our recommended values for the coefficient modulus can be easily accessed # through the functions # coeff_modulus_128bit(int) # coeff_modulus_192bit(int) # for 128-bit and 192-bit security levels. The integer parameter is the degree # of the polynomial modulus. # In SEAL the coefficient modulus is a positive composite number -- a product # of distinct primes of size up to 60 bits. When we talk about the size of the # coefficient modulus we mean the bit length of the product of the small primes. # The small primes are represented by instances of the SmallModulus class; for # example coeff_modulus_128bit(int) returns a vector of SmallModulus instances. # It is possible for the user to select their own small primes. Since SEAL uses # the Number Theoretic Transform (NTT) for polynomial multiplications modulo the # factors of the coefficient modulus, the factors need to be prime numbers # congruent to 1 modulo 2*degree(poly_modulus). We have generated a list of such # prime numbers of various sizes, that the user can easily access through the # functions # small_mods_60bit(int) # small_mods_50bit(int) # small_mods_40bit(int) # small_mods_30bit(int) # each of which gives access to an array of primes of the denoted size. These # primes are located in the source file util/globals.cpp. # Performance is mainly affected by the size of the polynomial modulus, and the # number of prime factors in the coefficient modulus. Thus, it is important to # use as few factors in the coefficient modulus as possible. # In this example we use the default coefficient modulus for a 128-bit security # level. Concretely, this coefficient modulus consists of only one 56-bit prime # factor: 0xfffffffff00001. parms.set_coeff_modulus(seal.coeff_modulus_128(2048)) # The plaintext modulus can be any positive integer, even though here we take # it to be a power of two. In fact, in many cases one might instead want it to # be a prime number; we will see this in example_batching(). The plaintext # modulus determines the size of the plaintext data type, but it also affects # the noise budget in a freshly encrypted ciphertext, and the consumption of # the noise budget in homomorphic multiplication. Thus, it is essential to try # to keep the plaintext data type as small as possible for good performance. # The noise budget in a freshly encrypted ciphertext is # ~ log2(coeff_modulus/plain_modulus) (bits) # and the noise budget consumption in a homomorphic multiplication is of the # form log2(plain_modulus) + (other terms). parms.set_plain_modulus(1 << 8) # Now that all parameters are set, we are ready to construct a SEALContext # object. This is a heavy class that checks the validity and properties of # the parameters we just set, and performs and stores several important # pre-computations. context = SEALContext(parms) # Print the parameters that we have chosen print_parameters(context) # Plaintexts in the FV scheme are polynomials with coefficients integers modulo # plain_modulus. To encrypt for example integers instead, one can use an # `encoding scheme' to represent the integers as such polynomials. SEAL comes # with a few basic encoders: # [IntegerEncoder] # Given an integer base b, encodes integers as plaintext polynomials as follows. # First, a base-b expansion of the integer is computed. This expansion uses # a `balanced' set of representatives of integers modulo b as the coefficients. # Namely, when b is odd the coefficients are integers between -(b-1)/2 and # (b-1)/2. When b is even, the integers are between -b/2 and (b-1)/2, except # when b is two and the usual binary expansion is used (coefficients 0 and 1). # Decoding amounts to evaluating the polynomial at x=b. For example, if b=2, # the integer # 26 = 2^4 + 2^3 + 2^1 # is encoded as the polynomial 1x^4 + 1x^3 + 1x^1. When b=3, # 26 = 3^3 - 3^0 # is encoded as the polynomial 1x^3 - 1. In memory polynomial coefficients are # always stored as unsigned integers by storing their smallest non-negative # representatives modulo plain_modulus. To create a base-b integer encoder, # use the constructor IntegerEncoder(plain_modulus, b). If no b is given, b=2 # is used. # [FractionalEncoder] # The FractionalEncoder encodes fixed-precision rational numbers as follows. # It expands the number in a given base b, possibly truncating an infinite # fractional part to finite precision, e.g. # 26.75 = 2^4 + 2^3 + 2^1 + 2^(-1) + 2^(-2) # when b=2. For the sake of the example, suppose poly_modulus is 1x^1024 + 1. # It then represents the integer part of the number in the same way as in # IntegerEncoder (with b=2 here), and moves the fractional part instead to the # highest degree part of the polynomial, but with signs of the coefficients # changed. In this example we would represent 26.75 as the polynomial # -1x^1023 - 1x^1022 + 1x^4 + 1x^3 + 1x^1. # In memory the negative coefficients of the polynomial will be represented as # their negatives modulo plain_modulus. # [PolyCRTBuilder] # If plain_modulus is a prime congruent to 1 modulo 2*degree(poly_modulus), the # plaintext elements can be viewed as 2-by-(degree(poly_modulus) / 2) matrices # with elements integers modulo plain_modulus. When a desired computation can be # vectorized, using PolyCRTBuilder can result in massive performance improvements # over naively encrypting and operating on each input number separately. Thus, # in more complicated computations this is likely to be by far the most important # and useful encoder. In example_batching() we show how to use and operate on # encrypted matrix plaintexts. # For performance reasons, in homomorphic encryption one typically wants to keep # the plaintext data types as small as possible, which can make it challenging to # prevent data type overflow in more complicated computations, especially when # operating on rational numbers that have been scaled to integers. When using # PolyCRTBuilder estimating whether an overflow occurs is a fairly standard task, # as the matrix slots are integers modulo plain_modulus, and each slot is operated # on independently of the others. When using IntegerEncoder or FractionalEncoder # it is substantially more difficult to estimate when an overflow occurs in the # plaintext, and choosing the plaintext modulus very carefully to be large enough # is critical to avoid unexpected results. Specifically, one needs to estimate how # large the largest coefficient in the polynomial view of all of the plaintext # elements becomes, and choose the plaintext modulus to be larger than this value. # SEAL comes with an automatic parameter selection tool that can help with this # task, as is demonstrated in example_parameter_selection(). # Here we choose to create an IntegerEncoder with base b=2. encoder = IntegerEncoder(context.plain_modulus()) # We are now ready to generate the secret and public keys. For this purpose we need # an instance of the KeyGenerator class. Constructing a KeyGenerator automatically # generates the public and secret key, which can then be read to local variables. # To create a fresh pair of keys one can call KeyGenerator::generate() at any time. keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() # To be able to encrypt, we need to construct an instance of Encryptor. Note that # the Encryptor only requires the public key. encryptor = Encryptor(context, public_key) # Computations on the ciphertexts are performed with the Evaluator class. evaluator = Evaluator(context) # We will of course want to decrypt our results to verify that everything worked, # so we need to also construct an instance of Decryptor. Note that the Decryptor # requires the secret key. decryptor = Decryptor(context, secret_key) # We start by encoding two integers as plaintext polynomials. value1 = 5 plain1 = encoder.encode(value1) print("Encoded " + (str)(value1) + " as polynomial " + plain1.to_string() + " (plain1)") value2 = -7 plain2 = encoder.encode(value2) print("Encoded " + (str)(value2) + " as polynomial " + plain2.to_string() + " (plain2)") # Encrypting the values is easy. encrypted1 = Ciphertext() encrypted2 = Ciphertext() print("Encrypting plain1: ") encryptor.encrypt(plain1, encrypted1) print("Done (encrypted1)") print("Encrypting plain2: ") encryptor.encrypt(plain2, encrypted2) print("Done (encrypted2)") # To illustrate the concept of noise budget, we print the budgets in the fresh # encryptions. print("Noise budget in encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") print("Noise budget in encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted2)) + " bits") # As a simple example, we compute (-encrypted1 + encrypted2) * encrypted2. # Negation is a unary operation. evaluator.negate(encrypted1) # Negation does not consume any noise budget. print("Noise budget in -encrypted1: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") # Addition can be done in-place (overwriting the first argument with the result, # or alternatively a three-argument overload with a separate destination variable # can be used. The in-place variants are always more efficient. Here we overwrite # encrypted1 with the sum. evaluator.add(encrypted1, encrypted2) # It is instructive to think that addition sets the noise budget to the minimum # of the input noise budgets. In this case both inputs had roughly the same # budget going on, and the output (in encrypted1) has just slightly lower budget. # Depending on probabilistic effects, the noise growth consumption may or may # not be visible when measured in whole bits. print("Noise budget in -encrypted1 + encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") # Finally multiply with encrypted2. Again, we use the in-place version of the # function, overwriting encrypted1 with the product. evaluator.multiply(encrypted1, encrypted2) # Multiplication consumes a lot of noise budget. This is clearly seen in the # print-out. The user can change the plain_modulus to see its effect on the # rate of noise budget consumption. print("Noise budget in (-encrypted1 + encrypted2) * encrypted2: " + (str)(decryptor.invariant_noise_budget(encrypted1)) + " bits") # Now we decrypt and decode our result. plain_result = Plaintext() print("Decrypting result: ") decryptor.decrypt(encrypted1, plain_result) print("Done") # Print the result plaintext polynomial. print("Plaintext polynomial: " + plain_result.to_string()) # Decode to obtain an integer result. print("Decoded integer: " + (str)(encoder.decode_int32(plain_result)))
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
parms.set_coeff_modulus(seal.coeff_modulus_128(8192)) parms.set_plain_modulus(1 << 21) context = SEALContext(parms) encoderF = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 30, 34, 3) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) ########################## encoding main matrix ################################ A = [Ciphertext(), Ciphertext(), Ciphertext(), Ciphertext()] for i in range(len(A)): encryptor.encrypt(encoderF.encode(i), A[i]) for j in range(10): evaluator.multiply(A[0], A[1]) evaluator.multiply(A[0], A[2]) evaluator.add(A[1], A[2]) for i in range(len(A)): print("Noise budget of [" + str(i) + "] :" + str((decryptor.invariant_noise_budget(A[i]))) + " bits") print("A[%d]: " % (i), ) print_value(A[i])