Only this pageAll pages
Powered by GitBook
1 of 37

0.2

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Loading...

High Level API

Loading...

Loading...

Loading...

Boolean

Loading...

Loading...

Loading...

Loading...

Shortint

Loading...

Loading...

Loading...

Loading...

Integer

Loading...

Loading...

Loading...

Loading...

C API

Loading...

Loading...

JS on WASM API

Loading...

Low-Level Core Cryptography

Loading...

Loading...

Developers

Loading...

API references

Cryptographic Parameters

integer does not come with its own set of parameters. Instead, it relies on parameters from shortint. Currently, parameter sets having the same space dedicated to the message and the carry (i.e. PARAM_MESSAGE_{X}_CARRY_{X} with X in [1,4]) are recommended. See here for more details about cryptographic parameters, and here to see how to properly instantiate integers depending on the chosen representation.

What is TFHE-rs?

📁 Github | 💛 Community support | 🟨 Zama Bounty Program

TFHE-rs is a pure Rust implementation of TFHE for Boolean and integer arithmetics over encrypted data. It includes a Rust and C API, as well as a client-side WASM API.

TFHE-rs is meant for developers and researchers who want full control over what they can do with TFHE, while not worrying about the low level implementation.

The goal is to have a stable, simple, high-performance, and production-ready library for all the advanced features of TFHE.

Key cryptographic concepts

The TFHE-rs library implements Zama’s variant of Fully Homomorphic Encryption over the Torus (TFHE). TFHE is based on Learning With Errors (LWE), a well-studied cryptographic primitive believed to be secure even against quantum computers.

In cryptography, a raw value is called a message (also sometimes called a cleartext), while an encoded message is called a plaintext and an encrypted plaintext is called a ciphertext.

The idea of homomorphic encryption is that you can compute on ciphertexts while not knowing messages encrypted within them. A scheme is said to be fully homomorphic, meaning any program can be evaluated with it, if at least two of the following operations are supported (xxxis a plaintext and E[x]E[x]E[x] is the corresponding ciphertext):

  • homomorphic univariate function evaluation: f(E[x])=E[f(x)]f(E[x]) = E[f(x)]f(E[x])=E[f(x)]

  • homomorphic addition: E[x]+E[y]=E[x+y]E[x] + E[y] = E[x + y]E[x]+E[y]=E[x+y]

  • homomorphic multiplication: E[x]∗E[y]=E[x∗y]E[x] * E[y] = E[x * y]E[x]∗E[y]=E[x∗y]

Zama's variant of TFHE is fully homomorphic and deals with fixed-precision numbers as messages. It implements all needed homomorphic operations, such as addition and function evaluation via Programmable Bootstrapping. You can read more about Zama's TFHE variant in the preliminary whitepaper.

Using FHE in a Rust program with TFHE-rs consists in:

  • generating a client key and a server key using secure parameters:

    • a client key encrypts/decrypts data and must be kept secret

    • a server key is used to perform operations on encrypted data and could be public (also called an evaluation key)

  • encrypting plaintexts using the client key to produce ciphertexts

  • operating homomorphically on ciphertexts with the server key

  • decrypting the resulting ciphertexts into plaintexts using the client key

If you would like to know more about the problems that FHE solves, we suggest you review our 6 minute introduction to homomorphic encryption.

Quick Start

The core_crypto module from TFHE-rs is dedicated to the implementation of the cryptographic tools related to TFHE. To construct an FHE application, the shortint and/or Boolean modules (based on this one) are recommended.

The core_crypto module offers an API to low-level cryptographic primitives and objects, like lwe_encryption or rlwe_ciphertext. Its goal is to propose an easy-to-use API for cryptographers.

The overall code architecture is split in two parts: one for entity definitions and another focused on algorithms. The entities contain the definition of useful types, like LWE ciphertext or bootstrapping keys. The algorithms are then naturally defined to work using these entities.

The API is convenient to easily add or modify existing algorithms or to have direct access to the raw data. Even if the LWE ciphertext object is defined along with functions giving access to the body, this is also possible to bypass these to get directly the ithi^{th}ith element of LWE mask.

For instance, the code to encrypt and then decrypt a message looks like:

use tfhe::core_crypto::prelude::*;

// DISCLAIMER: these toy example parameters are not guaranteed to be secure or yield correct
// computations
// Define parameters for LweCiphertext creation
let lwe_dimension = LweDimension(742);
let lwe_modular_std_dev = StandardDev(0.000007069849454709433);
let ciphertext_modulus = CiphertextModulus::new_native();

// Create the PRNG
let mut seeder = new_seeder();
let seeder = seeder.as_mut();
let mut encryption_generator =
    EncryptionRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed(), seeder);
let mut secret_generator =
    SecretRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed());

// Create the LweSecretKey
let lwe_secret_key =
    allocate_and_generate_new_binary_lwe_secret_key(lwe_dimension, &mut secret_generator);

// Create the plaintext
let msg = 3u64;
let plaintext = Plaintext(msg << 60);

// Create a new LweCiphertext
let mut lwe = LweCiphertext::new(0u64, lwe_dimension.to_lwe_size(), ciphertext_modulus);

encrypt_lwe_ciphertext(
    &lwe_secret_key,
    &mut lwe,
    plaintext,
    lwe_modular_std_dev,
    &mut encryption_generator,
);

let decrypted_plaintext = decrypt_lwe_ciphertext(&lwe_secret_key, &lwe);

// Round and remove encoding
// First create a decomposer working on the high 4 bits corresponding to our encoding.
let decomposer = SignedDecomposer::new(DecompositionBaseLog(4), DecompositionLevelCount(1));
let rounded = decomposer.closest_representable(decrypted_plaintext.0);

// Remove the encoding
let cleartext = rounded >> 60;

// Check we recovered the original message
assert_eq!(cleartext, msg);

Contributing

There are two ways to contribute to TFHE-rs. You can:

  • open issues to report bugs and typos and to suggest ideas;

  • ask to become an official contributor by emailing [email protected]. Only approved contributors can send pull requests, so get in touch before you do.

Operations

In tfhe::boolean, the available operations are mainly related to their equivalent Boolean gates (i.e., AND, OR... etc). What follows are examples of a unary gate (NOT) and a binary gate (XOR). The last one is about the ternary MUX gate, which allows homomorphic computation of conditional statements of the form If..Then..Else.

The NOT unary gate

Binary gates

The MUX ternary gate

Let ct_1, ct_2, ct_3 be three Boolean ciphertexts. Then, the MUX gate (abbreviation of MUltipleXer) is equivalent to the operation:

This example shows how to use the MUX ternary gate:

Installation

Importing into your project

To use TFHE-rs in your project, you first need to add it as a dependency in your Cargo.toml:

When running code that uses tfhe-rs, it is highly recommended to run in release mode with cargo's --release flag to have the best performances possible, eg: cargo run --release.

Choosing your features

TFHE-rs exposes different cargo features to customize the types and features used.

Kinds.

This crate exposes two kinds of data types. Each kind is enabled by activating its corresponding feature in the TOML line. Each kind may have multiple types:

Kind
Features
Type(s)

Serialization.

The different data types and keys exposed by the crate can be serialized / deserialized.

More information can be found for Boolean and for shortint.

Supported platforms

TFHE-rs is supported on Linux (x86, aarch64), macOS (x86, aarch64) and Windows (x86 with RDSEED instruction).

OS
x86
aarch64

Users who have ARM devices can use TFHE-rs by compiling using the nightly toolchain.

Using TFHE-rs with nightly toolchain.

Install the needed Rust toolchain:

Then, you can either:

  • Manually specify the toolchain to use in each of the cargo commands:

  • Or override the toolchain to use for the current project:

To check the toolchain that Cargo will use by default, you can use the following command:

Serialization/Deserialization

As explained in the Introduction, most types are meant to be shared with the server that performs the computations.

The easiest way to send these data to a server is to use the serialization and deserialization features. tfhe uses the framework. Serde's Serialize and Deserialize functions are implemented on TFHE's types.

To serialize our data, a should be picked. Here, is a good choice, mainly because it is a binary format.

Cryptographic Parameters

All parameter sets provide at least 128-bits of security according to the , with an error probability equal to when computing using programmable bootstrapping. This error probability is due to the randomness added at each encryption (see for more details about the encryption process).

Parameters and message precision

shortint comes with sets of parameters that permit the use of the library functionalities securely and efficiently. Each parameter set is associated to the message and carry precisions. Thus, each key pair is entangled to precision.

The user is allowed to choose which set of parameters to use when creating the pair of keys.

The difference between the parameter sets is the total amount of space dedicated to the plaintext and how it is split between the message buffer and the carry buffer. The syntax chosen for the name of a parameter is: PARAM_MESSAGE_{number of message bits}_CARRY_{number of carry bits}. For example, the set of parameters for a message buffer of 5 bits and a carry buffer of 2 bits is PARAM_MESSAGE_5_CARRY_2.

This example contains keys that are generated to have messages encoded over 2 bits (i.e., computations are done modulus ) with 2 bits of carry.

The PARAM_MESSAGE_2_CARRY_2 parameter set is the default shortint parameter set that you can also use through the tfhe::shortint::prelude::DEFAULT_PARAMETERS constant.

Impact of parameters on the operations

As shown , the choice of the parameter set impacts the operations available and their efficiency.

Generic bi-variate functions.

The computations of bi-variate functions is based on a trick: concatenating two ciphertexts into one. Where the carry buffer is not at least as large as the message one, this trick no longer works. Many bi-variate operations, such as comparisons, then cannot be correctly computed. The only exception concerns multiplication.

Multiplication.

In the case of multiplication, two algorithms are implemented: the first one relies on the bi-variate function trick, where the other one is based on the . To correctly compute a multiplication, the only requirement is to have at least one bit of carry (i.e., using parameter sets PARAM_MESSAGE_X_CARRY_Y with Y>=1). This method is slower than using the other one. Using the smart version of the multiplication automatically chooses which algorithm is used depending on the chosen parameters.

User-defined parameter sets

It is possible to define new parameter sets. To do so, it is sufficient to use the function unsecure_parameters() or to manually fill the Parameter structure fields.

For instance:

Serialization/Deserialization

Since the ServerKey and ClientKey types both implement the Serialize and Deserialize traits, you are free to use any serializer that suits you to save and load the keys to disk.

Here is an example using the bincode serialization library, which serializes to a binary format:

use tfhe::boolean::prelude::*;

fn main() {
// We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys();

// We use the client secret key to encrypt a message:
    let ct_1 = client_key.encrypt(true);

// We use the server public key to execute the NOT gate:
    let ct_not = server_key.not(&ct_1);

// We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_not);
    assert_eq!(output, false);
}
use tfhe::boolean::prelude::*;

fn main() {
// We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys();

// We use the client secret key to encrypt a message:
    let ct_1 = client_key.encrypt(true);
    let ct_2 = client_key.encrypt(false);

// We use the server public key to execute the XOR gate:
    let ct_xor = server_key.xor(&ct_1, &ct_2);

// We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_xor);
    assert_eq!(output, true^false);
}
if ct_1 {
    return ct_2
} else {
    return ct_3
}
use tfhe::boolean::prelude::*;

fn main() {
// We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys();

    let bool1 = true;
    let bool2 = false;
    let bool3 = true;

// We use the client secret key to encrypt a message:
    let ct_1 = client_key.encrypt(true);
    let ct_2 = client_key.encrypt(false);
    let ct_3 = client_key.encrypt(false);


// We use the server public key to execute the NOT gate:
    let ct_xor = server_key.mux(&ct_1, &ct_2, &ct_3);

// We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_xor);
    assert_eq!(output, if bool1 {bool2} else {bool3});
}
use std::fs::File;
use std::io::{Write, Read};
use tfhe::boolean::prelude::*;

fn main() {
// We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys();

// We serialize the keys to bytes:
    let encoded_server_key: Vec<u8> = bincode::serialize(&server_key).unwrap();
    let encoded_client_key: Vec<u8> = bincode::serialize(&client_key).unwrap();

    let server_key_file = "/tmp/ser_example_server_key.bin";
    let client_key_file = "/tmp/ser_example_client_key.bin";

// We write the keys to files:
    let mut file = File::create(server_key_file)
        .expect("failed to create server key file");
    file.write_all(encoded_server_key.as_slice()).expect("failed to write key to file");
    let mut file = File::create(client_key_file)
        .expect("failed to create client key file");
    file.write_all(encoded_client_key.as_slice()).expect("failed to write key to file");

// We retrieve the keys:
    let mut file = File::open(server_key_file)
        .expect("failed to open server key file");
    let mut encoded_server_key: Vec<u8> = Vec::new();
    file.read_to_end(&mut encoded_server_key).expect("failed to read the key");

    let mut file = File::open(client_key_file)
        .expect("failed to open client key file");
    let mut encoded_client_key: Vec<u8> = Vec::new();
    file.read_to_end(&mut encoded_client_key).expect("failed to read the key");

// We deserialize the keys:
    let loaded_server_key: ServerKey = bincode::deserialize(&encoded_server_key[..])
        .expect("failed to deserialize");
    let loaded_client_key: ClientKey = bincode::deserialize(&encoded_client_key[..])
        .expect("failed to deserialize");


    let ct_1 = client_key.encrypt(false);

// We check for equality:
    assert_eq!(false, loaded_client_key.decrypt(&ct_1));
}
tfhe = { version = "0.2.5", features = [ "boolean", "shortint", "integer", "x86_64-unix" ] }

Booleans

boolean

Booleans

ShortInts

shortint

Short unsigned integers

Integers

integer

Arbitrary-sized unsigned integers

Linux

x86_64-unix

aarch64-unix*

macOS

x86_64-unix

aarch64-unix*

Windows

x86_64

Unsupported

rustup toolchain install nightly
cargo +nightly build
cargo +nightly test
rustup override set nightly
# cargo will use the `nightly` toolchain.
cargo build
rustup show
here
here
# Cargo.toml

[dependencies]
# ...
tfhe = { version = "0.2.5", features = ["integer","x86_64-unix"]}
bincode = "1.3.3"
// main.rs

use bincode;

use std::io::Cursor;

use tfhe::{ConfigBuilder, ServerKey, generate_keys, set_server_key, FheUint8};
use tfhe::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>>{
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    let ( client_key, server_key) = generate_keys(config);

    let msg1 = 1;
    let msg2 = 0;

    let value_1 = FheUint8::encrypt(msg1, &client_key);
    let value_2 = FheUint8::encrypt(msg2, &client_key);

    // Prepare to send data to the server
    // The ClientKey is _not_ sent
    let mut serialized_data = Vec::new();
    bincode::serialize_into(&mut serialized_data, &server_key)?;
    bincode::serialize_into(&mut serialized_data, &value_1)?;
    bincode::serialize_into(&mut serialized_data, &value_2)?;

    // Simulate sending serialized data to a server and getting
    // back the serialized result
    let serialized_result = server_function(&serialized_data)?;
    let result: FheUint8 = bincode::deserialize(&serialized_result)?;

    let output: u8 = result.decrypt(&client_key);
    assert_eq!(output, msg1 + msg2);
    Ok(())
}


