A Step by Step Fully Homomorphic Encryption Example with TenSEAL in Python

We have recently focused on some partially homomorphic encryption algorithms in this blog such as RSA, ElGamal or Paillier. These algorithms comes with multiplicative or additive homomorphic features. On the other hand, we are able to perform both addition and multiplication on encrypted-encrypted vectors and encrypted-plain vectors with fully homomorphic encryption. In this post, we are going to implement a use case with fully homomorphic encryption library TenSEAL in Python.

Dewgong On Body Of Water by pexels

Vlog

You can either continue to read this tutorial or watch the following video. They both cover the implementation of fully homomorphic encryption with TenSEAL in Python on same use case.


🙋‍♂️ You may consider to enroll my top-rated cryptography course on Udemy

Cryptography Basiscs From Scratch In Python

Partially Homomorphic Encryption

Even though fully homomorphic encryption (FHE) has become available in recent times supportingly this tutorial, but when considering the trade-offs, partially homomorphic encryption (PHE) emerges as a more efficient and practical choice. If your specific task doesn’t demand the full homomorphic capabilities, opting for partial homomorphism is the logical decision. PHE is notably faster and demands fewer computational resources compared to FHE. Besides, it generates smaller ciphertexts, making it well-suited for memory-constrained environments. Finally, PHE strikes a favorable balance between security and efficiency for practical use cases.

Herein, LightPHE is a lightweight partially homomorphic encryption library for python. It wraps many partially homomorphic algorithms such as RSA, ElGamal, Exponential ElGamal, Elliptic Curve ElGamal, Paillier, Damgard-Jurik, Okamoto–Uchiyama, Benaloh, Naccache–Stern, Goldwasser–Micali. With LightPHE, you can build homomorphic crypto systems with a couple of lines of code, encrypt & decrypt your data and perform homomorphic operations such as homomorphic addition, homomorphic multiplication, homomorphic xor, regenerate cipher texts, multiplying your cipher text with a known plain constant according to the built algorithm.

# pip install lightphe
from lightphe import LightPHE

# supported algorithms
algorithms = [
  'RSA',
  'ElGamal',
  'Exponential-ElGamal',
  'Paillier',
  'Damgard-Jurik',
  'Okamoto-Uchiyama',
  'Benaloh',
  'Naccache-Stern',
  'Goldwasser-Micali',
  'EllipticCurve-ElGamal'
]

# build a Paillier cryptosystem which is homomorphic
# with respect to the addition
cs = LightPHE(algorithm_name = algorithms[3])

# define plaintexts
m1 = 17
m2 = 23

# calculate ciphertexts
c1 = cs.encrypt(m1)
c2 = cs.encrypt(m2)

# performing homomorphic addition on ciphertexts
assert cs.decrypt(c1 + c2) == m1 + m2

# scalar multiplication (increase its value 5%)
k = 1.05
assert cs.decrypt(k * c1) == k * m1

# pailier is not homomorphic with respect to the multiplication
with pytest.raises(ValueError):
  c1 * c2

# pailier is not homomorphic with respect to the xor
with pytest.raises(ValueError):
  c1 ^ c2

You may consider watching the following demo of LightPHE.

Let’s turn back to the fully homomorphic encryption.

Use case

Our use case requires two roles: data owner and data operator. Data owner is going to the HR department that deciding my salary and store it to the database encrypted. Thereafter, data operator would be a developer. It will update the salary of that person while it does not have the private key and plain salary. Calculations will be done on encrypted data and my new salary is going to be calculated.

Our base salary will be 10,000 USD. We firstly apply 20% wage increase and secondly add 600 USD bonus on encrypted salary. Base salary will be encrypted but we can optionally encrypt wage increase rate and bonus.





Subsidiary functions

We will not use a database in this experiment. Instead, we will save and read everything from file system. Encrypted texts will be in type of bytes. That is why, we are converting bytes data to base64 to store. Similarly, we are converting base64 data to bytes while we are reading data from file system.

import base64

def write_data(file_name: str, data: bytes):
    data = base64.b64encode(data)
    with open(file_name, 'wb') as f: 
        f.write(data)
 
def read_data(file_name: str) -> bytes:
    with open(file_name, 'rb') as f:
        data = f.read()
    return base64.b64decode(data)

Key Generation

We will use TenSEAL library for fully homomorphic encryption functionalities. I am currently using its 0.3.14 version in this experiment.

# !pip install tenseal
import tenseal

Then, we will work on real numbers. That is why, scheme will be CKKS in our context.

