WellAlly Logo
WellAlly康心伴
Development

Homomorphic Encryption: Compute on Encrypted Health Data (Python + TenSEAL)

Run analytics on encrypted patient data—never decrypt. CKKS scheme for real numbers, TenSEAL implementation. Calculate averages and trends while preserving HIPAA compliance.

W
2025-12-12
10 min read

Key Takeaways

  • Homomorphic Encryption Enables Computation on Ciphertext: HE allows servers to perform mathematical operations on encrypted data, producing encrypted results that decrypt to the correct answer—meaning sensitive data never needs to be exposed for processing.
  • CKKS Scheme Handles Real Numbers: The CKKS (Cheon-Kim-Kim-Song) encryption scheme is designed for computations on real numbers with approximate precision, making it ideal for biometric data like heart rates, temperatures, and lab values.
  • Client Keeps the Secret Key: In the HE model, only the client possesses the private key needed for decryption, while the server operates with just the public key—ensuring that even a compromised server cannot access sensitive health information.
  • Parameter Selection Balances Security and Performance: The poly_modulus_degree and coeff_mod_bit_sizes determine both security level and computational capacity—larger values provide more security and allow more complex calculations but significantly slow down operations.
  • Ciphertext Expansion is a Real Cost: Encrypting data dramatically increases its size—a small array of heart rate measurements can expand from a few dozen bytes to hundreds of kilobytes, impacting network bandwidth and storage requirements.

Imagine you're building a revolutionary health tech application. You want to provide users with insightful analytics about their biometric data, like their average resting heart rate over the past week. There's just one major hurdle: this data is incredibly sensitive. A data breach could expose personal health information, leading to a catastrophic loss of user trust and significant legal consequences.

Traditionally, to perform a calculation like an average, a server would need to decrypt the data. During that moment of computation, the plaintext data is vulnerable. What if you could perform calculations directly on the encrypted data? This is the promise of Homomorphic Encryption (HE), a cryptographic technique that feels like magic. It allows a third party (like a cloud server) to process and compute on ciphertext, producing an encrypted result that, when decrypted, is identical to the result of operations performed on the plaintext.

In this tutorial, we'll demystify Homomorphic Encryption by building a practical Python example. We will:

  1. Take a list of sensitive health data (e.g., daily heart rate measurements).
  2. Encrypt this data on the "client-side" using a powerful Python library called TenSEAL.
  3. Send the encrypted data to a "server" which will calculate the average heart rate without having the decryption key.
  4. Return the encrypted average to the client for secure decryption.

This hands-on approach will show you how to leverage advanced cryptography to build privacy-preserving features, a crucial skill in the age of data privacy.

Prerequisites:

  • Knowledge: A solid understanding of Python. No prior cryptography knowledge is required.
  • Tools: Python 3.7+ and the tenseal library.

Understanding the Problem: The Privacy Paradox in Health Tech

The health tech industry faces a paradox: the more data it collects, the more personalized and effective its services can be. However, this wealth of data, which includes everything from heart rates and sleep patterns to genomic sequences, is a prime target for attackers.

Current Solutions and Their Limitations:

  • Encryption at Rest and in Transit: Standard practice involves encrypting data when it's stored in a database (at rest) and when it's being transmitted over a network (in transit). This is essential, but it leaves a critical vulnerability: data must be decrypted for computation.
  • Anonymization: This involves stripping personally identifiable information (PII) from datasets. While useful, re-identification attacks are becoming increasingly sophisticated, and for personalized analytics, you often need to link data back to a specific user.

Our Approach: Computation on Ciphertext Homomorphic Encryption solves the "computation vulnerability." A server can run analytics, train machine learning models, and derive insights from user data without ever seeing the raw, sensitive information. This opens up new possibilities for secure cloud computing, multi-party collaboration in medical research, and truly private personal analytics. For our tutorial, we'll use the CKKS (Cheon-Kim-Kim-Song) scheme, which is specifically designed for computations on real (or approximate) numbers, making it perfect for biometric data.

Prerequisites: Setting Up Your Environment

Before we dive in, let's get our environment ready. You'll need to install the tenseal library.

Open your terminal and run the following command:

code
pip install tenseal
Code collapsed

