Note
Go to the end to download the full example code
Integer FHE with BFV scheme¶
Integer FHE Demo for Pyfhel, covering all the posibilities offered by Pyfhel regarding the BFV scheme (see https://eprint.iacr.org/2012/144.pdf).
import numpy as np
from Pyfhel import Pyfhel
1. BFV context and key setup¶
We take a look at the different parameters that can be set for the BFV scheme. Ideally, one should use as little n and t as possible while keeping the correctness in the operations. The noise budget in a freshly encrypted ciphertext is
~ log2(coeff_modulus/plain_modulus) (bits)
By far the most demanding operation is the homomorphic (ciphertext-ciphertext) multiplication, consuming a noise budget of around:
log2(plain_modulus) + (other terms).
HE = Pyfhel() # Creating empty Pyfhel object
bfv_params = {
'scheme': 'BFV', # can also be 'bfv'
'n': 2**13, # Polynomial modulus degree, the num. of slots per plaintext,
# of elements to be encoded in a single ciphertext in a
# 2 by n/2 rectangular matrix (mind this shape for rotations!)
# Typ. 2^D for D in [10, 16]
't': 65537, # Plaintext modulus. Encrypted operations happen modulo t
# Must be prime such that t-1 be divisible by 2^N.
't_bits': 20, # Number of bits in t. Used to generate a suitable value
# for t. Overrides t if specified.
'sec': 128, # Security parameter. The equivalent length of AES key in bits.
# Sets the ciphertext modulus q, can be one of {128, 192, 256}
# More means more security but also slower computation.
}
HE.contextGen(**bfv_params) # Generate context for bfv scheme
HE.keyGen() # Key Generation: generates a pair of public/secret keys
HE.rotateKeyGen() # Rotate key generation --> Allows rotation/shifting
HE.relinKeyGen() # Relinearization key generation
print("\n1. Pyfhel FHE context generation")
print(f"\t{HE}")
1. Pyfhel FHE context generation
<bfv Pyfhel obj at 0x7fecac106a30, [pk:Y, sk:Y, rtk:Y, rlk:Y, contx(n=8192, t=1032193, sec=128, qi=[], scale=1.0, )]>
2. Integer Array Encoding & Encryption¶
we will define two 1D integer arrays, encode and encrypt them: arr1 = [0, 1, … n-1] (length n) arr2 = [-t//2, -1, 1] (length 3) –> Encoding fills the rest of the array with zeros
arr1 = np.arange(bfv_params['n'], dtype=np.int64) # Max possible value is t/2-1. Always use type int64!
arr2 = np.array([-bfv_params['t']//2, -1, 1], dtype=np.int64) # Min possible value is -t/2.
ptxt1 = HE.encodeInt(arr1) # Creates a PyPtxt plaintext with the encoded arr1
ptxt2 = HE.encodeInt(arr2) # plaintexts created from arrays shorter than 'n' are filled with zeros.
ctxt1 = HE.encryptPtxt(ptxt1) # Encrypts the plaintext ptxt1 and returns a PyCtxt
ctxt2 = HE.encryptPtxt(ptxt2) # Alternatively you can use HE.encryptInt(arr2)
# Otherwise, a single call to `HE.encrypt` would detect the data type,
# encode it and encrypt it
#> ctxt1 = HE.encrypt(arr1)
print("\n2. Integer Encoding & Encryption, ")
print("->\tarr1 ", arr1,'\n\t==> ptxt1 ', ptxt1,'\n\t==> ctxt1 ', ctxt1)
print("->\tarr2 ", arr2,'\n\t==> ptxt2 ', ptxt2,'\n\t==> ctxt2 ', ctxt2)
2. Integer Encoding & Encryption,
-> arr1 [ 0 1 2 ... 8189 8190 8191]
==> ptxt1 <Pyfhel Plaintext at 0x7fecaf42b180, scheme=bfv, poly=76277x^8191 + F8950x^8190..., is_ntt=->
==> ctxt1 <Pyfhel Ciphertext at 0x7fecac112400, scheme=bfv, size=2/2, noiseBudget=146>
-> arr2 [-32769 -1 1]
==> ptxt2 <Pyfhel Plaintext at 0x7fecac0dc8c0, scheme=bfv, poly=68C4Bx^8191 + E2400x^8190..., is_ntt=->
==> ctxt2 <Pyfhel Ciphertext at 0x7fecac106360, scheme=bfv, size=2/2, noiseBudget=146>
3. Securely operating on encrypted ingeger arrays¶
- We try all the operations supported by Pyfhel.
Note that, to operate, the ciphertexts/plaintexts must be built with the same context. Internal checks prevent ops between ciphertexts of different contexts.
# Ciphertext-ciphertext ops:
ccSum = ctxt1 + ctxt2 # Calls HE.add(ctxt1, ctxt2, in_new_ctxt=True)
# `ctxt1 += ctxt2` for inplace operation
ccSub = ctxt1 - ctxt2 # Calls HE.sub(ctxt1, ctxt2, in_new_ctxt=True)
# `ctxt1 -= ctxt2` for inplace operation
ccMul = ctxt1 * ctxt2 # Calls HE.multiply(ctxt1, ctxt2, in_new_ctxt=True)
# `ctxt1 *= ctxt2` for inplace operation
cSq = ctxt1**2 # Calls HE.square(ctxt1, in_new_ctxt=True)
# `ctxt1 **= 2` for inplace operation
cNeg = -ctxt1 # Calls HE.negate(ctxt1, in_new_ctxt=True)
#
cPow = ctxt1**3 # Calls HE.power(ctxt1, 3, in_new_ctxt=True)
# `ctxt1 **= 3` for inplace operation
cRotR = ctxt1 >> 2 # Calls HE.rotate(ctxt1, k=2, in_new_ctxt=True)
# `ctxt1 >>= 2` for inplace operation
# WARNING! the encoded data is placed in a n//2 by 2
# matrix. Hence, these rotations apply independently
# to each of the rows!
cRotL = ctxt1 << 2 # Calls HE.rotate(ctxt1, k=-2, in_new_ctxt=True)
# `ctxt1 <<= 2` for inplace operation
cFlip = ctxt1 ^ 1 # Calls HE.flip(ctxt1, k=1, in_new_ctxt=True)
# `ctxt1 |= 1` for inplace operation
cCuAdd = (+ctxt1) # Calls HE.cumul_add(ctxt1, in_new_ctxt=True)
# There is no equivalent for in-place operator, use
# the above call with `in_new_ctxt=False` if required.
# Ciphetext-plaintext ops
cpSum = ctxt1 + ptxt2 # Calls HE.add_plain(ctxt1, ptxt2, in_new_ctxt=True)
# `ctxt1 += ctxt2` for inplace operation
cpSub = ctxt1 - ptxt2 # Calls HE.sub_plain(ctxt1, ptxt2, in_new_ctxt=True)
# `ctxt1 -= ctxt2` for inplace operation
cpMul = ctxt1 * ptxt2 # Calls HE.multiply_plain(ctxt1, ptxt2, in_new_ctxt=True)
# `ctxt1 *= ctxt2` for inplace operation
print("3. Secure operations")
print(" Ciphertext-ciphertext: ")
print("->\tctxt1 + ctxt2 = ccSum: ", ccSum)
print("->\tctxt1 - ctxt2 = ccSub: ", ccSub)
print("->\tctxt1 * ctxt2 = ccMul: ", ccMul)
print(" Single ciphertext: ")
print("->\tctxt1**2 = cSq : ", cSq )
print("->\t- ctxt1 = cNeg : ", cNeg )
print("->\tctxt1**3 = cPow : ", cPow )
print("->\tctxt1 >> 2 = cRotR: ", cRotR)
print("->\tctxt1 << 2 = cRotL: ", cRotL)
print("->\tctxt1 | 1 = cFlip: ", cFlip)
print("->\t(+ctxt1) = cCuAdd: ", cCuAdd)
print(" Ciphertext-plaintext: ")
print("->\tctxt1 + ptxt2 = cpSum: ", cpSum)
print("->\tctxt1 - ptxt2 = cpSub: ", cpSub)
print("->\tctxt1 * ptxt2 = cpMul: ", cpMul)
3. Secure operations
Ciphertext-ciphertext:
-> ctxt1 + ctxt2 = ccSum: <Pyfhel Ciphertext at 0x7fecaf294040, scheme=bfv, size=2/2, noiseBudget=146>
-> ctxt1 - ctxt2 = ccSub: <Pyfhel Ciphertext at 0x7fecac106590, scheme=bfv, size=2/2, noiseBudget=146>
-> ctxt1 * ctxt2 = ccMul: <Pyfhel Ciphertext at 0x7fecaf426ef0, scheme=bfv, size=3/3, noiseBudget=114>
Single ciphertext:
-> ctxt1**2 = cSq : <Pyfhel Ciphertext at 0x7fecac1204a0, scheme=bfv, size=3/3, noiseBudget=114>
-> - ctxt1 = cNeg : <Pyfhel Ciphertext at 0x7fecac120040, scheme=bfv, size=2/2, noiseBudget=146>
-> ctxt1**3 = cPow : <Pyfhel Ciphertext at 0x7fecac120bd0, scheme=bfv, size=2/2, noiseBudget=82>
-> ctxt1 >> 2 = cRotR: <Pyfhel Ciphertext at 0x7fecaf2a3400, scheme=bfv, size=2/2, noiseBudget=143>
-> ctxt1 << 2 = cRotL: <Pyfhel Ciphertext at 0x7fecaf2a3720, scheme=bfv, size=2/2, noiseBudget=143>
-> ctxt1 | 1 = cFlip: <Pyfhel Ciphertext at 0x7fecaf2a3130, scheme=bfv, size=2/2, noiseBudget=143>
-> (+ctxt1) = cCuAdd: <Pyfhel Ciphertext at 0x7fecaf287d10, scheme=bfv, size=2/2, noiseBudget=132>
Ciphertext-plaintext:
-> ctxt1 + ptxt2 = cpSum: <Pyfhel Ciphertext at 0x7fecaf287cc0, scheme=bfv, size=2/2, noiseBudget=146>
-> ctxt1 - ptxt2 = cpSub: <Pyfhel Ciphertext at 0x7fecaf540bd0, scheme=bfv, size=2/2, noiseBudget=146>
-> ctxt1 * ptxt2 = cpMul: <Pyfhel Ciphertext at 0x7fecb13f8810, scheme=bfv, size=2/2, noiseBudget=122>
4. BFV Relinearization: What, why, when¶
- Ciphertext-ciphertext multiplications increase the size of the polynoms
representing the resulting ciphertext. To prevent this growth, the relinearization technique is used (typically right after each c-c mult) to reduce the size of a ciphertext back to the minimal size (two polynoms c0 & c1). For this, a special type of public key called Relinearization Key is used.
- In Pyfhel, you can either generate a relin key with HE.RelinKeyGen() or skip it
and call HE.relinearize() directly, in which case a warning is issued.
Note that HE.power performs relinearization after every multiplication.
print("\n4. Relinearization-> Right after each multiplication.")
print(f"ccMul before relinearization (size {ccMul.size()}): {ccMul}")
~ccMul # Equivalent to HE.relinearize(ccMul). Relin always happens in-place.
print(f"ccMul after relinearization (size {ccMul.size()}): {ccMul}")
print(f"cPow after 2 mult&relin rounds: (size {cPow.size()}): {cPow}")
4. Relinearization-> Right after each multiplication.
ccMul before relinearization (size 3): <Pyfhel Ciphertext at 0x7fecaf426ef0, scheme=bfv, size=3/3, noiseBudget=114>
ccMul after relinearization (size 2): <Pyfhel Ciphertext at 0x7fecaf426ef0, scheme=bfv, size=2/3, noiseBudget=114>
cPow after 2 mult&relin rounds: (size 2): <Pyfhel Ciphertext at 0x7fecac120bd0, scheme=bfv, size=2/2, noiseBudget=82>
5. Decrypt & Decode results¶
- Time to decrypt results! We use HE.decryptInt for this.
HE.decrypt() could also be used, in which case the decryption type would be inferred from the ciphertext metadata.
r1 = HE.decryptInt(ctxt1)
r2 = HE.decryptInt(ctxt2)
rccSum = HE.decryptInt(ccSum)
rccSub = HE.decryptInt(ccSub)
rccMul = HE.decryptInt(ccMul)
rcSq = HE.decryptInt(cSq )
rcNeg = HE.decryptInt(cNeg )
rcPow = HE.decryptInt(cPow )
rcRotR = HE.decryptInt(cRotR)
rcRotL = HE.decryptInt(cRotL)
rcFlip = HE.decryptInt(cFlip)
rcCuAdd = HE.decryptInt(cCuAdd)
rcpSum = HE.decryptInt(cpSum)
rcpSub = HE.decryptInt(cpSub)
rcpMul = HE.decryptInt(cpMul)
print("5. Decrypting results")
print(" Original ciphertexts: ")
print(" ->\tctxt1 --(decr)--> ", r1)
print(" ->\tctxt2 --(decr)--> ", r2)
print(" Ciphertext-ciphertext Ops: ")
print(" ->\tctxt1 + ctxt2 = ccSum --(decr)--> ", rccSum)
print(" ->\tctxt1 - ctxt2 = ccSub --(decr)--> ", rccSub)
print(" ->\tctxt1 * ctxt2 = ccMul --(decr)--> ", rccMul)
print(" Single ciphertext: ")
print(" ->\tctxt1**2 = cSq --(decr)--> ", rcSq )
print(" ->\t- ctxt1 = cNeg --(decr)--> ", rcNeg )
print(" ->\tctxt1**3 = cPow --(decr)--> ", rcPow )
print(" ->\tctxt1 >> 2 = cRotR --(decr)--> ", rcRotR)
print(" ->\tctxt1 << 2 = cRotL --(decr)--> ", rcRotL)
print(" ->\tctxt1 | 1 = cFlip --(decr)--> ", rcFlip)
print(" ->\t(+tctxt1) = cCuAdd -(decr)--> ", rcCuAdd)
print(" Ciphertext-plaintext ops: ")
print(" ->\tctxt1 + ptxt2 = cpSum --(decr)--> ", rcpSum)
print(" ->\tctxt1 - ptxt2 = cpSub --(decr)--> ", rcpSub)
print(" ->\tctxt1 * ptxt2 = cpMul --(decr)--> ", rcpMul)
5. Decrypting results
Original ciphertexts:
-> ctxt1 --(decr)--> [ 0 1 2 ... 8189 8190 8191]
-> ctxt2 --(decr)--> [-32769 -1 1 ... 0 0 0]
Ciphertext-ciphertext Ops:
-> ctxt1 + ctxt2 = ccSum --(decr)--> [-32769 0 3 ... 8189 8190 8191]
-> ctxt1 - ctxt2 = ccSub --(decr)--> [32769 2 1 ... 8189 8190 8191]
-> ctxt1 * ctxt2 = ccMul --(decr)--> [ 0 -1 2 ... 0 0 0]
Single ciphertext:
-> ctxt1**2 = cSq --(decr)--> [ 0 1 4 ... -32824 -16445 -64]
-> - ctxt1 = cNeg --(decr)--> [ 0 -1 -2 ... -8189 -8190 -8191]
-> ctxt1**3 = cPow --(decr)--> [ 0 1 8 ... -425556 -499460 507969]
-> ctxt1 >> 2 = cRotR --(decr)--> [4094 4095 0 ... 8187 8188 8189]
-> ctxt1 << 2 = cRotL --(decr)--> [ 2 3 4 ... 8191 4096 4097]
-> ctxt1 | 1 = cFlip --(decr)--> [4096 4097 4098 ... 4093 4094 4095]
-> (+tctxt1) = cCuAdd -(decr)--> [-512033 -512033 -512033 ... -512033 -512033 -512033]
Ciphertext-plaintext ops:
-> ctxt1 + ptxt2 = cpSum --(decr)--> [-32769 0 3 ... 8189 8190 8191]
-> ctxt1 - ptxt2 = cpSub --(decr)--> [32769 2 1 ... 8189 8190 8191]
-> ctxt1 * ptxt2 = cpMul --(decr)--> [ 0 -1 2 ... 0 0 0]
Total running time of the script: (0 minutes 3.659 seconds)
Estimated memory usage: 10 MB