context = ts.context(ts.SCHEME_TYPE.CKKS, poly_modulus_degree = 8192, coeff_mod_bit_sizes = [60, 40, 40, 60])
context.generate_galois_keys()
context.global_scale = 2**40

Finally, we will generate private and public key pair. Notice that once you call make_context_public function, then it drops private key. We are then able to store public key without private key information.

secret_context = context.serialize(save_secret_key = True)
write_data('secret.txt', secret_context)
 
context.make_context_public()
public_context = context.serialize()
write_data('public.txt', public_context)

Data owner encrypts salary, wage increase rate and bonus

Key generation is the responsibility of data owner. We dropped the private key in the context to save public key. That is why, we need to restore context with private key information.

context = ts.context_from(read_data(“secret.txt”))

Suppose that my base salary is 10.000 USD. Let’s encrypt and store this into file system with the current context.

salary_plain = [10000]
salary_encrypted = ts.ckks_vector(context, salary_plain)
write_data(“salary_encrypted.txt”, salary_encrypted.serialize())

Besides, we will apply 20% wage increase and 600 USD bonus into this salary. Let’s encrypt those information as well. Multiplying base salary to 1.2 increases it 20%.

wage_weight = [1.2]
wage_weight_encrypted = ts.ckks_vector(context, wage_weight)
write_data('wage_weight_encrypted.txt', wage_weight_encrypted.serialize())

bonus_weight = [600]
bonus_weight_encrypted = ts.ckks_vector(context, bonus_weight)
write_data('bonus_weight_encrypted.txt', bonus_weight_encrypted.serialize())

Data operator calculates the new salary

Data operator will perform calculations while it does not know private key. Let’s load public key for data operator role.





context = ts.context_from(read_data('public.txt'))

Thereafter, data operator will read encrypted salary, wage increase rate and bonus information.

# base salary encrypted
salary_proto = read_data('salary_encrypted.txt')
salary_encrypted = ts.lazy_ckks_vector_from(salary_proto)
salary_encrypted.link_context(context)

# wage increase percentage encrypted
w_proto = read_data('wage_weight_encrypted.txt')
w_encrypted = ts.lazy_ckks_vector_from(w_proto)
w_encrypted.link_context(context)

# bonus encrypted
b_proto = read_data('bonus_weight_encrypted.txt')
b_encrypted = ts.lazy_ckks_vector_from(b_proto)
b_encrypted.link_context(context)

Once, encrypted base salary, encrypted wage increase percentage rate and encrypted bonus are loaded, then we are able to perform calculations on encrypted data.

new_salary_encrypted = ( salary_encrypted * w_encrypted ) + b_encrypted
write_data('new_salary_encrypted.txt', new_salary_encrypted)

Notice that the both wage increase percentage and bonus are encrypted in our use case. Sometimes, we might want to apply plain wage increase percentage and bonus to encrypted salary. This can be done with homomorphic encryption. But data operator will know these information if you will work with plain tensors.

w = ts.plain_vector([1.2])
b = ts.plain_vector([600])

new_salary_encrypted_v2 = ( salary_encrypted * w ) + b
write_data('new_salary_encrypted_v2.txt', new_salary_encrypted_v2)

Data owner decrypts new salary

Wage increase percentage and bonus are added to our base salary in the data operator side and data operator does not know our plain salary. Let’s decrypt the information created by data operator. Notice that context stores both private and public key in the data owner side.

m1_proto = read_data('new_salary_encrypted.txt')
m1 = ts.lazy_ckks_vector_from(m1_proto)
m1.link_context(context)
print(round(m1.decrypt()[0], 2))

m2_proto = read_data('new_salary_encrypted_v2.txt')
m2 = ts.lazy_ckks_vector_from(m2_proto)
m2.link_context(context)
print(round(m2.decrypt()[0], 2))

The both decrypted m1 and m2 values dump the value of 12,600. Our base salary was 10,000 USD; we applied 20% wage increase, and it becomes 12,000 USD; and finally we added 600 USD bonus and its final value becomes 12,600 USD.

Conclusion

So, we have implemented two use cases for fully homomorphic encryption with TenSEAL library in python. The both use case calculates our new salary with yearly wage increase and bonus. This requires both addition and multiplication. Base salary was encrypted in both use cases. On the other hand, we encrypted wage increase and bonus in one use case whereas we used plain wage increase and plain bonus in the second use case. The both use cases will give the same result for the new salary calculation while it does not our plain salary and private key.

Bonus

We have implemented fully homomorphic encryption for a facial recognition pipeline in the past.


Like this blog? Support me on Patreon

Buy me a coffee