def __init__(self, poly_modulus="1x^1024 + 1", coef_modulus_n_primes=20, plain_modulus=1 << 32): """ Set up encryption context for encoder and decoder :param poly_modulus: :param coef_modulus_n_primes: :param plain_modulus: """ self.params = EncryptionParameters() self.params.set_poly_modulus(poly_modulus) self.params.set_coeff_modulus([ seal.SmallModulus(p) for p in FracContext.primes[:coef_modulus_n_primes] ]) self.params.set_plain_modulus(plain_modulus) self.context = SEALContext(self.params) self.print_parameters(self.context) self.keygen = KeyGenerator(self.context) self.public_key = self.keygen.public_key() self.secret_key = self.keygen.secret_key() self.evaluator = Evaluator(self.context)
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 seal_obj(): # params obj params = EncryptionParameters() # set params params.set_poly_modulus("1x^4096 + 1") params.set_coeff_modulus(seal.coeff_modulus_128(4096)) params.set_plain_modulus(1 << 16) # get context context = SEALContext(params) # get evaluator evaluator = Evaluator(context) # gen keys keygen = KeyGenerator(context) public_key = keygen.public_key() private_key = keygen.secret_key() # evaluator keys ev_keys = EvaluationKeys() keygen.generate_evaluation_keys(30, ev_keys) # get encryptor and decryptor encryptor = Encryptor(context, public_key) decryptor = Decryptor(context, private_key) # float number encoder encoder = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 64, 32, 3) return evaluator, encoder.encode, encoder.decode, encryptor.encrypt, decryptor.decrypt, ev_keys
class ParticipantTest(unittest.TestCase): participant = Participant() parms = EncryptionParameters(scheme_type.BFV) parms.set_poly_modulus_degree(4096) parms.set_coeff_modulus(CoeffModulus.BFVDefault(4096)) parms.set_plain_modulus(1 << 8) def test_set_get_parms(self): self.participant.set_parms(self.parms) self.assertIsInstance(Participant(), type(self.participant)) self.assertEqual(self.participant.get_parms().poly_modulus_degree(), 4096) self.assertEqual(self.participant.get_parms().plain_modulus().value(), 1 << 8) self.assertIsInstance(self.participant._context, SEALContext) self.assertIsInstance(self.participant._encoder, IntegerEncoder) self.assertIsInstance(self.participant._evaluator, Evaluator) def test_save_load_c_array(self): client = Client() arr = np.array([3, 1, 0, -2, -4]) self.participant.set_parms(self.parms) arr_cipher = client.enc(arr) arr_string = self.participant.save_c_arr(arr_cipher) arr_cipher_load = self.participant.load_c_arr(arr_string) arr_load = client.dec(arr_cipher_load) self.assertTrue(np.array_equal(arr, arr_load))
def __init__(self, modulus, degree): self.modulus = modulus self.degree = degree self.acceptable_degree = [1024, 2048, 4096, 8192, 16384] if self.degree not in self.acceptable_degree: raise Exception(f"Get {self.degree}, but expect degree {self.acceptable_degree}") self.parms = EncryptionParameters(scheme_type.BFV) self.parms.set_poly_modulus_degree(self.degree) self.parms.set_coeff_modulus(CoeffModulus.BFVDefault(self.degree)) # self.parms.set_coeff_modulus(CoeffModulus.Create(self.degree, [60])) self.parms.set_plain_modulus(self.modulus) # print(self.parms.coeff_modulus()[0].value()) self.context = SEALContext.Create(self.parms) self.keygen = KeyGenerator(self.context) self.evaluator = Evaluator(self.context) self.batch_encoder = BatchEncoder(self.context)
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 _build_context(self, config): #set up encryption parameters and context parms = EncryptionParameters() parms.set_poly_modulus(config['poly_modulus']) parms.set_coeff_modulus(seal.coeff_modulus_128( config['coeff_modulus'])) parms.set_plain_modulus(1 << 18) context = SEALContext(parms) return context
def __init__( self, security_level=128, #128 or 192 for now poly_modulus_pwr2=12, # 11 through 15 coeff_modulus=None, plain_modulus=2**8, batch=False, ): """ security level: 128 or 192 poly_modulus_pwr2: 11,12,13,14,or 15 poly=x^(2^thisvariable)+1 will define our polynomial ring by Z[x]/poly. Larger number means more security but longer computations. coeff_modulus: default None, If set then security level is ignored. This is important to set for batching as it needs to be prime. batch: default False, setting to true will design encryption scheme to allow parallel predictions """ self.params = EncryptionParameters() self.batch = batch power = 2**poly_modulus_pwr2 self.params.set_poly_modulus(f"1x^{power} + 1") if coeff_modulus != None: st.write("Security level is ignored since coeff_modulus was set.") self.params.set_coeff_modulus(coeff_modulus) else: if security_level == 128: self.params.set_coeff_modulus(seal.coeff_modulus_128(power)) if security_level == 192: self.params.set_coeff_modulus(seal.coeff_modulus_192(power)) try: self.params.set_plain_modulus(plain_modulus) except: raise ValueError("There was a problem setting the plain modulus.") try: self._cont = SEALContext(self.params) except Exception as e: raise ValueError("There was a problem with your parameters.") st.write(f"There was a problem with your parameters: {e}") _keygen = KeyGenerator(self._cont) self._secretkey = _keygen.secret_key() self._publickey = _keygen.public_key()
def __init__(self, poly_modulus = 2048 ,bit_strength = 128 ,plain_modulus = 1<<8, integral_coeffs = 64, fractional_coeffs = 32, fractional_base = 3): parms = EncryptionParameters() parms.set_poly_modulus("1x^{} + 1".format(poly_modulus)) if (bit_strength == 128): parms.set_coeff_modulus(seal.coeff_modulus_128(poly_modulus)) else: parms.set_coeff_modulus(seal.coeff_modulus_192(poly_modulus)) parms.set_plain_modulus(plain_modulus) self.parms = parms context = SEALContext(parms) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() self.encryptor = Encryptor(context, public_key) self.evaluator = Evaluator(context) self.decryptor = Decryptor(context, secret_key) self.encoder = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), integral_coeffs, fractional_coeffs, fractional_base)
def __init__(self): # set parameters for encryption parms = EncryptionParameters() parms.set_poly_modulus("1x^2048 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(2048)) parms.set_plain_modulus(1 << 8) self.context = SEALContext(parms) keygen = KeyGenerator(self.context) self.encoder = IntegerEncoder(self.context.plain_modulus()) public_key = keygen.public_key() self.encryptor = Encryptor(self.context, public_key) secret_key = keygen.secret_key() self.decryptor = Decryptor(self.context, secret_key)
def config(): # PySEAL wrapper for the SEAL library is used. # SEAL implements somewhat FHE algorithmic solutions => # each operation has a limit - 'invariant noise budget' in bits. Operations consume the noise budget # at a rate determined py the encryption parameters. Additions free of noise budget consumption, multiplications are not # Noise budget consumption is getting worse in sequential multiplications => multiplicative depth of the arithmetic circuit that needs to be evaluated. # Noise budget in a ciphertext -> 0 => ciphertext too corrupted to be decrypted => large enough parameters to be eble to restore the result ############# # noise_budget = log2(coeff_modulus/plain_modulus) bits (in a freshly encrypted ciphertext) ############# ############# # noise_budget_cnsumption = log2(plain_modulus) + (other terms) ############# params = EncryptionParameters() # set the polynomial modulus. (1x^(power of 2) +1) - power of 2 cyclotomic polynomial # affects the security of the scheme # larger more secure and larger ciphertext size, computation slower # from 1024 to 32768 ##params.set_poly_modulus("1x^1024 + 1") params.set_poly_modulus("1x^2048 + 1") # coefficient modulus determines the noise budget of the ciphertext # the bigger the more the budget and lower security -> increase the polynomial modulus # choosing parameters for polynomial modulus http://HomomorphicEncryption.org ###params.set_coeff_modulus(seal.coeff_modulus_128(8192)) params.set_coeff_modulus( seal.coeff_modulus_128(2048)) #plain_modulus = k*4096+1 #~ log2(coeff_modulus/plain_modulus) (bits) # plaintext modulus determines the size of the plaintext datatype, affects the noise budget in multiplication => keep the plaintext data type as small as possible #65537 params.set_plain_modulus(12289) #####For batching ###params.set_plain_modulus(65537) #params.set_plain_modulus(2049) #params.set_plain_modulus(8192) # check the validity of the parameters set, performs and stores several important pre-computations context = SEALContext(params) # print the chosen parameters #performance_test_st(context) return context, params
def initialize_encryption(): print_example_banner("Example: Basics I"); parms = EncryptionParameters() parms.set_poly_modulus("1x^2048 + 1") # factor: 0xfffffffff00001. parms.set_coeff_modulus(seal.coeff_modulus_128(2048)) parms.set_plain_modulus(1 << 8) context = SEALContext(parms) print_parameters(context); # Here we choose to create an IntegerEncoder with base b=2. encoder = IntegerEncoder(context.plain_modulus()) 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) return encryptor, evaluator, decryptor, encoder, context
def do_per_amount(amount, subtract_from=15): """ Called on every message in the stream """ print("Transaction amount ", amount) 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) # Encode encoder = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 64, 32, 3) # 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() encryptor = Encryptor(context, public_key) plain1 = encoder.encode(amount) encoded2 = encoder.encode(subtract_from) # Encrypt encrypted1 = Ciphertext(parms) encryptor.encrypt(plain1, encrypted1) # Evaluate evaluator = Evaluator(context) evaluated = evaluate_subtraction_from_plain(evaluator, encrypted1, encoded2) # Decrypt and decode decryptor = Decryptor(context, secret_key) plain_result = Plaintext() decryptor.decrypt(evaluated, plain_result) result = encoder.decode(plain_result) str_result = "Amount left = " + str(result) print(str_result) return str_result
def example_rotation_ckks(): print("Example: Rotation / Rotation in CKKS"); #Rotations in the CKKS scheme work very similarly to rotations in BFV. parms = EncryptionParameters(scheme_type.CKKS) poly_modulus_degree = 8192 parms.set_poly_modulus_degree(poly_modulus_degree) parms.set_coeff_modulus(CoeffModulus.Create( poly_modulus_degree, IntVector([40, 40, 40, 40, 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() gal_keys = keygen.galois_keys() encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) ckks_encoder = CKKSEncoder(context) slot_count = ckks_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_vector(input) scale = 2.0**50 print("Encode and encrypt.") plain = Plaintext() ckks_encoder.encode(input, scale, plain) encrypted = Ciphertext() encryptor.encrypt(plain, encrypted) rotated = Ciphertext() print("Rotate 2 steps left.") evaluator.rotate_vector(encrypted, 2, gal_keys, rotated) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(rotated, plain) result = DoubleVector() ckks_encoder.decode(plain, result) print_vector(result)
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 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)
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)))
EncryptionParameters, \ Evaluator, \ IntegerEncoder, \ FractionalEncoder, \ KeyGenerator, \ MemoryPoolHandle, \ Plaintext, \ SEALContext, \ EvaluationKeys, \ GaloisKeys, \ PolyCRTBuilder, \ ChooserEncoder, \ ChooserEvaluator, \ ChooserPoly parms = EncryptionParameters() parms.set_poly_modulus("1x^4096 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(4096)) # Note that 40961 is a prime number and 2*4096 divides 40960. parms.set_plain_modulus(40961) context = SEALContext(parms) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() def inner_product(cypher1, cypher2): # We also set up an Encryptor, Evaluator, and Decryptor here. evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key)
for rowIndex in range(n): for colIndex in range(rowIndex+1,n): if M[rowIndex][colIndex]==None: M[rowIndex][colIndex]=Ciphertext(M[colIndex][rowIndex]) print_plain(M) if __name__ == '__main__': multiprocessing.freeze_support() ########################## paramaters required ################################# parms = EncryptionParameters() parms.set_poly_modulus("1x^16384 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(8192)) parms.set_plain_modulus(1 << 25) context = SEALContext(parms) encoderF = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 34, 30, 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) num_cores = multiprocessing.cpu_count() -1
def example_rotation_bfv(): print("Example: Rotation / Rotation in BFV") parms = EncryptionParameters(scheme_type.BFV) poly_modulus_degree = 8192 parms.set_poly_modulus_degree(poly_modulus_degree) parms.set_coeff_modulus(CoeffModulus.BFVDefault(poly_modulus_degree)) parms.set_plain_modulus(PlainModulus.Batching(poly_modulus_degree, 20)) 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) batch_encoder = BatchEncoder(context) slot_count = batch_encoder.slot_count() row_size = int(slot_count / 2) print("Plaintext matrix row size: {}".format(row_size)) pod_matrix = UInt64Vector([0]*slot_count) pod_matrix[0] = 0 pod_matrix[1] = 1 pod_matrix[2] = 2 pod_matrix[3] = 3 pod_matrix[row_size] = 4 pod_matrix[row_size + 1] = 5 pod_matrix[row_size + 2] = 6 pod_matrix[row_size + 3] = 7 print("Input plaintext matrix:") print_matrix(pod_matrix, row_size) #First we use BatchEncoder to encode the matrix into a plaintext. We encrypt #the plaintext as usual. plain_matrix = Plaintext() print("Encode and encrypt.") batch_encoder.encode(pod_matrix, plain_matrix) encrypted_matrix = Ciphertext() encryptor.encrypt(plain_matrix, encrypted_matrix) print(" + Noise budget in fresh encryption: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) #Rotations require yet another type of special key called `Galois keys'. These #are easily obtained from the KeyGenerator. gal_keys = keygen.galois_keys() #Now rotate both matrix rows 3 steps to the left, decrypt, decode, and print. print("Rotate rows 3 steps left.") evaluator.rotate_rows_inplace(encrypted_matrix, 3, gal_keys) plain_result = Plaintext() print(" + Noise budget after rotation: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_matrix) print_matrix(pod_matrix, row_size) #We can also rotate the columns, i.e., swap the rows. print("Rotate columns.") evaluator.rotate_columns_inplace(encrypted_matrix, gal_keys) print(" + Noise budget after rotation: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_matrix) print_matrix(pod_matrix, row_size) #Finally, we rotate the rows 4 steps to the right, decrypt, decode, and print. print("Rotate rows 4 steps right.") evaluator.rotate_rows_inplace(encrypted_matrix, -4, gal_keys) print(" + Noise budget after rotation: {} bits".format( decryptor.invariant_noise_budget(encrypted_matrix))) print(" + Decrypt and decode ...... Correct.") decryptor.decrypt(encrypted_matrix, plain_result) batch_encoder.decode(plain_result, pod_matrix) print_matrix(pod_matrix, row_size)
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
class FracContext: primes = [ 0xffffffffffc0001, 0xfffffffff840001, 0xfffffffff240001, 0xffffffffe7c0001, 0xffffffffe740001, 0xffffffffe4c0001, 0xffffffffe440001, 0xffffffffe400001, 0xffffffffdbc0001, 0xffffffffd840001, 0xffffffffd680001, 0xffffffffd000001, 0xffffffffcf00001, 0xffffffffcdc0001, 0xffffffffcc40001, 0xffffffffc300001, 0xffffffffbf40001, 0xffffffffbdc0001, 0xffffffffb880001, 0xffffffffaec0001, 0xffffffffa380001, 0xffffffffa200001, 0xffffffffa0c0001, 0xffffffff9600001, 0xffffffff91c0001, 0xffffffff8f40001, 0xffffffff8680001, 0xffffffff7e40001, 0xffffffff7bc0001, 0xffffffff76c0001, 0xffffffff7680001, 0xffffffff6fc0001, 0xffffffff6880001, 0xffffffff6340001, 0xffffffff5d40001, 0xffffffff54c0001, 0xffffffff4d40001, 0xffffffff4380001, 0xffffffff3e80001, 0xffffffff37c0001, 0xffffffff36c0001, 0xffffffff2100001, 0xffffffff1d80001, 0xffffffff1cc0001, 0xffffffff1900001, 0xffffffff1740001, 0xffffffff15c0001, 0xffffffff0e80001, 0xfffffffeff80001, 0xfffffffeff40001, 0xfffffffeefc0001, 0xfffffffee8c0001, 0xfffffffede40001, 0xfffffffedcc0001, 0xfffffffed040001, 0xfffffffecf40001, 0xfffffffecec0001, 0xfffffffecb00001, 0xfffffffec380001, 0xfffffffebb40001 ] def __init__(self, poly_modulus="1x^1024 + 1", coef_modulus_n_primes=20, plain_modulus=1 << 32): """ Set up encryption context for encoder and decoder :param poly_modulus: :param coef_modulus_n_primes: :param plain_modulus: """ self.params = EncryptionParameters() self.params.set_poly_modulus(poly_modulus) self.params.set_coeff_modulus([ seal.SmallModulus(p) for p in FracContext.primes[:coef_modulus_n_primes] ]) self.params.set_plain_modulus(plain_modulus) self.context = SEALContext(self.params) self.print_parameters(self.context) self.keygen = KeyGenerator(self.context) self.public_key = self.keygen.public_key() self.secret_key = self.keygen.secret_key() self.evaluator = Evaluator(self.context) def print_parameters(self, context: SEALContext): """ Parameters description :param context: SEALContext object """ print("/ Encryption parameters:") print("| poly_modulus: " + context.poly_modulus().to_string()) # Print the size of the true (product) coefficient modulus print("| coeff_modulus_size: " + (str)(context.total_coeff_modulus().significant_bit_count()) + " bits") print("| plain_modulus: " + (str)(context.plain_modulus().value())) print("| noise_standard_deviation: " + (str)(context.noise_standard_deviation()))
def example_levels(): print("Example: Levels") #In this examples we describe the concept of `levels' in BFV and CKKS and the #related objects that represent them in Microsoft SEAL. # #In Microsoft SEAL a set of encryption parameters (excluding the random number #generator) is identified uniquely by a 256-bit hash of the parameters. This #hash is called the `parms_id' and can be easily accessed and printed at any #time. The hash will change as soon as any of the parameters is changed. # #When a SEALContext is created from a given EncryptionParameters instance, #Microsoft SEAL automatically creates a so-called `modulus switching chain', #which is a chain of other encryption parameters derived from the original set. #The parameters in the modulus switching chain are the same as the original #parameters with the exception that size of the coefficient modulus is #decreasing going down the chain. More precisely, each parameter set in the #chain attempts to remove the last coefficient modulus prime from the #previous set; this continues until the parameter set is no longer valid #(e.g., plain_modulus is larger than the remaining coeff_modulus). It is easy #to walk through the chain and access all the parameter sets. Additionally, #each parameter set in the chain has a `chain index' that indicates its #position in the chain so that the last set has index 0. We say that a set #of encryption parameters, or an object carrying those encryption parameters, #is at a higher level in the chain than another set of parameters if its the #chain index is bigger, i.e., it is earlier in the chain. # #Each set of parameters in the chain involves unique pre-computations performed #when the SEALContext is created, and stored in a SEALContext::ContextData #object. The chain is basically a linked list of SEALContext::ContextData #objects, and can easily be accessed through the SEALContext at any time. Each #node can be identified by the parms_id of its specific encryption parameters #(poly_modulus_degree remains the same but coeff_modulus varies). parms = EncryptionParameters(scheme_type.BFV) poly_modulus_degree = 8192 parms.set_poly_modulus_degree(poly_modulus_degree) #In this example we use a custom coeff_modulus, consisting of 5 primes of #sizes 50, 30, 30, 50, and 50 bits. Note that this is still OK according to #the explanation in `1_bfv_basics.cpp'. Indeed, # # CoeffModulus::MaxBitCount(poly_modulus_degree) # #returns 218 (greater than 50+30+30+50+50=210). # #Due to the modulus switching chain, the order of the 5 primes is significant. #The last prime has a special meaning and we call it the `special prime'. Thus, #the first parameter set in the modulus switching chain is the only one that #involves the special prime. All key objects, such as SecretKey, are created #at this highest level. All data objects, such as Ciphertext, can be only at #lower levels. The special prime should be as large as the largest of the #other primes in the coeff_modulus, although this is not a strict requirement. # # special prime +---------+ # | # v #coeff_modulus: { 50, 30, 30, 50, 50 } +---+ Level 4 (all keys; `key level') # | # | # coeff_modulus: { 50, 30, 30, 50 } +---+ Level 3 (highest `data level') # | # | # coeff_modulus: { 50, 30, 30 } +---+ Level 2 # | # | # coeff_modulus: { 50, 30 } +---+ Level 1 # | # | # coeff_modulus: { 50 } +---+ Level 0 (lowest level) parms.set_coeff_modulus( CoeffModulus.Create(poly_modulus_degree, IntVector([50, 30, 30, 50, 50]))) #In this example the plain_modulus does not play much of a role; we choose #some reasonable value. parms.set_plain_modulus(PlainModulus.Batching(poly_modulus_degree, 20)) context = SEALContext.Create(parms) print_parameters(context) #There are convenience method for accessing the SEALContext::ContextData for #some of the most important levels: # # SEALContext::key_context_data(): access to key level ContextData # SEALContext::first_context_data(): access to highest data level ContextData # SEALContext::last_context_data(): access to lowest level ContextData # #We iterate over the chain and print the parms_id for each set of parameters. print("Print the modulus switching chain.") #First print the key level parameter information. context_data = context.key_context_data() print("----> Level (chain index): {} ...... key_context_data()".format( context_data.chain_index())) print(" parms_id: {}".format(list2hex(context_data.parms_id()))) print("coeff_modulus primes:", end=' ') for prime in context_data.parms().coeff_modulus(): print(hex(prime.value()), end=' ') print("") print("\\") print(" \\-->") #Next iterate over the remaining (data) levels. context_data = context.first_context_data() while (context_data): print(" Level (chain index): {} ".format(context_data.chain_index()), end='') if context_data.parms_id() == context.first_parms_id(): print(" ...... first_context_data()") elif context_data.parms_id() == context.last_parms_id(): print(" ...... last_context_data()") else: print("") print(" parms_id: {}".format(list2hex(context_data.parms_id()))) print("coeff_modulus primes:", end=' ') for prime in context_data.parms().coeff_modulus(): print(hex(prime.value()), end=' ') print("") print("\\") print(" \\-->") #Step forward in the chain. context_data = context_data.next_context_data() print(" End of chain reached") #We create some keys and check that indeed they appear at the highest level. keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() relin_keys = keygen.relin_keys() galois_keys = keygen.galois_keys() print("Print the parameter IDs of generated elements.") print(" + public_key: {}".format(list2hex(public_key.parms_id()))) print(" + secret_key: {}".format(list2hex(secret_key.parms_id()))) print(" + relin_keys: {}".format(list2hex(relin_keys.parms_id()))) print(" + galois_keys: {}".format(list2hex(galois_keys.parms_id()))) encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) #In the BFV scheme plaintexts do not carry a parms_id, but ciphertexts do. Note #how the freshly encrypted ciphertext is at the highest data level. plain = Plaintext("1x^3 + 2x^2 + 3x^1 + 4") encrypted = Ciphertext() encryptor.encrypt(plain, encrypted) print(" + plain: {} (not set in BFV)".format( list2hex(plain.parms_id()))) print(" + encrypted: {}".format(list2hex(encrypted.parms_id()))) #`Modulus switching' is a technique of changing the ciphertext parameters down #in the chain. The function Evaluator::mod_switch_to_next always switches to #the next level down the chain, whereas Evaluator::mod_switch_to switches to #a parameter set down the chain corresponding to a given parms_id. However, it #is impossible to switch up in the chain. print("Perform modulus switching on encrypted and print.") context_data = context.first_context_data() print("---->") while (context_data.next_context_data()): print(" Level (chain index): {} ".format(context_data.chain_index())) print(" parms_id of encrypted: {}".format( list2hex(encrypted.parms_id()))) print(" Noise budget at this level: {} bits".format( decryptor.invariant_noise_budget(encrypted))) print("\\") print(" \\-->") evaluator.mod_switch_to_next_inplace(encrypted) context_data = context_data.next_context_data() print(" Level (chain index): {}".format(context_data.chain_index())) print(" parms_id of encrypted: {}".format(encrypted.parms_id())) print(" Noise budget at this level: {} bits".format( decryptor.invariant_noise_budget(encrypted))) print("\\") print(" \\-->") print(" End of chain reached") #At this point it is hard to see any benefit in doing this: we lost a huge #amount of noise budget (i.e., computational power) at each switch and seemed #to get nothing in return. Decryption still works. print("Decrypt still works after modulus switching.") decryptor.decrypt(encrypted, plain) print(" + Decryption of encrypted: {} ...... Correct.".format( plain.to_string())) #However, there is a hidden benefit: the size of the ciphertext depends #linearly on the number of primes in the coefficient modulus. Thus, if there #is no need or intention to perform any further computations on a given #ciphertext, we might as well switch it down to the smallest (last) set of #parameters in the chain before sending it back to the secret key holder for #decryption. # #Also the lost noise budget is actually not an issue at all, if we do things #right, as we will see below. # #First we recreate the original ciphertext and perform some computations. print("Computation is more efficient with modulus switching.") print("Compute the 8th power.") encryptor.encrypt(plain, encrypted) print(" + Noise budget fresh: {} bits".format( decryptor.invariant_noise_budget(encrypted))) evaluator.square_inplace(encrypted) evaluator.relinearize_inplace(encrypted, relin_keys) print(" + Noise budget of the 2nd power: {} bits".format( decryptor.invariant_noise_budget(encrypted))) evaluator.square_inplace(encrypted) evaluator.relinearize_inplace(encrypted, relin_keys) print(" + Noise budget of the 4th power: {} bits".format( decryptor.invariant_noise_budget(encrypted))) #Surprisingly, in this case modulus switching has no effect at all on the #noise budget. evaluator.mod_switch_to_next_inplace(encrypted) print(" + Noise budget after modulus switching: {} bits".format( decryptor.invariant_noise_budget(encrypted))) #This means that there is no harm at all in dropping some of the coefficient #modulus after doing enough computations. In some cases one might want to #switch to a lower level slightly earlier, actually sacrificing some of the #noise budget in the process, to gain computational performance from having #smaller parameters. We see from the print-out that the next modulus switch #should be done ideally when the noise budget is down to around 25 bits. evaluator.square_inplace(encrypted) evaluator.relinearize_inplace(encrypted, relin_keys) print(" + Noise budget of the 8th power: {} bits".format( decryptor.invariant_noise_budget(encrypted))) evaluator.mod_switch_to_next_inplace(encrypted) print(" + Noise budget after modulus switching: {} bits".format( decryptor.invariant_noise_budget(encrypted))) #At this point the ciphertext still decrypts correctly, has very small size, #and the computation was as efficient as possible. Note that the decryptor #can be used to decrypt a ciphertext at any level in the modulus switching #chain. decryptor.decrypt(encrypted, plain) print(" + Decryption of the 8th power (hexadecimal) ...... Correct.") print(" {}".format(plain.to_string())) #In BFV modulus switching is not necessary and in some cases the user might #not want to create the modulus switching chain, except for the highest two #levels. This can be done by passing a bool `false' to SEALContext::Create. context = SEALContext.Create(parms, False) #We can check that indeed the modulus switching chain has been created only #for the highest two levels (key level and highest data level). The following #loop should execute only once. print("Optionally disable modulus switching chain expansion.") print("Print the modulus switching chain.") print("---->") context_data = context.key_context_data() while (context_data): print(" Level (chain index): {}".format(context_data.chain_index())) print(" parms_id: {}".format(list2hex(context_data.parms_id()))) print("coeff_modulus primes:", end=' ') for prime in context_data.parms().coeff_modulus(): print(hex(prime.value()), end=' ') print("") print("\\") print(" \\-->") context_data = context_data.next_context_data() print(" End of chain reached")
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)
def example_bfv_basics(): print("Example: BFV Basics") #In this example, we demonstrate performing simple computations (a polynomial #evaluation) on encrypted integers using the BFV encryption scheme. # #The first task is to set up an instance of the EncryptionParameters class. #It is critical to understand how the 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_degree (degree of polynomial modulus); # - coeff_modulus ([ciphertext] coefficient modulus); # - plain_modulus (plaintext modulus; only for the BFV scheme). # #The BFV scheme 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 #in 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 BFV the two basic #operations allowed on encrypted data 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 compounds in sequential multiplications, the most significant #factor in choosing appropriate encryption parameters is the multiplicative #depth of the arithmetic circuit that the user wants to evaluate on encrypted #data. Once the noise budget of 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(scheme_type.BFV) #The first parameter we set is the degree of the `polynomial modulus'. This #must be a positive power of 2, representing the degree of a power-of-two #cyclotomic polynomial; it is not necessary to understand what this means. # #Larger poly_modulus_degree makes ciphertext sizes larger and all operations #slower, but enables more complicated encrypted computations. Recommended #values are 1024, 2048, 4096, 8192, 16384, 32768, but it is also possible #to go beyond this range. # #In this example we use a relatively small polynomial modulus. Anything #smaller than this will enable only very restricted encrypted computations. poly_modulus_degree = 4096 parms.set_poly_modulus_degree(poly_modulus_degree) #Next we set the [ciphertext] `coefficient modulus' (coeff_modulus). This #parameter is a large integer, which is a product of distinct prime numbers, #each up to 60 bits in size. It is represented as a vector of these prime #numbers, each represented by an instance of the SmallModulus class. The #bit-length of coeff_modulus means the sum of the bit-lengths of its prime #factors. # #A larger coeff_modulus implies a larger noise budget, hence more encrypted #computation capabilities. However, an upper bound for the total bit-length #of the coeff_modulus is determined by the poly_modulus_degree, as follows: # # +----------------------------------------------------+ # | poly_modulus_degree | max coeff_modulus bit-length | # +---------------------+------------------------------+ # | 1024 | 27 | # | 2048 | 54 | # | 4096 | 109 | # | 8192 | 218 | # | 16384 | 438 | # | 32768 | 881 | # +---------------------+------------------------------+ # #These numbers can also be found in native/src/seal/util/hestdparms.h encoded #in the function SEAL_HE_STD_PARMS_128_TC, and can also be obtained from the #function # # CoeffModulus::MaxBitCount(poly_modulus_degree). # #For example, if poly_modulus_degree is 4096, the coeff_modulus could consist #of three 36-bit primes (108 bits). # #Microsoft SEAL comes with helper functions for selecting the coeff_modulus. #For new users the easiest way is to simply use # # CoeffModulus::BFVDefault(poly_modulus_degree), # #which returns std::vector<SmallModulus> consisting of a generally good choice #for the given poly_modulus_degree. parms.set_coeff_modulus(CoeffModulus.BFVDefault(poly_modulus_degree)) #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 later examples. The plaintext #modulus determines the size of the plaintext data type and the consumption #of noise budget in multiplications. Thus, it is essential to try to keep the #plaintext data type as small as possible for best 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). # #The plaintext modulus is specific to the BFV scheme, and cannot be set when #using the CKKS scheme. parms.set_plain_modulus(1024) #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. context = SEALContext.Create(parms) #Print the parameters that we have chosen. print("Set encryption parameters and print") print_parameters(context) print("~~~~~~ A naive way to calculate 4(x^2+1)(x+1)^2. ~~~~~~") #The encryption schemes in Microsoft SEAL are public key encryption schemes. #For users unfamiliar with this terminology, a public key encryption scheme #has a separate public key for encrypting data, and a separate secret key for #decrypting data. This way multiple parties can encrypt data using the same #shared public key, but only the proper recipient of the data can decrypt it #with the secret key. # #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 immediately be #read to local variables. 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, as expected. encryptor = Encryptor(context, public_key) #Computations on the ciphertexts are performed with the Evaluator class. In #a real use-case the Evaluator would not be constructed by the same party #that holds the secret key. 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) #As an example, we evaluate the degree 4 polynomial # # 4x^4 + 8x^3 + 8x^2 + 8x + 4 # #over an encrypted x = 6. The coefficients of the polynomial can be considered #as plaintext inputs, as we will see below. The computation is done modulo the #plain_modulus 1024. # #While this examples is simple and easy to understand, it does not have much #practical value. In later examples we will demonstrate how to compute more #efficiently on encrypted integers and real or complex numbers. # #Plaintexts in the BFV scheme are polynomials of degree less than the degree #of the polynomial modulus, and coefficients integers modulo the plaintext #modulus. For readers with background in ring theory, the plaintext space is #the polynomial quotient ring Z_T[X]/(X^N + 1), where N is poly_modulus_degree #and T is plain_modulus. # #To get started, we create a plaintext containing the constant 6. For the #plaintext element we use a constructor that takes the desired polynomial as #a string with coefficients represented as hexadecimal numbers. x = 6 x_plain = Plaintext(str(x)) print("Express x = {} as a plaintext polynomial 0x{}.".format( x, x_plain.to_string())) #We then encrypt the plaintext, producing a ciphertext. x_encrypted = Ciphertext() print("Encrypt x_plain to x_encrypted.") encryptor.encrypt(x_plain, x_encrypted) #In Microsoft SEAL, a valid ciphertext consists of two or more polynomials #whose coefficients are integers modulo the product of the primes in the #coeff_modulus. The number of polynomials in a ciphertext is called its `size' #and is given by Ciphertext::size(). A freshly encrypted ciphertext always #has size 2. print(" + size of freshly encrypted x: {}".format(x_encrypted.size())) #There is plenty of noise budget left in this freshly encrypted ciphertext. print(" + noise budget in freshly encrypted x: {} bits".format( decryptor.invariant_noise_budget(x_encrypted))) #We decrypt the ciphertext and print the resulting plaintext in order to #demonstrate correctness of the encryption. x_decrypted = Plaintext() decryptor.decrypt(x_encrypted, x_decrypted) print(" + decryption of x_encrypted: 0x{} ...... Correct.".format( x_decrypted.to_string())) #When using Microsoft SEAL, it is typically advantageous to compute in a way #that minimizes the longest chain of sequential multiplications. In other #words, encrypted computations are best evaluated in a way that minimizes #the multiplicative depth of the computation, because the total noise budget #consumption is proportional to the multiplicative depth. For example, for #our example computation it is advantageous to factorize the polynomial as # # 4x^4 + 8x^3 + 8x^2 + 8x + 4 = 4(x + 1)^2 * (x^2 + 1) # #to obtain a simple depth 2 representation. Thus, we compute (x + 1)^2 and #(x^2 + 1) separately, before multiplying them, and multiplying by 4. # #First, we compute x^2 and add a plaintext "1". We can clearly see from the #print-out that multiplication has consumed a lot of noise budget. The user #can vary the plain_modulus parameter to see its effect on the rate of noise #budget consumption. print("Compute x_sq_plus_one (x^2+1).") x_sq_plus_one = Ciphertext() evaluator.square(x_encrypted, x_sq_plus_one) plain_one = Plaintext("1") evaluator.add_plain_inplace(x_sq_plus_one, plain_one) #Encrypted multiplication results in the output ciphertext growing in size. #More precisely, if the input ciphertexts have size M and N, then the output #ciphertext after homomorphic multiplication will have size M+N-1. In this #case we perform a squaring, and observe both size growth and noise budget #consumption. print(" + size of x_sq_plus_one: {}".format(x_sq_plus_one.size())) print(" + noise budget in x_sq_plus_one: {} bits".format( decryptor.invariant_noise_budget(x_sq_plus_one))) #Even though the size has grown, decryption works as usual as long as noise #budget has not reached 0. decrypted_result = Plaintext() decryptor.decrypt(x_sq_plus_one, decrypted_result) print(" + decryption of x_sq_plus_one: 0x{} ...... Correct.".format( decrypted_result.to_string())) #Next, we compute (x + 1)^2. print("Compute x_plus_one_sq ((x+1)^2).") x_plus_one_sq = Ciphertext() evaluator.add_plain(x_encrypted, plain_one, x_plus_one_sq) evaluator.square_inplace(x_plus_one_sq) print(" + size of x_plus_one_sq: {}".format(x_plus_one_sq.size())) print(" + noise budget in x_plus_one_sq: {} bits".format( decryptor.invariant_noise_budget(x_plus_one_sq))) decryptor.decrypt(x_plus_one_sq, decrypted_result) print(" + decryption of x_plus_one_sq: 0x{} ...... Correct.".format( decrypted_result.to_string())) #Finally, we multiply (x^2 + 1) * (x + 1)^2 * 4. print("Compute encrypted_result (4(x^2+1)(x+1)^2).") encrypted_result = Ciphertext() plain_four = Plaintext("4") evaluator.multiply_plain_inplace(x_sq_plus_one, plain_four) evaluator.multiply(x_sq_plus_one, x_plus_one_sq, encrypted_result) print(" + size of encrypted_result: {}".format(encrypted_result.size())) print(" + noise budget in encrypted_result: {} bits".format( decryptor.invariant_noise_budget(encrypted_result))) print("NOTE: Decryption can be incorrect if noise budget is zero.") print("~~~~~~ A better way to calculate 4(x^2+1)(x+1)^2. ~~~~~~") #Noise budget has reached 0, which means that decryption cannot be expected #to give the correct result. This is because both ciphertexts x_sq_plus_one #and x_plus_one_sq consist of 3 polynomials due to the previous squaring #operations, and homomorphic operations on large ciphertexts consume much more #noise budget than computations on small ciphertexts. Computing on smaller #ciphertexts is also computationally significantly cheaper. #`Relinearization' is an operation that reduces the size of a ciphertext after #multiplication back to the initial size, 2. Thus, relinearizing one or both #input ciphertexts before the next multiplication can have a huge positive #impact on both noise growth and performance, even though relinearization has #a significant computational cost itself. It is only possible to relinearize #size 3 ciphertexts down to size 2, so often the user would want to relinearize #after each multiplication to keep the ciphertext sizes at 2. #Relinearization requires special `relinearization keys', which can be thought #of as a kind of public key. Relinearization keys can easily be created with #the KeyGenerator. #Relinearization is used similarly in both the BFV and the CKKS schemes, but #in this example we continue using BFV. We repeat our computation from before, #but this time relinearize after every multiplication. #We use KeyGenerator::relin_keys() to create relinearization keys. print("Generate relinearization keys.") relin_keys = keygen.relin_keys() #We now repeat the computation relinearizing after each multiplication. print("Compute and relinearize x_squared (x^2),") print("then compute x_sq_plus_one (x^2+1)") x_squared = Ciphertext() evaluator.square(x_encrypted, x_squared) print(" + size of x_squared: {}".format(x_squared.size())) evaluator.relinearize_inplace(x_squared, relin_keys) print(" + size of x_squared (after relinearization): {}".format( x_squared.size())) evaluator.add_plain(x_squared, plain_one, x_sq_plus_one) print(" + noise budget in x_sq_plus_one: {} bits".format( decryptor.invariant_noise_budget(x_sq_plus_one))) decryptor.decrypt(x_sq_plus_one, decrypted_result) print(" + decryption of x_sq_plus_one: 0x{} ...... Correct.".format( decrypted_result.to_string())) x_plus_one = Ciphertext() print("Compute x_plus_one (x+1),") print("then compute and relinearize x_plus_one_sq ((x+1)^2).") evaluator.add_plain(x_encrypted, plain_one, x_plus_one) evaluator.square(x_plus_one, x_plus_one_sq) print(" + size of x_plus_one_sq: {}".format(x_plus_one_sq.size())) evaluator.relinearize_inplace(x_plus_one_sq, relin_keys) print(" + noise budget in x_plus_one_sq: {} bits".format( decryptor.invariant_noise_budget(x_plus_one_sq))) decryptor.decrypt(x_plus_one_sq, decrypted_result) print(" + decryption of x_plus_one_sq: 0x{} ...... Correct.".format( decrypted_result.to_string())) print("Compute and relinearize encrypted_result (4(x^2+1)(x+1)^2).") evaluator.multiply_plain_inplace(x_sq_plus_one, plain_four) evaluator.multiply(x_sq_plus_one, x_plus_one_sq, encrypted_result) print(" + size of encrypted_result: {}".format(encrypted_result.size())) evaluator.relinearize_inplace(encrypted_result, relin_keys) print(" + size of encrypted_result (after relinearization): {}".format( encrypted_result.size())) print(" + noise budget in encrypted_result: {} bits".format( decryptor.invariant_noise_budget(encrypted_result))) print("NOTE: Notice the increase in remaining noise budget.") #Relinearization clearly improved our noise consumption. We have still plenty #of noise budget left, so we can expect the correct answer when decrypting. print("Decrypt encrypted_result (4(x^2+1)(x+1)^2).") decryptor.decrypt(encrypted_result, decrypted_result) print(" + decryption of 4(x^2+1)(x+1)^2 = 0x{} ...... Correct.".format( decrypted_result.to_string()))
def example_batching(): print_example_banner("Example: Batching with PolyCRTBuilder"); parms = EncryptionParameters() parms.set_poly_modulus("1x^4096 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(4096)) parms.set_plain_modulus(40961) context = SEALContext(parms) print_parameters(context) qualifiers = context.qualifiers() keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() gal_keys = GaloisKeys() keygen.generate_galois_keys(30, gal_keys) #ev_keys = EvaluationKeys() #keygen.generate_evaluation_keys(30, ev_keys) encryptor = Encryptor(context, public_key) evaluator = Evaluator(context) decryptor = Decryptor(context, secret_key) crtbuilder = PolyCRTBuilder(context) slot_count = (int)(crtbuilder.slot_count()) row_size = (int)(slot_count / 2) print("Plaintext matrix row size: " + (str)(row_size)) def print_matrix(matrix): print("") print_size = 5 current_line = " [" for i in range(print_size): current_line += ((str)(matrix[i]) + ", ") current_line += ("..., ") for i in range(row_size - print_size, row_size): current_line += ((str)(matrix[i])) if i != row_size-1: current_line += ", " else: current_line += "]" print(current_line) current_line = " [" for i in range(row_size, row_size + print_size): current_line += ((str)(matrix[i]) + ", ") current_line += ("..., ") for i in range(2*row_size - print_size, 2*row_size): current_line += ((str)(matrix[i])) if i != 2*row_size-1: current_line += ", " else: current_line += "]" print(current_line) print("") # [ 0, 1, 2, 3, 0, 0, ..., 0 ] # [ 4, 5, 6, 7, 0, 0, ..., 0 ] pod_matrix = [0]*slot_count pod_matrix[0] = 0 pod_matrix[1] = 1 pod_matrix[2] = 2 pod_matrix[3] = 3 pod_matrix[row_size] = 4 pod_matrix[row_size + 1] = 5 pod_matrix[row_size + 2] = 6 pod_matrix[row_size + 3] = 7 print("Input plaintext matrix:") print_matrix(pod_matrix) plain_matrix = Plaintext() crtbuilder.compose(pod_matrix, plain_matrix) encrypted_matrix = Ciphertext() print("Encrypting: ") encryptor.encrypt(plain_matrix, encrypted_matrix) print("Done") print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") pod_matrix2 = [] for i in range(slot_count): pod_matrix2.append((i % 2) + 1) plain_matrix2 = Plaintext() crtbuilder.compose(pod_matrix2, plain_matrix2) print("Second input plaintext matrix:") print_matrix(pod_matrix2) print("Adding and squaring: ") evaluator.add_plain(encrypted_matrix, plain_matrix2) evaluator.square(encrypted_matrix) evaluator.relinearize(encrypted_matrix, ev_keys) print("Done") print("Noise budget in result: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") plain_result = Plaintext() print("Decrypting result: ") decryptor.decrypt(encrypted_matrix, plain_result) print("Done") crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print("Result plaintext matrix:") print_matrix(pod_result) encryptor.encrypt(plain_matrix, encrypted_matrix) print("Unrotated matrix: ") print_matrix(pod_matrix) print("Noise budget in fresh encryption: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") # Now rotate the rows to the left 3 steps, decrypt, decompose, and print. evaluator.rotate_rows(encrypted_matrix, 3, gal_keys) print("Rotated rows 3 steps left: ") decryptor.decrypt(encrypted_matrix, plain_result) crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print_matrix(pod_result) print("Noise budget after rotation" + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") # Rotate columns (swap rows), decrypt, decompose, and print. evaluator.rotate_columns(encrypted_matrix, gal_keys) print("Rotated columns: ") decryptor.decrypt(encrypted_matrix, plain_result) crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print_matrix(pod_result) print("Noise budget after rotation: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits") # Rotate rows to the right 4 steps, decrypt, decompose, and print. evaluator.rotate_rows(encrypted_matrix, -4, gal_keys) print("Rotated rows 4 steps right: ") decryptor.decrypt(encrypted_matrix, plain_result) crtbuilder.decompose(plain_result) pod_result = [plain_result.coeff_at(i) for i in range(plain_result.coeff_count())] print_matrix(pod_result) print("Noise budget after rotation: " + (str)(decryptor.invariant_noise_budget(encrypted_matrix)) + " bits")
class FheBuilder(object): secret_key = None public_key = None galois_keys = None encryptor = None decryptor = None def __init__(self, modulus, degree): self.modulus = modulus self.degree = degree self.acceptable_degree = [1024, 2048, 4096, 8192, 16384] if self.degree not in self.acceptable_degree: raise Exception(f"Get {self.degree}, but expect degree {self.acceptable_degree}") self.parms = EncryptionParameters(scheme_type.BFV) self.parms.set_poly_modulus_degree(self.degree) self.parms.set_coeff_modulus(CoeffModulus.BFVDefault(self.degree)) # self.parms.set_coeff_modulus(CoeffModulus.Create(self.degree, [60])) self.parms.set_plain_modulus(self.modulus) # print(self.parms.coeff_modulus()[0].value()) self.context = SEALContext.Create(self.parms) self.keygen = KeyGenerator(self.context) self.evaluator = Evaluator(self.context) self.batch_encoder = BatchEncoder(self.context) def generate_keys(self): self.secret_key = self.keygen.secret_key() self.public_key = self.keygen.public_key() self.encryptor = Encryptor(self.context, self.public_key) self.decryptor = Decryptor(self.context, self.secret_key) def generate_galois_keys(self): self.galois_keys = self.keygen.galois_keys() def get_public_key_buffer(self) -> torch.Tensor: if self.public_key is None: self.public_key = self.keygen.public_key() return torch_from_buffer(self.public_key) def build_from_loaded_public_key(self): self.encryptor = Encryptor(self.context, self.public_key) def get_secret_key_buffer(self) -> torch.Tensor: if self.secret_key is None: self.secret_key = self.keygen.secret_key() return torch_from_buffer(self.secret_key) def build_from_loaded_secret_key(self): self.decryptor = Decryptor(self.context, self.secret_key) def build_enc(self, num_elem, is_cheap_init=False) -> FheEncTensor: return FheEncTensor(num_elem, self.modulus, self.degree, self.context, self.evaluator, self.batch_encoder, self.encryptor, is_cheap_init=is_cheap_init) def build_enc_from_torch(self, torch_tensor) -> FheEncTensor: res = self.build_enc(get_prod(torch_tensor.size())) res.encrypt_additive(torch_tensor) return res def build_plain(self, num_elem) -> FhePlainTensor: return FhePlainTensor(num_elem, self.modulus, self.degree, self.context, self.evaluator, self.batch_encoder) def build_plain_from_torch(self, torch_tensor) -> FhePlainTensor: res = self.build_plain(get_prod(torch_tensor.size())) res.load_from_torch(torch_tensor) return res def decrypt_to_torch(self, fhe_enc_tensor: FheEncTensor, dst=None, to="gpu") -> torch.Tensor: def sub_decrypt(enc): return enc.decrypt(self.decryptor, dst=dst, to=to) if isinstance(fhe_enc_tensor, FheEncTensor): return sub_decrypt(fhe_enc_tensor) elif isinstance(fhe_enc_tensor, list): return torch.stack(list(map(sub_decrypt, fhe_enc_tensor))) else: raise Exception(f"Unexpected type of fhe_enc_tensor: {fhe_enc_tensor}") def noise_budget(self, fhe_enc_tensor: FheEncTensor, name=None): noise_bits = self.decryptor.invariant_noise_budget(fhe_enc_tensor.cts[0]) if name is not None: print(f"{name} noise budget {noise_bits} bits") return noise_bits
assert (len(X[rowIndex]) <= n) X[rowIndex] += [None] * (n - len(X[rowIndex])) for rowIndex in range(n): for colIndex in range(rowIndex + 1, n): assert (X[rowIndex][colIndex] == None) X[rowIndex][colIndex] = X[colIndex][rowIndex] X_array = np.asarray(X) return (X_array) ########################## paramaters required ################################# #N= int(input("Enter dimension of matrix needed to reverse: ")) parms = EncryptionParameters() parms.set_poly_modulus("1x^32768 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(16384)) parms.set_plain_modulus(1 << 30) context = SEALContext(parms) encoderF = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 34, 30, 2) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_key() ev_keys = EvaluationKeys() keygen.generate_evaluation_keys(15, ev_keys) encryptor = Encryptor(context, public_key)
MemoryPoolHandle, \ Plaintext, \ SEALContext, \ EvaluationKeys, \ GaloisKeys, \ PolyCRTBuilder, \ ChooserEncoder, \ ChooserEvaluator, \ ChooserPoly # Set up an instance of the EncryptionParameters class # three encryption parameters that are necessary to set: # - poly_modulus (polynomial modulus); # - coeff_modulus ([ciphertext] coefficient modulus); # - plain_modulus (plaintext modulus). parms = EncryptionParameters() #polynomial modulus must be a power-of-2 cyclotomic polynomial # Recommended degrees for poly_modulus are 1024, 2048, 4096, 8192, 16384, 32768, # but it is also possible to go beyond this. parms.set_poly_modulus("1x^2048 + 1") # 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
def unit_test(): parms = EncryptionParameters() parms.set_poly_modulus("1x^8192 + 1") parms.set_coeff_modulus(seal.coeff_modulus_128(8192)) parms.set_plain_modulus(1 << 10) context = SEALContext(parms) # Print the parameters that we have chosen print_parameters(context) encoder = FractionalEncoder(context.plain_modulus(), context.poly_modulus(), 64, 32, 3) keygen = KeyGenerator(context) public_key = keygen.public_key() secret_key = keygen.secret_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) learningRate = 0.1 learningRate_data = encoder.encode(learningRate) learningRate_e = Ciphertext() encryptor.encrypt(learningRate_data, learningRate_e) updatedVals = [] updatedVals.append(50) updatedVals.append(50) updatedVals_unenc = [] updatedVals_unenc.append(updatedVals[0]) updatedVals_unenc.append(updatedVals[1]) for i in range(15): x = 1 y = 1 updatedVals = learn(x, y, evaluator, updatedVals[0], updatedVals[1], encoder, encryptor, learningRate_e, decryptor) ypred = updatedVals_unenc[0] * x + updatedVals_unenc[1] error = ypred - y updatedVals_unenc[0] = updatedVals_unenc[0] - x * error * learningRate updatedVals_unenc[1] = updatedVals_unenc[1] - error * learningRate print((str)(updatedVals[1]) + ":" + (str)(updatedVals[0]) + ":" + (str)(updatedVals_unenc[1]) + ":" + (str)(updatedVals_unenc[0])) x = 2 y = 3 updatedVals = learn(x, y, evaluator, updatedVals[0], updatedVals[1], encoder, encryptor, learningRate_e, decryptor) ypred = updatedVals_unenc[0] * x + updatedVals_unenc[1] error = ypred - y updatedVals_unenc[0] = updatedVals_unenc[0] - x * error * learningRate updatedVals_unenc[1] = updatedVals_unenc[1] - error * learningRate print((str)(updatedVals[1]) + ":" + (str)(updatedVals[0]) + ":" + (str)(updatedVals_unenc[1]) + ":" + (str)(updatedVals_unenc[0])) x = 4 y = 3 updatedVals = learn(x, y, evaluator, updatedVals[0], updatedVals[1], encoder, encryptor, learningRate_e, decryptor) ypred = updatedVals_unenc[0] * x + updatedVals_unenc[1] error = ypred - y updatedVals_unenc[0] = updatedVals_unenc[0] - x * error * learningRate updatedVals_unenc[1] = updatedVals_unenc[1] - error * learningRate print((str)(updatedVals[1]) + ":" + (str)(updatedVals[0]) + ":" + (str)(updatedVals_unenc[1]) + ":" + (str)(updatedVals_unenc[0])) x = 3 y = 2 updatedVals = learn(x, y, evaluator, updatedVals[0], updatedVals[1], encoder, encryptor, learningRate_e, decryptor) ypred = updatedVals_unenc[0] * x + updatedVals_unenc[1] error = ypred - y updatedVals_unenc[0] = updatedVals_unenc[0] - x * error * learningRate updatedVals_unenc[1] = updatedVals_unenc[1] - error * learningRate print((str)(updatedVals[1]) + ":" + (str)(updatedVals[0]) + ":" + (str)(updatedVals_unenc[1]) + ":" + (str)(updatedVals_unenc[0])) x = 5 y = 5 updatedVals = learn(x, y, evaluator, updatedVals[0], updatedVals[1], encoder, encryptor, learningRate_e, decryptor) ypred = updatedVals_unenc[0] * x + updatedVals_unenc[1] error = ypred - y updatedVals_unenc[0] = updatedVals_unenc[0] - x * error * learningRate updatedVals_unenc[1] = updatedVals_unenc[1] - error * learningRate print((str)(updatedVals[1]) + ":" + (str)(updatedVals[0]) + ":" + (str)(updatedVals_unenc[1]) + ":" + (str)(updatedVals_unenc[0]))