To verify the installation, start a Python interpreter and import the library:

code
import tenseal as ts
print(ts.__version__)
Code collapsed

You should see the installed version number without any errors.

Step 1: Creating the Homomorphic Encryption Context

The first step in any TenSEAL application is to create a TenSEALContext. This object holds all the crucial parameters for our encryption scheme, including the public key, the private (secret) key, and settings that define the security level and computational capabilities.

What we're doing

We'll configure a context for the CKKS encryption scheme. We need to define three key parameters:

  • poly_modulus_degree: This parameter influences the security level and the number of values we can encrypt in a single vector. It must be a power of two.
  • coeff_mod_bit_sizes: This defines the size of the prime numbers used in the coefficient modulus. It affects the "multiplicative depth" – how many multiplications we can perform on the encrypted data before the noise growth makes the ciphertext undecryptable.
  • global_scale: This determines the precision of our encrypted real numbers. We are essentially working with fixed-point numbers, and this scale factor defines where the decimal point is.

Implementation

code
# src/create_context.py
import tenseal as ts

def create_context():
    """
    Creates and configures a TenSEAL context for Homomorphic Encryption.
    """
    context = ts.context(
        ts.SCHEME_TYPE.CKKS,
        poly_modulus_degree=8192,
        coeff_mod_bit_sizes=
    )
    context.generate_galois_keys()
    context.global_scale = 2**40
    
    return context

if __name__ == '__main__':
    ctx = create_context()
    print("✅ TenSEAL context created successfully.")
    print(f"    Parameters: poly_modulus_degree={ctx.poly_modulus_degree()}, global_scale=2^{ctx.global_scale.bit_length()-1}")

    # The context holds the private key
    secret_key = ctx.secret_key()
    
    # We can also create a public version of the context that doesn't contain the secret key
    public_ctx = ctx.copy()
    public_ctx.make_context_public() # Drops the secret key
    
    print("🔑 Context contains private key:", ctx.is_private())
    print("🔑 Public context contains private key:", public_ctx.is_private())
Code collapsed

How it works

We initialize the context with the CKKS scheme. A poly_modulus_degree of 8192 is a common and secure choice. The coeff_mod_bit_sizes list allows for a certain number of chained multiplications. We also generate galois_keys, which are necessary for performing vector rotation operations—a key step for summing up all the elements in our encrypted vector. Finally, make_context_public() creates a version of the context that can be sent to the server; it contains the public key and evaluation keys but crucially lacks the secret key needed for decryption.

Step 2: Client-Side Encryption

Now that we have a context, let's simulate the client's role. A user has a week's worth of resting heart rate data that they want to get an average for, without revealing the individual numbers to the server.

What we're doing

We will take a list of floating-point numbers representing heart rates and encrypt them into a single CKKSVector. This vector object bundles all the data points into one encrypted entity.

Implementation

code
# src/client.py
import tenseal as ts
from create_context import create_context

def encrypt_health_data(context, data):
    """Encrypts a list of numbers into a CKKS vector."""
    return ts.ckks_vector(context, data)

if __name__ == '__main__':
    # (Client-side)
    # 1. Create context with a secret key
    context = create_context()

    # 2. Sample sensitive health data
    heart_rate_data = [72.5, 71.0, 73.2, 69.8, 70.5, 74.1, 71.7]
    print(f"Plaintext data (client): {heart_rate_data}")
    
    # 3. Encrypt the data
    encrypted_heart_rates = encrypt_health_data(context, heart_rate_data)
    print(f"🔒 Data encrypted successfully into a CKKS vector.")
    
    # 4. Serialize the encrypted data and the public context for sending
    serialized_encrypted_vector = encrypted_heart_rates.serialize()
    
    # Create and serialize the public context
    public_context = context.copy()
    public_context.make_context_public()
    serialized_public_context = public_context.serialize()
    
    # This is what we would send to the server
    # For this tutorial, we'll pass the objects directly
    # In a real app, this would be an API call
    #
    # server_receives(serialized_public_context, serialized_encrypted_vector)
Code collapsed

How it works

The ts.ckks_vector() function takes our context and the list of numbers. It encodes the numbers into a polynomial and then encrypts it using the public key stored within the context. The result is a single CKKSVector object that holds all our heart rate data in an encrypted state.

