Ejemplo n.º 1
0
def test2():
    img_path = '00001.jpg'
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    print("Original image dimensions {}".format(img.shape))
    sift = cv2.xfeatures2d.SIFT_create()
    (kps, desc) = sift.detectAndCompute(img, None)

    desc = preprocessing.normalize(np.array(
        desc.flatten()[:DESC_SIZE]).reshape(1, DESC_SIZE),
                                   norm='l2')
    descriptor_vec1 = desc
    descriptor_vec2 = descriptor_vec1
    context = config()
    public_key, secret_key = keygen(context)

    encoder = IntegerEncoder(context.plain_modulus())
    encryptor = Encryptor(context, public_key)
    crtbuilder = PolyCRTBuilder(context)
    evaluator = Evaluator(context)
    decryptor = Decryptor(context, secret_key)

    slot_count = (int)(crtbuilder.slot_count())
    print("slot count {}".format(slot_count))
    print("Plaintext shape", descriptor_vec1.shape)
    plain_matrix = decompose_plain(slot_count, descriptor_vec1, crtbuilder)

    for i in range(10000):
        encrypted_matrix = Ciphertext()
        print("Encrypting: ")
        time_start = time.time()
        encryptor.encrypt(plain_matrix, encrypted_matrix)
        time_end = time.time()
        print("Done in time {}".format((str)(1000 * (time_end - time_start))))

        print("Square:")
        time_start = time.time()
        evaluator.square(encrypted_matrix)
        time_end = time.time()
        print("Square is done in {} miliseconds".format(
            (str)(1000 * (time_end - time_start))))

        plain_result = Plaintext()
        print("Decryption plain: ")
        time_start = time.time()
        decryptor.decrypt(encrypted_matrix, plain_result)
        time_end = time.time()
        print("Decryption is done in {} miliseconds".format(
            (str)(1000 * (time_end - time_start))))
        # print("Plaintext polynomial: {}".format(plain_result.to_string()))
        # print("Decoded integer: {}".format(encoder.decode_int32(plain_result)))
        print("Noise budget {} bits".format(
            decryptor.invariant_noise_budget(encrypted_matrix)))
Ejemplo n.º 2
0
def example_ckks_basics():
    print("Example: CKKS Basics");

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

    parms = EncryptionParameters(scheme_type.CKKS)

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

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

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

    scale = 2.0**40

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    #First print the true result.

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

    #Decrypt, decode, and print the result.
    decryptor.decrypt(encrypted_result, plain_result)
    result = DoubleVector()
    encoder.decode(plain_result, result)
    print("    + Computed result ...... Correct.")
    print_vector(result)
Ejemplo n.º 3
0
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()))
Ejemplo n.º 4
0
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")
Ejemplo n.º 5
0
    print("\n[+] Proceding to homomorphic functions")

    # dimension of X ->  n (number of individuals) rows and 1+k (1+ number of covariates) cols
    # dimension of y -> vector of length n (number of individuals)
    # dimension of S ->  n (number of individuals) rows and m (number of SNPs)

    #restricting to 10 for calculation  purposes
    #########
    y_encrypted = y_encrypted[:10]
    k = len(X[0])  # k= 3

    print("Y : ")
    print_plain(y_encrypted)

    for elementY in y_encrypted:
        evaluator.square(elementY)
    y_star2 = y_encrypted
    del (y_encrypted)

    print("\nY squared: ")
    print_plain(y_star2)

    print("\nrandom X : ")
    print_plain(X)
    X_star = matrixOperations.colSquare_Sum(X)
    # dimension of S_star2 -> vector of length m (number of SNPs)

    print("\nCol Squared X : ")
    print_plain(X)
    print_plain(X_star)
    print("[=] Finished with homomorphic functions")