fn server_function(serialized_data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let mut serialized_data = Cursor::new(serialized_data);
    let server_key: ServerKey = bincode::deserialize_from(&mut serialized_data)?;
    let ct_1: FheUint8 = bincode::deserialize_from(&mut serialized_data)?;
    let ct_2: FheUint8 = bincode::deserialize_from(&mut serialized_data)?;

    set_server_key(server_key);

    let result = ct_1 + ct_2;

    let serialized_result = bincode::serialize(&result)?;

    Ok(serialized_result)
}
serde
data format
bincode
2−402^{-40}2−40
22=42^2 = 422=4
use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
   let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;
    let msg2 = 2;

    // We use the client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);
}
use tfhe::shortint::prelude::*;

fn main() {
    let param = unsafe {
        Parameters::new(
            LweDimension(656),
            GlweDimension(2),
            PolynomialSize(512),
            StandardDev(0.000034119201269311964),
            StandardDev(0.00000004053919869756513),
            DecompositionBaseLog(8),
            DecompositionLevelCount(2),
            DecompositionBaseLog(3),
            DecompositionLevelCount(4),
            StandardDev(0.00000000037411618952047216),
            DecompositionBaseLog(15),
            DecompositionLevelCount(1),
            DecompositionLevelCount(0),
            DecompositionBaseLog(0),
            MessageModulus(4),
            CarryModulus(1),
            CiphertextModulus::new_native(),
        )
    };
}
Lattice-Estimator
here
here
quarter square method

Tutorial

Using the JS on WASM API

Welcome to this TFHE-rs JS on WASM API tutorial.

TFHE-rs uses WASM to expose a JS binding to the client-side primitives, like key generation and encryption, of the Boolean and shortint modules.

There are several limitations at this time. Due to a lack of threading support in WASM, key generation can be too slow to be practical for bigger parameter sets.

Some parameter sets lead to FHE keys that are too big to fit in the 2GB memory space of WASM. This means that some parameter sets are virtually unusable.

First steps using TFHE-rs JS on WASM API

Setting-up TFHE-rs JS on WASM API for use in nodejs programs.

To build the JS on WASM bindings for TFHE-rs, you need to install wasm-pack in addition to a compatible (>= 1.65) rust toolchain.

In a shell, then run the following to clone the TFHE-rs repo (one may want to checkout a specific tag, here the default branch is used for the build):

$ git clone https://github.com/zama-ai/tfhe-rs.git
Cloning into 'tfhe-rs'...
...
Resolving deltas: 100% (3866/3866), done.
$ cd tfhe-rs
$ cd tfhe
$ rustup run wasm-pack build --release --target=nodejs --features=boolean-client-js-wasm-api,shortint-client-js-wasm-api
[INFO]: Compiling to Wasm...
...
[INFO]: :-) Your wasm pkg is ready to publish at ...

The command above targets nodejs. A binding for a web browser can be generated as well using --target=web. This use case will not be discussed in this tutorial.

Both Boolean and shortint features are enabled here, but it's possible to use one without the other.

After the build, a new directory pkg is present in the tfhe directory.

$ ls pkg
LICENSE  index.html  package.json  tfhe.d.ts  tfhe.js  tfhe_bg.txt  tfhe_bg.wasm  tfhe_bg.wasm.d.ts
$

Commented code to generate keys for shortint and encrypt a ciphertext

Be sure to update the path of the required clause in the example below for the TFHE package that was just built.

// Here import assert to check the decryption went well and panic otherwise
const assert = require('node:assert').strict;
// Import the Shortint module from the TFHE-rs package generated earlier
const { Shortint } = require("/path/to/built/tfhe/pkg");

function shortint_example() {
    // Get pre-defined parameters from the shortint module to manage messages with 4 bits of useful
    // information in total (2 bits of "message" and 2 bits of "carry")
    let params = Shortint.get_parameters(2, 2);
    // Create a new secret ClientKey, this must not be shared
    console.log("Generating client keys...")
    let cks = Shortint.new_client_key(params);
    // Encrypt 3 in a ciphertext
    console.log("Encrypting 3...")
    let ct = Shortint.encrypt(cks, BigInt(3));

    // Demonstrate ClientKey serialization (for example saving it on disk on the user device)
    let serialized_cks = Shortint.serialize_client_key(cks);
    // Deserialization
    let deserialized_cks = Shortint.deserialize_client_key(serialized_cks);

    // Demonstrate ciphertext serialization to send over the network
    let serialized_ct = Shortint.serialize_ciphertext(ct);
    // Deserialize a ciphertext received over the network for example
    let deserialized_ct = Shortint.deserialize_ciphertext(serialized_ct);

    // Decrypt with the deserialized objects
    console.log("Decrypting ciphertext...")
    let decrypted = Shortint.decrypt(deserialized_cks, deserialized_ct);
    // Check decryption works as expected
    assert.deepStrictEqual(decrypted, BigInt(3));
    console.log("Decryption successful!")

    // Generate public evaluation keys, also called ServerKey
    console.log("Generating compressed ServerKey...")
    let sks = Shortint.new_compressed_server_key(cks);

    // Can be serialized to send over the network to the machine doing the evaluation
    let serialized_sks = Shortint.serialize_compressed_server_key(sks);
    let deserialized_sks = Shortint.deserialize_compressed_server_key(serialized_sks);
    console.log("All done!")
}

shortint_example();

The example.js script can then be run using node, like so:

$ node example.js
Generating client keys...
Encrypting 3...
Decrypting ciphertext...
Decryption successful!
Generating compressed ServerKey...
All done!
$

Tutorial

The steps to homomorphically evaluate a circuit are described below.

Key generation

tfhe::shortint provides 3 key types:

  • ClientKey

  • ServerKey

  • PublicKey

The ClientKey is the key that encrypts and decrypts messages (integer values up to 8 bits here). It is meant to be kept private and should never be shared. This key is created from parameter values that will dictate both the security and efficiency of computations. The parameters also set the maximum number of bits of message encrypted in a ciphertext.

The ServerKey is the key that is used to actually do the FHE computations. Most importantly, it contains a bootstrapping key and a keyswitching key. This key is created from a ClientKey that needs to be shared to the server, therefore it is not meant to be kept private. A user with a ServerKey can compute on the encrypted data sent by the owner of the associated ClientKey.

Computation/operation methods are tied to the ServerKey type.

The PublicKey is the key used to encrypt messages. It can be publicly shared to allow users to encrypt data such that only the ClientKey holder will be able to decrypt. Encrypting with the PublicKey does not alter the homomorphic capabilities associated to the ServerKey.

use tfhe::shortint::prelude::*;

fn main()  {
    // We generate a set of client/server keys
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
}

Encrypting values

Once the keys have been generated, the client key is used to encrypt data:

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys
   let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 1;
    let msg2 = 0;

    // We use the client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);
}

Encrypting values using a public key

Once the keys have been generated, the client key is used to encrypt data:

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys
   let (client_key, _) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
   let public_key = PublicKeyBig::new(&client_key);

    let msg1 = 1;
    let msg2 = 0;

    // We use the client key to encrypt two messages:
    let ct_1 = public_key.encrypt(msg1);
    let ct_2 = public_key.encrypt(msg2);
}

Computing and decrypting

With the server_key, addition is now possible over encrypted values. The resulting plaintext is recovered after the decryption with the secret client key.

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 1;
    let msg2 = 0;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    // We use the server public key to execute an integer circuit:
    let ct_3 = server_key.unchecked_add(&ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_3);
    assert_eq!(output, (msg1 + msg2) % modulus as u64);
}

Cryptographic Parameters

Default parameters

The TFHE cryptographic scheme relies on a variant of Regev cryptosystem and is based on a problem so difficult that it is even post-quantum resistant.

Some cryptographic parameters will require tuning to ensure both the correctness of the result and the security of the computation.

To make it simpler, we've provided two sets of parameters, which ensure correct computations for a certain probability with the standard security of 128 bits. There exists an error probability due to the probabilistic nature of the encryption, which requires adding randomness (noise) following a Gaussian distribution. If this noise is too large, the decryption will not give a correct result. There is a trade-off between efficiency and correctness: generally, using a less efficient parameter set (in terms of computation time) leads to a smaller risk of having an error during homomorphic evaluation.

In the two proposed sets of parameters, the only difference lies in this probability error. The default parameter set ensures a probability error of at most 2−402^{-40}2−40 when computing a programmable bootstrapping (i.e., any gates but the not). The other one is closer to the error probability claimed in the original TFHE paper, namely 2−1652^{-165}2−165, but it is up-to-date regarding security requirements.

The following array summarizes this:

Parameter set
Error probability

DEFAULT_PARAMETERS

TFHE_LIB_PARAMETERS

User-defined parameters

You can also create your own set of parameters. This is an unsafe operation as failing to properly fix the parameters will result in an incorrect and/or insecure computation:

use tfhe::boolean::prelude::*;

fn main() {
// WARNING: might be insecure and/or incorrect
// You can create your own set of parameters
    let parameters = unsafe {
        BooleanParameters::new(
            LweDimension(586),
            GlweDimension(2),
            PolynomialSize(512),
            StandardDev(0.00008976167396834998),
            StandardDev(0.00000002989040792967434),
            DecompositionBaseLog(8),
            DecompositionLevelCount(2),
            DecompositionBaseLog(2),
            DecompositionLevelCount(5),
        )
    };
}
2−402^{-40}2−40
2−1652^{-165}2−165

High-Level API

#Using the High-level C API

This library exposes a C binding to the high-level TFHE-rs primitives to implement Fully Homomorphic Encryption (FHE) programs.

First steps using TFHE-rs C API

Setting-up TFHE-rs C API for use in a C program.

TFHE-rs C API can be built on a Unix x86_64 machine using the following command:

RUSTFLAGS="-C target-cpu=native" cargo +nightly build --release --features=x86_64-unix,high-level-c-api -p tfhe

or on a Unix aarch64 machine using the following command:

RUSTFLAGS="-C target-cpu=native" cargo build +nightly --release --features=aarch64-unix,high-level-c-api -p tfhe

The tfhe.h header as well as the static (.a) and dynamic (.so) libtfhe binaries can then be found in "${REPO_ROOT}/target/release/"

The build system needs to be set up so that the C or C++ program links against TFHE-rs C API binaries.

Here is a minimal CMakeLists.txt to do just that:

project(my-project)

cmake_minimum_required(VERSION 3.16)

set(TFHE_C_API "/path/to/tfhe-rs/binaries/and/header")

include_directories(${TFHE_C_API})
add_library(tfhe STATIC IMPORTED)
set_target_properties(tfhe PROPERTIES IMPORTED_LOCATION ${TFHE_C_API}/libtfhe.a)

if(APPLE)
    find_library(SECURITY_FRAMEWORK Security)
    if (NOT SECURITY_FRAMEWORK)
        message(FATAL_ERROR "Security framework not found")
    endif()
endif()

set(EXECUTABLE_NAME my-executable)
add_executable(${EXECUTABLE_NAME} main.c)
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(${EXECUTABLE_NAME} LINK_PUBLIC tfhe m pthread dl)
if(APPLE)
    target_link_libraries(${EXECUTABLE_NAME} LINK_PUBLIC ${SECURITY_FRAMEWORK})
endif()
target_compile_options(${EXECUTABLE_NAME} PRIVATE -Werror)

Commented code of a uint128 subtraction using TFHE-rs C API.

WARNING: The following example does not have proper memory management in the error case to make it easier to fit the code on this page.

To run the example below, the above CMakeLists.txt and main.c files need to be in the same directory. The commands to run are:

# /!\ Be sure to update CMakeLists.txt to give the absolute path to the compiled tfhe library
$ ls
CMakeLists.txt  main.c
$ mkdir build && cd build
$ cmake .. -DCMAKE_BUILD_TYPE=RELEASE
...
$ make
...
$ ./my-executable
Result: 2
$
#include <tfhe.h>

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>

int main(void)
{
    int ok = 0;
    // Prepare the config builder for the high level API and choose which types to enable
    ConfigBuilder *builder;
    Config *config;

    // Put the builder in a default state without any types enabled
    config_builder_all_disabled(&builder);
    // Enable the uint128 type using the small LWE key for encryption
    config_builder_enable_default_uint128_small(&builder);
    // Populate the config
    config_builder_build(builder, &config);

    ClientKey *client_key = NULL;
    ServerKey *server_key = NULL;

    // Generate the keys using the config
    generate_keys(config, &client_key, &server_key);
    // Set the server key for the current thread
    set_server_key(server_key);

    FheUint128 *lhs = NULL;
    FheUint128 *rhs = NULL;
    FheUint128 *result = NULL;

    // Encrypt a u128 using 64 bits words, we encrypt 20 << 64 | 10
    ok = fhe_uint128_try_encrypt_with_client_key_u128(10, 20, client_key, &lhs);
    assert(ok == 0);

    // Encrypt a u128 using words, we encrypt 2 << 64 | 1
    ok = fhe_uint128_try_encrypt_with_client_key_u128(1, 2, client_key, &rhs);
    assert(ok == 0);

    // Compute the subtraction
    ok = fhe_uint128_sub(lhs, rhs, &result);
    assert(ok == 0);

    uint64_t w0, w1;
    // Decrypt
    ok = fhe_uint128_decrypt(result, client_key, &w0, &w1);
    assert(ok == 0);

    // Here the subtraction allows us to compare each word
    assert(w0 == 9);
    assert(w1 == 18);

    // Destroy the ciphertexts
    fhe_uint128_destroy(lhs);
    fhe_uint128_destroy(rhs);
    fhe_uint128_destroy(result);

    // Destroy the keys
    client_key_destroy(client_key);
    server_key_destroy(server_key);
    return EXIT_SUCCESS;
}

Tutorial

The steps to homomorphically evaluate an integer circuit are described here.

Key Types

integer provides 3 basic key types:

  • ClientKey

  • ServerKey

  • PublicKey

The ClientKey is the key that encrypts and decrypts messages, thus this key is meant to be kept private and should never be shared. This key is created from parameter values that will dictate both the security and efficiency of computations. The parameters also set the maximum number of bits of message encrypted in a ciphertext.

The ServerKey is the key that is used to actually do the FHE computations. It contains a bootstrapping key and a keyswitching key. This key is created from a ClientKey that needs to be shared to the server, so it is not meant to be kept private. A user with a ServerKey can compute on the encrypted data sent by the owner of the associated ClientKey.

To reflect that, computation/operation methods are tied to the ServerKey type.

The PublicKey is a key used to encrypt messages. It can be publicly shared to allow users to encrypt data such that only the ClientKey holder will be able to decrypt. Encrypting with the PublicKey does not alter the homomorphic capabilities associated to the ServerKey.

1. Key Generation

To generate the keys, a user needs two parameters:

  • A set of shortint cryptographic parameters.

  • The number of ciphertexts used to encrypt an integer (we call them "shortint blocks").

