Homomorphic Operations, Part 1
Copyright ©️ 2025 CryptoLab, Inc. All rights reserved.
Table of Contents:
In this chapter, you’ll explore the basic homomorphic operations that serve as the foundation for building more complex computation models.
A homomorphic operation is a mathematical operation that behaves identically on both cleartext and encrypted data. This powerful property enables computations to be performed directly on ciphertexts—producing encrypted results that, once decrypted, match the results of operations performed on the original data.
You’ll be working with the HomEvaluator
class to perform these operations.
As you go through the examples, you’ll implement a simple linear model on encrypted data using homomorphic operations. Along the way, you'll also learn how evaluation keys play a crucial role in enabling these encrypted computations.
Week 4 at SuperSecure, Inc.
Up to now, we have encrypted client queries and evaluated the linear model, which has cleartext weights in Week 2. But the situation has changed. As our service continues to expand, we have grown more concerned about the security of model weights, which should also be treated as private and properly protected.
For this reason, the linear model should be inference on encrypted data. Since both the weights and queries are encrypted using the same secret key, we will provide the encryption key to clients instead of having them encrypt the data with their own secret key.
As the server takes on the role of the data owner, the clients are no longer responsible for key management. Furthermore, we separate the key management system from the computation server, which will consist of a cluster of compute nodes. Please separate the roles of each party but implement each code into a single example code.
TASK: The key manager generates both the encryption key and the evaluation keys. The encryption key is distributed to the client, while the evaluation keys are bundled into a KeyPack and transferred to the server.
The client encrypts the input data using the encryption key and sends the ciphertexts to the server.
The server loads the ciphertexts and the KeyPack, then performs homomorphic operations: one ciphertext-ciphertext multiplication and one constant addition.
Finally, the result is decrypted using the secret key, either by the server or the key manager.
Code
open code
#include "HEaaN/HEaaN.hpp"
#include <complex>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <sstream>
int main() {
const auto preset{HEaaN::ParameterPreset::SD3};
const auto context = HEaaN::makeContext(preset);
const auto sk = HEaaN::SecretKey(context);
const auto pack = HEaaN::KeyPack(context);
const auto keygen = HEaaN::KeyGenerator(context, sk, pack);
std::cout << "Generate encryption / multiplication key ... \n";
keygen.genEncKey();
keygen.genMultKey();
std::cout << "done \n";
std::stringstream stream_pack, stream_enc;
pack.save(stream_pack);
auto enc_key = pack.getEncKey();
save(*enc_key, stream_enc);
////////////////////
/// Client Side ////
////////////////////
HEaaN::Message message(HEaaN::getLogFullSlots(context));
const auto message_size = message.getSize();
for (int i = 0; i < message_size; ++i) {
message[i] = std::rand() / static_cast<HEaaN::Real>(RAND_MAX);
}
const auto enc = HEaaN::Encryptor(context);
auto pack_client = HEaaN::KeyPack(context);
pack_client.loadEncKey(stream_enc);
auto ctxt = HEaaN::Ciphertext(context);
enc.encrypt(message, pack_client, ctxt);
std::stringstream stream;
ctxt.save(stream);
////////////////////
/// Server Side ////
////////////////////
auto ctxt_query = HEaaN::Ciphertext(context);
ctxt_query.load(stream);
auto pack_server = HEaaN::KeyPack(context);
pack_server.load(stream_pack);
auto weights = HEaaN::Message(HEaaN::getLogFullSlots(context));
for (int i = 0; i < weights.getSize(); ++i) {
weights[i] = {static_cast<HEaaN::Real>(i) /
static_cast<HEaaN::Real>(message_size),
0.0};
}
const HEaaN::Real bias = 0.5;
auto ctxt_weights = HEaaN::Ciphertext(context);
enc.encrypt(weights, pack_server, ctxt_weights);
const auto eval = HEaaN::HomEvaluator(context, pack_server);
// TASK : Evaluate linear model on encrypted data: y = x * w + b
auto ctxt_res = HEaaN::Ciphertext(context);
eval.mult(ctxt_query, ctxt_weights, ctxt_res);
eval.add(ctxt_res, bias, ctxt_res);
const auto dec = HEaaN::Decryptor(context);
HEaaN::Message decrypted_message;
dec.decrypt(ctxt_res, sk, decrypted_message);
// Expected message result.
HEaaN::Message result;
eval.mult(message, weights, result);
eval.add(result, bias, result);
// Output last few elements of the computed and expected results
std::cout << "Cleartext message: \n";
std::cout << "Expected\t\tComputed\n";
for (HEaaN::u64 i = message_size - 10; i < message_size; i++) {
std::cout << std::fixed << std::setprecision(4) << result[i] << "\t\t"
<< decrypted_message[i] << '\n';
}
HEaaN::Real max_error = std::transform_reduce(
result.begin(), result.end(),
decrypted_message.begin(), 0.0,
[](auto x, auto y) { return std::max(x, y); },
[](auto x, auto y) { return abs(x - y); });
std::cout << "Log max error: " << std::log2(max_error) << "\n";
}
Explanation
Let's explore the code line by line!
We’ve structured the code into three logical parts: key manager side, client side, and server side. Don’t feel intimidated by the new role — the key manager is straightforward once you understand its responsibilities. The Key Manager is responsible for securely holding the secret key used for encryption. It could be implemented using secure hardware like an HSM (Hardware Security Module) or a TEE (Trusted Execution Environment).
const auto sk = HEaaN::SecretKey(context);
const auto pack = HEaaN::KeyPack(context);
const auto keygen = HEaaN::KeyGenerator(context, sk, pack);
We start by creating a KeyPack
object and a KeyGenerator
object. For the details of these classes, see the previous chapter.
std::cout << "Generate encryption / multiplication key ... \n";
keygen.genEncKey();
keygen.genMultKey();
std::cout << "done \n";
std::stringstream stream_pack, stream_enc;
pack.save(stream_pack);
auto enc_key = pack.getEncKey();
save(*enc_key, stream_enc);
The KeyGenerator
offers several genXXKey()
APIs to generate specific keys, such as encryption key and multiplication key, all of which are, by default, stored in the internal KeyPack
.
In our scenario:
- The client needs an encryption key to encrypt query data.
- The server needs a multiplication key to evaluate queries.
You can save keys individually or all at once by storing the entire KeyPack
. This is especially helpful on the server side, where a large number of keys are often required — managing them individually would be too much work.
// Client Side
// ...
const auto enc = HEaaN::Encryptor(context);
auto pack_client = HEaaN::KeyPack(context);
pack_client.loadEncKey(stream_enc);
auto ctxt = HEaaN::Ciphertext(context);
enc.encrypt(message, pack_client, ctxt);
std::stringstream stream;
ctxt.save(stream);
If you’ve followed along this far, you’re probably familiar with the basic encryption process. Here's how the client encrypts data using a received key:
The client creates an encryption object and an empty KeyPack
.
It loads the encryption key using the loadEncKey()
API from the KeyPack
.
It then performs public key encryption by passing the KeyPack
as an argument.
Once the data is encrypted, the ciphertext is sent to the server.
// Server side
auto ctxt_query = HEaaN::Ciphertext(context);
ctxt_query.load(stream);
auto pack_server = HEaaN::KeyPack(context);
pack_server.load(stream_pack);
On the server side, the query (now in ciphertext form) and the required keys are retrieved from the Key Manager.
The server loads the ciphertext using a Ciphertext
object from a stream and
retrieves the necessary keys from the preloaded KeyPack
.
auto ctxt_weights = HEaaN::Ciphertext(context);
enc.encrypt(weights, pack_server, ctxt_weights);
Since the weights are the most sensitive part of the model, we also encrypt them using public key encryption to ensure their confidentiality.
const auto eval = HEaaN::HomEvaluator(context, pack_server);
Now it’s time to perform encrypted computation using evaluation keys.
When constructing the Homevaluator
, we pass in the preloaded KeyPack
directly.
Alternatively, you can provide a path to the key directory, in which case the evaluator will build a KeyPack
from that path.
During computation, the evaluator internally fetches the necessary evaluation keys from the KeyPack
and performs the appropriate operations.
// TASK : Evaluate linear model on encrypted data: y = x * w + b
auto ctxt_res = HEaaN::Ciphertext(context);
eval.mult(ctxt_query, ctxt_weights, ctxt_res);
eval.add(ctxt_res, bias, ctxt_res);
Once the multiplication key is loaded, the Homevaluator
is now ready to perform ciphertext multiplication.
As you can see in the code, performing operations between ciphertexts is no different from operations involving cleartext values. Thanks to C++'s function overloading, you can use the same API names for both types — even though the underlying types may differ. This makes homomorphic computations feel intuitive and seamless in code.
const auto dec = HEaaN::Decryptor(context);
HEaaN::Message decrypted_message;
dec.decrypt(ctxt_res, sk, decrypted_message);
// precision test ...
After the inference is complete, the decryption process can be performed either by the server or the Key Manager, depending on the security architecture.
In our example, we assume the server performs the decryption and retrieves the final result. Just like in Chapter 3, this example also includes a precision test to verify that the linear model is working correctly.
Finally, you can run the code yourself to verify whether the decrypted output matches the original cleartext 🚀
Remark
The word ‘homomorphic’ stems from the algebra. It means that a function connecting two algebraic spaces preserves algebraic structures like multiplication. In our case, we have two algebraic spaces; one is a complex vector space that messages live in and the other one is polynomial rings that ciphertexts live in. Both spaces have their own multiplication (the former one is an element-wise multiplication) and the encrypt function preserves the multiplication. In conclusion, it allows multiplication on encrypted data as if it were cleartext data.
Copyright ©️ 2025 CryptoLab, Inc. All rights reserved.
No portion of this book may be reproduced in any form without written permission from CryptoLab, Inc.