Ejemplo n.º 6
0
def example_basics_ii():
    print_example_banner("Example: Basics II")

    # In this example we explain what relinearization is, how to use it, and how
    # it affects noise budget consumption.

    # First we set the parameters, create a SEALContext, and generate the public
    # and secret keys. We use slightly larger parameters than be fore to be able
    # to do more homomorphic multiplications.
    parms = EncryptionParameters()
    parms.set_poly_modulus("1x^8192 + 1")

    # The default coefficient modulus consists of the following primes:

    #     0x7fffffffba0001,
    #     0x7fffffffaa0001,
    #     0x7fffffff7e0001,
    #     0x3fffffffd60001.

    # The total size is 219 bits.
    parms.set_coeff_modulus(seal.coeff_modulus_128(8192))
    parms.set_plain_modulus(1 << 10)

    context = SEALContext(parms)
    print_parameters(context)

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

    # We also set up an Encryptor, Evaluator, and Decryptor here. We will
    # encrypt polynomials directly in this example, so there is no need for
    # an encoder.
    encryptor = Encryptor(context, public_key)
    evaluator = Evaluator(context)
    decryptor = Decryptor(context, secret_key)

    # There are actually two more types of keys in SEAL: `evaluation keys' and
    # `Galois keys'. Here we will discuss evaluation keys, and Galois keys will
    # be discussed later in example_batching().

    # In SEAL, a valid ciphertext consists of two or more polynomials with
    # coefficients integers modulo the product of the primes in coeff_modulus.
    # The current size of a ciphertext can be found using Ciphertext::size().
    # A freshly encrypted ciphertext always has size 2.
    #plain1 = Plaintext("1x^2 + 2x^1 + 3")
    plain1 = Plaintext("1x^2 + 2x^1 + 3")
    encrypted = Ciphertext()

    print("")
    print("Encrypting " + plain1.to_string() + ": ")
    encryptor.encrypt(plain1, encrypted)
    print("Done")
    print("Size of a fresh encryption: " + (str)(encrypted.size()))
    print("Noise budget in fresh encryption: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    # Homomorphic 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 square encrypted twice to observe this growth (also observe noise
    # budget consumption).
    evaluator.square(encrypted)
    print("Size after squaring: " + (str)(encrypted.size()))
    print("Noise budget after squaring: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")
    plain2 = Plaintext()
    decryptor.decrypt(encrypted, plain2)
    print("Second power: " + plain2.to_string())

    evaluator.square(encrypted)
    print("Size after squaring: " + (str)(encrypted.size()))
    print("Noise budget after squaring: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    # It does not matter that the size has grown -- decryption works as usual.
    # Observe from the print-out that the coefficients in the plaintext have
    # grown quite large. One more squaring would cause some of them to wrap
    # around plain_modulus (0x400), and as a result we would no longer obtain
    # the expected result as an integer-coefficient polynomial. We can fix this
    # problem to some extent by increasing plain_modulus. This would make sense,
    # since we still have plenty of noise budget left.
    plain2 = Plaintext()
    decryptor.decrypt(encrypted, plain2)
    print("Fourth power: " + plain2.to_string())

    # The problem here is that homomorphic operations on large ciphertexts are
    # computationally much more costly than on small ciphertexts. Specifically,
    # homomorphic multiplication on input ciphertexts of size M and N will require
    # O(M*N) polynomial multiplications to be performed, and an addition will
    # require O(M+N) additions. Relinearization reduces the size of the ciphertexts
    # after multiplication back to the initial size (2). Thus, relinearizing one
    # or both inputs before the next multiplication, or e.g. before serializing the
    # ciphertexts, can have a huge positive impact on performance.

    # Another problem is that the noise budget consumption in multiplication is
    # bigger when the input ciphertexts sizes are bigger. In a complicated
    # computation the contribution of the sizes to the noise budget consumption
    # can actually become the dominant term. We will point this out again below
    # once we get to our example.

    # Relinearization itself has both a computational cost and a noise budget cost.
    # These both depend on a parameter called `decomposition bit count', which can
    # be any integer at least 1 [dbc_min()] and at most 60 [dbc_max()]. A large
    # decomposition bit count makes relinearization fast, but consumes more noise
    # budget. A small decomposition bit count can make relinearization slower, but
    # might not change the noise budget by any observable amount.

    # Relinearization requires a special type of key called `evaluation keys'.
    # These can be created by the KeyGenerator for any decomposition bit count.
    # To relinearize a ciphertext of size M >= 2 back to size 2, we actually need
    # M-2 evaluation keys. Attempting to relinearize a too large ciphertext with
    # too few evaluation keys will result in an exception being thrown.

    # We repeat our computation, but this time relinearize after both squarings.
    # Since our ciphertext never grows past size 3 (we relinearize after every
    # multiplication), it suffices to generate only one evaluation key.

    # First, we need to create evaluation keys. We use a decomposition bit count
    # of 16 here, which can be thought of as quite small.
    ev_keys16 = EvaluationKeys()

    # This function generates one single evaluation key. Another overload takes
    # the number of keys to be generated as an argument, but one is all we need
    # in this example (see above).
    keygen.generate_evaluation_keys(16, ev_keys16)

    print("")
    print("Encrypting " + plain1.to_string() + ": ")
    encryptor.encrypt(plain1, encrypted)
    print("Done")
    print("Size of a fresh encryption: " + (str)(encrypted.size()))
    print("Noise budget in fresh encryption: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    evaluator.square(encrypted)
    print("Size after squaring: " + (str)(encrypted.size()))
    print("Noise budget after squaring: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    evaluator.relinearize(encrypted, ev_keys16)
    print("Size after relinearization: " + (str)(encrypted.size()))
    print("Noise budget after relinearizing (dbs = " +
          (str)(ev_keys16.decomposition_bit_count()) + "): " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    evaluator.square(encrypted)
    print("Size after second squaring: " + (str)(encrypted.size()) + " bits")
    print("Noise budget after second squaring: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    evaluator.relinearize(encrypted, ev_keys16)
    print("Size after relinearization: " + (str)(encrypted.size()))
    print("Noise budget after relinearizing (dbs = " +
          (str)(ev_keys16.decomposition_bit_count()) + "): " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    decryptor.decrypt(encrypted, plain2)
    print("Fourth power: " + plain2.to_string())

    # Of course the result is still the same, but this time we actually
    # used less of our noise budget. This is not surprising for two reasons:

    #     - We used a very small decomposition bit count, which is why
    #       relinearization itself did not consume the noise budget by any
    #       observable amount;
    #     - Since our ciphertext sizes remain small throughout the two
    #       squarings, the noise budget consumption rate in multiplication
    #       remains as small as possible. Recall from above that operations
    #       on larger ciphertexts actually cause more noise growth.

    # To make matters even more clear, we repeat the computation a third time,
    # now using the largest possible decomposition bit count (60). We are not
    # measuring the time here, but relinearization with these evaluation keys
    # is significantly faster than with ev_keys16.
    ev_keys60 = EvaluationKeys()
    keygen.generate_evaluation_keys(seal.dbc_max(), ev_keys60)

    print("")
    print("Encrypting " + plain1.to_string() + ": ")
    encryptor.encrypt(plain1, encrypted)
    print("Done")
    print("Size of a fresh encryption: " + (str)(encrypted.size()))
    print("Noise budget in fresh encryption: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    evaluator.square(encrypted)
    print("Size after squaring: " + (str)(encrypted.size()))
    print("Noise budget after squaring: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")
    evaluator.relinearize(encrypted, ev_keys60)
    print("Size after relinearization: " + (str)(encrypted.size()))
    print("Noise budget after relinearizing (dbc = " +
          (str)(ev_keys60.decomposition_bit_count()) + "): " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    evaluator.square(encrypted)
    print("Size after second squaring: " + (str)(encrypted.size()))
    print("Noise budget after second squaring: " +
          (str)(decryptor.invariant_noise_budget) + " bits")
    evaluator.relinearize(encrypted, ev_keys60)
    print("Size after relinearization: " + (str)(encrypted.size()))
    print("Noise budget after relinearizing (dbc = " +
          (str)(ev_keys60.decomposition_bit_count()) + "): " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    decryptor.decrypt(encrypted, plain2)
    print("Fourth power: " + plain2.to_string())

    # Observe from the print-out that we have now used significantly more of our
    # noise budget than in the two previous runs. This is again not surprising,
    # since the first relinearization chops off a huge part of the noise budget.

    # However, note that the second relinearization does not change the noise
    # budget by any observable amount. This is very important to understand when
    # optimal performance is desired: relinearization always drops the noise
    # budget from the maximum (freshly encrypted ciphertext) down to a fixed
    # amount depending on the encryption parameters and the decomposition bit
    # count. On the other hand, homomorphic multiplication always consumes the
    # noise budget from its current level. This is why the second relinearization
    # does not change the noise budget anymore: it is already consumed past the
    # fixed amount determinted by the decomposition bit count and the encryption
    # parameters.

    # We now perform a third squaring and observe an even further compounded
    # decrease in the noise budget. Again, relinearization does not consume the
    # noise budget at this point by any observable amount, even with the largest
    # possible decomposition bit count.
    evaluator.square(encrypted)
    print("Size after third squaring " + (str)(encrypted.size()))
    print("Noise budget after third squaring: " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")
    evaluator.relinearize(encrypted, ev_keys60)
    print("Size after relinearization: " + (str)(encrypted.size()))
    print("Noise budget after relinearizing (dbc = " +
          (str)(ev_keys60.decomposition_bit_count()) + "): " +
          (str)(decryptor.invariant_noise_budget(encrypted)) + " bits")

    decryptor.decrypt(encrypted, plain2)
    print("Eighth power: " + plain2.to_string())