We are now going to build a pair of keys that can encrypt an 8-bit integer by using 4 shortint blocks that store 2 bits of message each.

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);
}

2. Encrypting values

Once we have our keys, we can encrypt values:

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 128u64;
    let msg2 = 13u64;

    // We use the client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);
}

3. Encrypting values with the public key

Once the client key is generated, the public key can be derived and used to encrypt data.

use tfhe::integer::gen_keys_radix;
use tfhe::integer::PublicKeyBig;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let num_block = 4;
    let (client_key, _) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    //We generate the public key from the secret client key:
    let public_key = PublicKeyBig::new(&client_key);

    //encryption
    let msg1 = 128u64;
    let msg2 = 13u64;

    // We use the public key to encrypt two messages:
    let ct_1 = public_key.encrypt_radix(msg1, num_block);
    let ct_2 = public_key.encrypt_radix(msg2, num_block);
}

4. Computing and decrypting

With our server_key, and encrypted values, we can now do an addition and then decrypt the result.

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 128;
    let msg2 = 13;

    // message_modulus^vec_length
    let modulus = client_key.parameters().message_modulus.0.pow(num_block as u32) as u64;

    // We use the client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    // We use the server public key to execute an integer circuit:
    let ct_3 = server_key.unchecked_add(&ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output: u64 = client_key.decrypt(&ct_3);

    assert_eq!(output, (msg1 + msg2) % modulus);
}

Security and Cryptography

TFHE

TFHE-rs is a cryptographic library dedicated to Fully Homomorphic Encryption. As its name suggests, it is based on the TFHE scheme.

It is necessary to understand some basics about TFHE to comprehend where the limitations are coming from, both in terms of precision (number of bits used to represent plaintext values) and execution time (why TFHE operations are slower than native operations).

LWE ciphertexts

Although there are many kinds of ciphertexts in TFHE, all the encrypted values in TFHE-rs are mainly stored as LWE ciphertexts.

The security of TFHE relies on the LWE problem, which stands for Learning With Errors. The problem is believed to be secure against quantum attacks.

An LWE Ciphertext is a collection of 32-bit or 64-bit unsigned integers. Before encrypting a message in an LWE ciphertext, one must first encode it as a plaintext. This is done by shifting the message to the most significant bits of the unsigned integer type used.

Then, a little random value called noise is added to the least significant bits. This noise (also called error for Learning With Errors) is crucial to the security of the ciphertext.

plaintext=(Δ∗m)+eplaintext = (\Delta * m) + eplaintext=(Δ∗m)+e

To go from a plaintext to a ciphertext, one must encrypt the plaintext using a secret key.

An LWE secret key is a list of n random integers: S=(s0,...,sn)S = (s_0, ..., s_n)S=(s0​,...,sn​). nnn is called the LweDimensionLweDimensionLweDimension

A LWE ciphertext is composed of two parts:

  • The mask (a0,...,an−1)(a_0, ..., a_{n-1})(a0​,...,an−1​)

  • The body bbb

The mask of a fresh ciphertext (one that is the result of an encryption and not an operation, such as ciphertext addition) is a list of n uniformly random values.

The body is computed as follows:

b=(∑i=0n−1ai∗si)+plaintextb = (\sum_{i = 0}^{n-1}{a_i * s_i}) + plaintextb=(∑i=0n−1​ai​∗si​)+plaintext

Now that the encryption scheme is defined, let's review the example of the addition between ciphertexts to illustrate why it is slower to compute over encrypted data.

To add two ciphertexts, we must add their $mask$ and $body$:

ct0=(a0,...,an,b)ct1=(a1′,...,an′,b′)ct2=ct0+ct1ct2=(a0+a0′,...,an+an′,b+b′)b+b′=(∑i=0n−1ai∗si)+plaintext+(∑i=0n−1ai′∗si)+plaintext′b+b′=(∑i=0n−1(ai+ai′)∗si)+Δm+Δm′+e+e′ct_0 = (a_{0}, ..., a_{n}, b) \\ ct_1 = (a_{1}^{'}, ..., a_{n}^{'}, b^{'}) \\ ct_{2} = ct_0 + ct_1 \\ ct_{2} = (a_{0} + a_{0}^{'}, ..., a_{n} + a_{n}^{'}, b + b^{'})\\ b + b^{'} = (\sum_{i = 0}^{n-1}{a_i * s_i}) + plaintext + (\sum_{i = 0}^{n-1}{a_i^{'} * s_i}) + plaintext^{'}\\ b + b^{'} = (\sum_{i = 0}^{n-1}{(a_i + a_i^{'})* s_i}) + \Delta m + \Delta m^{'} + e + e^{'}\\ct0​=(a0​,...,an​,b)ct1​=(a1′​,...,an′​,b′)ct2​=ct0​+ct1​ct2​=(a0​+a0′​,...,an​+an′​,b+b′)b+b′=(i=0∑n−1​ai​∗si​)+plaintext+(i=0∑n−1​ai′​∗si​)+plaintext′b+b′=(i=0∑n−1​(ai​+ai′​)∗si​)+Δm+Δm′+e+e′

To add ciphertexts, it is sufficient to add their masks and bodies. Instead of just adding two integers, one needs to add n+1n + 1n+1 elements. The addition is an intuitive example to show the slowdown of FHE computation compared to plaintext computation, but other operations are far more expensive (e.g., the computation of a lookup table using Programmable Bootstrapping).

Understanding noise and padding

In FHE, there are two types of operations that can be applied to ciphertexts:

  • leveled operations, which increase the noise in the ciphertext

  • bootstrapped operations, which reduce the noise in the ciphertext

In FHE, noise must be tracked and managed to guarantee the correctness of the computation.

Bootstrapping operations are used across the computation to decrease noise within the ciphertexts, preventing it from tampering the message. The rest of the operations are called leveled because they do not need bootstrapping operations and are usually really fast as a result.

The following sections explain the concept of noise and padding in ciphertexts.

Noise.

For it to be secure, LWE requires random noise to be added to the message at encryption time.

In TFHE, this random noise is drawn from a Centered Normal Distribution, parameterized by a standard deviation. This standard deviation is a security parameter. With all other security parameters set, the more secure the encryption, the larger the standard deviation.

In TFHE-rs, noise is encoded in the least significant bits of the plaintexts. Each leveled computation increases the noise. If too many computations are performed, the noise will eventually overflow onto the significant data bits of the message and lead to an incorrect result.

The figure below illustrates this problem in case of an addition, where an extra bit of noise is incurred as a result.

Noise overtaking the plaintexts after homomorphic addition. Most significant bits are on the left.

TFHE-rs offers the ability to automatically manage noise by performing bootstrapping operations to reset the noise.

Padding.

Since encoded values have a fixed precision, operating on them can produce results that are outside the original interval. To avoid losing precision or wrapping around the interval, TFHE-rs uses additional bits by defining bits of padding on the most significant bits.

As an example, consider adding two ciphertexts. Adding two values could end up outside the range of either ciphertext, and thus necessitate a carry, which would then be carried onto the first padding bit. In the figure below, each plaintext over 32 bits has one bit of padding on its left (i.e., the most significant bit). After the addition, the padding bit is no longer available, as it has been used in order for the carry. This is referred to as consuming bits of padding. Since no padding is left, there is no guarantee that further additions would yield correct results.

If you would like to know more about TFHE, you can find more information in our TFHE Deep Dive.

Security.

By default, the cryptographic parameters provided by TFHE-rs ensure at least 128 bits of security. The security has been evaluated using the latest versions of the Lattice Estimator (repository) with red_cost_model = reduction.RC.BDGL16.

For all sets of parameters, the error probability when computing a univariate function over one ciphertext is 2−402^{-40}2−40. Note that univariate functions might be performed when arithmetic functions are computed (i.e., the multiplication of two ciphertexts).

Public key encryption.

In public key encryption, the public key contains a given number of ciphertexts all encrypting the value 0. By setting the number of encryptions to 0 in the public key at m=⌈(n+1)log⁡(q)⌉+λm = \lceil (n+1) \log(q) \rceil + \lambdam=⌈(n+1)log(q)⌉+λ, where nnn is the LWE dimension, qqq is the ciphertext modulus, and λ\lambdaλ is the number of security bits. This construction is secure due to the leftover hash lemma, which relates to the impossibility of breaking the underlying multiple subset sum problem. This guarantees both a high-density subset sum and an exponentially large number of possible associated random vectors per LWE sample (a,b).

Benchmarks

Due to their nature, homomorphic operations are naturally slower than their clear equivalent. Some timings are exposed for basic operations. For completeness, benchmarks for other libraries are also given.

All benchmarks were launched on an AWS m6i.metal with the following specifications: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz and 512GB of RAM.

Boolean

This measures the execution time of a single binary Boolean gate.

tfhe-rs::boolean.

Parameter set
Concrete FFT
Concrete FFT + avx512

tfhe-lib.

Parameter set
fftw
spqlios-fma

OpenFHE.

Parameter set
GINX
GINX (Intel HEXL)

Shortint

This measures the execution time for some operations and some parameter sets of tfhe-rs::shortint.

This uses the Concrete FFT + avx512 configuration.

Parameter set
unchecked_add
unchecked_mul_lsb
keyswitch_programmable_bootstrap

Next, the timings for the operation flavor default are given. This flavor ensures predictable timings of an operation all along the circuit by clearing the carry space after each operation.

Parameter set
add
mul_lsb
keyswitch_programmable_bootstrap

Integer

This measures the execution time for some operation sets of tfhe-rs::integer.

All timings are related to parallelized Radix-based integer operations, where each block is encrypted using PARAM_MESSAGE_2_CARRY_2. To ensure predictable timings, the operation flavor is the default one: a carry propagation is computed after each operation. Operation cost could be reduced by using unchecked, checked, or smart.

Plaintext size
add
mul
greater_than (gt)
min

DEFAULT_PARAMETERS

8.8ms

6.8ms

TFHE_LIB_PARAMETERS

13.6ms

10.9ms

default_128bit_gate_bootstrapping_parameters

28.9ms

15.7ms

STD_128

172ms

78ms

MEDIUM

113ms

50.2ms

PARAM_MESSAGE_1_CARRY_1

338 ns

8.3 ms

8.1 ms

PARAM_MESSAGE_2_CARRY_2

406 ns

18.4 ms

18.4 ms

PARAM_MESSAGE_3_CARRY_3

3.06 µs

134 ms

129.4 ms

PARAM_MESSAGE_4_CARRY_4

11.7 µs

854 ms

828.1 ms

PARAM_MESSAGE_1_CARRY_1

7.90 ms

8.00 ms

8.10 ms

PARAM_MESSAGE_2_CARRY_2

18.4 ms

18.1 ms

18.4 ms

PARAM_MESSAGE_3_CARRY_3

131.5 ms

129.5 ms

129.4 ms

PARAM_MESSAGE_4_CARRY_4

852.5 ms

839.7 ms

828.1 ms

8 bits

129.0 ms

227.2 ms

111.9 ms

186.8 ms

16 bits

256.3 ms

756.0 ms

145.3 ms

233.1 ms

32 bits

469.4 ms

2.10 s

192.0 ms

282.9 ms

40 bits

608.0 ms

3.37 s

228.4 ms

318.6 ms

64 bits

959.9 ms

5.53 s

249.0 ms

336.5 ms

128 bits

1.88 s

14.1 s

294.7 ms

398.6 ms

256 bits

3.66 s

29.2 s

361.8 ms

509.1 ms

Serialization/Deserialization

As explained in the introduction, some types (Serverkey, Ciphertext) are meant to be shared with the server that does the computations.

The easiest way to send these data to a server is to use the serialization and deserialization features. TFHE-rs uses the serde framework, so serde's Serialize and Deserialize are implemented.

To be able to serialize our data, a data format needs to be picked. Here, bincode is a good choice, mainly because it is binary format.

# Cargo.toml

[dependencies]
# ...
bincode = "1.3.3"
// main.rs

use bincode;

use std::io::Cursor;
use tfhe::integer::{gen_keys_radix, ServerKey, RadixCiphertextBig};
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;


fn main() -> Result<(), Box<dyn std::error::Error>> {
    // We generate a set of client/server keys, using the default parameters:
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 201;
    let msg2 = 12;

    // message_modulus^vec_length
    let modulus = client_key.parameters().message_modulus.0.pow(num_block as u32) as u64;
    
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    let mut serialized_data = Vec::new();
    bincode::serialize_into(&mut serialized_data, &server_key)?;
    bincode::serialize_into(&mut serialized_data, &ct_1)?;
    bincode::serialize_into(&mut serialized_data, &ct_2)?;

    // Simulate sending serialized data to a server and getting
    // back the serialized result
    let serialized_result = server_function(&serialized_data)?;
    let result: RadixCiphertextBig = bincode::deserialize(&serialized_result)?;

    let output: u64 = client_key.decrypt(&result);
    assert_eq!(output, (msg1 + msg2) % modulus);
    Ok(())
}


fn server_function(serialized_data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let mut serialized_data = Cursor::new(serialized_data);
    let server_key: ServerKey = bincode::deserialize_from(&mut serialized_data)?;
    let ct_1: RadixCiphertextBig = bincode::deserialize_from(&mut serialized_data)?;
    let ct_2: RadixCiphertextBig = bincode::deserialize_from(&mut serialized_data)?;

    let result = server_key.unchecked_add(&ct_1, &ct_2);

    let serialized_result = bincode::serialize(&result)?;

    Ok(serialized_result)
}

Quick Start

This library makes it possible to execute homomorphic operations over encrypted data, where the data are either Booleans, short integers (named shortint in the rest of this documentation), or integers up to 256 bits. It allows you to execute a circuit on an untrusted server because both circuit inputs and outputs are kept private. Data are indeed encrypted on the client side, before being sent to the server. On the server side, every computation is performed on ciphertexts.

The server, however, has to know the circuit to be evaluated. At the end of the computation, the server returns the encryption of the result to the user. Then the user can decrypt it with the secret key.

General method to write an homomorphic circuit program

The overall process to write an homomorphic program is the same for all types. The basic steps for using the TFHE-rs library are the following:

  1. Choose a data type (Boolean, shortint, integer)

  2. Import the library

  3. Create client and server keys

  4. Encrypt data with the client key

  5. Compute over encrypted data using the server key

  6. Decrypt data with the client key

API levels.

This library has different modules, with different levels of abstraction.

There is the core_crypto module, which is the lowest level API with the primitive functions and types of the TFHE scheme.

Above the core_crypto module, there are the Boolean, shortint, and integer modules, which simply allow evaluation of Boolean, short integer, and integer circuits.

Finally, there is the high-level module built on top of the Boolean, shortint, integer modules. This module is meant to abstract cryptographic complexities: no cryptographical knowledge is required to start developing an FHE application. Another benefit of the high-level module is the drastically simplified development process compared to lower level modules.

high-level API

TFHE-rs exposes a high-level API by default that includes datatypes that try to match Rust's native types by having overloaded operators (+, -, ...).

Here is an example of how the high-level API is used:

Use the --release flag to run this example (eg: cargo run --release)

Boolean example

Here is an example of how the library can be used to evaluate a Boolean circuit:

Use the --release flag to run this example (eg: cargo run --release)

shortint example

Here is a full example using shortint:

Use the --release flag to run this example (eg: cargo run --release)

integer example

Use the --release flag to run this example (eg: cargo run --release)

The library is simple to use and can evaluate homomorphic circuits of arbitrary length. The description of the algorithms can be found in the paper (also available as ).

use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheUint8};
use tfhe::prelude::*;

fn main() {
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    let (client_key, server_key) = generate_keys(config);

    set_server_key(server_key);

    let clear_a = 27u8;
    let clear_b = 128u8;

    let a = FheUint8::encrypt(clear_a, &client_key);
    let b = FheUint8::encrypt(clear_b, &client_key);

    let result = a + b;

    let decrypted_result: u8 = result.decrypt(&client_key);

    let clear_result = clear_a + clear_b;

    assert_eq!(decrypted_result, clear_result);
}
use tfhe::boolean::prelude::*;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys();

    // We use the client secret key to encrypt two messages:
    let ct_1 = client_key.encrypt(true);
    let ct_2 = client_key.encrypt(false);

    // We use the server public key to execute a boolean circuit:
    // if ((NOT ct_2) NAND (ct_1 AND ct_2)) then (NOT ct_2) else (ct_1 AND ct_2)
    let ct_3 = server_key.not(&ct_2);
    let ct_4 = server_key.and(&ct_1, &ct_2);
    let ct_5 = server_key.nand(&ct_3, &ct_4);
    let ct_6 = server_key.mux(&ct_5, &ct_3, &ct_4);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_6);
    assert_eq!(output, true);
}
use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys
    // using parameters with 2 bits of message and 2 bits of carry
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 1;
    let msg2 = 0;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    // We use the server public key to execute an integer circuit:
    let ct_3 = server_key.unchecked_add(&ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_3);
    assert_eq!(output, (msg1 + msg2) % modulus as u64);
}
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    // We create keys for radix represention to create 16 bits integers
    // using 8 blocks of 2 bits
    let (cks, sks) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, 8);

    let clear_a = 2382u16;
    let clear_b = 29374u16;

    let mut a = cks.encrypt(clear_a as u64);
    let mut b = cks.encrypt(clear_b as u64);

    let encrypted_max = sks.smart_max_parallelized(&mut a, &mut b);
    let decrypted_max: u64 = cks.decrypt(&encrypted_max);

    assert_eq!(decrypted_max as u16, clear_a.max(clear_b))
}
TFHE
ePrint 2018/421