Step 3: Server-Side Computation on Encrypted Data

This is where the magic happens. The "server" receives the encrypted vector and the public context. Its task is to compute the average of the numbers inside the vector without the secret key.

An average is calculated as sum(values) / count(values). We can do this in two steps:

  1. Sum: Homomorphically sum the elements of the encrypted vector.
  2. Divide: Homomorphically divide the encrypted sum by the public count of elements (which is just multiplication by the inverse).

What we're doing

To sum the elements of the vector, we use a "rotate and sum" strategy. Imagine our vector is [v1, v2, v3, v4]. We can rotate it by one position to get [v2, v3, v4, v1], then add this to the original. We repeat this process, doubling the number of elements being summed in each step, until all elements have been accumulated into each slot of the vector. Finally, we multiply the resulting vector by 1/n to get the average.

Implementation

code
# src/server.py
import math

def calculate_encrypted_average(public_context, encrypted_vector):
    """
    Performs homomorphic operations to calculate the average of an encrypted vector.
    """
    # The server can get the size of the vector without decrypting
    n_values = len(encrypted_vector)

    # 1. Homomorphically sum all elements in the vector
    # This is the most complex step. We use rotations to achieve a horizontal sum.
    encrypted_sum = encrypted_vector.sum()
        
    print("🖥️  Server: Encrypted sum calculated.")

    # 2. Multiply by the inverse of the number of values to get the average
    # Division by a plaintext number is equivalent to multiplication by its inverse.
    encrypted_average = encrypted_sum * (1 / n_values)
    print(f"🖥️  Server: Encrypted average calculated by multiplying with {1/n_values:.4f}.")
    
    return encrypted_average

if __name__ == '__main__':
    # This block simulates the full pipeline
    
    # ====== CLIENT SIDE ======
    from create_context import create_context
    import tenseal as ts

    # 1. Setup
    context = create_context()
    heart_rate_data = [72.5, 71.0, 73.2, 69.8, 70.5, 74.1, 71.7]
    print(f"Plaintext data (client): {heart_rate_data}")
    print(f"Plaintext average: {sum(heart_rate_data) / len(heart_rate_data):.2f}")

    # 2. Encrypt
    encrypted_heart_rates = ts.ckks_vector(context, heart_rate_data)
    print("🔒 Client: Data encrypted.")
    
    # ====== DATA TRANSFER (Client -> Server) ======
    # In a real app, you'd serialize and send over HTTP/RPC
    serialized_vector = encrypted_heart_rates.serialize()
    public_context = context.copy()
    public_context.make_context_public()
    serialized_context = public_context.serialize()

    # ====== SERVER SIDE ======
    # The server only has the public context and the encrypted data
    server_context = ts.context_from(serialized_context)
    server_encrypted_vector = ts.ckks_vector_from(server_context, serialized_vector)
    
    print("☁️  Server: Received encrypted data and public context.")
    
    # 3. Compute
    encrypted_average = calculate_encrypted_average(server_context, server_encrypted_vector)
    
    # ====== DATA TRANSFER (Server -> Client) ======
    serialized_encrypted_average = encrypted_average.serialize()
    
    # ====== CLIENT SIDE ======
    # 4. Decrypt
    print("🔙 Client: Received encrypted average.")
    result_vector = ts.ckks_vector_from(context, serialized_encrypted_average) # Use original context with secret key
    decrypted_average = result_vector.decrypt() # The result is in the first slot
    
    print("\n======== RESULTS ========")
    print(f"✅ Decrypted Average Heart Rate: {decrypted_average:.2f}")
    print("=========================")

Code collapsed

How it works

The encrypted_vector.sum() method in TenSEAL abstracts away the complex "rotate and sum" logic. It uses the Galois keys we generated in Step 1 to perform efficient rotations and additions, ultimately producing a new encrypted vector where each element is the sum of all original elements.

Next, encrypted_sum * (1 / n_values) performs an element-wise multiplication between the encrypted sum and a plain (unencrypted) number. The CKKS scheme supports this operation seamlessly. The server now has an encrypted vector representing the average, which it can send back to the client.

Upon receiving the result, the client uses its original context (which still contains the secret key) to decrypt the message and retrieve the final, accurate average.

