This library is built upon SEAL-Python, showcasing the power and potential of Homomorphic Encryption (HE) in the context of the zkMe project.
# Configuration for Homomorphic Encryption (HE)

# Degree of the polynomial modulus. This parameter directly affects the security,
# performance, and noise growth in the encryption scheme.
# A higher value increases security and capacity for computations
# but also increases noise growth and reduces performance.
POLY_MODULUS_DEGREE = 8192

# Bit sizes of the coefficient modulus. This is a list of primes that represent the coefficient modulus.
# The number and size of these primes determine the level of security, noise growth,
# and the maximum computation depth.
COEFF_MODULUS_BIT_SIZES = [60, 40, 40, 60]

# The scale used in encoding real numbers into plaintext polynomials.
# It's crucial for the precision of calculations.
# A common choice is a power of two for ease of use in rescaling operations.
SCALE = 2.0 ** 40

# File name for the secret key. The secret key is used for decryption.
SECRET_KEY_NAME = 'secret_key.bin'

# File name for the public key. The public key is used for encryption.
PUBLIC_KEY_NAME = 'public_key.bin'

# File name for the relinearization key.
# This key is used to change encrypted data back to its original form after multiplication operations.
RELIN_KEY_NAME = 'relin_key.bin'

# File name for the Galois key.
# This key is used for operations like rotations in encrypted vectors.
GAL_KEY_NAME = 'gal_key.bin'

config.py
zkMeLabs/zkme-python-seal
import math
from typing import List
import seal
from seal import Ciphertext

from config import SCALE
from lib.get_context import get_context
from lib.create_keys import create_keys
from lib.load_keys import load_keys
from lib.get_he_tools import get_he_tools


class HECryptor(object):
    """
    HECryptor class provides methods for encrypting, decrypting, and performing
    operations on encrypted data using Homomorphic Encryption (HE).
    """
    def __init__(self, keys_dir: str = None):
        """
        Initialize the HECryptor with necessary tools and keys.

        Args:
        - keys_dir (str, optional): Directory where the keys are stored. If not provided, new keys will be generated.
        """
        self.context = get_context()
        if keys_dir is not None:
            self.secret_key, self.public_key, self.relin_key, self.gal_key = load_keys(keys_dir, self.context)
        else:
            self.secret_key, self.public_key, self.relin_key, self.gal_key = create_keys(context=self.context)
        self.scale = SCALE
        self.ckks_encoder, self.slot_count, self.encryptor, self.evaluator, self.decryptor = get_he_tools(
            self.public_key, self.secret_key, self.context)

    def encrypt_data(self, data: List[float]) -> seal.Ciphertext:
        """
        Encrypt a list of floating-point numbers into a ciphertext.

        Args:
        - data (List[float]): List of floating-point numbers to be encrypted.

        Returns:
        - seal.Ciphertext: Encrypted data.
        """
        plain = self.ckks_encoder.encode(data, self.scale)
        cipher = self.encryptor.encrypt(plain)

        return cipher

    def decrypt_data(self, cipher: seal.Ciphertext) -> List[float]:
        """
        Decrypt a ciphertext back into a list of floating-point numbers.

        Args:
        - cipher (seal.Ciphertext): The encrypted data.

        Returns:
        - List[float]: Decrypted list of floating-point numbers.
        """
        plain = self.decryptor.decrypt(cipher)
        data = self.ckks_encoder.decode(plain)

        return data

    def cal_cipher_l2(self, cipher1, cipher2):
        """
        Calculate the L2 (Euclidean) distance between two ciphertexts.

        The process involves the following steps:
        1. Compute the difference between the two ciphertexts.
        2. Square the result.
        3. Sum the squared result.
        4. Compute the square root of the sum.

        Args:
        - cipher1, cipher2: The two ciphertexts for which the L2 distance is to be calculated.

        Returns:
        - seal.Ciphertext: Encrypted L2 distance.
        """
        # a = x - y
        temp = Ciphertext(cipher1)
        self.evaluator.sub_inplace(temp, cipher2)
        # b = a * a
        self.evaluator.multiply_inplace(temp, temp)
        self.evaluator.relinearize_inplace(temp, self.relin_key)
        # c = sum(b)
        encrypted_result = Ciphertext(temp)
        k = 0
        while k < math.log(self.slot_count, 2):
            temp = self.evaluator.rotate_vector(encrypted_result, pow(2, k), self.gal_key)
            self.evaluator.add_inplace(encrypted_result, temp)
            k += 1
        self.evaluator.relinearize_inplace(encrypted_result, self.relin_key)

        # Lower the cipher scale.
        self.evaluator.rescale_to_next_inplace(encrypted_result)

        return encrypted_result


lib/HECryptor.py
zkMeLabs/zkme-python-seal
import os
from seal import KeyGenerator
from config import (
    SECRET_KEY_NAME, PUBLIC_KEY_NAME, RELIN_KEY_NAME, GAL_KEY_NAME
)
from lib.get_context import get_context


