Hello, HEaaN World!
Copyright ©️ 2025 CryptoLab, Inc. All rights reserved.
Table of Contents:
Welcome to the secure world of HEaaN! Thoughout this book, you will explore the functionalities of HEaaN library, which provides a state-of-the-art implementation for the CKKS fully homomorphic encryption (FHE) scheme 1. Although you are familiar with conventional encryption technologies, homomorphic encryption may feel different from those conventional technologies, especially when various calculations on the ciphertexts are involved. You might be unfamiliar with HEaaN, CKKS, or even FHE itself - but don't worry. We'll guide you through HEaaN step by step throughout the book!
In this chapter, we are going to make a very simple, "hello, world!" program in HEaaN: just an encryption and a decryption with a single secret key. Cryptographers call this a symmetric encryption. Let's see how we can create a ciphertext from a cleartext message and decrypt it back.
Week 1 at SuperSecure, Inc.
One of our potential clients is looking to run an AI model on their own data—but with a key requirement: security. Like many others, they’re facing limited computational resources and are actively seeking an AI inference service. The challenge? Their data is highly sensitive, tightly regulated, and it’s only natural they’re hesitant to share it in plain form.
That’s where we come in. Our company provides a secure computing environment that allows clients to offload AI computations without compromising privacy.
As a new intern, your job is to help make this possible. You won’t need to implement the AI model itself—instead, your focus will be on building the encryption and decryption flow that ensures the client’s data remains private throughout the process.
TASK: One of the simplest use case for homomorphic encryption involves a single client ("data owner") and a computation server. The client encrypts their data using their own secret key, and sends the encrypted ciphertext to the server.
The server receives the ciphertext, conducts whatever computation that needs to be done, and send back the resulting ciphertext to the client.
Then the client can decrypts it using the secret key, which is the same key for the encryption in the first place, and checks out the plain results.
Code
open code
#include "HEaaN/HEaaN.hpp"
#include <iomanip>
#include <iostream>
int main() {
const auto preset{HEaaN::ParameterPreset::SD3};
const auto context = HEaaN::makeContext(preset);
HEaaN::Message message(HEaaN::getLogFullSlots(context));
const auto message_size = message.getSize();
for (HEaaN::u64 i = 0; i < message_size; ++i) {
message[i] = {static_cast<HEaaN::Real>(i) /
static_cast<HEaaN::Real>(message_size),
0.0};
}
const auto sk = HEaaN::SecretKey(context);
const auto enc = HEaaN::Encryptor(context);
auto ctxt = HEaaN::Ciphertext(context);
enc.encrypt(message, sk, ctxt);
const auto eval = HEaaN::HomEvaluator(context);
eval.add(ctxt, 1.0, ctxt);
const auto dec = HEaaN::Decryptor(context);
HEaaN::Message decrypted_message;
dec.decrypt(ctxt, sk, decrypted_message);
std::cout << "Cleartext message: \n";
std::cout << "Original\t\tDecrypted\n";
for (int i = 0; i < 10; i++) {
std::cout << std::fixed << std::setprecision(4) << message[i] << "\t\t"
<< decrypted_message[i] << '\n';
}
}
Explanation
Now let's explore the code line-by-line.
#include "HEaaN/HEaaN.hpp"
The header file HEaaN/HEaaN.hpp
is an all-in-one solution for using HEaaN. While the library also provides many individual headers that define specific objects and components, you can simply include HEaaN/HEaaN.hpp
to access them all.
If you're someone who prefers minimalism, it's perfectly fine to include only the specific headers you need.
const auto preset{HEaaN::ParameterPreset::SD3};
const auto context = HEaaN::makeContext(preset);
These two lines set up the ParameterPreset
and Context
in HEaaN.
A ParameterPreset
in HEaaN is a predefined set of CKKS parameters — from the basic RLWE parameters such as ciphertext dimension and modulus chain length, all the way up to highly technical tuning parameters for better performance. For now, you don’t need to worry too much about the details — just pick one of the presets we provide. Since there are many parameters to choose from, it might not be clear which one is best — but for now, we'll go with ParameterPreset::SD3
, which stands for a somewhat 2 homomorphic encryption parameter ("S") supporting three multiplications.
Context
in HEaaN is the general manager of parameters. As you can see, you’ll need to supply a context object to other agent classes such as Encryptor
or Decryptor
, as well as to data objects like Ciphertext
.
HEaaN::Message message(HEaaN::getLogFullSlots(context));
const auto message_size = message.getSize();
for (int i = 0; i < message_size; ++i) {
message[i] = {static_cast<HEaaN::Real>(i) /
static_cast<HEaaN::Real>(message_size),
0.0};
}
Here you can create and fill input messages. Behind the scenes, a Message
object is nothing more than a vector of complex numbers.
How many complex numbers are in the vector? In CKKS, the size must be a power-of-two, and the upper bound is determined by the preset you've chosen. Since the size is a power-of-two, we can specify its logarithm (base 2) as an argument when creating a Message
object.
For example, HEaaN::Message message(1)
creates a message of size \(2^1 = 2\), and HEaaN::Message message(4)
creates a message of size \(2^4 = 16\). In our case, the public function HEaaN::getLogFullSlots(context)
helps construct a Message
of maximum size, say \(M\). Here, the word slots refers to the components of the complex vector in mathematical terms.
After constructing the Message
, you can fill its slots with any values that are meaningful to you.
In this example, we fill the \(i\)th slot with a complex number whose real part is \(i / M\), where \(M\) is the total number of slots, and whose imaginary part is 0.
const auto sk = HEaaN::SecretKey(context);
Here's the last thing we need to do before encrypting the message: we need to create a secret key from the given context. Behind the scenes, HEaaN will automatically generate a random secret with the help of a cryptographically secure pseudo-random number generator (CSPRNG).
const auto enc = HEaaN::Encryptor(context);
auto ctxt = HEaaN::Ciphertext(context);
enc.encrypt(message, sk, ctxt);
Encryptor
is an agent object in HEaaN responsible for—guess what—encryption.
We construct one, and also create a data object called Ciphertext
, in which the encrypted message will be safely stored. The line enc.encrypt(message, sk, ctxt)
; encrypts message
into ctxt
using the secret key sk
. This is the core routine for symmetric-key encryption.
HEaaN also supports public-key encryption. If you look at the signatures of the overloaded Encryptor::encrypt
functions, you'll see that you can pass a public key object in the second argument (the position currently used for sk
). We'll show you more about that later.
So far, everything described must be done on the client side—that is, the data owner’s side. The encrypted ciphertext ctxt
can then be sent to a computation server. Only the owner of sk
can decrypt and view the contents of ctxt
, so make sure to keep sk
secure.
Once the server receives ctxt
, it can perform any homomorphic operations required. In this example, we add a scalar value of 1.0 to each component of the complex vector that ctxt
encrypts. The result is also a ciphertext, so we store it back into ctxt
to save memory.
const auto eval = HEaaN::HomEvaluator(context);
eval.add(ctxt, 1.0, ctxt);
All the computations the server must do have been done indeed. Now, the server can send the resulting ciphertext back to the client. Suppose the client received the results.
const auto dec = HEaaN::Decryptor(context);
HEaaN::Message decrypted_message;
dec.decrypt(ctxt, sk, decrypted_message);
The few lines above demonstrate how to decrypt the resulting ciphertext on the client side.
Unlike encryption, we can only decrypt with the secret key sk
— although in this example, we also used sk
for encryption.
Now, let’s print out the result using C++'s standard console output functionality.
std::cout << "Cleartext message: \n";
std::cout << "Original\t\tDecrypted\n";
for (int i = 0; i < message_size; i++) {
std::cout << std::fixed << std::setprecision(4) << message[i] << "\t\t"
<< decrypted_message[i] << '\n';
}
Let's run this example. Can you try it yourself? Can you modify the original message and print out similar results?
Copyright ©️ 2025 CryptoLab, Inc. All rights reserved.
No portion of this book may be reproduced in any form without written permission from CryptoLab, Inc.
-
Somewhat homomorphic encryption is a homomorphic encryption with addition and multiplication with a limited number of multiplications. ↩