Supported Operations

Boolean

The list of supported operations by the homomorphic Booleans is:

Operation Name
type

not

Unary

and

Binary

or

Binary

xor

Binary

nor

Binary

xnor

Binary

cmux

Ternary

A walk-through using homomorphic Booleans can be found here.

Shortint

In TFHE-rs, shortint represents short unsigned integers encoded over a maximum of 8 bits. A complete homomorphic arithmetic is provided, along with the possibility to compute univariate and bi-variate functions. Some operations are only available for integers up to 4 bits. More technical details can be found here.

The list of supported operations is:

Operation name
Type

Negation

Unary

Addition

Binary

Subtraction

Binary

Multiplication

Binary

Division*

Binary

Modular reduction

Binary

Comparisons

Binary

Left/Right Shift

Binary

And

Binary

Or

Binary

Xor

Binary

Exact Function Evaluation

Unary/Binary

The division operation implements a subtlety: since data is encrypted, it might be possible to compute a division by 0. The division is tweaked so that dividing by 0 returns 0.

A walk-through example can be found here, and more examples and explanations can be found here.

Integer

In TFHE-rs, integers represent unsigned integers up to 256 bits. They are encoded using Radix representations by default (more details here).

The list of supported operations is:

Operation name
Type

Negation

Unary

Addition

Binary

Subtraction

Binary

Multiplication

Binary

Bitwise OR, AND, XOR

Binary

Equality

Binary

Left/Right Shift

Binary

Comparisons <,<=,>, >=

Binary

Min, Max

Binary

A walk-through example can be found here.

Serialization/Deserialization

As explained in the introduction, some types (Serverkey, Ciphertext) are meant to be shared with the server that performs the computations.

The easiest way to send these data to a server is to use the serialization and deserialization features. tfhe::shortint uses the serde framework. Serde's Serialize and Deserialize are then implemented on tfhe::shortint's types.

To serialize the data, we need to pick a data format. For our use case, bincode is a good choice, mainly because it is binary format.

# Cargo.toml

[dependencies]
# ...
bincode = "1.3.3"
// main.rs

use bincode;
use std::io::Cursor;
use tfhe::shortint::prelude::*;


fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 1;
    let msg2 = 0;

    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    let mut serialized_data = Vec::new();
    bincode::serialize_into(&mut serialized_data, &server_key)?;
    bincode::serialize_into(&mut serialized_data, &ct_1)?;
    bincode::serialize_into(&mut serialized_data, &ct_2)?;

    // Simulate sending serialized data to a server and getting
    // back the serialized result
    let serialized_result = server_function(&serialized_data)?;
    let result: CiphertextBig = bincode::deserialize(&serialized_result)?;

    let output = client_key.decrypt(&result);
    assert_eq!(output, msg1 + msg2);
    Ok(())
}


fn server_function(serialized_data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let mut serialized_data = Cursor::new(serialized_data);
    let server_key: ServerKey = bincode::deserialize_from(&mut serialized_data)?;
    let ct_1: CiphertextBig = bincode::deserialize_from(&mut serialized_data)?;
    let ct_2: CiphertextBig = bincode::deserialize_from(&mut serialized_data)?;

    let result = server_key.unchecked_add(&ct_1, &ct_2);

    let serialized_result = bincode::serialize(&result)?;

    Ok(serialized_result)
}

Shortint API

Using the shortint C API

This library exposes a C binding to the TFHE-rs shortint API to implement Fully Homomorphic Encryption (FHE) programs.

First steps using TFHE-rs C API

Setting up TFHE-rs C API for use in a C program.

TFHE-rs C API can be built on a Unix x86_64 machine using the following command:

RUSTFLAGS="-C target-cpu=native" cargo +nightly build --release --features=x86_64-unix,boolean-c-api,shortint-c-api -p tfhe

or on a Unix aarch64 machine using the following command:

RUSTFLAGS="-C target-cpu=native" cargo build +nightly --release --features=aarch64-unix,boolean-c-api,shortint-c-api -p tfhe

All features are opt-in, but for simplicity here, the C API is enabled for Boolean and shortint.

The tfhe.h header as well as the static (.a) and dynamic (.so) libtfhe binaries can then be found in "${REPO_ROOT}/target/release/"

The build system needs to be set up so that the C or C++ program links against TFHE-rs C API binaries.

Here is a minimal CMakeLists.txt to do just that:

project(my-project)

cmake_minimum_required(VERSION 3.16)

set(TFHE_C_API "/path/to/tfhe-rs/binaries/and/header")

include_directories(${TFHE_C_API})
add_library(tfhe STATIC IMPORTED)
set_target_properties(tfhe PROPERTIES IMPORTED_LOCATION ${TFHE_C_API}/libtfhe.a)

if(APPLE)
    find_library(SECURITY_FRAMEWORK Security)
    if (NOT SECURITY_FRAMEWORK)
        message(FATAL_ERROR "Security framework not found")
    endif()
endif()

set(EXECUTABLE_NAME my-executable)
add_executable(${EXECUTABLE_NAME} main.c)
target_include_directories(${EXECUTABLE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(${EXECUTABLE_NAME} LINK_PUBLIC tfhe m pthread dl)
if(APPLE)
    target_link_libraries(${EXECUTABLE_NAME} LINK_PUBLIC ${SECURITY_FRAMEWORK})
endif()
target_compile_options(${EXECUTABLE_NAME} PRIVATE -Werror)

Commented code of a PBS doubling a 2-bits encrypted message using TFHE-rs C API.

The steps required to perform the multiplication by 2 of a 2-bits ciphertext using a PBS are detailed. This is NOT the most efficient way of doing this operation, but it can help to show the management required to run a PBS manually using the C API.

WARNING: The following example does not have proper memory management in the error case to make it easier to fit the code on this page.

To run the example below, the above CMakeLists.txt and main.c files need to be in the same directory. The commands to run are:

# /!\ Be sure to update CMakeLists.txt to give the absolute path to the compiled tfhe library
$ ls
CMakeLists.txt  main.c
$ mkdir build && cd build
$ cmake .. -DCMAKE_BUILD_TYPE=RELEASE
...
$ make
...
$ ./my-executable
Result: 2
$
#include "tfhe.h"
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>

uint64_t double_accumulator_2_bits_message(uint64_t in) { return (in * 2) % 4; }

uint64_t get_max_value_of_accumulator_generator(uint64_t (*accumulator_func)(uint64_t),
                                                size_t message_bits)
{
    uint64_t max_value = 0;
    for (size_t idx = 0; idx < (1 << message_bits); ++idx)
    {
        uint64_t acc_value = accumulator_func((uint64_t)idx);
        max_value = acc_value > max_value ? acc_value : max_value;
    }

    return max_value;
}

int main(void)
{
    ShortintPBSLookupTable *accumulator = NULL;
    ShortintClientKey *cks = NULL;
    ShortintServerKey *sks = NULL;
    ShortintParameters *params = NULL;

    // Get the parameters for 2 bits messages with 2 bits of carry
    int get_params_ok = shortint_get_parameters(2, 2, &params);
    assert(get_params_ok == 0);

    // Generate the keys with the parameters
    int gen_keys_ok = shortint_gen_keys_with_parameters(params, &cks, &sks);
    assert(gen_keys_ok == 0);

    // Generate the accumulator for the PBS
    int gen_acc_ok = shortint_server_key_generate_pbs_accumulator(
        sks, double_accumulator_2_bits_message, &accumulator);
    assert(gen_acc_ok == 0);

    ShortintCiphertext *ct = NULL;
    ShortintCiphertext *ct_out = NULL;

    // We will compute 1 * 2 using a PBS, it's not the recommended way to perform a multiplication,
    // but it shows how to manage a PBS manually in the C API
    uint64_t in_val = 1;

    // Encrypt the input value
    int encrypt_ok = shortint_client_key_encrypt(cks, in_val, &ct);
    assert(encrypt_ok == 0);

    // Check the degree is set to the maximum value that can be encrypted on 2 bits, i.e. 3
    // This check is not required and is just added to show, the degree information can be retrieved
    // in the C APi
    size_t degree = -1;
    int get_degree_ok = shortint_ciphertext_get_degree(ct, &degree);
    assert(get_degree_ok == 0);

    assert(degree == 3);

    // Apply the PBS on our encrypted input
    int pbs_ok = shortint_server_key_programmable_bootstrap(sks, accumulator, ct, &ct_out);
    assert(pbs_ok == 0);

    // Set the degree to keep consistency for potential further computations
    // Note: This is only required for the PBS
    size_t degree_to_set =
        (size_t)get_max_value_of_accumulator_generator(double_accumulator_2_bits_message, 2);

    int set_degree_ok = shortint_ciphertext_set_degree(ct_out, degree_to_set);
    assert(set_degree_ok == 0);

    // Decrypt the result
    uint64_t result = -1;
    int decrypt_non_assign_ok = shortint_client_key_decrypt(cks, ct_out, &result);
    assert(decrypt_non_assign_ok == 0);

    // Check the result is what we expect i.e. 2
    assert(result == double_accumulator_2_bits_message(in_val));
    printf("Result: %ld\n", result);

    // Destroy entities from the C API
    destroy_shortint_ciphertext(ct);
    destroy_shortint_ciphertext(ct_out);
    destroy_shortint_pbs_accumulator(accumulator);
    destroy_shortint_client_key(cks);
    destroy_shortint_server_key(sks);
    destroy_shortint_parameters(params);
    return EXIT_SUCCESS;
}

Tutorial

Using the core_crypto primitives

Welcome to this tutorial about TFHE-rs core_crypto module.

Setting up TFHE-rs to use the core_crypto module

To use TFHE-rs, first it has to be added as a dependency in the Cargo.toml:

tfhe = { version = "0.2.5", features = [ "x86_64-unix" ] }

This enables the x86_64-unix feature to have efficient implementations of various algorithms for x86_64 CPUs on a Unix-like system. The 'unix' suffix indicates that the UnixSeeder, which uses /dev/random to generate random numbers, is activated as a fallback if no hardware number generator is available, like rdseed on x86_64 or if the Randomization Services on Apple platforms are not available. To avoid having the UnixSeeder as a potential fallback or to run on non-Unix systems (e.g., Windows), the x86_64 feature is sufficient.

For Apple Silicon, the aarch64-unix or aarch64 feature should be enabled. aarch64 is not supported on Windows as it's currently missing an entropy source required to seed the CSPRNGs used in TFHE-rs.

In short: For x86_64-based machines running Unix-like OSes:

tfhe = { version = "0.2.5", features = ["x86_64-unix"] }

For Apple Silicon or aarch64-based machines running Unix-like OSes:

tfhe = { version = "0.2.5", features = ["aarch64-unix"] }

For x86_64-based machines with the rdseed instruction running Windows:

tfhe = { version = "0.2.5", features = ["x86_64"] }

Commented code to double a 2-bits message in a leveled fashion and using a PBS with the core_crypto module.

As a complete example showing the usage of some common primitives of the core_crypto APIs, the following Rust code homomorphically computes 2 * 3 using two different methods. First using a cleartext multiplication and then using a PBS.

use tfhe::core_crypto::prelude::*;

pub fn main() {
    // DISCLAIMER: these toy example parameters are not guaranteed to be secure or yield correct
    // computations
    // Define the parameters for a 4 bits message able to hold the doubled 2 bits message
    let small_lwe_dimension = LweDimension(742);
    let glwe_dimension = GlweDimension(1);
    let polynomial_size = PolynomialSize(2048);
    let lwe_modular_std_dev = StandardDev(0.000007069849454709433);
    let glwe_modular_std_dev = StandardDev(0.00000000000000029403601535432533);
    let pbs_base_log = DecompositionBaseLog(23);
    let pbs_level = DecompositionLevelCount(1);
    let ciphertext_modulus = CiphertextModulus::new_native();

    // Request the best seeder possible, starting with hardware entropy sources and falling back to
    // /dev/random on Unix systems if enabled via cargo features
    let mut boxed_seeder = new_seeder();
    // Get a mutable reference to the seeder as a trait object from the Box returned by new_seeder
    let seeder = boxed_seeder.as_mut();

    // Create a generator which uses a CSPRNG to generate secret keys
    let mut secret_generator =
        SecretRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed());

    // Create a generator which uses two CSPRNGs to generate public masks and secret encryption
    // noise
    let mut encryption_generator =
        EncryptionRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed(), seeder);

    println!("Generating keys...");

    // Generate an LweSecretKey with binary coefficients
    let small_lwe_sk =
        LweSecretKey::generate_new_binary(small_lwe_dimension, &mut secret_generator);

    // Generate a GlweSecretKey with binary coefficients
    let glwe_sk =
        GlweSecretKey::generate_new_binary(glwe_dimension, polynomial_size, &mut secret_generator);

    // Create a copy of the GlweSecretKey re-interpreted as an LweSecretKey
    let big_lwe_sk = glwe_sk.clone().into_lwe_secret_key();

    // Generate the bootstrapping key, we use the parallel variant for performance reason
    let std_bootstrapping_key = par_allocate_and_generate_new_lwe_bootstrap_key(
        &small_lwe_sk,
        &glwe_sk,
        pbs_base_log,
        pbs_level,
        glwe_modular_std_dev,
        ciphertext_modulus,
        &mut encryption_generator,
    );

    // Create the empty bootstrapping key in the Fourier domain
    let mut fourier_bsk = FourierLweBootstrapKey::new(
        std_bootstrapping_key.input_lwe_dimension(),
        std_bootstrapping_key.glwe_size(),
        std_bootstrapping_key.polynomial_size(),
        std_bootstrapping_key.decomposition_base_log(),
        std_bootstrapping_key.decomposition_level_count(),
    );

    // Use the conversion function (a memory optimized version also exists but is more complicated
    // to use) to convert the standard bootstrapping key to the Fourier domain
    convert_standard_lwe_bootstrap_key_to_fourier(&std_bootstrapping_key, &mut fourier_bsk);
    // We don't need the standard bootstrapping key anymore
    drop(std_bootstrapping_key);

    // Our 4 bits message space
    let message_modulus = 1u64 << 4;

    // Our input message
    let input_message = 3u64;

    // Delta used to encode 4 bits of message + a bit of padding on u64
    let delta = (1_u64 << 63) / message_modulus;

    // Apply our encoding
    let plaintext = Plaintext(input_message * delta);

    // Allocate a new LweCiphertext and encrypt our plaintext
    let lwe_ciphertext_in: LweCiphertextOwned<u64> = allocate_and_encrypt_new_lwe_ciphertext(
        &small_lwe_sk,
        plaintext,
        lwe_modular_std_dev,
        ciphertext_modulus,
        &mut encryption_generator,
    );

    // Compute a cleartext multiplication by 2
    let mut cleartext_multiplication_ct = lwe_ciphertext_in.clone();
    println!("Performing cleartext multiplication...");
    lwe_ciphertext_cleartext_mul(
        &mut cleartext_multiplication_ct,
        &lwe_ciphertext_in,
        Cleartext(2),
    );

    // Decrypt the cleartext multiplication result
    let cleartext_multiplication_plaintext: Plaintext<u64> =
        decrypt_lwe_ciphertext(&small_lwe_sk, &cleartext_multiplication_ct);

    // Create a SignedDecomposer to perform the rounding of the decrypted plaintext
    // We pass a DecompositionBaseLog of 5 and a DecompositionLevelCount of 1 indicating we want to
    // round the 5 MSB, 1 bit of padding plus our 4 bits of message
    let signed_decomposer =
        SignedDecomposer::new(DecompositionBaseLog(5), DecompositionLevelCount(1));

    // Round and remove our encoding
    let cleartext_multiplication_result: u64 =
        signed_decomposer.closest_representable(cleartext_multiplication_plaintext.0) / delta;

    println!("Checking result...");
    assert_eq!(6, cleartext_multiplication_result);
    println!(
        "Cleartext multiplication result is correct! \
        Expected 6, got {cleartext_multiplication_result}"
    );

    // Now we will use a PBS to compute the same multiplication, it is NOT the recommended way of
    // doing this operation in terms of performance as it's much more costly than a multiplication
    // with a cleartext, however it resets the noise in a ciphertext to a nominal level and allows
    // to evaluate arbitrary functions so depending on your use case it can be a better fit.

    // Here we will define a helper function to generate an accumulator for a PBS
    fn generate_accumulator<F>(
        polynomial_size: PolynomialSize,
        glwe_size: GlweSize,
        message_modulus: usize,
        ciphertext_modulus: CiphertextModulus<u64>,
        delta: u64,
        f: F,
    ) -> GlweCiphertextOwned<u64>
    where
        F: Fn(u64) -> u64,
    {
        // N/(p/2) = size of each block, to correct noise from the input we introduce the notion of
        // box, which manages redundancy to yield a denoised value for several noisy values around
        // a true input value.
        let box_size = polynomial_size.0 / message_modulus;

        // Create the accumulator
        let mut accumulator_u64 = vec![0_u64; polynomial_size.0];

        // Fill each box with the encoded denoised value
        for i in 0..message_modulus {
            let index = i * box_size;
            accumulator_u64[index..index + box_size]
                .iter_mut()
                .for_each(|a| *a = f(i as u64) * delta);
        }

        let half_box_size = box_size / 2;

        // Negate the first half_box_size coefficients to manage negacyclicity and rotate
        for a_i in accumulator_u64[0..half_box_size].iter_mut() {
            *a_i = (*a_i).wrapping_neg();
        }

        // Rotate the accumulator
        accumulator_u64.rotate_left(half_box_size);

        let accumulator_plaintext = PlaintextList::from_container(accumulator_u64);

        let accumulator =
            allocate_and_trivially_encrypt_new_glwe_ciphertext(
                glwe_size,
                &accumulator_plaintext,
                ciphertext_modulus,
            );

        accumulator
    }

    // Generate the accumulator for our multiplication by 2 using a simple closure
    let accumulator: GlweCiphertextOwned<u64> = generate_accumulator(
        polynomial_size,
        glwe_dimension.to_glwe_size(),
        message_modulus as usize,
        ciphertext_modulus,
        delta,
        |x: u64| 2 * x,
    );

    // Allocate the LweCiphertext to store the result of the PBS
    let mut pbs_multiplication_ct = LweCiphertext::new(
        0u64,
        big_lwe_sk.lwe_dimension().to_lwe_size(),
        ciphertext_modulus,
    );
    println!("Computing PBS...");
    programmable_bootstrap_lwe_ciphertext(
        &lwe_ciphertext_in,
        &mut pbs_multiplication_ct,
        &accumulator,
        &fourier_bsk,
    );

    // Decrypt the PBS multiplication result
    let pbs_multipliation_plaintext: Plaintext<u64> =
        decrypt_lwe_ciphertext(&big_lwe_sk, &pbs_multiplication_ct);

    // Round and remove our encoding
    let pbs_multiplication_result: u64 =
        signed_decomposer.closest_representable(pbs_multipliation_plaintext.0) / delta;

    println!("Checking result...");
    assert_eq!(6, pbs_multiplication_result);
    println!(
        "Mulitplication via PBS result is correct! Expected 6, got {pbs_multiplication_result}"
    );
}

Tutorial

This library is meant to be used both on the server side and the client side. The typical use case should follow the subsequent steps:

  1. On the client side, generate the client and server keys.

  2. Send the server key to the server.

  3. Then any number of times:

    • On the client side, encrypt the input data with the client key.

    • Transmit the encrypted input to the server.

    • On the server side, perform homomorphic computation with the server key.

    • Transmit the encrypted output to the client.

    • On the client side, decrypt the output data with the client key.

Setup

In the first step, the client creates two keys, the client key and the server key, with the concrete_boolean::gen_keys function:

use tfhe::boolean::prelude::*;

fn main() {

// We generate the client key and the server key,
// using the default parameters:
    let (client_key, server_key): (ClientKey, ServerKey) = gen_keys();
}
  • The client_key is of type ClientKey. It is secret and must never be transmitted. This key will only be used to encrypt and decrypt data.

  • The server_key is of type ServerKey. It is a public key and can be shared with any party. This key has to be sent to the server because it is required for homomorphic computation.

Note that both the client_key and server_key implement the Serialize and Deserialize traits. This way you can use any compatible serializer to store/send the data. To store the server_key in a binary file, you can use the bincode library:

use std::fs::File;
use std::io::{Write, Read};
use tfhe::boolean::prelude::*;

fn main() {

//---------------------------- CLIENT SIDE ----------------------------

// We generate a client key and a server key, using the default parameters:
    let (client_key, server_key) = gen_keys();

// We serialize the server key to bytes, and store them in a file:
    let encoded: Vec<u8> = bincode::serialize(&server_key).unwrap();

    let server_key_file = "/tmp/tutorial_server_key.bin";

// We write the server key to a file:
    let mut file = File::create(server_key_file)
        .expect("failed to create server key file");
    file.write_all(encoded.as_slice()).expect("failed to write key to file");

// ...
// We send the key to server side
// ...


//---------------------------- SERVER SIDE ----------------------------

// We read the file:
    let mut file = File::open(server_key_file)
        .expect("failed to open server key file");
    let mut encoded: Vec<u8> = Vec::new();
    file.read_to_end(&mut encoded).expect("failed to read key");

// We deserialize the server key:
    let key: ServerKey = bincode::deserialize(&encoded[..])
        .expect("failed to deserialize");
}

Encrypting inputs

Once the server key is available on the server side, it is possible to perform some homomorphic computations. The client needs to encrypt some data and send it to the server. Again, the Ciphertext type implements the Serialize and the Deserialize traits, so that any serializer and communication tool suiting your use case can be employed:

use tfhe::boolean::prelude::*;

fn main() {
    // Don't consider the following line; you should follow the procedure above.
    let (client_key, _) = gen_keys();

//---------------------------- SERVER SIDE

// We use the client key to encrypt the messages:
    let ct_1 = client_key.encrypt(true);
    let ct_2 = client_key.encrypt(false);

// We serialize the ciphertexts:
    let encoded_1: Vec<u8> = bincode::serialize(&ct_1).unwrap();
    let encoded_2: Vec<u8> = bincode::serialize(&ct_2).unwrap();

// ...
// And we send them to the server somehow
// ...
}

Encrypting inputs using a public key

Once the server key is available on the server side, it is possible to perform some homomorphic computations. The client simply needs to encrypt some data and send it to the server. Again, the Ciphertext type implements the Serialize and the Deserialize traits, so that any serializer and communication tool suiting your use case can be utilized:

use tfhe::boolean::prelude::*;

fn main() {
    // Don't consider the following line; you should follow the procedure above.
    let (client_key, _) = gen_keys();
    let public_key = PublicKey::new(&client_key);

//---------------------------- SERVER SIDE

// We use the public key to encrypt the messages:
    let ct_1 = public_key.encrypt(true);
    let ct_2 = public_key.encrypt(false);

// We serialize the ciphertexts:
    let encoded_1: Vec<u8> = bincode::serialize(&ct_1).unwrap();
    let encoded_2: Vec<u8> = bincode::serialize(&ct_2).unwrap();

// ...
// And we send them to the server somehow
// ...
}

Executing a Boolean circuit

Once the encrypted inputs are on the server side, the server_key can be used to homomorphically execute the desired Boolean circuit:

use std::fs::File;
use std::io::{Write, Read};
use tfhe::boolean::prelude::*;

fn main() {
    // Don't consider the following lines; you should follow the procedure above.
    let (client_key, server_key) = gen_keys();
    let ct_1 = client_key.encrypt(true);
    let ct_2 = client_key.encrypt(false);
    let encoded_1: Vec<u8> = bincode::serialize(&ct_1).unwrap();
    let encoded_2: Vec<u8> = bincode::serialize(&ct_2).unwrap();

//---------------------------- ON SERVER SIDE ----------------------------

// We deserialize the ciphertexts:
    let ct_1: Ciphertext = bincode::deserialize(&encoded_1[..])
        .expect("failed to deserialize");
    let ct_2: Ciphertext = bincode::deserialize(&encoded_2[..])
        .expect("failed to deserialize");

// We use the server key to execute the boolean circuit:
// if ((NOT ct_2) NAND (ct_1 AND ct_2)) then (NOT ct_2) else (ct_1 AND ct_2)
    let ct_3 = server_key.not(&ct_2);
    let ct_4 = server_key.and(&ct_1, &ct_2);
    let ct_5 = server_key.nand(&ct_3, &ct_4);
    let ct_6 = server_key.mux(&ct_5, &ct_3, &ct_4);

// Then we serialize the output of the circuit:
    let encoded_output: Vec<u8> = bincode::serialize(&ct_6)
        .expect("failed to serialize output");

// ...
// And we send the output to the client
// ...
}

Decrypting the output

Once the encrypted output is on the client side, the client_key can be used to decrypt it:

use std::fs::File;
use std::io::{Write, Read};
use tfhe::boolean::prelude::*;

fn main() {
    // Don't consider the following lines; you should follow the procedure above.
    let (client_key, server_key) = gen_keys();
    let ct_6 = client_key.encrypt(true);
    let encoded_output: Vec<u8> = bincode::serialize(&ct_6).unwrap();

//---------------------------- ON CLIENT SIDE

// We deserialize the output ciphertext:
    let output: Ciphertext = bincode::deserialize(&encoded_output[..])
        .expect("failed to deserialize");

// Finally, we decrypt the output:
    let output = client_key.decrypt(&output);

// And check that the result is the expected one:
    assert_eq!(output, true);
}

Operations

The structure and operations related to the integers are described in this section.

How an integer is represented

In integer, the encrypted data is split amongst many ciphertexts encrypted with the shortint library. Below is a scheme representing an integer composed by k shortint ciphertexts.

This crate implements two ways to represent an integer:

  • the Radix representation

  • the CRT (Chinese Reminder Theorem) representation

Radix-based integers.

The first possibility to represent a large integer is to use a Radix-based decomposition on the plaintexts. Let B∈NB \in \mathbb{N}B∈N be a basis such that the size of BBB is smaller than (or equal to) 4 bits. Then, an integer m∈Nm \in \mathbb{N}m∈N can be written as m=m0+m1∗B+m2∗B2+...m = m_0 + m_1*B + m_2*B^2 + ...m=m0​+m1​∗B+m2​∗B2+..., where each mim_imi​ is strictly smaller than BBB. Each mim_imi​ is then independently encrypted. In the end, an Integer ciphertext is defined as a set of shortint ciphertexts.

The definition of an integer requires a basis and a number of blocks. This is done at key generation. Below, the keys are dedicated to unsigned integers encrypting messages over 8 bits, using a basis over 2 bits (i.e., B=22B=2^2B=22) and 4 blocks.

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);
}

In this representation, the correctness of operations requires to propagate the carries between the ciphertext. This operation is costly since it relies on the computation of many programmable bootstrapping operations over shortints.

CRT-based integers.

The second approach to represent large integers is based on the Chinese Remainder Theorem. In this case, the basis BBB is composed of several integers bib_ibi​, such that there are pairwise coprime, and each b_ib\_ib_i has a size smaller than 4 bits. The CRT-based integer are defined modulus ∏bi\prod b_i∏bi​. For an integer mmm, its CRT decomposition is simply defined as mm % b_0, m % b_1, ...m. Each part is then encrypted as a shortint ciphertext. In the end, an Integer ciphertext is defined as a set of shortint ciphertexts.

In the following example, the chosen basis is B=[2,3,5]B = [2, 3, 5]B=[2,3,5]. The integer is defined modulus 2∗3∗5=302*3*5 = 302∗3∗5=30. There is no need to pre-size the number of blocks since it is determined from the number of values composing the basis. Here, the integer is split over three blocks.

use tfhe::integer::CrtClientKey;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    let basis = vec![2, 3, 5];
    let cks = CrtClientKey::new(PARAM_MESSAGE_2_CARRY_2, basis);
}

This representation has many advantages: no carry propagation is required, cleaning the carry buffer of each ciphertext block is enough. This implies that operations can easily be parallelized. It also allows the efficient computation of PBS in the case where the function is CRT-compliant.

A variant of the CRT is proposed, where each block might be associated to a different key couple. In the end, a keychain to the computations is required, but performance might be improved.

List of available operations

The list of operations available in integer depends on the type of representation:

Operation name
Radix-based
CRT-based

Negation

✔️

✔️

Addition

✔️

✔️

Scalar Addition

✔️

✔️

Subtraction

✔️

✔️

Scalar Subtraction

✔️

✔️

Multiplication

✔️

✔️

Scalar Multiplication

✔️

✔️

Bitwise OR, AND, XOR

✔️

✔️

Equality

✔️

✔️

Left/Right Shift

✔️

✖️

Comparisons <,<=,>, >=

✔️

✖️

Min, Max

✔️

✖️

Types of operations

Much like shortint, the operations available via a ServerKey may come in different variants:

  • operations that take their inputs as encrypted values.

  • scalar operations take at least one non-encrypted value as input.

For example, the addition has both variants:

  • ServerKey::unchecked_add, which takes two encrypted values and adds them.

  • ServerKey::unchecked_scalar_add, which takes an encrypted value and a clear value (the so-called scalar) and adds them.

Each operation may come in different 'flavors':

  • unchecked: Always does the operation, without checking if the result may exceed the capacity of the plaintext space.

  • checked: Checks are done before computing the operation, returning an error if operation cannot be done safely.

  • smart: Always does the operation, if the operation cannot be computed safely, the smart operation will propagate the carry buffer to make the operation possible.

  • default: Always compute the operation and always clear the carry. Could be slower than smart, but ensure that the timings are consistent from one call to another.

Not all operations have these 4 flavors, as some of them are implemented in a way that the operation is always possible without ever exceeding the plaintext space capacity.

How to use each operation type

Let's try to do a circuit evaluation using the different flavors of already introduced operations. For a very small circuit, the unchecked flavor may be enough to do the computation correctly. Otherwise, checked and smart are the best options.

As an example, let's do a scalar multiplication, a subtraction, and an addition.

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 12u64;
    let msg2 = 11u64;
    let msg3 = 9u64;
    let scalar = 3u64;

    // message_modulus^vec_length
    let modulus = client_key.parameters().message_modulus.0.pow(num_block as u32) as u64;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);
    let ct_3 = client_key.encrypt(msg2);

    server_key.unchecked_small_scalar_mul_assign(&mut ct_1, scalar);

    server_key.unchecked_sub_assign(&mut ct_1, &ct_2);

    server_key.unchecked_add_assign(&mut ct_1, &ct_3);

    // We use the client key to decrypt the output of the circuit:
    let output: u64 = client_key.decrypt(&ct_1);
    // The carry buffer has been overflowed, the result is not correct
    assert_ne!(output, ((msg1 * scalar as u64 - msg2) + msg3) % modulus as u64);
}

During this computation the carry buffer has been overflowed, and the output may be incorrect as all the operations were unchecked.

If the same circuit is done but using the checked flavor, a panic will occur:

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    let num_block = 2;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 12u64;
    let msg2 = 11u64;
    let msg3 = 9u64;
    let scalar = 3u64;

    // message_modulus^vec_length
    let modulus = client_key.parameters().message_modulus.0.pow(num_block as u32) as u64;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);
    let ct_3 = client_key.encrypt(msg3);

    let result = server_key.checked_small_scalar_mul_assign(&mut ct_1, scalar);
    assert!(result.is_ok());

    let result = server_key.checked_sub_assign(&mut ct_1, &ct_2);
    assert!(result.is_err());

    // We use the client key to decrypt the output of the circuit:
    // Only the scalar multiplication could be done
    let output: u64 = client_key.decrypt(&ct_1);
    assert_eq!(output, (msg1 * scalar) % modulus as u64);
}

The checked flavor permits the manual management of the overflow of the carry buffer by raising an error if correctness is not guaranteed.

Using the smart flavor will output the correct result all the time. However, the computation may be slower as the carry buffer may be propagated during the computations.

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 12;
    let msg2 = 11;
    let msg3 = 9;
    let scalar = 3;

    // message_modulus^vec_length
    let modulus = client_key.parameters().message_modulus.0.pow(num_block as u32) as u64;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let mut ct_2 = client_key.encrypt(msg2);
    let mut ct_3 = client_key.encrypt(msg3);

    server_key.smart_scalar_mul_assign(&mut ct_1, scalar);

    server_key.smart_sub_assign(&mut ct_1, &mut ct_2);

    server_key.smart_add_assign(&mut ct_1, &mut ct_3);

    // We use the client key to decrypt the output of the circuit:
    let output: u64 = client_key.decrypt(&ct_1);
    assert_eq!(output, ((msg1 * scalar as u64 - msg2) + msg3) % modulus as u64);
}

The main advantage of the default flavor is to ensure predictable timings, as long as only this kind of operation is used. Only the parallelized version of the operations is provided.

Using default could slow down computations.

use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;

fn main() {
    let num_block = 4;
    let (client_key, server_key) = gen_keys_radix(&PARAM_MESSAGE_2_CARRY_2, num_block);

    let msg1 = 12;
    let msg2 = 11;
    let msg3 = 9;
    let scalar = 3;

    // message_modulus^vec_length
    let modulus = client_key.parameters().message_modulus.0.pow(num_block as u32) as u64;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let mut ct_2 = client_key.encrypt(msg2);
    let mut ct_3 = client_key.encrypt(msg3);

    server_key.scalar_mul_assign_parallelized(&mut ct_1, scalar);

    server_key.sub_assign_parallelized(&mut ct_1, &mut ct_2);

    server_key.add_assign_parallelized(&mut ct_1, &mut ct_3);

    // We use the client key to decrypt the output of the circuit:
    let output: u64 = client_key.decrypt(&ct_1);
    assert_eq!(output, ((msg1 * scalar as u64 - msg2) + msg3) % modulus as u64);
}

Operations

The structure and the operations related to the short integers are described in this section.

How a shortint is represented

In shortint, the encrypted data is stored in an LWE ciphertext.

Conceptually, the message stored in an LWE ciphertext is divided into a carry buffer and a message buffer.

The message buffer is the space where the actual message is stored. This represents the modulus of the input messages (denoted by MessageModulus in the code). When doing computations on a ciphertext, the encrypted message can overflow the message modulus. The exceeding information is stored in the carry buffer. The size of the carry buffer is defined by another modulus, called CarryModulus.

Together, the message modulus and the carry modulus form the plaintext space that is available in a ciphertext. This space cannot be overflowed, otherwise the computation may result in incorrect outputs.

In order to ensure the correctness of the computation, we track the maximum value encrypted in a ciphertext via an associated attribute called the degree. When the degree reaches a defined threshold, the carry buffer may be emptied to safely resume the computations. In shortint the carry modulus is considered useful as a means to do more computations.

Types of operations

The operations available via a ServerKey may come in different variants:

  • operations that take their inputs as encrypted values

  • scalar operations that take at least one non-encrypted value as input

For example, the addition has both variants:

  • ServerKey::unchecked_add, which takes two encrypted values and adds them.

  • ServerKey::unchecked_scalar_add, which takes an encrypted value and a clear value (the so-called scalar) and adds them.

Each operation may come in different 'flavors':

  • unchecked: Always does the operation, without checking if the result may exceed the capacity of the plaintext space. Using this operation might have an impact on the correctness of the following operations;

  • checked: Checks are done before computing the operation, returning an error if operation cannot be done safely;

  • smart: Always does the operation. If the operation cannot be computed safely, the smart operation will clear the carry modulus to make the operation possible;

  • default: Always does the operation and always clears the carry. Could be slower than smart, but it ensures that the timings are consistent from one call to another.

Not all operations have these 4 flavors, as some of them are implemented in a way that the operation is always possible without ever exceeding the plaintext space capacity.

How to use operation types

Let's try to do a circuit evaluation using the different flavors of operations we already introduced. For a very small circuit, the unchecked flavour may be enough to do the computation correctly. Otherwise,checked and smart are the best options.

Let's do a scalar multiplication, a subtraction, and a multiplication.

use tfhe::shortint::prelude::*;


fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;
    let msg2 = 3;
    let scalar = 4;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    server_key.unchecked_scalar_mul_assign(&mut ct_1, scalar);
    server_key.unchecked_sub_assign(&mut ct_1, &ct_2);
    server_key.unchecked_mul_lsb_assign(&mut ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_1);
    println!("expected {}, found {}", ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64, output);
}

During this computation, the carry buffer has been overflowed and, as all the operations were unchecked, the output may be incorrect.

If we redo this same circuit with the checked flavor, a panic will occur:

use tfhe::shortint::prelude::*;

use std::error::Error;

fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;
    let msg2 = 3;
    let scalar = 4;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    let mut ops = || -> Result<(), Box<dyn Error>> {
        server_key.checked_scalar_mul_assign(&mut ct_1, scalar)?;
        server_key.checked_sub_assign(&mut ct_1, &ct_2)?;
        server_key.checked_mul_lsb_assign(&mut ct_1, &ct_2)?;
        Ok(())
    };

    match ops() {
        Ok(_) => (),
        Err(e) => {
            println!("correctness of operations is not guaranteed due to error: {}", e);
            return;
        },
    }

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_1);
    assert_eq!(output, ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64);
}

The checked flavor permits manual management of the overflow of the carry buffer by raising an error if correctness is not guaranteed.

Using the smart flavor will output the correct result all the time. However, the computation may be slower as the carry buffer may be cleaned during the computations.

use tfhe::shortint::prelude::*;


fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;
    let msg2 = 3;
    let scalar = 4;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let mut ct_2 = client_key.encrypt(msg2);

    server_key.smart_scalar_mul_assign(&mut ct_1, scalar);
    server_key.smart_sub_assign(&mut ct_1, &mut ct_2);
    server_key.smart_mul_lsb_assign(&mut ct_1, &mut ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_1);
    assert_eq!(output, ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64);
}

The main advantage of the default flavor is to ensure predictable timings as long as only this kind of operation is used.

Using default could slow-down computations.

use tfhe::shortint::prelude::*;


fn main() {
    // We generate a set of client/server keys, using the default parameters:
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;
    let msg2 = 3;
    let scalar = 4;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the client key to encrypt two messages:
    let mut ct_1 = client_key.encrypt(msg1);
    let mut ct_2 = client_key.encrypt(msg2);

    server_key.scalar_mul_assign(&mut ct_1, scalar);
    server_key.sub_assign(&mut ct_1, &mut ct_2);
    server_key.mul_lsb_assign(&mut ct_1, &mut ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_1);
    assert_eq!(output, ((msg1 * scalar as u64 - msg2) * msg2) % modulus as u64);
}

#List of available operations

Certain operations can only be used if the parameter set chosen is compatible with the bivariate programmable bootstrapping, meaning the carry buffer is larger than or equal to the message buffer. These operations are marked with a star (*).

The list of implemented operations for shortint is:

  • addition between two ciphertexts

  • addition between a ciphertext and an unencrypted scalar

  • comparisons <, <=, >, >=, ==, != between a ciphertext and an unencrypted scalar

  • division of a ciphertext by an unencrypted scalar

  • LSB multiplication between two ciphertexts returning the result truncated to fit in the message buffer

  • multiplication of a ciphertext by an unencrypted scalar

  • bitwise shift <<, >>

  • subtraction of a ciphertext by another ciphertext

  • subtraction of a ciphertext by an unencrypted scalar

  • negation of a ciphertext

  • bitwise and, or and xor (*)

  • comparisons <, <=, >, >=, ==, != between two ciphertexts (*)

  • division between two ciphertexts (*)

  • MSB multiplication between two ciphertexts returning the part overflowing the message buffer (*)

Public key encryption.

TFHE-rs supports both private and public key encryption methods. The only difference between both lies in the encryption step: in this case, the encryption method is called using public_key instead of client_key.

Here is a small example on how to use public encryption:

use tfhe::shortint::prelude::*;

fn main() {
    // Generate the client key and the server key:
    let (cks, _) = gen_keys(PARAM_MESSAGE_2_CARRY_2);
    let pks = PublicKeyBig::new(&cks);

    let msg = 2;
    // Encryption of one message:
    let ct = pks.encrypt(msg);
    // Decryption:
    let dec = cks.decrypt(&ct);
    assert_eq!(dec, msg);
}

Arithmetic operations.

Classical arithmetic operations are supported by shortint:

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 2;
    let msg2 = 1;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the private client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    // We use the server public key to execute an integer circuit:
    let ct_3 = server_key.unchecked_add(&ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_3);
    assert_eq!(output, (msg1 + msg2) % modulus as u64);
}

bitwise operations

Short homomorphic integer types support some bitwise operations.

A simple example on how to use these operations:

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 2;
    let msg2 = 1;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the private client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    // We use the server public key to homomorphically compute a bitwise AND:
    let ct_3 = server_key.unchecked_bitand(&ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_3);
    assert_eq!(output, (msg1 & msg2) % modulus as u64);
}

comparisons

Short homomorphic integer types support comparison operations.

A simple example on how to use these operations:

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 2;
    let msg2 = 1;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the private client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let ct_2 = client_key.encrypt(msg2);

    // We use the server public key to execute an integer circuit:
    let ct_3 = server_key.unchecked_greater_or_equal(&ct_1, &ct_2);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_3);
    assert_eq!(output, (msg1 >= msg2) as u64 % modulus as u64);
}

univariate function evaluations

A simple example on how to use this operation to homomorphically compute the hamming weight (i.e., the number of bits equal to one) of an encrypted number.

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;

    let modulus = client_key.parameters.message_modulus.0;

    // We use the private client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);

    //define the accumulator as the
    let acc = server_key.generate_accumulator(|n| n.count_ones().into());

    // add the two ciphertexts
    let ct_res = server_key.apply_lookup_table(&ct_1, &acc);


    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_res);
    assert_eq!(output, msg1.count_ones() as u64);
}

bi-variate function evaluations

Using the shortint types offers the possibility to evaluate bi-variate functions, or functions that take two ciphertexts as input. This requires choosing a parameter set such that the carry buffer size is at least as large as the message (i.e., PARAM_MESSAGE_X_CARRY_Y with X <= Y).

Here is a simple code example:

use tfhe::shortint::prelude::*;

fn main() {
    // We generate a set of client/server keys to compute over Z/2^2Z, with 2 carry bits
    let (client_key, server_key) = gen_keys(PARAM_MESSAGE_2_CARRY_2);

    let msg1 = 3;
    let msg2 = 2;

    let modulus = client_key.parameters.message_modulus.0 as u64;

    // We use the private client key to encrypt two messages:
    let ct_1 = client_key.encrypt(msg1);
    let mut ct_2 = client_key.encrypt(msg2);

    // Compute the accumulator for the bivariate functions
    let acc = server_key.generate_accumulator_bivariate(|x,y| (x.count_ones()
        + y.count_ones()) as u64 % modulus );

    let ct_res = server_key.smart_apply_lookup_table_bivariate(&ct_1, &mut ct_2, &acc);

    // We use the client key to decrypt the output of the circuit:
    let output = client_key.decrypt(&ct_res);
    assert_eq!(output, (msg1.count_ones() as u64 + msg2.count_ones() as u64) % modulus);
}

Operations

The structure and operations related to all types (ì.e., Booleans, shortint and integer) are described in this section.

Booleans

Native homomorphic Booleans support common Boolean operations.

The list of supported operations is:

name
symbol
type

ShortInt

Native small homomorphic integer types (e.g., FheUint3 or FheUint4) easily compute various operations. In general, computing over encrypted data is as easy as computing over clear data, since the same operation symbol is used. The addition between two ciphertexts is done using the symbol + between two FheUint. Many operations can be computed between a clear value (i.e. a scalar) and a ciphertext.

In Rust native types, any operation is modular. In Rust, u8, computations are done modulus 2^8. The similar idea is applied for FheUintX, where operations are done modulus 2^X. In the type FheUint3, operations are done modulo 8.

Arithmetic operations.

Small homomorphic integer types support all common arithmetic operations, meaning +, -, x, /, mod.

The division operation implements a subtlety: since data is encrypted, it might be possible to compute a division by 0. In this case, the division is tweaked so that dividing by 0 returns 0.

The list of supported operations is:

name
symbol
type

A simple example on how to use these operations:

Bitwise operations.

Small homomorphic integer types support some bitwise operations.

The list of supported operations is:

name
symbol
type

A simple example on how to use these operations:

Comparisons.

Small homomorphic integer types support comparison operations.

Due to some Rust limitations, it is not possible to overload the comparison symbols because of the inner definition of the operations. Rust expects to have a Boolean as an output, whereas a ciphertext encrypted result is returned when using homomorphic types.

You will need to use the different methods instead of using symbols for the comparisons. These methods follow the same naming conventions as the two standard Rust traits:

A simple example on how to use these operations:

Univariate function evaluations.

The shortint type also supports the computation of univariate functions, which deep down uses TFHE's programmable bootstrapping.

A simple example on how to use these operations:

Bivariate function evaluations.

Using the shortint type allows you to evaluate bivariate functions (i.e., functions that takes two ciphertexts as input).

A simple code example:

Integer

In TFHE-rs, integers are used to encrypt any messages larger than 4 bits. All supported operations are listed below.

Arithmetic operations.

Homomorphic integer types support arithmetic operations.

The list of supported operations is:

name
symbol
type

A simple example on how to use these operations:

Bitwise operations.

Homomorphic integer types support some bitwise operations.

The list of supported operations is:

name
symbol
type

A simple example on how to use these operations:

Comparisons.

Homomorphic integers support comparison operations. Since Rust does not allow the overloading of these operations, a simple function has been associated to each one.

The list of supported operations is:

name
symbol
type

A simple example on how to use these operations:

Min/Max.

Homomorphic integers support the min/max operations.

name
symbol
type

A simple example on how to use these operations:

BitAnd

&

Binary

BitOr

|

Binary

BitXor

^

Binary

Neg

!

Unary

Add

+

Binary

Sub

-

Binary

Mul

*

Binary

Div

/

Binary

Rem

%

Binary

Neg

!

Unary

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint3};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint3().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);
    
    let clear_a = 7;
    let clear_b = 3;
    let clear_c = 2;

    let mut a = FheUint3::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint3::try_encrypt(clear_b, &keys)?;
    let mut c = FheUint3::try_encrypt(clear_c, &keys)?;


    a = a * &b;  // Clear equivalent computations: 7 * 3 mod 8 = 5
    b = &b + &c; // Clear equivalent computations: 3 + 2 mod 8 = 5
    b = b - 5;   // Clear equivalent computations: 5 - 5 mod 8 = 0
    
    let dec_a = a.decrypt(&keys);
    let dec_b = b.decrypt(&keys);
    
    // We homomorphically swapped values using bitwise operations
    assert_eq!(dec_a, (clear_a * clear_b) % 8);
    assert_eq!(dec_b, ((clear_b + clear_c) - 5) % 8);

    Ok(())
}

BitAnd

&

Binary

BitOr

|

Binary

BitXor

^

Binary

Shr

>>

Binary

Shl

<<

Binary

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint3};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint3().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);
    
    let clear_a = 7;
    let clear_b = 3;
    
    let mut a = FheUint3::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint3::try_encrypt(clear_b, &keys)?;
    
    a = a ^ &b;
    b = b ^ &a;
    a = a ^ &b;
    
    let dec_a = a.decrypt(&keys);
    let dec_b = b.decrypt(&keys);
    
    // We homomorphically swapped values using bitwise operations
    assert_eq!(dec_a, clear_b);
    assert_eq!(dec_b, clear_a);

    Ok(())
}
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint3};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint3().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);
    
    let clear_a = 7;
    let clear_b = 3;
    
    let mut a = FheUint3::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint3::try_encrypt(clear_b, &keys)?;
    
    assert_eq!(a.gt(&b).decrypt(&keys) != 0, true);
    assert_eq!(b.le(&a).decrypt(&keys) != 0, true);

    Ok(())
}
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint4};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint4().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);

    let pow_5 = |value: u64| {
        value.pow(5) % FheUint4::MODULUS as u64
    };

    let clear_a = 12;
    let a = FheUint4::try_encrypt(12, &keys)?;

    let c = a.map(pow_5);
    let decrypted = c.decrypt(&keys);
    assert_eq!(decrypted, pow_5(clear_a) as u8);

    Ok(())
}
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint2};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint2().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);
    
    let clear_a = 1;
    let clear_b = 3;
    let a = FheUint2::try_encrypt(clear_a, &keys)?;
    let b = FheUint2::try_encrypt(clear_b, &keys)?;

    
    let c = a.bivariate_function(&b, std::cmp::max);
    let decrypted = c.decrypt(&keys);
    assert_eq!(decrypted, std::cmp::max(clear_a, clear_b) as u8);

    Ok(())
}

Add

+

Binary

Sub

-

Binary

Mul

*

Binary

Neg

!

Unary

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint8().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);
    
    let clear_a = 15_u64;
    let clear_b = 27_u64;
    let clear_c = 43_u64;

    let mut a = FheUint8::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint8::try_encrypt(clear_b, &keys)?;
    let mut c = FheUint8::try_encrypt(clear_c, &keys)?;


    a = a * &b;  // Clear equivalent computations: 15 * 27 mod 256 = 149
    b = &b + &c; // Clear equivalent computations: 27 + 43 mod 256 = 70
    b = b - 76u8;   // Clear equivalent computations: 70 - 76 mod 256 = 250
    
    let dec_a: u8 = a.decrypt(&keys);
    let dec_b: u8 = b.decrypt(&keys);
    
    assert_eq!(dec_a, ((clear_a * clear_b) % 256_u64) as u8);
    assert_eq!(dec_b, (((clear_b  + clear_c).wrapping_sub(76_u64)) % 256_u64) as u8);

    Ok(())
}

BitAnd

&

Binary

BitOr

|

Binary

BitXor

^

Binary

Shr

>>

Binary

Shl

<<

Binary

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint8().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);
    
    let clear_a = 164;
    let clear_b = 212;

    let mut a = FheUint8::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint8::try_encrypt(clear_b, &keys)?;

    a = a ^ &b;
    b = b ^ &a;
    a = a ^ &b;

    let dec_a: u8 = a.decrypt(&keys);
    let dec_b: u8 = b.decrypt(&keys);

    // We homomorphically swapped values using bitwise operations
    assert_eq!(dec_a, clear_b);
    assert_eq!(dec_b, clear_a);

    Ok(())
}

Greater than

gt

Binary

Greater or equal than

ge

Binary

Lower than

lt

Binary

Lower or equal than

le

Binary

Equal

eq

Binary

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint8().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);

    let clear_a:u8 = 164;
    let clear_b:u8 = 212;

    let mut a = FheUint8::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint8::try_encrypt(clear_b, &keys)?;

    let greater = a.gt(&b);
    let greater_or_equal = a.ge(&b);
    let lower = a.lt(&b);
    let lower_or_equal = a.le(&b);
    let equal = a.eq(&b);

    let dec_gt : u8 = greater.decrypt(&keys);
    let dec_ge : u8 = greater_or_equal.decrypt(&keys);
    let dec_lt : u8 = lower.decrypt(&keys);
    let dec_le : u8 = lower_or_equal.decrypt(&keys);
    let dec_eq : u8 = equal.decrypt(&keys);

    // We homomorphically swapped values using bitwise operations
    assert_eq!(dec_gt, (clear_a > clear_b ) as u8);
    assert_eq!(dec_ge, (clear_a >= clear_b) as u8);
    assert_eq!(dec_lt, (clear_a < clear_b ) as u8);
    assert_eq!(dec_le, (clear_a <= clear_b) as u8);
    assert_eq!(dec_eq, (clear_a == clear_b) as u8);

    Ok(())
}

Min

min

Binary

Max

max

Binary

use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::all_disabled().enable_default_uint8().build();
    let (keys, server_keys) = generate_keys(config);
    set_server_key(server_keys);

    let clear_a:u8 = 164;
    let clear_b:u8 = 212;

    let mut a = FheUint8::try_encrypt(clear_a, &keys)?;
    let mut b = FheUint8::try_encrypt(clear_b, &keys)?;

    let min = a.min(&b);
    let max = a.max(&b);

    let dec_min : u8 = min.decrypt(&keys);
    let dec_max : u8 = max.decrypt(&keys);

    // We homomorphically swapped values using bitwise operations
    assert_eq!(dec_min, u8::min(clear_a, clear_b));
    assert_eq!(dec_max, u8::max(clear_a, clear_b));

    Ok(())
}
PartialOrd
PartialEq

Tutorial

Quick Start

The basic steps for using the high-level API of TFHE-rs are:

  1. Importing TFHE-rs prelude;

  2. Client-side: Configuring and creating keys;

  3. Client-side: Encrypting data;

  4. Server-side: Setting the server key;

  5. Server-side: Computing over encrypted data;

  6. Client-side: Decrypting data.

Here is the full example (mixing client and server parts):

use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheUint8};
use tfhe::prelude::*;

fn main() {
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    // Client-side
    let (client_key, server_key) = generate_keys(config);

    let clear_a = 27u8;
    let clear_b = 128u8;

    let a = FheUint8::encrypt(clear_a, &client_key);
    let b = FheUint8::encrypt(clear_b, &client_key);

    //Server-side
    set_server_key(server_key);
    let result = a + b;

    //Client-side
    let decrypted_result: u8 = result.decrypt(&client_key);

    let clear_result = clear_a + clear_b;

    assert_eq!(decrypted_result, clear_result);
}

Default configuration for x86 Unix machines:

tfhe = { version = "0.2.5", features = ["integer", "x86_64-unix"]}

Other configurations can be found here.

Imports.

tfhe uses traits to have a consistent API for creating FHE types and enable users to write generic functions. To be able to use associated functions and methods of a trait, the trait has to be in scope.

To make it easier, the prelude 'pattern' is used. All tfhe important traits are in a prelude module that you glob import. With this, there is no need to remember or know the traits to import.

use tfhe::prelude::*;

1. Configuring and creating keys.

The first step is the creation of the configuration. The configuration is used to declare which type you will use or not use, as well as enabling you to use custom crypto-parameters for these types for more advanced usage / testing.

Creating a configuration is done using the ConfigBuilder type.

In this example, 8-bit unsigned integers with default parameters are used. The integers feature must also be enabled, as per the table on the Getting Started page.

The config is done by first creating a builder with all types deactivated. Then, the uint8 type with default parameters is activated.

use tfhe::{ConfigBuilder, generate_keys};

fn main() {
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    let (client_key, server_key) = generate_keys(config);
}

The generate_keys command returns a client key and a server key.

The client_key is meant to stay private and not leave the client whereas the server_key can be made public and sent to a server for it to enable FHE computations.

2. Setting the server key.

The next step is to call set_server_key

This function will move the server key to an internal state of the crate and manage the details to give a simpler interface.

use tfhe::{ConfigBuilder, generate_keys, set_server_key};

fn main() {
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    let (client_key, server_key) = generate_keys(config);

    set_server_key(server_key);
}

3. Encrypting data.

Encrypting data is done via the encrypt associated function of the [FheEncrypt] trait.

Types exposed by this crate implement at least one of [FheEncrypt] or [FheTryEncrypt] to allow enryption.

let clear_a = 27u8;
let clear_b = 128u8;

let a = FheUint8::encrypt(clear_a, &client_key);
let b = FheUint8::encrypt(clear_b, &client_key);

4. Computation and decryption.

Computations should be as easy as normal Rust to write, thanks to operator overloading.

let result = a + b;

The decryption is done by using the decrypt method, which comes from the [FheDecrypt] trait.

let decrypted_result: u8 = result.decrypt(&client_key);

let clear_result = clear_a + clear_b;

assert_eq!(decrypted_result, clear_result);

A first complete example: FheLatinString (Integer)

The goal of this tutorial is to build a data type that represents a Latin string in FHE while implementing the to_lower and to_upper functions.

The allowed characters in a Latin string are:

  • Uppercase letters: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

  • Lowercase letters: a b c d e f g h i j k l m n o p q r s t u v w x y z

For the code point of the letters,ascii codes are used:

  • The uppercase letters are in the range [65, 90]

  • The lowercase letters are in the range [97, 122]

lower_case = upper_case + 32 <=> upper_case = lower_case - 32

For this type, the FheUint8 type is used.

Types and methods.

This type will hold the encrypted characters as a Vec<FheUint8>, as well as the encrypted constant 32 to implement the functions that change the case.

In the FheLatinString::encrypt function, some data validation is done:

  • The input string can only contain ascii letters (no digit, no special characters).

  • The input string cannot mix lower and upper case letters.

These two points are to work around a limitation of FHE. It is not possible to create branches, meaning the function cannot use conditional statements. Checking if the 'char' is an uppercase letter to modify it to a lowercase one cannot be done, like in the example below.

fn to_lower(string: &String) -> String {
    let mut result = String::with_capacity(string.len());
    for char in string.chars() {
        if char.is_uppercase() {
            result.extend(char.to_lowercase().to_string().chars())
        }
    }
    result
}

With these preconditions checked, implementing to_lower and to_upper is rather simple.

To use the FheUint8 type, the integer feature must be activated:

# Cargo.toml

[dependencies]
# Default configuration for x86 Unix machines:
tfhe = { version = "0.2.5", features = ["integer", "x86_64-unix"]}

Other configurations can be found here.

use tfhe::{FheUint8, ConfigBuilder, generate_keys, set_server_key, ClientKey};
use tfhe::prelude::*;

struct FheLatinString{
    bytes: Vec<FheUint8>,
    // Constant used to switch lower case <=> upper case
    cst: FheUint8,
}

impl FheLatinString {
    fn encrypt(string: &str, client_key: &ClientKey) -> Self {
        assert!(
            string.chars().all(|char| char.is_ascii_alphabetic()),
            "The input string must only contain ascii letters"
        );

        let has_mixed_case = string.as_bytes().windows(2).any(|window| {
            let first = char::from(*window.first().unwrap());
            let second = char::from(*window.last().unwrap());

            (first.is_ascii_lowercase() && second.is_ascii_uppercase())
                || (first.is_ascii_uppercase() && second.is_ascii_lowercase())
        });

        assert!(
            !has_mixed_case,
            "The input string cannot mix lower case and upper case letters"
        );

        let fhe_bytes = string
            .bytes()
            .map(|b| FheUint8::encrypt(b, client_key))
            .collect::<Vec<FheUint8>>();
        let cst = FheUint8::encrypt(32, client_key);

        Self {
            bytes: fhe_bytes,
            cst,
        }
    }

    fn decrypt(&self, client_key: &ClientKey) -> String {
        let ascii_bytes = self
            .bytes
            .iter()
            .map(|fhe_b| fhe_b.decrypt(client_key))
            .collect::<Vec<u8>>();
        String::from_utf8(ascii_bytes).unwrap()
    }

    fn to_upper(&self) -> Self {
        Self {
            bytes: self
                .bytes
                .iter()
                .map(|b| b - &self.cst)
                .collect::<Vec<FheUint8>>(),
            cst: self.cst.clone(),
        }
    }

    fn to_lower(&self) -> Self {
        Self {
            bytes: self
                .bytes
                .iter()
                .map(|b| b + &self.cst)
                .collect::<Vec<FheUint8>>(),
            cst: self.cst.clone(),
        }
    }
}


fn main() {
    let config = ConfigBuilder::all_disabled()
        .enable_default_uint8()
        .build();

    let (client_key, server_key) = generate_keys(config);

    set_server_key(server_key);

    let my_string = FheLatinString::encrypt("zama", &client_key);
    let verif_string = my_string.decrypt(&client_key);
    println!("{}", verif_string);

    let my_string_upper = my_string.to_upper();
    let verif_string = my_string_upper.decrypt(&client_key);
    println!("{}", verif_string);
    assert_eq!(verif_string, "ZAMA");

    let my_string_lower = my_string_upper.to_lower();
    let verif_string = my_string_lower.decrypt(&client_key);
    println!("{}", verif_string);
    assert_eq!(verif_string, "zama");
}

A more complex example: Parity Bit (Boolean)

This example is dedicated to the building of a small function that homomorphically computes a parity bit.

First, a non-generic function is written. Then, generics are used to handle the case where the function inputs are both FheBools and clear bools.

The parity bit function takes as input two parameters:

  • A slice of Boolean

  • A mode (Odd or Even)

This function returns a Boolean that will be either true or false so that the sum of Booleans (in the input and the returned one) is either an Odd or Even number, depending on the requested mode.


Non-generic version.

To use Booleans, the booleans feature in our Cargo.toml must be enabled:

# Cargo.toml

# Default configuration for x86 Unix machines:
tfhe = { version = "0.2.5", features = ["boolean", "x86_64-unix"]}

Other configurations can be found here.

function definition

First, the verification function is defined.

The way to find the parity bit is to initialize it to false, then XOR it with all the bits, one after the other, adding negation depending on the requested mode.

A validation function is also defined to sum together the number of the bit set within the input with the computed parity bit and check that the sum is an even or odd number, depending on the mode.

use tfhe::FheBool;
use tfhe::prelude::*;

#[derive(Copy, Clone, Debug)]
enum ParityMode {
    // The sum bits of message + parity bit must an odd number
    Odd,
    // The sum bits of message + parity bit must an even number
    Even,
}

fn compute_parity_bit(fhe_bits: &[FheBool], mode: ParityMode) -> FheBool {
    let mut parity_bit = fhe_bits[0].clone();
    for fhe_bit in &fhe_bits[1..] {
        parity_bit = fhe_bit ^ parity_bit
    }

    match mode {
        ParityMode::Odd => !parity_bit,
        ParityMode::Even => parity_bit,
    }
}

fn is_even(n: u8) -> bool {
    (n & 1) == 0
}

fn is_odd(n: u8) -> bool {
    !is_even(n)
}

fn check_parity_bit_validity(bits: &[bool], mode: ParityMode, parity_bit: bool) -> bool {
    let num_bit_set = bits
        .iter()
        .map(|bit| *bit as u8)
        .fold(parity_bit as u8, |acc, bit| acc + bit);

    match mode {
        ParityMode::Even => is_even(num_bit_set),
        ParityMode::Odd => is_odd(num_bit_set),
    }
}

final code

After the mandatory configuration steps, the function is called:

use tfhe::{FheBool, ConfigBuilder, generate_keys, set_server_key};
use tfhe::prelude::*;

#[derive(Copy, Clone, Debug)]
enum ParityMode {
    // The sum bits of message + parity bit must an odd number
    Odd,
    // The sum bits of message + parity bit must an even number
    Even,
}

fn compute_parity_bit(fhe_bits: &[FheBool], mode: ParityMode) -> FheBool {
    let mut parity_bit = fhe_bits[0].clone();
    for fhe_bit in &fhe_bits[1..] {
        parity_bit = fhe_bit ^ parity_bit
    }

    match mode {
        ParityMode::Odd => !parity_bit,
        ParityMode::Even => parity_bit,
    }
}

fn is_even(n: u8) -> bool {
    (n & 1) == 0
}

fn is_odd(n: u8) -> bool {
    !is_even(n)
}

fn check_parity_bit_validity(bits: &[bool], mode: ParityMode, parity_bit: bool) -> bool {
    let num_bit_set = bits
        .iter()
        .map(|bit| *bit as u8)
        .fold(parity_bit as u8, |acc, bit| acc + bit);

    match mode {
        ParityMode::Even => is_even(num_bit_set),
        ParityMode::Odd => is_odd(num_bit_set),
    }
}

fn main() {
    let config = ConfigBuilder::all_disabled().enable_default_bool().build();

    let (client_key, server_key) = generate_keys(config);

    set_server_key(server_key);

    let clear_bits = [0, 1, 0, 0, 0, 1, 1].map(|b| (b != 0) as bool);

    let fhe_bits = clear_bits
        .iter()
        .map(|bit| FheBool::encrypt(*bit, &client_key))
        .collect::<Vec<FheBool>>();

    let mode = ParityMode::Odd;
    let fhe_parity_bit = compute_parity_bit(&fhe_bits, mode);
    let decrypted_parity_bit = fhe_parity_bit.decrypt(&client_key);
    let is_parity_bit_valid = check_parity_bit_validity(&clear_bits, mode, decrypted_parity_bit);
    println!("Parity bit is set: {} for mode: {:?}", decrypted_parity_bit, mode);
    assert!(is_parity_bit_valid);

    let mode = ParityMode::Even;
    let fhe_parity_bit = compute_parity_bit(&fhe_bits, mode);
    let decrypted_parity_bit = fhe_parity_bit.decrypt(&client_key);
    let is_parity_bit_valid = check_parity_bit_validity(&clear_bits, mode, decrypted_parity_bit);
    println!("Parity bit is set: {} for mode: {:?}", decrypted_parity_bit, mode);
    assert!(is_parity_bit_valid);
}

Generic version.

To make the compute_parity_bit function compatible with both FheBool and bool, generics have to be used.

Writing a generic function that accepts FHE types as well as clear types can help test the function to see if it is correct. If the function is generic, it can run with clear data, allowing the use of print-debugging or a debugger to spot errors.

Writing generic functions that use operator overloading for our FHE types can be trickier than normal, since FHE types are not copy. So using the reference & is mandatory, even though this is not the case when using native types, which are all Copy.

This will make the generic bounds trickier at first.

writing the correct trait bounds

The function has the following signature:

fn check_parity_bit_validity(
    fhe_bits: &[FheBool],
    mode: ParityMode,
) -> bool

To make it generic, the first step is:

fn compute_parity_bit<BoolType>(
    fhe_bits: &[BoolType],
    mode: ParityMode,
) -> BoolType

Next, the generic bounds have to be defined with the where clause.

In the function, the following operators are used:

  • ! (trait: Not)

  • ^ (trait: BitXor)

By adding them to where, this gives:

where
    BoolType: Clone + Not<Output = BoolType>,
    BoolType: BitXor<BoolType, Output=BoolType>,

However, the compiler will complain:

---- src/user_doc_tests.rs - user_doc_tests (line 199) stdout ----
error[E0369]: no implementation for `&BoolType ^ BoolType`
--> src/user_doc_tests.rs:218:30
    |
21  | parity_bit = fhe_bit ^ parity_bit
    |              ------- ^ ---------- BoolType
    |             |
    |             &BoolType
    |
help: consider extending the `where` bound, but there might be an alternative better way to express this requirement
    |
17  | BoolType: BitXor<BoolType, Output=BoolType>, &BoolType: BitXor<BoolType>
    |                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error

fhe_bit is a reference to a BoolType (&BoolType) since it is borrowed from the fhe_bits slice when iterating over its elements. The first try is to change the BitXor bounds to what the Compiler suggests by requiring &BoolType to implement BitXor and not BoolType.

where
    BoolType: Clone + Not<Output = BoolType>,
    &BoolType: BitXor<BoolType, Output=BoolType>,

The Compiler is still not happy:

---- src/user_doc_tests.rs - user_doc_tests (line 236) stdout ----
error[E0637]: `&` without an explicit lifetime name cannot be used here
  --> src/user_doc_tests.rs:251:5
   |
17 |     &BoolType: BitXor<BoolType, Output=BoolType>,
   |     ^ explicit lifetime name needed here

error[E0310]: the parameter type `BoolType` may not live long enough
  --> src/user_doc_tests.rs:251:16
   |
17 |     &BoolType: BitXor<BoolType, Output=BoolType>,
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the reference type `&'static BoolType` does not outlive the data it points at
   |
help: consider adding an explicit lifetime bound...
   |
15 |     BoolType: Clone + Not<Output = BoolType> + 'static,
   |

The way to fix this is to use Higher-Rank Trait Bounds:

where
    BoolType: Clone + Not<Output = BoolType>,
    for<'a> &'a BoolType: BitXor<BoolType, Output = BoolType>,

The final code will look like this:

use std::ops::{Not, BitXor};

#[derive(Copy, Clone, Debug)]
enum ParityMode {
    // The sum bits of message + parity bit must an odd number
    Odd,
    // The sum bits of message + parity bit must an even number
    Even,
}

fn compute_parity_bit<BoolType>(fhe_bits: &[BoolType], mode: ParityMode) -> BoolType
where
    BoolType: Clone + Not<Output = BoolType>,
    for<'a> &'a BoolType: BitXor<BoolType, Output = BoolType>,
{
    let mut parity_bit = fhe_bits[0].clone();
    for fhe_bit in &fhe_bits[1..] {
        parity_bit = fhe_bit ^ parity_bit
    }

    match mode {
        ParityMode::Odd => !parity_bit,
        ParityMode::Even => parity_bit,
    }
}

final code

Here is a complete example that uses this function for both clear and FHE values:

use tfhe::{FheBool, ConfigBuilder, generate_keys, set_server_key};
use tfhe::prelude::*;

use std::ops::{Not, BitXor};

#[derive(Copy, Clone, Debug)]
enum ParityMode {
    // The sum bits of message + parity bit must an odd number
    Odd,
    // The sum bits of message + parity bit must an even number
    Even,
}

fn compute_parity_bit<BoolType>(fhe_bits: &[BoolType], mode: ParityMode) -> BoolType
    where
        BoolType: Clone + Not<Output=BoolType>,
        for<'a> &'a BoolType: BitXor<BoolType, Output=BoolType>,
{
    let mut parity_bit = fhe_bits[0].clone();
    for fhe_bit in &fhe_bits[1..] {
        parity_bit = fhe_bit ^ parity_bit
    }

    match mode {
        ParityMode::Odd => !parity_bit,
        ParityMode::Even => parity_bit,
    }
}

fn is_even(n: u8) -> bool {
    (n & 1) == 0
}

fn is_odd(n: u8) -> bool {
    !is_even(n)
}

fn check_parity_bit_validity(bits: &[bool], mode: ParityMode, parity_bit: bool) -> bool {
    let num_bit_set = bits
        .iter()
        .map(|bit| *bit as u8)
        .fold(parity_bit as u8, |acc, bit| acc + bit);

    match mode {
        ParityMode::Even => is_even(num_bit_set),
        ParityMode::Odd => is_odd(num_bit_set),
    }
}

fn main() {
    let config = ConfigBuilder::all_disabled().enable_default_bool().build();

    let ( client_key, server_key) = generate_keys(config);

    set_server_key(server_key);

    let clear_bits = [0, 1, 0, 0, 0, 1, 1].map(|b| (b != 0) as bool);

    let fhe_bits = clear_bits
        .iter()
        .map(|bit| FheBool::encrypt(*bit, &client_key))
        .collect::<Vec<FheBool>>();

    let mode = ParityMode::Odd;
    let clear_parity_bit = compute_parity_bit(&clear_bits, mode);
    let fhe_parity_bit = compute_parity_bit(&fhe_bits, mode);
    let decrypted_parity_bit = fhe_parity_bit.decrypt(&client_key);
    let is_parity_bit_valid = check_parity_bit_validity(&clear_bits, mode, decrypted_parity_bit);
    println!("Parity bit is set: {} for mode: {:?}", decrypted_parity_bit, mode);
    assert!(is_parity_bit_valid);
    assert_eq!(decrypted_parity_bit, clear_parity_bit);

    let mode = ParityMode::Even;
    let clear_parity_bit = compute_parity_bit(&clear_bits, mode);
    let fhe_parity_bit = compute_parity_bit(&fhe_bits, mode);
    let decrypted_parity_bit = fhe_parity_bit.decrypt(&client_key);
    let is_parity_bit_valid = check_parity_bit_validity(&clear_bits, mode, decrypted_parity_bit);
    println!("Parity bit is set: {} for mode: {:?}", decrypted_parity_bit, mode);
    assert!(is_parity_bit_valid);
    assert_eq!(decrypted_parity_bit, clear_parity_bit);
}