def create_keys(save_dir=None, context=None):
    """
    Create and save Homomorphic Encryption (HE) keys.

    Args:
    - save_dir (str, optional): Directory where the keys will be saved. If not provided, keys won't be saved to disk.
    - context (SEALContext, optional): The SEAL context. If not provided, it will be generated.

    Returns:
    - tuple: A tuple containing the secret key, public key, relinearization key, and Galois key.
    """
    # Check if the provided directory exists, if not, create it.
    if (save_dir is not None) and (not os.path.exists(save_dir)):
        os.makedirs(save_dir)
    # If no context is provided, generate one.
    if context is None:
        context = get_context()

    # Initialize the key generator with the given context. Generate the keys.
    keygen = KeyGenerator(context)
    public_key = keygen.create_public_key()
    secret_key = keygen.secret_key()
    relin_key = keygen.create_relin_keys()
    gal_key = keygen.create_galois_keys()

    # If a save directory is provided, save the keys to the specified directory.
    if save_dir is not None:
        secret_key.save(os.path.join(save_dir, SECRET_KEY_NAME))
        public_key.save(os.path.join(save_dir, PUBLIC_KEY_NAME))
        relin_key.save(os.path.join(save_dir, RELIN_KEY_NAME))
        gal_key.save(os.path.join(save_dir, GAL_KEY_NAME))

    # Return the generated keys.
    return secret_key, public_key, relin_key, gal_key

lib/create_keys.py
zkMeLabs/zkme-python-seal
from seal import EncryptionParameters, scheme_type, CoeffModulus, SEALContext
from config import POLY_MODULUS_DEGREE, COEFF_MODULUS_BIT_SIZES


def get_context(
        scheme=scheme_type.ckks, poly_modulus_degree=POLY_MODULUS_DEGREE,
        coeff_modulus_bit_sizes=COEFF_MODULUS_BIT_SIZES):
    """
    Create and return a SEAL context based on the provided or default configuration parameters.

    Args:
    - scheme (scheme_type, optional): The encryption scheme type (e.g., CKKS or BFV). Default is CKKS.
    - poly_modulus_degree (int, optional): Degree of the polynomial modulus. Default is from the config.
    - coeff_modulus_bit_sizes (list of int, optional): Bit sizes of the coefficient modulus. Default is from the config.

    Returns:
    - SEALContext: The generated SEAL context.
    """
    # Initialize encryption parameters with the specified scheme.
    parms = EncryptionParameters(scheme)
    # Set the polynomial modulus degree for the encryption parameters.
    parms.set_poly_modulus_degree(poly_modulus_degree)
    # Set the coefficient modulus for the encryption parameters.
    # CoeffModulus.Create is a helper function that
    # generates a list of prime numbers that represent the coefficient modulus.
    parms.set_coeff_modulus(CoeffModulus.Create(poly_modulus_degree, coeff_modulus_bit_sizes))
    # Create and return the SEAL context using the specified encryption parameters.
    context = SEALContext(parms)

    return context

lib/get_context.py
zkMeLabs/zkme-python-seal
from seal import CKKSEncoder, Encryptor, Evaluator, Decryptor
from lib.get_context import get_context


def get_he_tools(public_key, secret_key, context=None):
    """
    Initialize and return the necessary tools for Homomorphic Encryption (HE) operations.

    Args:
    - public_key: The public key used for encryption.
    - secret_key: The secret key used for decryption.
    - context (SEALContext, optional): The SEAL context. If not provided, it will be generated.

    Returns:
    - tuple: A tuple containing the CKKS encoder, slot count, encryptor, evaluator, and decryptor.
    """
    # If no context is provided, generate one.
    if context is None:
        context = get_context()
    # Initialize the CKKS encoder with the given context.
    # The encoder is responsible for encoding and decoding plaintexts.
    ckks_encoder = CKKSEncoder(context)
    # Get the slot count. In CKKS, a slot can hold a single real or complex number.
    # The slot count often equals the poly modulus degree / 2.
    slot_count = ckks_encoder.slot_count()
    # Initialize the encryptor with the given context and public key.
    encryptor = Encryptor(context, public_key)
    # Initialize the evaluator with the given context.
    # The evaluator is responsible for performing operations on ciphertexts.
    evaluator = Evaluator(context)
    # Initialize the decryptor with the given context and secret key.
    decryptor = Decryptor(context, secret_key)

    return ckks_encoder, slot_count, encryptor, evaluator, decryptor

lib/get_he_tools.py
zkMeLabs/zkme-python-seal
import os
from seal import SecretKey, PublicKey, RelinKeys, GaloisKeys
from config import (
    SECRET_KEY_NAME, PUBLIC_KEY_NAME, RELIN_KEY_NAME, GAL_KEY_NAME
)
from lib.get_context import get_context


def load_keys(keys_dir, context=None):
    """
    Load Homomorphic Encryption (HE) keys from the specified directory on disk.

    Args:
    - keys_dir (str): The directory where the keys are stored.
    - context (SEALContext, optional): The SEAL context. If not provided, it will be generated.

    Returns:
    - tuple: A tuple containing the loaded secret key, public key, relinearization key, and Galois key.
    """
    # If no context is provided, generate one.
    if context is None:
        context = get_context()

    # Load the key from the specified file in the keys directory.
    secret_key = SecretKey()
    secret_key.load(context, os.path.join(keys_dir, SECRET_KEY_NAME))
    public_key = PublicKey()
    public_key.load(context, os.path.join(keys_dir, PUBLIC_KEY_NAME))
    relin_key = RelinKeys()
    relin_key.load(context, os.path.join(keys_dir, RELIN_KEY_NAME))
    gal_key = GaloisKeys()
    gal_key.load(context, os.path.join(keys_dir, GAL_KEY_NAME))

    # Return the loaded keys.
    return secret_key, public_key, relin_key, gal_key

lib/load_keys.py
zkMeLabs/zkme-python-seal