Performance Considerations

Homomorphic Encryption is powerful, but it's not free. The computations are significantly slower and more memory-intensive than their plaintext counterparts.

  • Latency: Encrypting, decrypting, and especially computing on encrypted data takes time. For our simple average, it's milliseconds, but for complex machine learning models, it could be seconds or minutes.
  • Ciphertext Size: The serialized encrypted vector is much larger than the original plaintext data. A list of 7 floats (56 bytes) might become a ciphertext of several hundred kilobytes.
  • Parameter Tuning: The security and performance of the CKKS scheme depend heavily on the context parameters (poly_modulus_degree, coeff_mod_bit_sizes). Larger parameters provide more security and allow for more complex computations (more multiplications) but are slower. Choosing the right parameters for your use case is a critical balancing act.

Security Best Practices

  • Key Management: The secret key is the most critical piece of this system. It should never leave the client's device. Standard secure key management practices are paramount.
  • Parameter Selection: Use well-established, secure parameters. The TenSEAL defaults are a good starting point, but for production systems, consult cryptographic literature for recommended security levels (e.g., 128-bit security).
  • Side-Channel Attacks: While the server can't see the data, it can observe computation times and memory access patterns. In high-stakes scenarios, these "side channels" could leak information. Designing systems to be resistant to such attacks is an advanced topic.
  • Don't Roll Your Own Crypto: Always use well-vetted, peer-reviewed libraries like TenSEAL (which is built on Microsoft SEAL).

Conclusion

We've successfully walked through a complete, end-to-end example of performing a calculation on encrypted data. We acted as a client, encrypting sensitive heart rate data, then switched hats to a server that computed the average without ever seeing the raw numbers. This powerful privacy-preserving technique is no longer just theoretical; it's accessible to Python developers through libraries like TenSEAL.

While HE has performance trade-offs, it unlocks the ability to build a new class of secure applications, especially in sensitive domains like healthcare and finance.

Next Steps for Readers:

  • Experiment with other calculations, like calculating the variance or a simple linear regression.
  • Explore the performance impact of changing the context parameters.
  • Try serializing the context and encrypted vectors to files to simulate a more realistic client-server interaction.

Resources

Frequently Asked Questions

Q: Is homomorphic encryption practical for real-time applications like live fitness tracking?

A: For simple operations like averages or sums, HE can work with latencies in the tens to hundreds of milliseconds—potentially acceptable for some use cases. However, complex operations like machine learning inference can take seconds or longer. HE is better suited for batch analytics, scheduled reports, or non-interactive processing rather than real-time feedback loops.

Q: How accurate are the results from CKKS encryption compared to plaintext calculations?

A: CKKS provides approximate results with precision controlled by the global_scale parameter. Each operation introduces small amounts of noise, and multiplications compound this noise. For simple calculations like averages on health data, precision is typically more than sufficient. For complex multi-step calculations, you may need to tune parameters or use techniques like bootstrapping to maintain accuracy.

Q: Can homomorphic encryption work with machine learning models for health predictions?

A: Yes! This is an active research area called privacy-preserving machine learning. You can perform inference on encrypted data using HE-compatible models, though this requires specialized model architectures and often involves precomputed lookup tables for non-linear functions like activations. Libraries like TenSEAL provide examples of encrypted neural network inference.

Q: What happens if the client loses their secret key—is all their encrypted health data lost forever?

A: Yes, this is a fundamental property of HE (and most encryption). Without the secret key, encrypted data is permanently unrecoverable. This is why key management is critical—you might implement key recovery mechanisms, secure backup strategies, or key escrow systems depending on your threat model and requirements.

Q: How does homomorphic encryption compare to other privacy-preserving techniques like differential privacy or secure multi-party computation?

A: HE provides provable security by encrypting individual data records, but has significant computational overhead. Differential privacy adds statistical noise to query results, providing privacy guarantees for aggregate analytics. Secure multi-party computation distributes computation across parties so no single party sees complete data. These techniques can be complementary—HE for strong individual privacy, differential privacy for aggregate insights.

Related Articles

#

Article Tags

python
security
privacy
cryptography
datascience
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare Technology
Software Development
User Experience
AI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey