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

Gallery generated by Sphinx-Gallery