Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
TFHE-rs is a pure Rust implementation of Fully Homomorphic Encryption over the Torus (TFHE) to perform Boolean and integer arithmetic on encrypted data.
TFHE-rs implements advanced TFHE features, empowering developers and researchers with fine-grained control over TFHE so that they can focus on high-level functionality without delving into low-level implementation.
TFHE-rs includes:
Rust API: the primary API for working with TFHE-rs in Rust projects.
C API: for developers who prefer to use C.
Client-side WASM API: to integrate TFHE-rs functionalities into WebAssembly applications.
TFHE is a Fully Homomorphic Encryption (FHE) scheme based on Learning With Errors (LWE), which is a secure cryptographic primitive against even quantum computers. The TFHE-rs library implements Zama’s variant of TFHE.
The basic elements of cryptography:
Message (or Cleartext): raw values before encryption.
Plaintext: encoded messages.
Ciphertext: encrypted messages.
FHE allows to compute on ciphertexts without revealing the content of the messages. A scheme is fully homomorphic if it supports at least two of the following operations when evaluating any programs. ( is a plaintext and is the corresponding ciphertext):
Homomorphic univariate function evaluation:
Homomorphic addition:
Homomorphic multiplication:
Zama's variant of TFHE is a fully homomorphic scheme that takes fixed-precision numbers as messages. It implements all homomorphic operations needed, such as addition and function evaluation via Programmable Bootstrapping.
Refer to the preliminary whitepaper for more details.
Using TFHE-rs in Rust includes the following steps:
Key generation: generate a pair of keys using secure parameters.
Client key: used for encryption and decryption of data. This key must be kept secret.
Server key (or Evaluation key): used for performing operations on encrypted data. This key could be public.
Encryption: encrypt plaintexts using the client key to produce ciphertexts.
Homomorphic operation: perform operations on ciphertexts using the server key.
Decryption: decrypt the resulting ciphertexts back to plaintexts using the client key.
To understand more about FHE applications, see the 6-minute introduction to homomorphic encryption.
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.
Learn the basics of TFHE-rs, set it up, and make it run with ease.
Start building with TFHE-rs by exploring its core features, discovering essential guides, and learning more with user-friendly tutorials.
Access to additional resources and join the Zama community.
Explore step-by-step guides that walk you through real-world uses of TFHE-rs.
: Learn how to implement a parity bit calculation over encrypted data
: See how to process string data securely by changing cases while keeping the data encrypted.
: Delve into a more complex example: implementing the SHA256 hash function entirely on encrypted boolean values.
: A complete list of all available tutorials in one place.tutorials: A complete list of all available tutorials in one place.
Take a deep dive into TFHE-rs, exploring APIs from the highest to the lowest level of abstraction and accessing additional resources for in-depth explanations.
: High-level API that abstracts cryptographic complexities and simplifies the development and more
: Mid-level APIs that enable evaluation of Boolean, short integer, and integer circuits
: Low-level API with the primitive functions and types of the TFHE scheme
: Resources that explain the Fully Homomorphic Encryption scheme - TFHE
: Document describing algorithms implemented in TFHE-rs
Ask technical questions and discuss with the community. Our team of experts usually answers within 24 hours during working days.
Collaborate with us to advance the FHE spaces and drive innovation together.
Zama 5-Question Developer Survey
We want to hear from you! Take 1 minute to share your thoughts and helping us enhance our documentation and libraries. 👉 to participate.
This document provides instructions to set up TFHE-rs in your project.
First, add TFHE-rs as a dependency in your Cargo.toml
.
Performance: for optimal performance, it is highly recommended to run code that uses TFHE-rs
in release mode with cargo's --release
flag.
TFHE-rs currently supports the following platforms:
By default, TFHE-rs makes the assumption that hardware AES features are enabled on the target CPU. The required CPU features are:
x86_64: sse2, aesni
aarch64: aes, neon
To add support for older CPU, import TFHE-rs with the software-prng
feature in your Cargo.toml
:
tfhe = { version = "~1.2.0", features = ["boolean", "shortint", "integer"] }
Linux
Supported
Supported*
macOS
Supported
Supported*
Windows
Supported with RDSEED
instruction
Unsupported
tfhe = { version = "~1.2.0", features = ["boolean", "shortint", "integer", "software-prng"] }
This document provides instructions on how to decrypt data.
To decrypt data, use the decrypt
method from the FheDecrypt
trait:
use tfhe::prelude::*;
use tfhe::{generate_keys, ConfigBuilder, FheUint8};
fn main() {
let config = ConfigBuilder::default().build();
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);
let decrypted_a: u8 = a.decrypt(&client_key);
let decrypted_b: u8 = b.decrypt(&client_key);
assert_eq!(decrypted_a, clear_a);
assert_eq!(decrypted_b, clear_b);
}
FHE Computations
Run FHE computation on encrypted data.
Configuration
Advanced configuration for better performance.
Integration
Use TFHE-rs in different contexts or platforms..
This document details the performance benchmarks of zero-knowledge proofs for compact public key encryption using TFHE-rs.
Benchmarks for the zero-knowledge proofs have been run on a m6i.4xlarge
with 16 cores to simulate a usual client configuration. The verifications are done on an hpc7a.96xlarge
AWS instance to mimic a powerful server.
This document details the CPU performance benchmarks of homomorphic operations using TFHE-rs.
By their nature, homomorphic operations run slower than their cleartext equivalents.
This document details the GPU performance benchmarks of homomorphic operations using TFHE-rs.
By their nature, homomorphic operations run slower than their cleartext equivalents.
This document details the HPU performance benchmarks of homomorphic operations on integers using TFHE-rs.
The cryptographic parameters HPU_PARAM_MESSAGE_2_CARRY_2_KS32_PBS_TUNIFORM_2M64
were used.
Below are the results for the execution on a single Alveo v80 board. The following table shows the performance when the inputs of the benchmarked operation are encrypted:
The following table shows the performance when the left input of the benchmarked operation is encrypted and the other is a clear scalar of the same size:
TFHE-rs benchmarks can be easily reproduced from the .
The following example shows how to reproduce TFHE-rs benchmarks:
This document explains how to call the function set_server_key
.
This function will move the server key to an internal state of the crate and manage the details for a simpler interface.
Here is an example:
This document explains how to initialize the configuration and generate keys.
The configuration specifies the selected data types and their custom crypto-parameters. You should only use custom parameters for advanced usage and/or testing.
To create a configuration, use the ConfigBuilder
type. The following example shows the setup using 8-bit unsigned integers with default parameters. Additionally, ensure the integers
feature is enabled, as indicated in the table on .
The configuration is initialized by creating a builder with all types deactivated. Then, the integer types with default parameters are activated, for using FheUint8
values.
The generate_keys
command returns a client key and a server key:
Client_key: this key should remain private and never leave the client.
Server_key: this key can be public and sent to a server to enable FHE computations.
Please refer to the for detailed performance benchmark results.
When measuring GPU times on your own on Linux, set the environment variable CUDA_MODULE_LOADING=EAGER
to avoid CUDA API overheads during the first kernel execution.
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 for more details about cryptographic parameters, and to see how to properly instantiate integers depending on the chosen representation.
use tfhe::{ConfigBuilder, generate_keys, set_server_key};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
}
use tfhe::{ConfigBuilder, generate_keys};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
}
#Integer benchmarks:
make bench_integer_hpu
This document describes the main integer types of encrypted data in TFHE-rs and explains how to specify bit sizes for encryption.
TFHE-rs supports two main types of encrypted data:
FheUint
: homomorphic equivalent of Rust unsigned integers u8, u16, ...
FheInt
: homomorphic equivalent of Rust signed integers i8, i16, ...
TFHE-rs uses integers to encrypt all messages which are larger than 4 bits.
Similar to Rust integers, you need to specify the bit size of data when declaring a variable:
// let clear_a: u64 = 7;
let mut a = FheUint64::try_encrypt(clear_a, &keys)?;
// let clear_b: i8 = 3;
let mut b = FheInt8::try_encrypt(clear_b, &keys)?;
// let clear_c: u128 = 2;
let mut c = FheUint128::try_encrypt(clear_c, &keys)?;
This document details the casting operations supported by TFHE-rs.
You can cast between integer types using either the cast_from
associated function or the cast_into
method.
The following example shows how to perform casting operations:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt16, FheUint8, FheUint32, FheUint16};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
// Casting requires server_key to set
// (encryptions/decryptions do not need server_key to be set)
set_server_key(server_key);
{
let clear = 12_837u16;
let a = FheUint16::encrypt(clear, &client_key);
// Downcasting
let a: FheUint8 = a.cast_into();
let da: u8 = a.decrypt(&client_key);
assert_eq!(da, clear as u8);
// Upcasting
let a: FheUint32 = a.cast_into();
let da: u32 = a.decrypt(&client_key);
assert_eq!(da, (clear as u8) as u32);
}
{
let clear = 12_837u16;
let a = FheUint16::encrypt(clear, &client_key);
// Upcasting
let a = FheUint32::cast_from(a);
let da: u32 = a.decrypt(&client_key);
assert_eq!(da, clear as u32);
// Downcasting
let a = FheUint8::cast_from(a);
let da: u8 = a.decrypt(&client_key);
assert_eq!(da, (clear as u32) as u8);
}
{
let clear = 12_837i16;
let a = FheInt16::encrypt(clear, &client_key);
// Casting from FheInt16 to FheUint16
let a = FheUint16::cast_from(a);
let da: u16 = a.decrypt(&client_key);
assert_eq!(da, clear as u16);
}
Ok(())
}
This document details the min/max operations supported by TFHE-rs.
Homomorphic integers support the min/max operations:
Min
min
Binary
Max
max
Binary
The following example shows how to perform min/max operations:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default().build();
let (keys, server_keys) = generate_keys(config);
set_server_key(server_keys);
let clear_a:u8 = 164;
let clear_b:u8 = 212;
let a = FheUint8::try_encrypt(clear_a, &keys)?;
let 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);
assert_eq!(dec_min, u8::min(clear_a, clear_b));
assert_eq!(dec_max, u8::max(clear_a, clear_b));
Ok(())
}
This document details the dot product operations supported by TFHE-rs.
Dot Product
dot_product
Binary
Currently, the dot product supports the following case:
One operand is a slice of FheBool
The other operand is a slice of clear values (e.g., u64
)
Both slices must be of the same length
The following example shows how to perform dot product:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheBool, FheUint8};
fn main() {
let (client_key, server_key) = generate_keys(ConfigBuilder::default());
set_server_key(server_key);
let a = [true, false, true]
.into_iter()
.map(|b| FheBool::encrypt(b, &client_key))
.collect::<Vec<_>>();
let b = [2u8, 3u8, 4u8];
let result = FheUint8::dot_product(&a, &b);
let decrypted: u8 = result.decrypt(&client_key);
assert_eq!(decrypted, 6u8);
}
This document explains how to encrypt data.
To encrypt data, use the encrypt
method from the FheEncrypt
trait. This crate provides types that implement either FheEncrypt
or FheTryEncrypt
or both, to enable encryption.
Here is an example:
use tfhe::prelude::*;
use tfhe::{generate_keys, ConfigBuilder, FheUint8};
fn main() {
let config = ConfigBuilder::default().build();
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);
}
This document explains the mechanism and steps to generate an oblivious encrypted random value using only server keys.
The goal is to give to the server the possibility to generate a random value, which will be obtained in an encrypted format and will remain unknown to the server. The implementation is based on this article.
This is possible through two methods on FheUint
and FheInt
:
generate_oblivious_pseudo_random
which return an integer taken uniformly in the full integer range ([0; 2^N[
for a FheUintN
and [-2^(N-1); 2^(N-1)[
for a FheIntN
).
generate_oblivious_pseudo_random_bounded
which return an integer taken uniformly in [0; 2^random_bits_count[
. For a FheUintN
, we must have random_bits_count <= N
. For a FheIntN
, we must have random_bits_count <= N - 1
.
Both methods functions take a seed Seed
as input, which could be any u128
value.
They both rely on the use of the usual server key.
The output is reproducible, i.e., the function is deterministic from the inputs: assuming the same hardware, seed and server key, this function outputs the same random encrypted value.
Here is an example of the usage:
use tfhe::prelude::FheDecrypt;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8, FheInt8, Seed};
pub fn main() {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
let random_bits_count = 3;
let ct_res = FheUint8::generate_oblivious_pseudo_random(Seed(0));
let dec_result: u8 = ct_res.decrypt(&client_key);
let ct_res = FheUint8::generate_oblivious_pseudo_random_bounded(Seed(0), random_bits_count);
let dec_result: u8 = ct_res.decrypt(&client_key);
assert!(dec_result < (1 << random_bits_count));
let ct_res = FheInt8::generate_oblivious_pseudo_random(Seed(0));
let dec_result: i8 = ct_res.decrypt(&client_key);
let ct_res = FheInt8::generate_oblivious_pseudo_random_bounded(Seed(0), random_bits_count);
let dec_result: i8 = ct_res.decrypt(&client_key);
assert!(dec_result < (1 << random_bits_count));
}
This document explains public key encryption and provides instructions for 2 methods.
Public key encryption refers to the cryptographic paradigm where the encryption key can be publicly distributed, whereas the decryption key remains secret to the owner. This differs from the usual case where the same secret key is used to encrypt and decrypt the data. In TFHE-rs, there are two methods for public key encryptions:
Classical public key: the first method involves the public key containing many encryptions of zero, as detailed in Guide to Fully Homomorphic Encryption over the [Discretized] Torus, Appendix A.
Compact public key: the second method is based on the paper TFHE Public-Key Encryption Revisited, allowing for significantly smaller key sizes compared to the first method.
Public keys can also be compressed to reduce size.
This example shows how to use classical public keys.
use tfhe::prelude::*;
use tfhe::{ConfigBuilder, generate_keys, FheUint8, PublicKey};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, _) = generate_keys(config);
let public_key = PublicKey::new(&client_key);
let a = FheUint8::try_encrypt(255u8, &public_key).unwrap();
let clear: u8 = a.decrypt(&client_key);
assert_eq!(clear, 255u8);
}
This example shows how to use compact public keys. The main difference is in the ConfigBuilder
where the parameter set has been changed.
For more information on using compact public keys to encrypt data and generate a zero-knowledge proof of correct encryption at the same time, see the guide on ZK proofs.
use tfhe::prelude::*;
use tfhe::{
generate_keys, CompactCiphertextList, CompactPublicKey, ConfigBuilder, FheUint8,
};
fn main() {
let config = ConfigBuilder::default()
.use_custom_parameters(
tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M128,
)
.build();
let (client_key, _) = generate_keys(config);
let public_key = CompactPublicKey::new(&client_key);
let compact_list = CompactCiphertextList::builder(&public_key)
.push(255u8)
.build();
let expanded = compact_list.expand().unwrap();
let a: FheUint8 = expanded.get(0).unwrap().unwrap();
let clear: u8 = a.decrypt(&client_key);
assert_eq!(clear, 255u8);
}
This document describes how to use trivial encryption in TFHE-rs to initialize server-side values.
Sometimes, the server side needs to initialize a value. For example, when computing the sum of a list of ciphertexts, you typically initialize the sum
variable to 0
.
Instead of asking the client to send an actual encrypted zero, the server can use a trivial encryption. A trivial encryption creates a ciphertext that contains the desired value but isn't securely encrypted - essentially anyone, any key can decrypt it.
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};
let config = ConfigBuilder::default().build();
let (client_key, sks) = generate_keys(config);
set_server_key(sks);
let a = FheUint8::try_encrypt_trivial(234u8).unwrap();
let clear: u8 = a.decrypt(&client_key);
assert_eq!(clear, 234);
Note that when you want to do an operation that involves a ciphertext and a clear value (often called scalar operation), you should only use trivial encryption of the clear value if the scalar operations that you want to run are not supported.
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint32};
let config = ConfigBuilder::default().build();
let (client_key, sks) = generate_keys(config);
set_server_key(sks);
// This is going to be faster
let a = FheUint32::try_encrypt(2097152u32, &client_key).unwrap();
let shift = 1u32;
let shifted = a << shift;
let clear: u32 = shifted.decrypt(&client_key);
assert_eq!(clear, 2097152 << 1);
// This is going to be slower
let a = FheUint32::try_encrypt(2097152u32, &client_key).unwrap();
let shift = FheUint32::try_encrypt_trivial(1u32).unwrap();
let shifted = a << shift;
let clear: u32 = shifted.decrypt(&client_key);
assert_eq!(clear, 2097152 << 1);
This document provides basic instructions to configure the Rust toolchain and features for TFHE-rs.
TFHE-rs requires a nightly Rust toolchain to build the C API and utilize advanced SIMD instructions. However, for other uses, a stable toolchain (version 1.81 or later) is sufficient.
Follow the following instructions to install the necessary Rust toolchain:
# If you don't need the C API or the advanced still unstable SIMD instructions use this
rustup toolchain install stable
# Otherwise install a nightly toolchain
rustup toolchain install nightly
You can set the toolchain using either of the following methods.
Manually specify the toolchain for each cargo command:
# By default the +stable should not be needed, but we add it here for completeness
cargo +stable build --release
cargo +stable test --release
# Or
cargo +nightly build --release
cargo +nightly test --release
Override the toolchain for the current project:
# This should not be necessary by default, but if you want to make sure your configuration is
# correct you can still set the overridden toolchain to stable
rustup override set stable
# cargo will use the `stable` toolchain.
cargo build --release
# Or
rustup override set nightly
# cargo will use the `nightly` toolchain.
cargo build --release
To verify the default toolchain used by Cargo, execute:
rustup show
TFHE-rs provides various cargo features to customize the types and features used.
This crate provides 3 kinds of data types. Each kind is enabled by activating the corresponding feature in the TOML line and has multiple types:
Booleans
boolean
Booleans
ShortInts
shortint
Short integers
Integers
integer
Arbitrary-sized integers
While the library generally selects automatically the best instruction sets available by the host, in the case of 'AVX-512', you have to choose it explicitly. This requires to use a nightly toolchain with the feature nightly-avx512
.
cargo +nightly build --release --features=nightly-avx512
Dark Market with TFHE-rs - July 7, 2023
Regular Expression Engine with TFHE-rs - June 30, 2023
Implement signed integers using TFHE-rs - Nov 8, 2023
This document details the HPU performance benchmarks of homomorphic operations using TFHE-rs.
By their nature, homomorphic operations run slower than their cleartext equivalents.
Please refer to the HPU benchmarks for detailed performance benchmark results.
This document summarizes the timings of some homomorphic operations over 64-bit encrypted integers, depending on the hardware. More details are given for the CPU, the GPU, the HPU or zeros-knowledge proofs.
The cryptographic parameters used for benchmarking follow a tweaked uniform (TUniform) noise distribution instead of a Gaussian. The main advantage of this distribution is to be bounded, whereas the usual Gaussian one is not. In some practical cases, this can simplify the use of homomorphic computation. See the noise section of the Security and cryptography documentation page for more information on the noise distributions.
You can get the parameters used for benchmarks by cloning the repository and checking out the commit you want to use (starting with the v0.8.0 release) and run the following make command:
make print_doc_bench_parameters
This document details the CPU performance benchmarks of homomorphic operations on integers using TFHE-rs.
By their nature, homomorphic operations run slower than their cleartext equivalents.
The following tables benchmark the execution time of some operation sets using FheUint
(unsigned integers). The FheInt
(signed integers) performs similarly.
The next table shows the operation timings on CPU when all inputs are encrypted:
The next table shows the operation timings on CPU when the left input is encrypted and the right is a clear scalar of the same size:
The next table shows the operation timings on CPU when all inputs are encrypted:
The next table shows the operation timings on CPU when the left input is encrypted and the right is a clear scalar of the same size:
All timings are based on parallelized Radix-based integer operations where each block is encrypted using the default parameters PARAM_MESSAGE_2_CARRY_2_KS_PBS
. To ensure predictable timings, we perform operations in the default
mode, which ensures that the input and output encoding are similar (i.e., the carries are always emptied).
You can minimize operational costs by selecting from 'unchecked', 'checked', or 'smart' modes from the fine-grained APIs, each balancing performance and correctness differently. For more details about parameters, see here. You can find the benchmark results on GPU for all these operations on GPU here and on HPU here.
TFHE-rs benchmarks can be easily reproduced from the source.
The following example shows how to reproduce TFHE-rs benchmarks:
#Integer benchmarks:
make bench_integer
This document details the GPU performance benchmarks of homomorphic operations on integers using TFHE-rs.
The cryptographic parameters PARAM_GPU_MULTI_BIT_GROUP_4_MESSAGE_2_CARRY_2_KS_PBS
were used.
Below come the results for the execution on a single H100. The following table shows the performance when the inputs of the benchmarked operation are encrypted:
The following table shows the performance when the left input of the benchmarked operation is encrypted and the other is a clear scalar of the same size:
Below come the results for the execution on two H100's. The following table shows the performance when the inputs of the benchmarked operation are encrypted:
The following table shows the performance when the left input of the benchmarked operation is encrypted and the other is a clear scalar of the same size:
TFHE-rs benchmarks can be easily reproduced from the source.
The following example shows how to reproduce TFHE-rs benchmarks:
#Integer benchmarks:
make bench_integer_gpu
This document details the GPU performance benchmarks of programmable bootstrapping and keyswitch operations using TFHE-rs.
TFHE-rs benchmarks can be easily reproduced from the source.
The following example shows how to reproduce TFHE-rs benchmarks:
#PBS benchmarks:
make bench_pbs_gpu
#KS-PBS benchmarks:
make bench_ks_pbs_gpu
This document details the Booleans operations supported by TFHE-rs.
Native homomorphic Booleans support the following common Boolean operations:
&
Binary
|
Binary
^
Binary
!
Unary
This document explains how TFHE-rs implements specific operations to detect overflows in computations.
The mechanism of detecting overflow consists in returning an encrypted flag with a specific ciphertext that reflects the state of the computation. When an overflow occurs, this flag is set to true. Since the server is not able to evaluate this encrypted value, the client has to check the flag value when decrypting to determine if an overflow has happened.
These operations might be slower than their non-overflow-detecting equivalent, so they are not enabled by default. To use them, you must explicitly call specific operators. At the moment, only additions, subtractions, and multiplications are supported. We plan to add more operations in future releases.
Here's the list of operations supported along with their symbol:
overflow_add
Binary
overflow_sub
Binary
overflow_mul
Binary
The usage of these operations is similar to the standard ones. The key difference is in the decryption process, as shown in following example:
// Adds two [FheUint] and returns a boolean indicating overflow.
//
// * The operation is modular, i.e on overflow the result wraps around.
// * On overflow the [FheBool] is true, otherwise false
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint16};
let (client_key, server_key) = generate_keys(ConfigBuilder::default());
set_server_key(server_key);
let a = FheUint16::encrypt(u16::MAX, &client_key);
let b = FheUint16::encrypt(1u16, &client_key);
let (result, overflowed) = (&a).overflowing_add(&b);
let result: u16 = result.decrypt(&client_key);
assert_eq!(result, u16::MAX.wrapping_add(1u16));
assert_eq!(
overflowed.decrypt(&client_key),
u16::MAX.overflowing_add(1u16).1
);
assert!(overflowed.decrypt(&client_key));
This document details the CPU performance benchmarks of programmable bootstrapping and keyswitch operations using TFHE-rs.
The next tables show the execution time of a single programmable bootstrapping as well as keyswitch followed by a programmable bootstrapping depending on the precision of the input message. The associated parameters set are given. The configuration is tfhe-fft + AVX-512.
Note that these benchmarks use Gaussian parameters. MB-PBS
stands for multi-bit programmable bootstrapping.
TFHE-rs benchmarks can be easily reproduced from the .
The following example shows how to reproduce TFHE-rs benchmarks:
This document details the ternary operations supported by TFHE-rs.
The ternary conditional operator execute conditional instructions in the form if cond { choice_if_true } else { choice_if_false }
.
The syntax is encrypted_condition.select(encrypted_choice_if_true, encrypted_choice_if_false)
. The valid encrypted_condition
must be an encryption of 0 or 1.
The following example shows how to perform ternary conditional operations:
This document explains how the choice of cryptographic parameters impacts both the security and efficiency of FHE algorithms. The chosen parameters determine the error probability (sometimes referred to failure probability) and overall performance of computations using fully homomorphic encryption. This error probability is due to the noisy nature of FHE computations (see for more details about the encryption process).
All parameter sets provide at least 128-bits of security according to the .
Currently, the default parameters use blocks that contain 2 bits of message and 2 bits of carry - a tweaked uniform (TUniform, defined ) noise distribution, and have a bootstrapping failure probability . These are particularly suitable for applications that need to be secure in the IND-CPA^D model (see for more details). The GPU backend still uses an error probability smaller than by default. Those will be updated soon.
When using the high-level API of TFHE-rs, you can create a key pair using the default recommended set of parameters. For example:
Parameter sets are versioned for backward compatibility. This means that each set of parameters can be tied to a specific version of TFHE-rs, so that they remain unchanged and compatible after an upgrade.
All parameter sets are stored as variables inside the tfhe::shortint::parameters
module, with submodules named after the versions of TFHE-rs in which these parameters where added. For example, parameters added in TFHE-rs v1.0 can be found inside tfhe::shortint::parameters::v1_0
.
The naming convention of these parameters indicates their capabilities. Taking tfhe::parameters::v1_0::V1_0_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128
as an example:
V1_0
: these parameters were introduced in TFHE-rs v1.0
MESSAGE_2
: LWE blocks include 2 bits of message
CARRY_2
: LWE blocks include 2 bits of carry
KS_PBS
: the keyswitch is computed before the bootstrap
TUNIFORM
: the tweaked uniform noise distribution is used
2M128
: the probability of failure for the bootstrap is
For convenience, aliases are provided for the most used sets of parameters and stored in the module tfhe::shortint::parameters::aliases
. Note, however, that these parameters are not stable over time and are always updated to the latest TFHE-rs version. For this reason, they should only be used for prototyping and are not suitable for production use cases.
You can override the default parameters with the with_custom_parameters(block_parameters)
method of the Config
object. For example, to use a Gaussian distribution instead of the TUniform one, you can modify your configuration as follows:
This document explains how to use the PBS statistics feature in TFHE-rs' shortint API to assess the overall computational intensity in FHE applications.
The shortint
API now includes a global counter to track the number of Programmable Bootstrapping (PBS) executed with the pbs-stats
feature. This feature enables precise tracking of PBS executions in a circuit. It helps to estimate the overall compute intensity of FHE code using either the shortint
, integer,
or High-Level APIs.
To know how many PBSes were executed, call get_pbs_count
. To reset the PBS count, call reset_pbs_count
. You can combine two functions to understand how many PBSes were executed in each part of your code.
When combined with the , this feature allows for quick estimations during iterations on the FHE code.
Here is an example of how to use the PBS counter:
This document explains a feature to facilitate debugging.
Starting from TFHE-rs 0.5, introduce a new feature to facilitate debugging. This feature supports a debugger, print statements, and faster execution, significantly reducing waiting time and enhancing the development pace of FHE applications.
Trivial ciphertexts are not secure. An application released/deployed in production must never receive trivial ciphertext from a client.
To use this feature, simply call your circuits/functions with trivially encrypted values that are created using encrypt_trivial
(instead of real encryptions that are created using encrypt
):
This example is going to print:
If any input to mul_all
is not a trivial ciphertexts, the computations will be done 100% in FHE, and the program will output:
Using trivial encryptions as input, the example runs in 980 ms on a standard 12-core laptop, compared to 7.5 seconds on a 128-core machine using real encryptions.
This document explains how to compress ciphertexts using the GPU - even after homomorphic computations - just like on the .
Compressing ciphertexts after computation using GPU is very similar to how it's done on the CPU. The following example shows how to compress and decompress a list containing 4 messages:
One 32-bits integer
One 64-bit integer
One Boolean
One 2-bit integer
This document explains how to use array types on GPU, just as .
Here is an example:
This contains the operations available in tfhe::boolean, along with code examples.
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:
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 needs to be picked. Here, is a good choice, mainly because it is binary format.
Ternary operator
select
Ternary
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt32};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Basic configuration to use homomorphic integers
let config = ConfigBuilder::default().build();
// Key generation
let (client_key, server_keys) = generate_keys(config);
let clear_a = 32i32;
let clear_b = -45i32;
// Encrypting the input data using the (private) client_key
// FheInt32: Encrypted equivalent to i32
let encrypted_a = FheInt32::try_encrypt(clear_a, &client_key)?;
let encrypted_b = FheInt32::try_encrypt(clear_b, &client_key)?;
// On the server side:
set_server_key(server_keys);
// Clear equivalent computations: 32 > -45
let encrypted_comp = &encrypted_a.gt(&encrypted_b);
let clear_res = encrypted_comp.decrypt(&client_key);
assert_eq!(clear_res, clear_a > clear_b);
// `encrypted_comp` is a FheBool, thus it encrypts a boolean value.
// This acts as a condition on which the
// `select` function can be applied on.
// Clear equivalent computations:
// if 32 > -45 {result = 32} else {result = -45}
let encrypted_res = &encrypted_comp.select(&encrypted_a, &encrypted_b);
let clear_res: i32 = encrypted_res.decrypt(&client_key);
assert_eq!(clear_res, clear_a);
// Ternary conditional also supports operands that are in clear (except for the condition)
// with the `scalar` prefix
let encrypted_res = &encrypted_comp.scalar_select(&encrypted_a, clear_b);
let clear_res: i32 = encrypted_res.decrypt(&client_key);
assert_eq!(clear_res, clear_a);
let encrypted_res = &encrypted_comp.scalar_select(clear_a, &encrypted_b);
let clear_res: i32 = encrypted_res.decrypt(&client_key);
assert_eq!(clear_res, clear_a);
// When both possible results are in clear the form to be used is
let encrypted_res = FheInt32::select(encrypted_comp, clear_a, clear_b);
let clear_res: i32 = encrypted_res.decrypt(&client_key);
assert_eq!(clear_res, clear_a);
Ok(())
}
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!(!output);
}
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 tfhe::{ConfigBuilder, generate_keys};
fn main() {
let config = ConfigBuilder::default().build();
// Client-side
let (client_key, server_key) = generate_keys(config);
// encryption and FHE operations
}
use tfhe::{ConfigBuilder, generate_keys};
use tfhe::shortint::parameters::v1_2::V1_2_PARAM_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M128;
fn main() {
let config =
ConfigBuilder::with_custom_parameters(V1_2_PARAM_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M128)
.build();
// Client-side
let (client_key, server_key) = generate_keys(config);
// encryption and FHE operations
}
use tfhe::prelude::*;
use tfhe::*;
pub fn main() {
// Config and key generation
let config = ConfigBuilder::default().build();
let (cks, sks) = generate_keys(config);
// Encryption
let a = FheUint32::encrypt(42u32, &cks);
let b = FheUint32::encrypt(16u32, &cks);
// Set the server key
set_server_key(sks);
// Compute and get the PBS count for the 32 bits multiplication
let c = &a * &b;
let mul_32_count = get_pbs_count();
// Reset the PBS count, and get the PBS count for a 32 bits bitwise AND
reset_pbs_count();
let d = &a & &b;
let and_32_count = get_pbs_count();
// Display the result
println!("mul_32_count: {mul_32_count}");
println!("and_32_count: {and_32_count}");
}
use tfhe::prelude::*;
use tfhe::{set_server_key, generate_keys, ConfigBuilder, FheUint128};
fn mul_all(a: &FheUint128, b: &FheUint128, c: &FheUint128) -> FheUint128 {
// Use the debug format ('{:?}'), if you don't want to unwrap()
// and panic if the value is not a trivial.
println!(
"a: {:?}, b: {:?}, c: {:?}",
a.try_decrypt_trivial::<u128>(),
b.try_decrypt_trivial::<u128>(),
c.try_decrypt_trivial::<u128>(),
);
let tmp = a * b;
println!("a * b = {:?}", tmp.try_decrypt_trivial::<u128>());
tmp * c
}
fn main() {
let (cks, sks) = generate_keys(ConfigBuilder::default().build());
set_server_key(sks);
let a = FheUint128::encrypt_trivial(1234u128);
let b = FheUint128::encrypt_trivial(4567u128);
let c = FheUint128::encrypt_trivial(89101112u128);
// since all inputs are trivially encrypted, this is going to be
// much faster
let result = mul_all(&a, &b, &c);
}
a: Ok(1234), b: Ok(4567), c: Ok(89101112)
a * b = Ok(5635678)
a: Err(NotTrivialCiphertextError), b: Err(NotTrivialCiphertextError), c: Err(NotTrivialCiphertextError)
a * b = Err(NotTrivialCiphertextError)
use tfhe::prelude::*;
use tfhe::shortint::parameters::{
COMP_PARAM_GPU_MULTI_BIT_GROUP_4_MESSAGE_2_CARRY_2_KS_PBS, PARAM_GPU_MULTI_BIT_GROUP_4_MESSAGE_2_CARRY_2_KS_PBS,
};
use tfhe::{
set_server_key, CompressedCiphertextList, CompressedCiphertextListBuilder, FheBool,
FheInt64, FheUint16, FheUint2, FheUint32,
};
fn main() {
let config =
tfhe::ConfigBuilder::with_custom_parameters(PARAM_GPU_MULTI_BIT_GROUP_4_MESSAGE_2_CARRY_2_KS_PBS)
.enable_compression(COMP_PARAM_GPU_MULTI_BIT_GROUP_4_MESSAGE_2_CARRY_2_KS_PBS)
.build();
let ck = tfhe::ClientKey::generate(config);
let compressed_server_key = tfhe::CompressedServerKey::new(&ck);
let gpu_key = compressed_server_key.decompress_to_gpu();
set_server_key(gpu_key);
let ct1 = FheUint32::encrypt(17_u32, &ck);
let ct2 = FheInt64::encrypt(-1i64, &ck);
let ct3 = FheBool::encrypt(false, &ck);
let ct4 = FheUint2::encrypt(3u8, &ck);
let compressed_list = CompressedCiphertextListBuilder::new()
.push(ct1)
.push(ct2)
.push(ct3)
.push(ct4)
.build()
.unwrap();
let serialized = bincode::serialize(&compressed_list).unwrap();
println!("Serialized size: {} bytes", serialized.len());
let compressed_list: CompressedCiphertextList = bincode::deserialize(&serialized).unwrap();
let a: FheUint32 = compressed_list.get(0).unwrap().unwrap();
let b: FheInt64 = compressed_list.get(1).unwrap().unwrap();
let c: FheBool = compressed_list.get(2).unwrap().unwrap();
let d: FheUint2 = compressed_list.get(3).unwrap().unwrap();
let a: u32 = a.decrypt(&ck);
assert_eq!(a, 17);
let b: i64 = b.decrypt(&ck);
assert_eq!(b, -1);
let c = c.decrypt(&ck);
assert!(!c);
let d: u8 = d.decrypt(&ck);
assert_eq!(d, 3);
}
use tfhe::{ConfigBuilder, set_server_key, ClearArray, ClientKey, CompressedServerKey};
use tfhe::array::GpuFheUint32Array;
use tfhe::prelude::*;
fn main() {
let config = ConfigBuilder::default().build();
let cks = ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&cks);
let gpu_key = compressed_server_key.decompress_to_gpu();
set_server_key(gpu_key);
let num_elems = 4 * 4;
let clear_xs = (0..num_elems as u32).collect::<Vec<_>>();
let clear_ys = vec![1u32; num_elems];
// Encrypted 2D array with values
// [[ 0, 1, 2, 3]
// [ 4, 5, 6, 7]
// [ 8, 9, 10, 11]
// [ 12, 13, 14, 15]]
let xs = GpuFheUint32Array::try_encrypt((clear_xs.as_slice(), vec![4, 4]), &cks).unwrap();
// Encrypted 2D array with values
// [[ 1, 1, 1, 1]
// [ 1, 1, 1, 1]
// [ 1, 1, 1, 1]
// [ 1, 1, 1, 1]]
let ys = GpuFheUint32Array::try_encrypt((clear_ys.as_slice(), vec![4, 4]), &cks).unwrap();
assert_eq!(xs.num_dim(), 2);
assert_eq!(xs.shape(), &[4, 4]);
assert_eq!(ys.num_dim(), 2);
assert_eq!(ys.shape(), &[4, 4]);
// Take a sub slice
// [[ 10, 11]
// [ 14, 15]]
let xss = xs.slice(&[2..4, 2..4]);
// Take a sub slice
// [[ 1, 1]
// [ 1, 1]]
let yss = ys.slice(&[2..4, 2..4]);
assert_eq!(xss.num_dim(), 2);
assert_eq!(xss.shape(), &[2, 2]);
assert_eq!(yss.num_dim(), 2);
assert_eq!(yss.shape(), &[2, 2]);
let r = &xss + &yss;
// Result is
// [[ 11, 12]
// [ 15, 16]]
let result: Vec<u32> = r.decrypt(&cks);
assert_eq!(result, vec![11, 12, 15, 16]);
// Clear 2D array with values
// [[ 10, 20]
// [ 30, 40]]
let clear_array = ClearArray::new(vec![10u32, 20u32, 30u32, 40u32], vec![2, 2]);
let r = &xss + &clear_array;
// Result is
// [[ 20, 31]
// [ 44, 55]]
let r: Vec<u32> = r.decrypt(&cks);
assert_eq!(r, vec![20, 31, 44, 55]);
}
# Cargo.toml
[dependencies]
# ...
bincode = "1.3.3"
// main.rs
use std::io::Cursor;
use tfhe::integer::{gen_keys_radix, ServerKey, RadixCiphertext};
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
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_KS_PBS, 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);
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: RadixCiphertext = 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: RadixCiphertext = bincode::deserialize_from(&mut serialized_data)?;
let ct_2: RadixCiphertext = 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)
}
#PBS benchmarks:
make bench_pbs
#KS-PBS benchmarks:
make bench_ks_pbs
This document explains the basic steps of using the high-level API of TFHE-rs.
If you already know how to set up a Rust project, feel free to go directly to the next section.
First, install the Rust programming language tools. Visit https://rustup.rs/ and follow the instructions. For alternative installation methods, refer to the official Rust installation page.
After installing Rust, you can call the build and package manager Cargo
:
$ cargo --version
cargo 1.81.0 (2dbb1af80 2024-08-20)
Your version may differ depending on when you installed Rust. To update your installation, invoke rustup update
.
Now you can invoke Cargo
and create a new default Rust project:
$ cargo new tfhe-example
Creating binary (application) `tfhe-example` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
This will create a tfhe-example
directory and populate it with the following:
$ tree tfhe-example/
tfhe-example/
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
You now have a minimal Rust project.
In the next section, we'll explain how to add TFHE-rs as a dependency to the project and start using it to perform FHE computations.
To use TFHE-rs, you need to add it as a dependency to tfhe-example
.
The Cargo.toml
file is located at the root of the project. Initially, the file is minimal and doesn't contain any dependencies:
[package]
name = "tfhe-example"
version = "0.1.0"
edition = "2021"
[dependencies]
Then add the following configuration to include TFHE-rs:
tfhe = { version = "~1.2.0", features = ["integer"] }
Your updated Cargo.toml
file should look like this:
[package]
name = "tfhe-example"
version = "0.1.0"
edition = "2021"
[dependencies]
tfhe = { version = "~1.2.0", features = ["integer"] }
If you are on a different platform please refer to the installation documentation for configuration options of other supported platforms.
Now that the project has TFHE-rs as a dependency here are the detailed steps to use its high-level API:
Import the TFHE-rs prelude with the following Rust code: use tfhe::prelude::*;
Client-side: configure and generate keys
Client-side: encrypt data
Server-side: set the server key
Server-side: compute over encrypted data
Client-side: decrypt data
This example demonstrates the basic workflow combining the client and server parts:
use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheUint8};
use tfhe::prelude::*;
fn main() {
let config = ConfigBuilder::default().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);
}
You can learn more about homomorphic types and associated compilation features in the configuration documentation.
This document explains the FheAsciiString type for handling encrypted strings in TFHE-rs.
TFHE-rs has supports for ASCII strings with the type FheAsciiString. You can enable this feature using the flag: --features=strings
Strings are not yet compatible with CompactCiphertextList
and CompressedCiphertextList
A variety of common operations are supported for FheAsciiString
. These include:
Comparisons: eq
, ne
, lt
, le
, gt
, ge
, eq_ignore_case
Case conversion: to_lowercase
/ to_uppercase
String checks: starts_with
/ ends_with
/ contains
Trimming: trim_start
/ trim_end
/ trim
Prefix/suffix operations: strip_prefix
/ strip_suffix
Search: find
/ rfind
When encrypting strings, you can add padding to hide the actual length of strings. The null character (b'\0') is used as the padding. Here is an example:
# Cargo.toml
[dependencies]
tfhe = { version = "~1.2.0", features = ["integer", "strings"] }
use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheAsciiString, FheStringLen, ClearString};
use tfhe::prelude::*;
use tfhe::safe_serialization::safe_serialize;
fn main() {
let config = ConfigBuilder::default().build();
let (cks, sks) = generate_keys(config);
set_server_key(sks);
let r = FheAsciiString::try_encrypt("café is french for coffee", &cks);
// As the input string is not strictly ASCII, it is not compatible
assert!(r.is_err());
let string = FheAsciiString::try_encrypt("tfhe-rs", &cks).unwrap();
// This adds 3 chars of padding to the chars of the input string
let padded_string = FheAsciiString::try_encrypt_with_padding("tfhe-rs", 3, &cks).unwrap();
// This makes it so the string has 10 chars (adds padding or truncates input as necessary)
let other_string = FheAsciiString::try_encrypt_with_fixed_sized("tfhe", 10, &cks).unwrap();
let mut buffer1 = vec![];
safe_serialize(&padded_string, &mut buffer1, 1 << 30).unwrap();
let mut buffer2 = vec![];
safe_serialize(&other_string, &mut buffer2, 1 << 30).unwrap();
// The two strings created with padding, have the same
// memory/disk footprint, even though the lengths are not the same
assert_eq!(buffer1.len(), buffer2.len());
// When a string has no padding, its length is known in clear
let len = string.len();
assert!(matches!(len, FheStringLen::NoPadding(7)));
// When a string has padding, its length is only known as an encrypted value
let FheStringLen::Padding(encrypted_len) = padded_string.len() else {
panic!("Expected len to be encrypted");
};
let padded_string_len: u16 = encrypted_len.decrypt(&cks);
assert_eq!(padded_string_len, 7); // Note padding chars are not counted
// The enum resulting of a len() / is_empty() call can be transformed
// to a FheUint16 using `into_ciphertext`
assert!(string.len().into_ciphertext().is_trivial());
assert!(!padded_string.len().into_ciphertext().is_trivial());
let other_string_len: u16 = other_string.len().into_ciphertext().decrypt(&cks);
assert_eq!(other_string_len, 4);
// Padded and un-padded strings are equal if the content is
assert!(padded_string.eq(&string).decrypt(&cks));
let prefix = ClearString::new("tfhe".to_string());
let (stripped_string, has_been_stripped) = string.strip_prefix(&prefix);
// Notice that stripping, makes the string as being considered as padded
// as it is not possible to homomorphically remove chars
let FheStringLen::Padding(encrypted_len) = stripped_string.len() else {
panic!("Expected len to be encrypted");
};
let stripped_string_len: u16 = encrypted_len.decrypt(&cks);
assert_eq!(stripped_string_len, 3);
let decrypted = stripped_string.decrypt(&cks);
assert_eq!(decrypted, "-rs");
assert!(has_been_stripped.decrypt(&cks));
}
This document describes the array types provided by the High-level API.
This new encrypted types allow you to easily perform array and tensor operations on encrypted data, taking care of the iteration and shape logic for you.
It also implements efficient algorithms in some cases, like summing elements of an array.
The following example shows a complete workflow of working with encrypted arrays, including:
Generating keys
Encrypting arrays of integers
Performing operations such as:
slicing arrays
computing on a sub array, adding encrypted data to it
computing on a sub array, adding clear data to it
Decrypting the result, getting back a Rust Vec
of decrypted values
# Cargo.toml
[dependencies]
tfhe = { version = "~1.2.0", features = ["integer"] }
use tfhe::{ConfigBuilder, generate_keys, set_server_key, CpuFheUint32Array, ClearArray};
use tfhe::prelude::*;
fn main() {
let config = ConfigBuilder::default().build();
let (cks, sks) = generate_keys(config);
set_server_key(sks);
let num_elems = 4 * 4;
let clear_xs = (0..num_elems as u32).collect::<Vec<_>>();
let clear_ys = vec![1u32; num_elems];
// Encrypted 2D array with values
// [[ 0, 1, 2, 3]
// [ 4, 5, 6, 7]
// [ 8, 9, 10, 11]
// [ 12, 13, 14, 15]]
let xs = CpuFheUint32Array::try_encrypt((clear_xs.as_slice(), vec![4, 4]), &cks).unwrap();
// Encrypted 2D array with values
// [[ 1, 1, 1, 1]
// [ 1, 1, 1, 1]
// [ 1, 1, 1, 1]
// [ 1, 1, 1, 1]]
let ys = CpuFheUint32Array::try_encrypt((clear_ys.as_slice(), vec![4, 4]), &cks).unwrap();
assert_eq!(xs.num_dim(), 2);
assert_eq!(xs.shape(), &[4, 4]);
assert_eq!(ys.num_dim(), 2);
assert_eq!(ys.shape(), &[4, 4]);
// Take a sub slice
// [[ 10, 11]
// [ 14, 15]]
let xss = xs.slice(&[2..4, 2..4]);
// Take a sub slice
// [[ 1, 1]
// [ 1, 1]]
let yss = ys.slice(&[2..4, 2..4]);
assert_eq!(xss.num_dim(), 2);
assert_eq!(xss.shape(), &[2, 2]);
assert_eq!(yss.num_dim(), 2);
assert_eq!(yss.shape(), &[2, 2]);
let r = &xss + &yss;
// Result is
// [[ 11, 12]
// [ 15, 16]]
let result: Vec<u32> = r.decrypt(&cks);
assert_eq!(result, vec![11, 12, 15, 16]);
// Clear 2D array with values
// [[ 10, 20]
// [ 30, 40]]
let clear_array = ClearArray::new(vec![10u32, 20u32, 30u32, 40u32], vec![2, 2]);
let r = &xss + &clear_array;
// Result is
// [[ 20, 31]
// [ 44, 55]]
let r: Vec<u32> = r.decrypt(&cks);
assert_eq!(r, vec![20, 31, 44, 55]);
}
This document describes how to perform computation on encrypted data.
With TFHE-rs, the program can be as straightforward as conventional Rust coding by using operator overloading.
The following example illustrates the complete process of encryption, computation using Rust’s built-in operators, and decryption:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
let clear_a = 35u8;
let clear_b = 7u8;
// Encryption
let a = FheUint8::encrypt(clear_a, &client_key);
let b = FheUint8::encrypt(clear_b, &client_key);
// Take a reference to avoid moving data when doing the computation
let a = &a;
let b = &b;
// Computation using Rust's built-in operators
let add = a + b;
let sub = a - b;
let mul = a * b;
let div = a / b;
let rem = a % b;
let and = a & b;
let or = a | b;
let xor = a ^ b;
let neg = -a;
let not = !a;
let shl = a << b;
let shr = a >> b;
// Comparison operations need to use specific functions as the definition of the operators in
// rust require to return a boolean which we cannot do in FHE
let eq = a.eq(b);
let ne = a.ne(b);
let gt = a.gt(b);
let lt = a.lt(b);
// Decryption and verification of proper execution
let decrypted_add: u8 = add.decrypt(&client_key);
let clear_add = clear_a + clear_b;
assert_eq!(decrypted_add, clear_add);
let decrypted_sub: u8 = sub.decrypt(&client_key);
let clear_sub = clear_a - clear_b;
assert_eq!(decrypted_sub, clear_sub);
let decrypted_mul: u8 = mul.decrypt(&client_key);
let clear_mul = clear_a * clear_b;
assert_eq!(decrypted_mul, clear_mul);
let decrypted_div: u8 = div.decrypt(&client_key);
let clear_div = clear_a / clear_b;
assert_eq!(decrypted_div, clear_div);
let decrypted_rem: u8 = rem.decrypt(&client_key);
let clear_rem = clear_a % clear_b;
assert_eq!(decrypted_rem, clear_rem);
let decrypted_and: u8 = and.decrypt(&client_key);
let clear_and = clear_a & clear_b;
assert_eq!(decrypted_and, clear_and);
let decrypted_or: u8 = or.decrypt(&client_key);
let clear_or = clear_a | clear_b;
assert_eq!(decrypted_or, clear_or);
let decrypted_xor: u8 = xor.decrypt(&client_key);
let clear_xor = clear_a ^ clear_b;
assert_eq!(decrypted_xor, clear_xor);
let decrypted_neg: u8 = neg.decrypt(&client_key);
let clear_neg = clear_a.wrapping_neg();
assert_eq!(decrypted_neg, clear_neg);
let decrypted_not: u8 = not.decrypt(&client_key);
let clear_not = !clear_a;
assert_eq!(decrypted_not, clear_not);
let decrypted_shl: u8 = shl.decrypt(&client_key);
let clear_shl = clear_a << clear_b;
assert_eq!(decrypted_shl, clear_shl);
let decrypted_shr: u8 = shr.decrypt(&client_key);
let clear_shr = clear_a >> clear_b;
assert_eq!(decrypted_shr, clear_shr);
let decrypted_eq = eq.decrypt(&client_key);
let eq = clear_a == clear_b;
assert_eq!(decrypted_eq, eq);
let decrypted_ne = ne.decrypt(&client_key);
let ne = clear_a != clear_b;
assert_eq!(decrypted_ne, ne);
let decrypted_gt = gt.decrypt(&client_key);
let gt = clear_a > clear_b;
assert_eq!(decrypted_gt, gt);
let decrypted_lt = lt.decrypt(&client_key);
let lt = clear_a < clear_b;
assert_eq!(decrypted_lt, lt);
}
This document explains how to save and load versioned data using the data versioning feature.
Starting from v0.6.4, TFHE-rs supports versioned data types. This allows you to store data and load it in the future without compatibility concerns. This feature is done by the tfhe-versionable
crate.
This versioning scheme is compatible with all the data formats supported by serde.
To use the versioning feature, wrap your types in their versioned equivalents before serialization using the versionize
method.
You can load serialized data with the unversionize
function, even in newer versions of TFHE-rs where the data types might evolve. The unversionize
function manages any necessary data type upgrades, ensuring compatibility.
# Cargo.toml
[dependencies]
# ...
tfhe = { version = "~1.2.0", features = ["integer"] }
tfhe-versionable = "0.5.0"
bincode = "1.3.3"
// main.rs
use std::io::Cursor;
use tfhe::prelude::{FheDecrypt, FheEncrypt};
use tfhe::{ClientKey, ConfigBuilder, FheUint8};
use tfhe_versionable::{Unversionize, Versionize};
fn main() {
let config = ConfigBuilder::default().build();
let client_key = ClientKey::generate(config);
let msg = 1;
let ct = FheUint8::encrypt(msg, &client_key);
// Versionize the data and store it
let mut serialized_data = Vec::new();
let versioned_client_key = client_key.versionize();
let versioned_ct = ct.versionize();
bincode::serialize_into(&mut serialized_data, &versioned_client_key).unwrap();
bincode::serialize_into(&mut serialized_data, &versioned_ct).unwrap();
// Load the data. This can be done in the future with a more recent version of tfhe-rs
let mut serialized_data = Cursor::new(serialized_data);
let versioned_client_key = bincode::deserialize_from(&mut serialized_data).unwrap();
let versioned_ct = bincode::deserialize_from(&mut serialized_data).unwrap();
let loaded_client_key =
ClientKey::unversionize(versioned_client_key).unwrap();
let loaded_ct =
FheUint8::unversionize(versioned_ct).unwrap();
let output: u8 = loaded_ct.decrypt(&loaded_client_key);
assert_eq!(msg, output);
}
Calling .versionize()
on a value will add versioning tags. This is done recursively so all the subtypes that compose it are versioned too. Under the hood, it converts the value into an enum where each version of a type is represented by a new variant. The returned object can be serialized using serde:
let versioned_client_key = client_key.versionize();
bincode::serialize_into(&mut serialized_data, &versioned_client_key).unwrap();
The Type::unversionize()
function takes a versioned value, upgrades it to the latest version of its type and removes the version tags. To do that, it matches the version in the versioned enum and eventually apply a conversion function that upgrades it to the most recent version. The resulting value can then be used inside TFHE-rs
let versioned_client_key = bincode::deserialize_from(&mut serialized_data).unwrap();
let loaded_client_key =
ClientKey::unversionize(versioned_client_key).unwrap();
When possible, data will be upgraded automatically without any kind of interraction. However, some changes might need information that are only known by the user of the library. These are called data breaking changes. In these occasions, TFHE-rs provides a way to upgrade these types manually.
You will find below a list of breaking changes and how to upgrade them.
Since the ServerKey
and ClientKey
types both implement the Serialize
andDeserialize
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 std::fs::{File, create_dir_all};
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();
// Create a tmp dir with the current user name to avoid cluttering the /tmp dir
let user = std::env::var("USER").unwrap_or_else(|_| "unknown_user".to_string());
let tmp_dir_for_user = &format!("/tmp/{user}");
create_dir_all(tmp_dir_for_user).unwrap();
let server_key_file = &format!("{tmp_dir_for_user}/ser_example_server_key.bin");
let client_key_file = &format!("{tmp_dir_for_user}/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!(!loaded_client_key.decrypt(&ct_1));
}
This document details the arithmetic operations supported by TFHE-rs.
Homomorphic integer types (FheUint
and FheInt
) support the following arithmetic operations:
-
Unary
+
Binary
-
Binary
*
Binary
*
/
Binary
*
%
Binary
Specifications for operations with zero:
Division by zero: returns modulus - 1.
Example: for FheUint8 (modulus = ), dividing by zero returns an encryption of 255.
Remainder operator: returns the first input unchanged.
Example: if ct1 = FheUint8(63)
and ct2 = FheUint8(0)
, then ct1 % ct2 returns FheUint8(63).
The following example shows how to perform arithmetic operations:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt8, FheUint8};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default().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 clear_d = -87_i64;
let mut a = FheUint8::try_encrypt(clear_a, &keys)?;
let mut b = FheUint8::try_encrypt(clear_b, &keys)?;
let c = FheUint8::try_encrypt(clear_c, &keys)?;
let mut d = FheInt8::try_encrypt(clear_d, &keys)?;
a *= &b; // Clear equivalent computations: 15 * 27 mod 256 = 149
b = &b + &c; // Clear equivalent computations: 27 + 43 mod 256 = 70
b -= 76u8; // Clear equivalent computations: 70 - 76 mod 256 = 250
d -= 13i8; // Clear equivalent computations: -87 - 13 = 100 in [-128, 128[
let dec_a: u8 = a.decrypt(&keys);
let dec_b: u8 = b.decrypt(&keys);
let dec_d: i8 = d.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);
assert_eq!(dec_d, (clear_d - 13) as i8);
Ok(())
}
This document details the bitwise operations supported by TFHE-rs.
Homomorphic integer types support the following bitwise operations:
!
Unary
&
Binary
|
Binary
^
Binary
>>
Binary
<<
Binary
rotate_right
Binary
rotate_left
Binary
The following example shows how to perform bitwise operations:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint8};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default().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 ^= &b;
b ^= &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(())
}
This document details the comparison operations supported by TFHE-rs.
Homomorphic integers support comparison operations. However, due to Rust's limitations, you cannot overload comparison symbols. This is because Rust requires Boolean outputs from such operations, but homomorphic types return ciphertexts. Therefore, you should use the following methods, which conform to the naming conventions of Rust’s standard traits:
Supported operations:
eq
Binary
ne
Binary
gt
Binary
ge
Binary
lt
Binary
le
Binary
The following example shows how to perform comparison operations:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt8};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default().build();
let (keys, server_keys) = generate_keys(config);
set_server_key(server_keys);
let clear_a: i8 = -121;
let clear_b: i8 = 87;
let a = FheInt8::try_encrypt(clear_a, &keys)?;
let b = FheInt8::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 = greater.decrypt(&keys);
let dec_ge = greater_or_equal.decrypt(&keys);
let dec_lt = lower.decrypt(&keys);
let dec_le = lower_or_equal.decrypt(&keys);
let dec_eq = equal.decrypt(&keys);
assert_eq!(dec_gt, clear_a > clear_b);
assert_eq!(dec_ge, clear_a >= clear_b);
assert_eq!(dec_lt, clear_a < clear_b);
assert_eq!(dec_le, clear_a <= clear_b);
assert_eq!(dec_eq, clear_a == clear_b);
Ok(())
}
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 error probability. The default parameter set ensures an error probability of at most 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 , but it is up-to-date regarding security requirements.
The following array summarizes this:
DEFAULT_PARAMETERS
TFHE_LIB_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 = BooleanParameters::new(
LweDimension(586),
GlweDimension(2),
PolynomialSize(512),
DynamicDistribution::new_gaussian_from_std_dev(
StandardDev(0.00008976167396834998),
),
DynamicDistribution::new_gaussian_from_std_dev(
StandardDev(0.00000002989040792967434),
),
DecompositionBaseLog(8),
DecompositionLevelCount(2),
DecompositionBaseLog(2),
DecompositionLevelCount(5),
EncryptionKeyChoice::Small,
);
}
TFHE is a fully homomorphic encryption scheme that enables fast homomorphic operations on booleans, integers and reals.
By enabling both leveled and bootstrapped operations, TFHE can be used for a wide range of usecases, from homomorphic boolean circuits to homomorphic neural networks.
Here are a series of articles that guide you to go deeper into the understanding of the scheme:
The article Guide to Fully Homomorphic Encryption over the Discretized Torus gives more mathematical details about the TFHE scheme.
You can also watch the video record of the original talk by Ilaria Chillotti for FHE.org:
This document serves as a practical reference for implementing generic functions in Rust that use operators across mixed references and values. The following explanations help you to understand the trait necessary to handle such operations.
Operators such as +
, *
, >>,
and so on are tied to traits in std:::ops
. For instance, the +
operator corresponds to std::ops::Add
. When writing a generic function that uses the +
operator, you need to specify std::ops::Add
as a trait bound.
The trait bound varies slightly depending on whether the left-hand side / right-hand side is an owned value or a reference. The following table shows the different scenarios:
This guide explains how to update your existing program to leverage GPU acceleration, or to start a new program using GPU.
TFHE-rs now supports a GPU backend with CUDA implementation, enabling integer arithmetic operations on encrypted data.
Cuda version >= 10
Compute Capability >= 3.0
>= 8.0 - check this for more details about nvcc/gcc compatible versions
>= 3.24
libclang, to match Rust bingen >= 9.0
Rust version - check this
To use the TFHE-rs GPU backend in your project, add the following dependency in your Cargo.toml
.
For optimal performance when using TFHE-rs, run your code in release mode with the --release
flag.
TFHE-rs GPU backend is supported on Linux (x86, aarch64).
Comparing to the , GPU set up differs in the key creation, as detailed
Here is a full example (combining the client and server parts):
Beware that when the GPU feature is activated, when calling: let config = ConfigBuilder::default().build();
, the cryptographic parameters differ from the CPU ones, used when the GPU feature is not activated. Indeed, TFHE-rs uses dedicated parameters for the GPU in order to achieve better performance.
The configuration of the key is different from the CPU. More precisely, if both client and server keys are still generated by the client (which is assumed to run on a CPU), the server key has then to be decompressed by the server to be converted into the right format. To do so, the server should run this function: decompressed_to_gpu()
.
Once decompressed, the operations between CPU and GPU are identical.
On the client-side, the method to encrypt the data is exactly the same than the CPU one, as shown in the following example:
The server first need to set up its keys with set_server_key(gpu_key)
.
Then, homomorphic computations are performed using the same approach as the .
Finally, the client decrypts the results using:
All parameter sets provide at least 128-bits of security according to the , with an error probability equal to when using programmable bootstrapping. This error probability is due to the randomness added at each encryption (see for more details about the encryption process).
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. Therefore, 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, how it is split between the message buffer and the carry buffer, and the order in which the keyswitch (KS) and bootstrap (PBS) are computed. The syntax chosen for the name of a parameter is: PARAM_MESSAGE_{number of message bits}_CARRY_{number of carry bits}_{KS_PBS | PBS_KS}
. For example, the set of parameters for a message buffer of 5 bits, a carry buffer of 2 bits and where the keyswitch is computed before the bootstrap is PARAM_MESSAGE_5_CARRY_2_KS_PBS
.
Note that the KS_PBS
order should have better performance at the expense of ciphertext size, PBS_KS
is the opposite.
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_KS_PBS
parameter set is the default shortint
parameter set that you can also use through the tfhe::shortint::prelude::DEFAULT_PARAMETERS
constant.
As shown , the choice of the parameter set impacts the operations available and their efficiency.
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 buffer, this trick no longer works. In this case, many bi-variate operations, such as comparisons, cannot be correctly computed. The only exception concerns 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.
It is possible to define new parameter sets. To do so, it is sufficient to use the function new()
or to manually fill the ClassicPBSParameters
structure fields.
For instance:
This document describes the implementation and benefits of parallelized (PBS) in TFHE-rs, including code examples for using multi-bit PBS parameters and ensuring deterministic execution.
Programmable Bootstrapping is inherently a sequential operation. However, some showed that introducing parallelism is feasible at the expense of larger keys, thereby enhancing the performance of PBS. This new PBS is called a multi-bit PBS.
TFHE-rs can already perform parallel execution of integer homomorphic operations. Activating this feature can lead to performance improvements, particularly in the case of high core-count CPUs when enough cores are available, or when dealing with operations that require small input message precision.
The following example shows how to use parallelized bootstrapping by choosing multi-bit PBS parameters:
By nature, the parallelized PBS might not be deterministic: while the resulting ciphertext will always decrypt to the correct plaintext, the order of the operations could vary, resulting in different output ciphertext. To ensure a consistent ciphertext output regardless of execution order, add the with_deterministic_execution()
suffix to the parameters.
Here's an example:
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 and/or modules (based on core_crypto
) are recommended.
The core_crypto
module offers an API to low-level cryptographic primitives and objects, like lwe_encryption
or rlwe_ciphertext
. The 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 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, it is also possible to bypass these to get directly the element of LWE mask.
For instance, the code to encrypt and then decrypt a message looks like:
T $op T
T: $Op<T, Output=T>
T $op &T
T: for<'a> $Op<&'a T, Output=T>
&T $op T
for<'a> &'a T: $Op<T, Output=T>
&T $op &T
for<'a> &'a T: $Op<&'a T, Output=T>
use std::ops::{Add, Mul};
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint32, FheUint64};
pub fn ex1<'a, FheType, ClearType>(ct: &'a FheType, pt: ClearType) -> FheType
where
&'a FheType: Add<ClearType, Output = FheType>,
{
ct + pt
}
pub fn ex2<'a, FheType, ClearType>(a: &'a FheType, b: &'a FheType, pt: ClearType) -> FheType
where
&'a FheType: Mul<&'a FheType, Output = FheType>,
FheType: Add<ClearType, Output = FheType>,
{
(a * b) + pt
}
pub fn ex3<FheType, ClearType>(a: FheType, b: FheType, pt: ClearType) -> FheType
where
for<'a> &'a FheType: Add<&'a FheType, Output = FheType>,
FheType: Add<FheType, Output = FheType> + Add<ClearType, Output = FheType>,
{
let tmp = (&a + &b) + (&a + &b);
tmp + pt
}
pub fn ex4<FheType, ClearType>(a: FheType, b: FheType, pt: ClearType) -> FheType
where
FheType: Clone + Add<FheType, Output = FheType> + Add<ClearType, Output = FheType>,
{
let tmp = (a.clone() + b.clone()) + (a.clone() + b.clone());
tmp + pt
}
fn main() {
let config = ConfigBuilder::default()
.build();
let (client_key, server_keys) = generate_keys(config);
set_server_key(server_keys);
// Use FheUint32
{
let clear_a = 46546u32;
let clear_b = 6469u32;
let clear_c = 64u32;
let a = FheUint32::try_encrypt(clear_a, &client_key).unwrap();
let b = FheUint32::try_encrypt(clear_b, &client_key).unwrap();
assert_eq!(
ex1(&clear_a, clear_c),
ex1(&a, clear_c).decrypt(&client_key)
);
assert_eq!(
ex2(&clear_a, &clear_b, clear_c),
ex2(&a, &b, clear_c).decrypt(&client_key)
);
assert_eq!(
ex3(clear_a, clear_b, clear_c),
ex3(a.clone(), b.clone(), clear_c).decrypt(&client_key)
);
assert_eq!(
ex4(clear_a, clear_b, clear_c),
ex4(a, b, clear_c).decrypt(&client_key)
);
}
// Use FheUint64
{
let clear_a = 46544866u64;
let clear_b = 6469446677u64;
let clear_c = 647897u64;
let a = FheUint64::try_encrypt(clear_a, &client_key).unwrap();
let b = FheUint64::try_encrypt(clear_b, &client_key).unwrap();
assert_eq!(
ex1(&clear_a, clear_c),
ex1(&a, clear_c).decrypt(&client_key)
);
assert_eq!(
ex2(&clear_a, &clear_b, clear_c),
ex2(&a, &b, clear_c).decrypt(&client_key)
);
assert_eq!(
ex3(clear_a, clear_b, clear_c),
ex3(a.clone(), b.clone(), clear_c).decrypt(&client_key)
);
assert_eq!(
ex4(clear_a, clear_b, clear_c),
ex4(a, b, clear_c).decrypt(&client_key)
);
}
}
tfhe = { version = "~1.2.0", features = ["boolean", "shortint", "integer", "gpu"] }
Linux
Supported
Supported*
macOS
Unsupported
Unsupported*
Windows
Unsupported
Unsupported
use tfhe::{ConfigBuilder, set_server_key, FheUint8, ClientKey, CompressedServerKey};
use tfhe::prelude::*;
fn main() {
let config = ConfigBuilder::default().build();
let client_key= ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
let gpu_key = compressed_server_key.decompress_to_gpu();
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(gpu_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);
}
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(gpu_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);
let decrypted_result: u8 = result.decrypt(&client_key);
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_KS_PBS);
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::*;
use tfhe::shortint::parameters::DynamicDistribution;
fn main() {
// WARNING: might be insecure and/or incorrect
// You can create your own set of parameters
let param = ClassicPBSParameters {
lwe_dimension: LweDimension(879),
glwe_dimension: GlweDimension(1),
polynomial_size: PolynomialSize(2048),
lwe_noise_distribution: DynamicDistribution::new_t_uniform(46),
glwe_noise_distribution: DynamicDistribution::new_t_uniform(17),
pbs_base_log: DecompositionBaseLog(23),
pbs_level: DecompositionLevelCount(1),
ks_base_log: DecompositionBaseLog(3),
ks_level: DecompositionLevelCount(5),
message_modulus: MessageModulus(4),
carry_modulus: CarryModulus(4),
max_noise_level: MaxNoiseLevel::new(5),
log2_p_fail: -71.625,
ciphertext_modulus: CiphertextModulus::new_native(),
encryption_key_choice: EncryptionKeyChoice::Big,
modulus_switch_noise_reduction_params: None,
};
}
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint32};
use tfhe::shortint::parameters::v1_2::V1_2_PARAM_MULTI_BIT_GROUP_3_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M64;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default()
.use_custom_parameters(V1_2_PARAM_MULTI_BIT_GROUP_3_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M64)
.build();
let (keys, server_keys) = generate_keys(config);
set_server_key(server_keys);
let clear_a = 673u32;
let clear_b = 6u32;
let a = FheUint32::try_encrypt(clear_a, &keys)?;
let b = FheUint32::try_encrypt(clear_b, &keys)?;
let c = &a >> &b;
let decrypted: u32 = c.decrypt(&keys);
assert_eq!(decrypted, clear_a >> clear_b);
Ok(())
}
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheUint32};
use tfhe::shortint::parameters::v1_2::V1_2_PARAM_MULTI_BIT_GROUP_3_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M64;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default()
.use_custom_parameters(
V1_2_PARAM_MULTI_BIT_GROUP_3_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M64.with_deterministic_execution(),
)
.build();
let (keys, server_keys) = generate_keys(config);
set_server_key(server_keys);
let clear_a = 673u32;
let clear_b = 6u32;
let a = FheUint32::try_encrypt(clear_a, &keys)?;
let b = FheUint32::try_encrypt(clear_b, &keys)?;
let c = &a >> &b;
let decrypted: u32 = c.decrypt(&keys);
assert_eq!(decrypted, clear_a >> clear_b);
Ok(())
}
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_noise_distribution =
Gaussian::from_dispersion_parameter(StandardDev(0.000007069849454709433), 0.0);
let ciphertext_modulus = CiphertextModulus::new_native();
// Create the PRNG
let mut seeder = new_seeder();
let seeder = seeder.as_mut();
let mut encryption_generator =
EncryptionRandomGenerator::<DefaultRandomGenerator>::new(seeder.seed(), seeder);
let mut secret_generator =
SecretRandomGenerator::<DefaultRandomGenerator>::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_noise_distribution,
&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);
This tutorial demonstrates how to build your own data type that represents an ASCII string in Fully Homomorphic Encryption (FHE) by implementing to_lower and to_upper functions.
An ASCII character is stored in 7 bits. In this tutorial, we use the FheUint8
to store an encrypted ASCII:
The uppercase letters are in the range [65, 90]
The lowercase letters are in the range [97, 122]
The relationship between uppercase and lowercase letters is defined as follows:
lower_case
= upper_case
+ UP_LOW_DISTANCE
upper_case
= lower_case
- UP_LOW_DISTANCE
Where UP_LOW_DISTANCE = 32
This type stores the encrypted characters as a Vec<FheUint8>
to implement case conversion functions.
To use the FheUint8
type, enable the integer
feature:
# Cargo.toml
[dependencies]
tfhe = { version = "~1.2.0", features = ["integer"] }
The MyFheString::encrypt
function performs data validation to ensure the input string contains only ASCII characters.
In FHE operations, direct branching on encrypted values is not possible. However, you can evaluate a boolean condition to obtain the desired outcome. Here is an example to check and convert the 'char' to a lowercase without using a branch:
#![allow(dead_code)]
const UP_LOW_DISTANCE: u8 = 32;
fn to_lower(c: u8) -> u8 {
if c > 64 && c < 91 {
c + UP_LOW_DISTANCE
} else {
c
}
}
You can remove the branch this way:
#![allow(dead_code)]
const UP_LOW_DISTANCE: u8 = 32;
fn to_lower(c: u8) -> u8 {
c + ((c > 64) as u8 & (c < 91) as u8) * UP_LOW_DISTANCE
}
This method can adapt to operations on homomorphic integers:
#![allow(dead_code)]
use tfhe::prelude::*;
use tfhe::FheUint8;
const UP_LOW_DISTANCE: u8 = 32;
fn to_lower(c: &FheUint8) -> FheUint8 {
c + FheUint8::cast_from(c.gt(64) & c.lt(91)) * UP_LOW_DISTANCE
}
Full example:
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ClientKey, ConfigBuilder, FheUint8};
const UP_LOW_DISTANCE: u8 = 32;
struct MyFheString {
bytes: Vec<FheUint8>,
}
fn to_upper(c: &FheUint8) -> FheUint8 {
c - FheUint8::cast_from(c.gt(96) & c.lt(123)) * UP_LOW_DISTANCE
}
fn to_lower(c: &FheUint8) -> FheUint8 {
c + FheUint8::cast_from(c.gt(64) & c.lt(91)) * UP_LOW_DISTANCE
}
impl MyFheString {
fn encrypt(string: &str, client_key: &ClientKey) -> Self {
assert!(
string.is_ascii(),
"The input string must only contain ascii letters"
);
let fhe_bytes: Vec<FheUint8> = string
.bytes()
.map(|b| FheUint8::encrypt(b, client_key))
.collect();
Self { bytes: fhe_bytes }
}
fn decrypt(&self, client_key: &ClientKey) -> String {
let ascii_bytes: Vec<u8> = self
.bytes
.iter()
.map(|fhe_b| fhe_b.decrypt(client_key))
.collect();
String::from_utf8(ascii_bytes).unwrap()
}
fn to_upper(&self) -> Self {
Self {
bytes: self.bytes.iter().map(to_upper).collect(),
}
}
fn to_lower(&self) -> Self {
Self {
bytes: self.bytes.iter().map(to_lower).collect(),
}
}
}
fn main() {
let config = ConfigBuilder::default()
.build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
let my_string = MyFheString::encrypt("Hello Zama, how is it going?", &client_key);
let verif_string = my_string.decrypt(&client_key);
println!("Start string: {verif_string}");
let my_string_upper = my_string.to_upper();
let verif_string = my_string_upper.decrypt(&client_key);
println!("Upper string: {verif_string}");
assert_eq!(verif_string, "HELLO ZAMA, HOW IS IT GOING?");
let my_string_lower = my_string_upper.to_lower();
let verif_string = my_string_lower.decrypt(&client_key);
println!("Lower string: {verif_string}");
assert_eq!(verif_string, "hello zama, how is it going?");
}
This code can be greatly simplified by using the strings
feature from TFHE-rs.
First, add the feature in your Cargo.toml
# Cargo.toml
[dependencies]
tfhe = { version = "~1.2.0", features = ["strings"] }
The FheAsciiString
type allows to simply do homomorphic case changing of encrypted strings (and much more!):
use tfhe::prelude::*;
use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheAsciiString};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
let my_string =
FheAsciiString::try_encrypt("Hello Zama, how is it going?", &client_key).unwrap();
let verif_string = my_string.decrypt(&client_key);
println!("Start string: {verif_string}");
let my_string_upper = my_string.to_uppercase();
let verif_string = my_string_upper.decrypt(&client_key);
println!("Upper string: {verif_string}");
assert_eq!(verif_string, "HELLO ZAMA, HOW IS IT GOING?");
let my_string_lower = my_string_upper.to_lowercase();
let verif_string = my_string_lower.decrypt(&client_key);
println!("Lower string: {verif_string}");
assert_eq!(verif_string, "hello zama, how is it going?");
}
You can read more about this in the FHE strings documentation
tfhe::integer
is dedicated to integers smaller than 256 bits. The steps to homomorphically evaluate an integer circuit are described here.
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 this, 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
.
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 8-bit integers (signed or unsigned) 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_KS_PBS;
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_KS_PBS, num_block);
}
Once we have our keys, we can encrypt values:
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
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_KS_PBS, 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);
}
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::PublicKey;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
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_KS_PBS, num_block);
//We generate the public key from the secret client key:
let public_key = PublicKey::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);
}
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_KS_PBS;
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_KS_PBS, 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);
// 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.add_parallelized(&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);
}
tfhe::shortint
is dedicated to the manipulation of small unsigned integers that fit in a single LWE ciphertext. The actual size depends on the chosen parameters, but is always smaller than 8 bits. For example, with the PARAM_MESSAGE_2_CARRY_2_KS_PBS
parameters, you can encode messages of 2 bits inside a shortint
.
The integer and high-level API leverage shortints to allow homomorphic computations over larger integers.
The steps to homomorphically evaluate a shortint
circuit are described below.
tfhe::shortint
provides 3 key types:
ClientKey
ServerKey
PublicKey
The ClientKey
is the key that encrypts and decrypts messages (small integer values). 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 evaluate 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 (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_KS_PBS);
}
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_KS_PBS);
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);
}
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_KS_PBS);
let public_key = PublicKey::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);
}
Using the server_key
, addition is possible over encrypted values. The resulting plaintext is recovered after the decryption via 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_KS_PBS);
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.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);
}
This document explains how to implement the zero-knowledge proofs function for compact public key encryption to verify the encryption process without revealing the encrypted information.
TFHE-rs can generate zero-knowledge proofs to verify that the compact public key encryption process is correct. In other words, TFHE-rs generates the proof without revealing any information other than the already known range of the encrypted message. This technique is derived from .
To use this feature, you must first generate a CRS (Common Reference String). The CRS is a piece of cryptographic data that is necessary to ensure the security of zero-knowledge proofs. The CRS should be generated in advance and shared between all the clients and the server. A CRS can be reused for multiple encryptions with the same parameters.
Once the CRS is generated, using zero-knowledge proofs is straightforward: during encryption, the client generates the proof, and the server validates it before performing any homomorphic computations.
Note that you need to use dedicated parameters for the compact public key encryption. This helps to reduce the size of encrypted data and speed up the zero-knowledge proof computation.
The following example shows how a client can encrypt and prove a ciphertext, and how a server can verify and compute the ciphertext:
Performance can be improved by setting lto="fat"
in Cargo.toml
and by building the code for the native CPU architecture and in release mode, e.g. by calling RUSTFLAGS="-C target-cpu=native" cargo run --release
.
The ZK scheme used to generate and verify proofs is available in two versions:
ZKV1: This version is close to the original paper from .
ZKV2: Differing from the paper, this version provides better performance for provers and verifiers.
TFHE-rs selects automatically the scheme to use based on the encryption parameters during the CRS generation. With default parameters, ZKV2 is selected.
The following example shows how to generate a CRS and proofs for ZKV1. Compared to the previous example, only the parameters are changed:
Please refer to the for detailed performance benchmark results.
This document describes how to use Rayon for parallel processing in TFHE-rs, detailing configurations for single and multi-client applications with code examples.
is a popular Rust crate that simplifies writing multi-threaded code. You can use Rayon to write multi-threaded TFHE-rs code. However, due to the specifications of Rayon and TFHE-rs, certain setups are necessary.
The high-level API requires to call set_server_key
on each thread where computations need to be done. So a first attempt to use Rayon with TFHE-rs might look like this:
However, due to Rayon's work-stealing mechanism and TFHE-rs' internals, this may create BorrowMutError
.
The correct way is to call rayon::broadcast
as follows:
For applications that need to operate concurrently on data from different clients and require each client to use multiple threads, you need to create separate Rayon thread pools:
This can be useful if you have some rust #[test]
, see the example below:
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
.
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:
Choose a data type (Boolean, shortint, integer)
Import the library
Create client and server keys
Encrypt data with the client key
Compute over encrypted data using the server key
Decrypt data with the client key
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 contain easy to use APIs enabling 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.
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
)
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
)
Here is a full example using shortint:
Use the --release
flag to run this example (eg: cargo run --release
)
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 ).
This document describes the C bindings to the TFHE-rs high-level primitives for creating Fully Homomorphic Encryption (FHE) programs.
You can build TFHE-rs C API using the following command:
Locate files in the right path:
In ${REPO\_ROOT}/target/release/
, you can find:
The tfhe.h
header
The static (.a) and dynamic (.so) libtfhe
binaries
In ${REPO\_ROOT}/target/release/deps/
, you can find:
The tfhe-c-api-dynamic-buffer.h
header
The static (.a) and dynamic (.so) libraries
Ensure your build system configures the C or C++ program links against TFHE-rs C API binaries and the dynamic buffer library.
The following is a minimal CMakeLists.txt
configuration example:
TFHE-rs C API
.The following example demonstrates uint128 subtraction using the TFHE-rs C API:
WARNING: this example omits proper memory management in the error case to improve code readability.
Ensure the above CMakeLists.txt
and main.c
files are in the same directory. Use the following commands to execute the example:
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 framework. Serde's Serialize and Deserialize are then implemented on the tfhe::shortint
types.
To serialize the data, we need to pick a . For our use case, is a good choice, mainly because it is a binary format.
RUSTFLAGS="-C target-cpu=native" cargo +nightly build --release --features=high-level-c-api -p tfhe
project(my-project)
cmake_minimum_required(VERSION 3.16)
set(TFHE_C_API "/path/to/tfhe-rs/target/release")
include_directories(${TFHE_C_API})
include_directories(${TFHE_C_API}/deps)
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)
# /!\ 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
FHE computation successful!
$
#include "tfhe.h"
#include <assert.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_default(&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;
// A 128-bit unsigned integer containing value: 20 << 64 | 10
U128 clear_lhs = { .w0 = 10, .w1 = 20 };
// A 128-bit unsigned integer containing value: 2 << 64 | 1
U128 clear_rhs = { .w0 = 1, .w1 = 2 };
ok = fhe_uint128_try_encrypt_with_client_key_u128(clear_lhs, client_key, &lhs);
assert(ok == 0);
ok = fhe_uint128_try_encrypt_with_client_key_u128(clear_rhs, client_key, &rhs);
assert(ok == 0);
// Compute the subtraction
ok = fhe_uint128_sub(lhs, rhs, &result);
assert(ok == 0);
U128 clear_result;
// Decrypt
ok = fhe_uint128_decrypt(result, client_key, &clear_result);
assert(ok == 0);
// Here the subtraction allows us to compare each word
assert(clear_result.w0 == 9);
assert(clear_result.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);
printf("FHE computation successful!\n");
return EXIT_SUCCESS;
}
use rand::prelude::*;
use tfhe::prelude::*;
use tfhe::set_server_key;
use tfhe::zk::{CompactPkeCrs, ZkComputeLoad};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = thread_rng();
let params = tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128;
// Indicate which parameters to use for the Compact Public Key encryption
let cpk_params = tfhe::shortint::parameters::PARAM_PKE_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128;
// And parameters allowing to keyswitch/cast to the computation parameters.
let casting_params = tfhe::shortint::parameters::PARAM_KEYSWITCH_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128;
// Enable the dedicated parameters on the config
let config = tfhe::ConfigBuilder::with_custom_parameters(params)
.use_dedicated_compact_public_key_parameters((cpk_params, casting_params)).build();
// The CRS should be generated in an offline phase then shared to all clients and the server
let crs = CompactPkeCrs::from_config(config, 64).unwrap();
// Then use TFHE-rs as usual
let client_key = tfhe::ClientKey::generate(config);
let server_key = tfhe::ServerKey::new(&client_key);
let public_key = tfhe::CompactPublicKey::try_new(&client_key).unwrap();
// This can be left empty, but if provided allows to tie the proof to arbitrary data
let metadata = [b'T', b'F', b'H', b'E', b'-', b'r', b's'];
let clear_a = rng.gen::<u64>();
let clear_b = rng.gen::<u64>();
let proven_compact_list = tfhe::ProvenCompactCiphertextList::builder(&public_key)
.push(clear_a)
.push(clear_b)
.build_with_proof_packed(&crs, &metadata, ZkComputeLoad::Verify)?;
// Server side
let result = {
set_server_key(server_key);
// Verify the ciphertexts
let expander =
proven_compact_list.verify_and_expand(&crs, &public_key, &metadata)?;
let a: tfhe::FheUint64 = expander.get(0)?.unwrap();
let b: tfhe::FheUint64 = expander.get(1)?.unwrap();
a + b
};
// Back on the client side
let a_plus_b: u64 = result.decrypt(&client_key);
assert_eq!(a_plus_b, clear_a.wrapping_add(clear_b));
Ok(())
}
[profile.release]
lto = "fat"
use rand::prelude::*;
use tfhe::prelude::*;
use tfhe::set_server_key;
use tfhe::zk::{CompactPkeCrs, ZkComputeLoad};
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = thread_rng();
let params = tfhe::shortint::parameters::v1_2::V1_2_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128;
// Indicate which parameters to use for the Compact Public Key encryption
let cpk_params = tfhe::shortint::parameters::v1_2::compact_public_key_only::p_fail_2_minus_128::ks_pbs::V1_2_PARAM_PKE_TO_SMALL_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128_ZKV1;
// And parameters allowing to keyswitch/cast to the computation parameters.
let casting_params = tfhe::shortint::parameters::v1_2::key_switching::p_fail_2_minus_128::ks_pbs::V1_2_PARAM_KEYSWITCH_PKE_TO_SMALL_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128_ZKV1;
// Enable the dedicated parameters on the config
let config = tfhe::ConfigBuilder::with_custom_parameters(params)
.use_dedicated_compact_public_key_parameters((cpk_params, casting_params)).build();
// The CRS should be generated in an offline phase then shared to all clients and the server
let crs = CompactPkeCrs::from_config(config, 64).unwrap();
// Then use TFHE-rs as usual
let client_key = tfhe::ClientKey::generate(config);
let server_key = tfhe::ServerKey::new(&client_key);
let public_key = tfhe::CompactPublicKey::try_new(&client_key).unwrap();
// This can be left empty, but if provided allows to tie the proof to arbitrary data
let metadata = [b'T', b'F', b'H', b'E', b'-', b'r', b's'];
let clear_a = rng.gen::<u64>();
let clear_b = rng.gen::<u64>();
let proven_compact_list = tfhe::ProvenCompactCiphertextList::builder(&public_key)
.push(clear_a)
.push(clear_b)
.build_with_proof_packed(&crs, &metadata, ZkComputeLoad::Verify)?;
// Server side
let result = {
set_server_key(server_key);
// Verify the ciphertexts
let expander =
proven_compact_list.verify_and_expand(&crs, &public_key, &metadata)?;
let a: tfhe::FheUint64 = expander.get(0)?.unwrap();
let b: tfhe::FheUint64 = expander.get(1)?.unwrap();
a + b
};
// Back on the client side
let a_plus_b: u64 = result.decrypt(&client_key);
assert_eq!(a_plus_b, clear_a.wrapping_add(clear_b));
Ok(())
}
use tfhe::prelude::*;
use tfhe::{ConfigBuilder, set_server_key, FheUint8, generate_keys};
fn main() {
let (cks, sks) = generate_keys(ConfigBuilder::default());
let xs = [
FheUint8::encrypt(1u8, &cks),
FheUint8::encrypt(2u8, &cks),
];
let ys = [
FheUint8::encrypt(3u8, &cks),
FheUint8::encrypt(4u8, &cks),
];
// set_server_key in each closure as they might be
// running in different threads
let (a, b) = rayon::join(
|| {
set_server_key(sks.clone());
&xs[0] + &ys[0]
},
|| {
set_server_key(sks.clone());
&xs[1] + &ys[1]
}
);
}
use tfhe::prelude::*;
use tfhe::{ConfigBuilder, set_server_key, FheUint8, generate_keys};
fn main() {
let (cks, sks) = generate_keys(ConfigBuilder::default());
// set the server key in all of the rayon's threads so that
// we won't need to do it later
rayon::broadcast(|_| set_server_key(sks.clone()));
// Set the server key in the main thread
set_server_key(sks);
let xs = [
FheUint8::encrypt(1u8, &cks),
FheUint8::encrypt(2u8, &cks),
];
let ys = [
FheUint8::encrypt(3u8, &cks),
FheUint8::encrypt(4u8, &cks),
];
let (a, b) = rayon::join(
|| {
&xs[0] + &ys[0]
},
|| {
&xs[1] + &ys[1]
}
);
let a: u8 = a.decrypt(&cks);
let b: u8 = b.decrypt(&cks);
assert_eq!(a, 4u8);
assert_eq!(b, 6u8);
}
use tfhe::prelude::*;
use tfhe::{ConfigBuilder, set_server_key, FheUint8, generate_keys};
fn main() {
let (cks1, sks1) = generate_keys(ConfigBuilder::default());
let xs1 = [
FheUint8::encrypt(1u8, &cks1),
FheUint8::encrypt(2u8, &cks1),
];
let ys1 = [
FheUint8::encrypt(3u8, &cks1),
FheUint8::encrypt(4u8, &cks1),
];
let (cks2, sks2) = generate_keys(ConfigBuilder::default());
let xs2 = [
FheUint8::encrypt(100u8, &cks2),
FheUint8::encrypt(200u8, &cks2),
];
let ys2 = [
FheUint8::encrypt(103u8, &cks2),
FheUint8::encrypt(204u8, &cks2),
];
let client_1_pool = rayon::ThreadPoolBuilder::new().num_threads(4).build().unwrap();
let client_2_pool = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
client_1_pool.broadcast(|_| set_server_key(sks1.clone()));
client_2_pool.broadcast(|_| set_server_key(sks2.clone()));
let ((a1, b1), (a2, b2)) = rayon::join(|| {
client_1_pool.install(|| {
rayon::join(
|| {
&xs1[0] + &ys1[0]
},
|| {
&xs1[1] + &ys1[1]
}
)
})
}, || {
client_2_pool.install(|| {
rayon::join(
|| {
&xs2[0] + &ys2[0]
},
|| {
&xs2[1] + &ys2[1]
}
)
})
});
let a1: u8 = a1.decrypt(&cks1);
let b1: u8 = b1.decrypt(&cks1);
assert_eq!(a1, 4u8);
assert_eq!(b1, 6u8);
let a2: u8 = a2.decrypt(&cks2);
let b2: u8 = b2.decrypt(&cks2);
assert_eq!(a2, 203u8);
assert_eq!(b2, 148u8);
}
// Pseudo code
#[test]
fn test_1() {
let pool = rayon::ThreadPoolBuilder::new().num_threads(4).build().unwrap();
pool.broadcast(|_| set_server_key(sks1.clone()));
pool.install(|| {
let result = call_to_a_multithreaded_function(...);
assert_eq!(result, expected_value);
})
}
#[test]
fn test_2() {
let pool = rayon::ThreadPoolBuilder::new().num_threads(4).build().unwrap();
pool.broadcast(|_| set_server_key(sks1.clone()));
pool.install(|| {
let result = call_to_another_multithreaded_function(...);
assert_eq!(result, expected_value);
})
}
use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheUint8};
use tfhe::prelude::*;
fn main() {
let config = ConfigBuilder::default()
.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!(output);
}
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.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);
}
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2;
fn main() {
// We generate keys to encrypt 16 bits radix-encoded 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))
}
# Cargo.toml
[dependencies]
# ...
bincode = "1.3.3"
// main.rs
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_KS_PBS);
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: Ciphertext = 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: Ciphertext = bincode::deserialize_from(&mut serialized_data)?;
let ct_2: Ciphertext = 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)
}
This document gives a high-level overview of various operations on encrypted integers supported by TFHE-rs.
TFHE-rs supports various operations on encrypted integers (Enc
) of any size between 1 and 256 bits. These operations can also work between encrypted integers and clear integers (Int
).
name
symbol
Enc
/Enc
Enc
/ Int
Neg
-
✔️
✔️
Add
+
✔️
✔️
Sub
-
✔️
✔️
Mul
*
✔️
✔️
Div
/
✔️
✔️
Rem
%
✔️
✔️
Not
!
✔️
✔️
BitAnd
&
✔️
✔️
BitOr
|
✔️
✔️
BitXor
^
✔️
✔️
Shr
>>
✔️
✔️
Shl
<<
✔️
✔️
Min
min
✔️
✔️
Max
max
✔️
✔️
Greater than
gt
✔️
✔️
Greater or equal than
ge
✔️
✔️
Less than
lt
✔️
✔️
Less or equal than
le
✔️
✔️
Equal
eq
✔️
✔️
Cast (into dest type)
cast_into
✔️
✖️
Cast (from src type)
cast_from
✔️
✖️
Ternary operator
select
✔️
✖️
This document introduces the cryptographic concepts of the scheme of Fully Homomorphic Encryption over the Torus (TFHE) and the security considerations of TFHE-rs.
TFHE-rs is a cryptographic library that implements Fully Homomorphic Encryption using the TFHE scheme. You should understand the basics of TFHE to consider its limitations, such as:
The precision: TFHE has limitations on the number of bits used to represent plaintext values.
The execution time: TFHE operations are slower than native operations due to their complexity.
TFHE-rs primarily utilizes Learning With Errors (LWE) ciphertexts. The LWE problem forms the basis of TFHE's security and is considered resistant to quantum attacks.
An LWE Ciphertext is a collection of 32-bit or 64-bit unsigned integers. Before encrypting a message in an LWE ciphertext, you first need to encode it as a plaintext by shifting the message to the most significant bits of the unsigned integer type used.
Then, you add a small random value called noise to the least significant bits. This noise is crucial in ensuring the security of the ciphertext.
To get a ciphertext from a plaintext, you must encrypt the plaintext using a secret key.
An LWE secret key is a list of n
random integers: . is called the
An LWE ciphertext is composed of two parts:
The mask
The body
The mask of a fresh ciphertext (the result of an encryption, and not the result of operations such as ciphertext addition) is a list of n
uniformly random values.
The body is computed as follows:
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 and :
To add ciphertexts, it is necessary to add both their masks and bodies. The operation involves adding elements, rather than just adding two integers. This is an intuitive example to show how FHE computation is slower compared to plaintext computation. However, other operations are far more expensive (for example, the computation of a lookup table using Programmable Bootstrapping).
In FHE, two types of operations can be applied to ciphertexts:
Leveled operations, which increase the noise in the ciphertext
Bootstrapped operations, which reduce the noise in the ciphertext
Noise is critical in FHE because it can tamper with the message if not tracked and managed properly. Bootstrapping operations decrease noise within the ciphertexts and guarantee the correctness of computation. The rest of the operations do not need bootstrapping operations, thus they are called leveled operations and are usually very fast as a result.
The following sections explain the concept of noise and padding in ciphertexts.
To ensure security, LWE requires random noise to be added to the message during encryption.
TFHE scheme draws this random noise either from:
A Centered Normal Distribution with a standard deviation parameter. The choice of standard deviation impacts the security level: increasing the standard deviation enhances security while keeping other factors constant.
A Tweaked Uniform (TUniform) Distribution with a bound parameter defined as follows: any value in the interval is selected with probability , with the two end points and being selected with probability . The main advantage of this distribution is to be bounded, whereas the usual Central Normal Distribution one is not. In some practical cases, this can simplify the use of homomorphic computation. The choice of the bound impacts the security level: increasing the bound enhances security while keeping other factors constant.
TFHE-rs encodes the noise in the least significant bits of each plaintext. Each leveled computation increases the value of the noise. If too many computations are performed, the noise will eventually overflow into the message bits and lead to an incorrect result.
The following figure illustrates how the extra bit of noise is incurred during an addition operation.
TFHE-rs enables automatic noise management by performing bootstrapping operations to reset the noise.
The bootstrapping of TFHE is programmable. This allows any function to be homomorphically computed over an encrypted input, while also reducing the noise. These functions are represented by look-up tables.
In general, the computation of a PBS is preceded or followed by a keyswitch, an operation to change the encryption key. The output ciphertext is then encrypted with the same key as the input one. To do this, two (public) evaluation keys are required: a bootstrapping key and a keyswitching key.
These operations are quite complex to describe in short, you can find more details about these operations (or about TFHE in general) in the TFHE Deep Dive.
Since encoded values have a fixed precision, operating on them can produce results that are outside of 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.
For example, when adding two ciphertexts, the sum could exceed the range of either ciphertext, and thus necessitate a carry that would then be transferred onto the first padding bit. In the following figure, each plaintext over 32 bits has one bit of padding on its left (the most significant bit). After the addition, the padding bit gets consumed to accommodate the carry. We refer to this process as consuming bits of padding. Without any padding-left, further additions may not produce accurate results.
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 the High-Level API the default parameters are selected with a bootstrapping failure probability (or error probability) fixed at for the x86 CPU backend, and for the GPU backend. A failure probability below ensures that our implementation is resilient against attacks in the IND-CPA-D model [1]. In the case where only the IND-CPA model is considered, there is a possibility to choose parameters with a , see the dedicated Parameters section
The parameter sets for the x86 CPU backend with a are obtained using the drift mitigation technique described in [2].
In classical 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 , where is the LWE dimension, is the ciphertext modulus, and 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 .
This guide explains the multi GPU support of TFHE-rs, and walks through a practical example of performing a large batch of encrypted 64-bit additions using manual GPU dispatching to improve the performance.
TFHE-rs supports platforms with multiple GPUs. There is nothing to change in the code to execute on such platforms. To keep the API as user-friendly as possible, the configuration is automatically set, i.e., the user has no fine-grained control over the number of GPUs to be used. However, you can decide to have operations be executed on a single GPU of your choice. In many cases this provides better throughput than using all the available GPUs to perform the operation. Indeed, except for integer precisions above 64 bits and for the multiplication, which involves many bootstrap computations in parallel, most operations on up to 64 bits do not necessitate the full power of a GPU. You will then be able to maximize throughput on multiple GPUs with TFHE-rs.
By default, when multiple GPUs are available on the machine, TFHE-rs automatically uses them all to perform encrypted operations. Under the hood, it includes a hard-coded logic to dispatch work across all the GPUs and to copy essential data—like the server key—to each GPU. This approach is efficient for operations that load the GPU extensively (e.g. the 64-bit multiplication), but not so much for smaller operations like the encrypted addition or comparison on 64-bits. To address this, TFHE-rs also provides a mechanism to manually select which GPU to operate on.
When selecting a specific GPU to execute on, there are two essential requirements that are different from a default GPU execution:
You must create a GPU server key on each GPU individually.
The batch of operations must be distributed on all the GPUs manually.
Instead of a single server key being used across all GPUs automatically, you’ll need specifically decompress the server key to each GPU, so that the key is available in memory. For example, by default, the GPU server key is decompressed and loaded onto all available GPUs automatically as follows:
However, to use the multi-GPU selection feature, you can create a vector of server keys, each on a specific GPU:
We will be doing 100 additions in parallel on each GPU:
At this stage, the left and right inputs reside on the CPU. They have not yet been copied to the GPU.
Now you need to split the calculation into as many chunks as there are GPUs.
TFHE-rs allows you to execute additions in parallel across multiple GPUs by leveraging .
CUDA stream management is not explicit in the High-Level(HL) API of TFHE-rs: streams are implicitly
created through calls to set_server_key
in a CPU thread.
As a result, when you use .par_iter()
on encrypted data within the HL API, and that computation is dispatched to a GPU, it behaves as expected—executing in parallel using CUDA streams.
We’ll take advantage of this behavior to maximize throughput on a multi-GPU machine. In the following example, we split a large batch of encrypted 64-bit additions across multiple GPUs. Each GPU processes its own chunk of data in parallel, thanks to the creation of CUDA streams under the hood:
In this example, par_chunks
divides the input vectors into num_gpus
chunks—one per GPU. Each chunk is then processed in parallel using .par_iter()
. Inside the inner loop, calling set_server_key(sks_vec[i].clone())
sets the context for the GPU i
and implicitly creates a new CUDA stream for GPU i
. This enables parallel execution on each device.
It’s important to note that, in this example, when using the +
operator on encrypted inputs, data is first transferred from the CPU to the GPU before computation, the result then resides on the GPU i
.
You can learn more about how to inspect on which GPU a piece of data resides from the examples in this file: tfhe/src/high_level_api/tests/gpu_selection.rs
.
While the behavior of .par_iter()
in TFHE-rs' HL API aligns with expectations and provides parallelism over encrypted data, it can become a performance bottleneck in some cases. This is due to the way CUDA streams are managed.
CUDA streams allow for parallel execution on the GPU, but when too many are created, scheduling becomes inefficient. Instead of running in parallel, operations may fall back to sequential execution. In practice, having more than 10 streams already starts to negatively impact throughput.
To address this, we can limit the number of streams used per GPU. The optimal number depends on the type of operation, but the general rule is: use as few streams as possible while still fully utilizing the GPU.
For example, in the case of 64-bit encrypted additions, using 4 streams per GPU offers a good balance. Each GPU processes inputs in chunks of 4 operations in parallel, repeating this in batches until all inputs are handled.
Here’s how this approach looks in code:
In this version, we:
Define a number of streams per GPU
Split the load between the streams by calling par_chunks()
on the batch assigned to each GPU.
This method provides a more fine-controlled form of parallelism, reaching an optimal performance on multiple GPUs with TFHE-rs.
use tfhe::{ConfigBuilder, set_server_key, ClientKey, CompressedServerKey};
use tfhe::prelude::*;
use rayon::prelude::*;
fn main() {
let config = ConfigBuilder::default().build();
let client_key = ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
let sks = compressed_server_key.decompress_to_gpu();
}
use tfhe::{ConfigBuilder, set_server_key, ClientKey, CompressedServerKey, GpuIndex};
use tfhe::prelude::*;
use rayon::prelude::*;
use tfhe::core_crypto::gpu::get_number_of_gpus;
fn main() {
let config = ConfigBuilder::default().build();
let client_key = ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
let num_gpus = get_number_of_gpus();
let sks_vec = (0..num_gpus)
.map(|i| compressed_server_key.decompress_to_specific_gpu(GpuIndex::new(i)))
.collect::<Vec<_>>();
}
use tfhe::{ConfigBuilder, set_server_key, ClientKey, CompressedServerKey, FheUint64, GpuIndex};
use tfhe::prelude::*;
use rayon::prelude::*;
use tfhe::core_crypto::gpu::get_number_of_gpus;
use rand::{thread_rng, Rng};
fn main() {
let config = ConfigBuilder::default().build();
let client_key = ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
let num_gpus = get_number_of_gpus();
let sks_vec = (0..num_gpus)
.map(|i| compressed_server_key.decompress_to_specific_gpu(GpuIndex::new(i)))
.collect::<Vec<_>>();
let batch_size = num_gpus * 100;
let mut rng = thread_rng();
let left_inputs = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
let right_inputs = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
}
use tfhe::{ConfigBuilder, set_server_key, ClientKey, CompressedServerKey, FheUint64, GpuIndex};
use tfhe::prelude::*;
use rayon::prelude::*;
use tfhe::core_crypto::gpu::get_number_of_gpus;
use rand::{thread_rng, Rng};
fn main() {
let config = ConfigBuilder::default().build();
let client_key = ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
let num_gpus = get_number_of_gpus();
let sks_vec = (0..num_gpus)
.map(|i| compressed_server_key.decompress_to_specific_gpu(GpuIndex::new(i)))
.collect::<Vec<_>>();
let batch_size = num_gpus * 100;
let mut rng = thread_rng();
let left_inputs = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
let right_inputs = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
let chunk_size = (batch_size / num_gpus) as usize;
left_inputs
.par_chunks(chunk_size)
.zip(
right_inputs
.par_chunks(chunk_size)
)
.enumerate()
.for_each(
|(i, (left_inputs_on_gpu_i, right_inputs_on_gpu_i))| {
left_inputs_on_gpu_i
.par_iter()
.zip(right_inputs_on_gpu_i.par_iter())
.for_each(|(left_input, right_input)| {
set_server_key(sks_vec[i].clone());
let _ = left_input + right_input;
});
},
);
}
use tfhe::{ConfigBuilder, set_server_key, ClientKey, CompressedServerKey, FheUint64, GpuIndex};
use tfhe::prelude::*;
use rayon::prelude::*;
use tfhe::core_crypto::gpu::get_number_of_gpus;
use rand::{thread_rng, Rng};
fn main() {
let config = ConfigBuilder::default().build();
let client_key= ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
let num_gpus = get_number_of_gpus();
let sks_vec = (0..num_gpus)
.map(|i| compressed_server_key.decompress_to_specific_gpu(GpuIndex::new(i)))
.collect::<Vec<_>>();
let batch_size = num_gpus * 100;
let mut rng = thread_rng();
let left_inputs = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
let right_inputs = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
let amounts = (0..batch_size)
.map(|_| FheUint64::encrypt(rng.gen::<u64>(), &client_key))
.collect::<Vec<_>>();
let chunk_size = (batch_size / num_gpus) as usize;
let num_streams_per_gpu = 4;
left_inputs
.par_chunks(chunk_size)
.zip(
right_inputs
.par_chunks(chunk_size)
.zip(amounts.par_chunks(chunk_size)),
)
.enumerate()
.for_each(
|(i, (left_inputs_gpu_i, (right_inputs_gpu_i, amount_gpu_i)))| {
let stream_chunk_size = left_inputs_gpu_i.len() / num_streams_per_gpu;
left_inputs_gpu_i
.par_chunks(stream_chunk_size)
.zip(right_inputs_gpu_i.par_chunks(stream_chunk_size))
.zip(amount_gpu_i.par_chunks(stream_chunk_size))
.for_each(
|((left_inputs_chunk, right_inputs_chunk), amount_chunk)| {
set_server_key(sks_vec[i].clone());
left_inputs_chunk
.iter()
.zip(right_inputs_chunk.iter().zip(amount_chunk.iter()))
.for_each(|(left_input, (right_input, amount))| {
let _ = left_input + right_input;
});
},
);
},
);
}
This document explains the serialization
and deserialization
features that are useful to send data to a server to perform the computations.
When dealing with sensitive types, it's important to implement safe serialization and safe deserialization functions to prevent runtime errors and enhance security. TFHE-rs provide easy to use functions for this purpose, such as safe_serialize
, safe_deserialize
and safe_deserialize_conformant
.
Here is a basic example on how to use it:
// main.rs
use tfhe::safe_serialization::{safe_deserialize_conformant, safe_serialize};
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
use tfhe::ServerKey;
use tfhe::{generate_keys, ConfigBuilder};
fn main() {
let params_1 = PARAM_MESSAGE_2_CARRY_2_KS_PBS;
let config = ConfigBuilder::with_custom_parameters(params_1).build();
let (client_key, server_key) = generate_keys(config);
let mut buffer = vec![];
// The last argument is the max allowed size for the serialized buffer
safe_serialize(&server_key, &mut buffer, 1 << 30).unwrap();
let _server_key_deser: ServerKey =
safe_deserialize_conformant(buffer.as_slice(), 1 << 30, &config.into()).unwrap();
}
The safe deserialization must take the output of a safe-serialization as input. During the process, the following validation occurs:
Type match: deserializing type A
from a serialized type B
raises an error indicating "On deserialization, expected type A, got type B".
Version compatibility: data serialized in previous versions of TFHE-rs are automatically upgraded to the latest version using the data versioning feature.
Parameter compatibility: deserializing an object of type A
with one set of crypto parameters from an object of type A
with another set of crypto parameters raises an error indicating "Deserialized object of type A not conformant with given parameter set"
If both parameter sets have the same LWE dimension for ciphertexts, a ciphertext from param 1 may not fail this deserialization check with param 2.
This check can't distinguish ciphertexts/server keys from independent client keys with the same parameters.
This check is meant to prevent runtime errors in server homomorphic operations by checking that server keys and ciphertexts are compatible with the same parameter set.
You can use the standalone is_conformant
method to check parameter compatibility. Besides, the safe_deserialize_conformant
function includes the parameter compatibility check, and the safe_deserialize
function does not include the compatibility check.
Size limit: both serialization and deserialization processes expect a size limit (measured in bytes) for the serialized data:
On serialization, an error is raised if the serialized output exceeds the specific limit.
On deserialization, an error is raised if the serialized input exceeds the specific limit.
This feature aims to gracefully return an error in case of an attacker trying to cause an out-of-memory error on deserialization.
Here is a more complete example:
// main.rs
use tfhe::conformance::ParameterSetConformant;
use tfhe::prelude::*;
use tfhe::safe_serialization::{safe_serialize, safe_deserialize_conformant};
use tfhe::shortint::parameters::{
PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128,
PARAM_MESSAGE_3_CARRY_3_KS_PBS_GAUSSIAN_2M128};
use tfhe::conformance::ListSizeConstraint;
use tfhe::{
generate_keys, FheUint8, CompactCiphertextList, FheUint8ConformanceParams,
CompactPublicKey, ConfigBuilder, CompactCiphertextListConformanceParams
};
fn main() {
let params_1 = PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128;
let params_2 = PARAM_MESSAGE_3_CARRY_3_KS_PBS_GAUSSIAN_2M128;
assert_ne!(params_1, params_2);
let config = ConfigBuilder::with_custom_parameters(params_1).build();
let (client_key, server_key) = generate_keys(config);
let conformance_params_1 = FheUint8ConformanceParams::from(params_1);
let conformance_params_2 = FheUint8ConformanceParams::from(params_2);
let public_key = CompactPublicKey::new(&client_key);
let msg = 27u8;
let ct = FheUint8::try_encrypt(msg, &client_key).unwrap();
assert!(ct.is_conformant(&conformance_params_1));
assert!(!ct.is_conformant(&conformance_params_2));
let mut buffer = vec![];
safe_serialize(&ct, &mut buffer, 1 << 20).unwrap();
assert!(safe_deserialize_conformant::<FheUint8>(buffer.as_slice(), 1 << 20, &conformance_params_2)
.is_err());
let ct2: FheUint8 = safe_deserialize_conformant(buffer.as_slice(), 1 << 20, &conformance_params_1)
.unwrap();
let dec: u8 = ct2.decrypt(&client_key);
assert_eq!(msg, dec);
// Example with a compact list:
let msgs = [27, 188u8];
let mut builder = CompactCiphertextList::builder(&public_key);
builder.extend(msgs.iter().copied());
let compact_list = builder.build();
let mut buffer = vec![];
safe_serialize(&compact_list, &mut buffer, 1 << 20).unwrap();
let conformance_params = CompactCiphertextListConformanceParams {
shortint_params: params_1.to_shortint_conformance_param(),
num_elements_constraint: ListSizeConstraint::exact_size(2),
};
safe_deserialize_conformant::<CompactCiphertextList>(buffer.as_slice(), 1 << 20, &conformance_params)
.unwrap();
}
The safe serialization and deserialization use bincode
internally.
To selectively disable some of the features of the safe serialization, you can use SerializationConfig
/DeserializationConfig
builders. For example, it is possible to disable the data versioning:
// main.rs
use tfhe::safe_serialization::{safe_deserialize_conformant, SerializationConfig};
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
use tfhe::ServerKey;
use tfhe::{generate_keys, ConfigBuilder};
fn main() {
let params_1 = PARAM_MESSAGE_2_CARRY_2_KS_PBS;
let config = ConfigBuilder::with_custom_parameters(params_1).build();
let (client_key, server_key) = generate_keys(config);
let mut buffer = vec![];
SerializationConfig::new(1 << 30).disable_versioning().serialize_into(&server_key, &mut buffer).unwrap();
// You will still be able to load this item with `safe_deserialize_conformant`, but only using the current version of TFHE-rs
let _server_key_deser: ServerKey =
safe_deserialize_conformant(buffer.as_slice(), 1 << 30, &config.into()).unwrap();
}
TFHE-rs uses the Serde framework and implements Serde's Serialize
and Deserialize
traits.
This allows you to serialize into any data format supported by serde. However, this is a more bare bone approach as none of the checks described in the previous section will be performed for you.
In the following example, we use bincode for its binary format:
# Cargo.toml
[dependencies]
# ...
tfhe = { version = "~1.2.0", features = ["integer"] }
bincode = "1.3.3"
// main.rs
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::default().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)
}
This document explains the mechanism and steps to compress ciphertext and keys to reduce the storage needed as well as transmission times.
Most TFHE-rs entities contain random numbers generated by a Pseudo Random Number Generator (PRNG). Since the implemented PRNG is deterministic, storing only the random seed used to generate those numbers preserves all necessary information. When decompressing the entity, using the same PRNG and the same seed will reconstruct the full chain of random values.
In TFHE-rs, compressible entities are prefixed with Compressed
. For instance, a compressed FheUint256
is declared as CompressedFheUint256
.
In the following example code, we use the bincode
crate dependency to serialize in a binary format and compare serialized sizes.
This example shows how to compress a ciphertext encrypting messages over 16 bits:
use tfhe::prelude::*;
use tfhe::{ConfigBuilder, generate_keys, CompressedFheUint16};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, _) = generate_keys(config);
let clear = 12_837u16;
let compressed = CompressedFheUint16::try_encrypt(clear, &client_key).unwrap();
println!(
"compressed size : {}",
bincode::serialize(&compressed).unwrap().len()
);
let decompressed = compressed.decompress();
println!(
"decompressed size: {}",
bincode::serialize(&decompressed).unwrap().len()
);
let clear_decompressed: u16 = decompressed.decrypt(&client_key);
assert_eq!(clear_decompressed, clear);
}
You can compress ciphertexts at any time, even after performing multiple homomorphic operations.
To do so, you need to build a list containing all the ciphertexts that have to be compressed. This list might contain ciphertexts of different types, e.g., FheBool, FheUint32, FheInt64,... There is no constraint regarding the size of the list.
There are two possible approaches:
Single list: Compressing several ciphertexts into a single list. This generally yields a better compression ratio between output and input sizes;
Multiple lists: Using multiple lists. This offers more flexibility, since compression might happen at different times in the code, but could lead to larger outputs.
In more details, the optimal ratio is achieved with a list whose size is
equal to the lwe_per_glwe
field from the CompressionParameters
.
The following example shows how to compress and decompress a list containing 4 messages: one 32-bits integer, one 64-bit integer, one boolean, and one 2-bit integer.
use tfhe::prelude::*;
use tfhe::shortint::parameters::{
COMP_PARAM_MESSAGE_2_CARRY_2, PARAM_MESSAGE_2_CARRY_2,
};
use tfhe::{
set_server_key, CompressedCiphertextList, CompressedCiphertextListBuilder, FheBool,
FheInt64, FheUint16, FheUint2, FheUint32,
};
fn main() {
let config =
tfhe::ConfigBuilder::with_custom_parameters(PARAM_MESSAGE_2_CARRY_2)
.enable_compression(COMP_PARAM_MESSAGE_2_CARRY_2)
.build();
let ck = tfhe::ClientKey::generate(config);
let sk = tfhe::ServerKey::new(&ck);
set_server_key(sk);
let ct1 = FheUint32::encrypt(17_u32, &ck);
let ct2 = FheInt64::encrypt(-1i64, &ck);
let ct3 = FheBool::encrypt(false, &ck);
let ct4 = FheUint2::encrypt(3u8, &ck);
let compressed_list = CompressedCiphertextListBuilder::new()
.push(ct1)
.push(ct2)
.push(ct3)
.push(ct4)
.build()
.unwrap();
let serialized = bincode::serialize(&compressed_list).unwrap();
println!("Serialized size: {} bytes", serialized.len());
let compressed_list: CompressedCiphertextList = bincode::deserialize(&serialized).unwrap();
let a: FheUint32 = compressed_list.get(0).unwrap().unwrap();
let b: FheInt64 = compressed_list.get(1).unwrap().unwrap();
let c: FheBool = compressed_list.get(2).unwrap().unwrap();
let d: FheUint2 = compressed_list.get(3).unwrap().unwrap();
let a: u32 = a.decrypt(&ck);
assert_eq!(a, 17);
let b: i64 = b.decrypt(&ck);
assert_eq!(b, -1);
let c = c.decrypt(&ck);
assert!(!c);
let d: u8 = d.decrypt(&ck);
assert_eq!(d, 3);
// Out of bound index
assert!(compressed_list.get::<FheBool>(4).unwrap().is_none());
// Incorrect type
assert!(compressed_list.get::<FheInt64>(0).is_err());
// Correct type but wrong number of bits
assert!(compressed_list.get::<FheUint16>(0).is_err());
}
This example shows how to compress the server keys:
use tfhe::prelude::*;
use tfhe::{
set_server_key, ClientKey, CompressedServerKey, ConfigBuilder, FheUint8,
};
fn main() {
let config = ConfigBuilder::default().build();
let cks = ClientKey::generate(config);
let compressed_sks = CompressedServerKey::new(&cks);
println!(
"compressed size : {}",
bincode::serialize(&compressed_sks).unwrap().len()
);
let sks = compressed_sks.decompress();
println!(
"decompressed size: {}",
bincode::serialize(&sks).unwrap().len()
);
set_server_key(sks);
let clear_a = 12u8;
let a = FheUint8::try_encrypt(clear_a, &cks).unwrap();
let c = a + 234u8;
let decrypted: u8 = c.decrypt(&cks);
assert_eq!(decrypted, clear_a.wrapping_add(234));
}
This example shows how to compress the classical public keys:
It is not currently recommended to use the CompressedPublicKey
to encrypt ciphertexts without first decompressing them. If the resulting PublicKey is too large to fit in memory, it may result in significant slowdowns.
This issue has been identified and will be addressed in future releases.
use tfhe::prelude::*;
use tfhe::{ConfigBuilder, generate_keys, FheUint8, CompressedPublicKey};
fn main() {
let config = ConfigBuilder::default().build();
let (client_key, _) = generate_keys(config);
let compressed_public_key = CompressedPublicKey::new(&client_key);
println!("compressed size : {}", bincode::serialize(&compressed_public_key).unwrap().len());
let public_key = compressed_public_key.decompress();
println!("decompressed size: {}", bincode::serialize(&public_key).unwrap().len());
let a = FheUint8::try_encrypt(213u8, &public_key).unwrap();
let clear: u8 = a.decrypt(&client_key);
assert_eq!(clear, 213u8);
}
This example shows how to use compressed compact public keys:
use tfhe::prelude::*;
use tfhe::{
generate_keys, CompactCiphertextList, CompressedCompactPublicKey,
ConfigBuilder, FheUint8,
};
fn main() {
let config = ConfigBuilder::default()
.use_custom_parameters(
tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS_GAUSSIAN_2M128,
)
.build();
let (client_key, _) = generate_keys(config);
let public_key_compressed = CompressedCompactPublicKey::new(&client_key);
println!(
"compressed size : {}",
bincode::serialize(&public_key_compressed).unwrap().len()
);
let public_key = public_key_compressed.decompress();
println!(
"decompressed size: {}",
bincode::serialize(&public_key).unwrap().len()
);
let compact_list = CompactCiphertextList::builder(&public_key)
.push(255u8)
.build();
let expanded = compact_list.expand().unwrap();
let a: FheUint8 = expanded.get(0).unwrap().unwrap();
let clear: u8 = a.decrypt(&client_key);
assert_eq!(clear, 255u8);
}
This document outlines how to use the TFHE-rs WebAssembly (WASM) client API for key generation, encryption, and decryption, providing setup examples for Node.js and web browsers.
TFHE-rs supports WASM client API, which includes functionality for key generation, encryption, and decryption. However, it does not support FHE computations.
TFHE-rs supports 3 WASM targets
:
Node.js: For use in Node.js applications or packages
Web: For use in web browsers
Web-parallel: For use in web browsers with multi-threading support
The core of the API remains the same, requiring only minor changes in the initialization functions.
Example:
const {
init_panic_hook,
ShortintParametersName,
ShortintParameters,
TfheClientKey,
TfheCompactPublicKey,
TfheCompressedServerKey,
TfheConfigBuilder,
CompactCiphertextList
} = require("/path/to/built/pkg/tfhe.js");
const assert = require("node:assert").strict;
function fhe_uint32_example() {
// Makes it so that if a rust thread panics,
// the error message will be displayed in the console
init_panic_hook();
const U32_MAX = 4294967295;
const block_params = new ShortintParameters(ShortintParametersName.V1_2_PARAM_MESSAGE_2_CARRY_2_COMPACT_PK_PBS_KS_GAUSSIAN_2M64);
let config = TfheConfigBuilder.default()
.build();
let clientKey = TfheClientKey.generate(config);
let compressedServerKey = TfheCompressedServerKey.new(clientKey);
let publicKey = TfheCompactPublicKey.new(clientKey);
let values = [0, 1, 2394, U32_MAX];
let builder = CompactCiphertextList.builder(publicKey);
for (let i = 0; i < values.length; i++) {
builder.push_u32(values[i]);
}
let compact_list = builder.build();
let serialized_list = compact_list.serialize();
let deserialized_list = CompactCiphertextList.deserialize(serialized_list);
let encrypted_list = deserialized_list.expand();
assert.deepStrictEqual(encrypted_list.len(), values.length);
for (let i = 0; i < values.length; i++)
{
let decrypted = encrypted_list.get_uint32(i).decrypt(clientKey);
assert.deepStrictEqual(decrypted, values[i]);
}
}
fhe_uint32_example();
When using the Web WASM target, you should call an additional init
function. With parallelism enabled, you need to call another additional initThreadPool
function.
Example:
import init, {
initThreadPool, // only available with parallelism
init_panic_hook,
ShortintParametersName,
ShortintParameters,
TfheClientKey,
TfhePublicKey,
} from "./pkg/tfhe.js";
async function example() {
await init()
await initThreadPool(navigator.hardwareConcurrency);
await init_panic_hook();
const block_params = new ShortintParameters(ShortintParametersName.V1_2_PARAM_MESSAGE_2_CARRY_2_COMPACT_PK_PBS_KS_GAUSSIAN_2M64);
// ....
}
Use the provided Makefile in the TFHE-rs repository to compile for the desired target:
make build_node_js_api
for the Node.js API
make build_web_js_api
for the browser API
make build_web_js_api_parallel
for the browser API with parallelism
The compiled WASM packages are located in tfhe/pkg
.
When using the browser API with parallelism, some extra step might be needed depending on the bundler used:
If you're using Webpack v5 (version >= 5.25.1), you don't need to do anything special, as it already supports bundling Workers out of the box.
Parcel v2 also recognises the used syntax and works out of the box.
For Rollup, you'll need @surma/rollup-plugin-off-main-thread
plugin (version >= 2.1.0) which brings the same functionality and was tested with this crate.
Alternatively, you can use Vite which has necessary plugins built-in.
(Taken from RReverser/wasm-bindgen-rayon)
TFHE-rs uses WASM to provide a JavaScript (JS) binding to the client-side primitives, like key generation and encryption within the Boolean and shortint modules.
Currently, there are several limitations. 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 the FHE keys exceeding the 2GB memory limit of WASM, making these parameter sets virtually unusable.
To build the JS on WASM bindings for TFHE-rs, install wasm-pack
and the necessary rust toolchain
. Cone the TFHE-rs repository and build using the following commands (this will build using the default branch, you can check out a specific tag depending on your requirements):
$ 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 Node.js. To generate a binding for a web browser, use --target=web
. However, this tutorial does not cover that particular use case.
Both Boolean and shortint features are enabled here, but it's possible to use them individually.
After the build, a new directory pkg is available 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
$
// 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, ShortintParametersName, ShortintParameters } = 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_name = ShortintParametersName.PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128;
let params = new ShortintParameters(params_name);
// 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();
Then, you can run the example.js
script using node
as follows:
$ node example.js
Generating client keys...
Encrypting 3...
Decrypting ciphertext...
Decryption successful!
Generating compressed ServerKey...
All done!
$
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
.
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:
On the client side, generate the client
and server keys
.
Send the server key
to the server.
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
.
In the first step, the client creates two keys, the client key
and the server key
, with the tfhe::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, create_dir_all};
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();
// Create a tmp dir with the current user name to avoid cluttering the /tmp dir
let user = std::env::var("USER").unwrap_or_else(|_| "unknown_user".to_string());
let tmp_dir_for_user = &format!("/tmp/{user}");
create_dir_all(tmp_dir_for_user).unwrap();
let server_key_file = &format!("{tmp_dir_for_user}/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");
}
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();
//---------------------------- CLIENT 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
// ...
}
Anyone (the server or a third party) with the public key can also encrypt some (or all) of the inputs. The public key can only be used to encrypt, not to decrypt.
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 or THIRD_PARTY 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 (if not on the server already):
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 to be deserialized (if not on the server already)
// ...
}
Once the encrypted inputs are on the server side, the server_key
can be used to homomorphically execute the desired Boolean circuit:
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
// ...
}
Once the encrypted output is on the client side, the client_key
can be used to decrypt it:
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!(output);
}
core_crypto
primitivesWelcome to this tutorial about TFHE-rs
core_crypto
module.
core_crypto
moduleTo use TFHE-rs
, it first has to be added as a dependency in the Cargo.toml
:
tfhe = { version = "~1.2.0" }
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_noise_distribution =
Gaussian::from_dispersion_parameter(StandardDev(0.000007069849454709433), 0.0);
let glwe_noise_distribution =
Gaussian::from_dispersion_parameter(StandardDev(0.00000000000000029403601535432533), 0.0);
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::<DefaultRandomGenerator>::new(seeder.seed());
// Create a generator which uses two CSPRNGs to generate public masks and secret encryption
// noise
let mut encryption_generator =
EncryptionRandomGenerator::<DefaultRandomGenerator>::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_noise_distribution,
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_noise_distribution,
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.
// Generate the accumulator for our multiplication by 2 using a simple closure
let accumulator: GlweCiphertextOwned<u64> = generate_programmable_bootstrap_glwe_lut(
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_multiplication_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_multiplication_plaintext.0) / delta;
println!("Checking result...");
assert_eq!(6, pbs_multiplication_result);
println!(
"Multiplication via PBS result is correct! Expected 6, got {pbs_multiplication_result}"
);
}
This guide explains how to update your existing program to leverage HPU acceleration, or to start a new program using HPU.
TFHE-rs now supports a HPU backend based on FPGA implementation, enabling integer arithmetic operations on encrypted data.
An installed on a server running Linux with kernel 5.15.0-*
A HPU bitstream that you can find (or build) in and load in V80 flash and FPGA using its
AMI linux device driver version from this
QDMA linux device driver version from this
Rust version - check this
To use the TFHE-rs HPU backend in your project, add the following dependency in your Cargo.toml
.
For optimal performance when using TFHE-rs, run your code in release mode with the --release
flag.
TFHE-rs HPU backend is supported on Linux (x86, aarch64).
Comparing to the , HPU set up differs in the key creation and device registration, as detailed
Here is a full example (combining the client and server parts):
An HPU device is built for a given parameter set. At this point, because HPU is still a prototype, the software provided is retrieving this parameter set from an instantiated HpuDevice. Once retrieved, reading some HPU registers, this parameter set is used by the example applications to generate both client and compressed server keys. Server key has then to be decompressed by the server to be converted into the right format and uploaded to the device. Once decompressed, the operations between CPU and HPU are identical.
On the client-side, the method to encrypt the data is exactly the same than the CPU one, as shown in the following example:
The server first needs to set up its keys with set_server_key((hpu_device, compressed_server_key))
.
Then, homomorphic computations are performed using the same approach as the .
Finally, the client decrypts the result using:
The HPU backend includes the following operations for unsigned encrypted integers:
This document provides guidance on how to contribute to TFHE-rs.
There are two ways to contribute:
Report issues: Open issues on GitHub to report bugs, suggest improvements, or note typos.
Submit codes: To become an official contributor, you must sign our Contributor License Agreement (CLA). Our CLA-bot will guide you through this process when you open your first pull request.
Start by the TFHE-rs repository.
To get more details about the library, please refer to the .
When creating your branch, make sure to use the following format :
For example:
Each commit to TFHE-rs should conform to the standards of the project. In particular, every source code, docker or workflows files should be linted to prevent programmatic and stylistic errors.
Rust source code linters: clippy
Typescript/Javascript source code linters: eslint
, prettier
To apply automatic code formatting, run:
You can perform linting of all Cargo targets with:
Your contributions must include comprehensive documentation and tests without breaking existing tests. To run pre-commit checks, execute:
This command ensure that all the targets in the library are building correctly. For a faster check, use:
If you're contributing to GPU code, run also:
Unit testing suites are heavy and can require a lot of computing power and RAM availability. Whilst tests are run automatically in continuous integration pipeline, you can run tests locally.
All unit tests have a command formatted as:
Run make help
to display a list of all the commands available.
To quickly test your changes locally, follow these steps:
Locate where the code has changed.
Add (or modify) a Cargo test filter to the corresponding make
target in Makefile.
Run the target.
make test_<something>
will print the underlying cargo command in STDOUT. You can quickly test your changes by copy/pasting the command and then modify it to suit your needs.
For example, if you made changes in tfhe/src/integer/*
, you can test them with the following steps:
In test_integer
target, replace the filter -- integer::
by -- my_new_test
.
Run make test_integer
.
TFHE-rs follows the conventional commit specification to maintain a consistent commit history, essential for Semantic Versioning (). Commit messages are automatically checked in CI and will be rejected if they do not comply, so make sure that you follow the commit conventions detailed on .
Before creating a pull request, rebase your branch on the repository's main
branch. Merge commits are not permitted, thus rebasing ensures fewer conflicts and a smoother PR review process.
Once your changes are ready, open a pull request.
For instructions on creating a PR from a fork, refer to GitHub's .
Before a pull request can be merged, several test suites run automatically. Below is an overview of the CI process:
pipeline is triggered by humans
review team is located in Paris timezone, pipeline launch will most likely happen during office hours
direct changes to CI related files are not allowed for external contributors
run make pcc
to fix any build errors before pushing commits
Data serialized with TFHE-rs must remain backward compatible. This is done using the crate.
If you modify a type that derives Versionize
in a backward-incompatible way, an upgrade implementation must be provided.
For example, these changes are data breaking:
Adding a field to a struct.
Changing the order of the fields within a struct or the variants within an enum.
Renaming a field of a struct or a variant of an enum.
Changing the type of field in a struct or a variant in an enum.
On the contrary, these changes are not data breaking:
Renaming a type (unless it implements the Named
trait).
Adding a variant to the end of an enum.
Suppose you want to add an i32 field to a type named MyType
. The original type is defined as:
And you want to change it to:
Follow these steps:
Navigate to the definition of the dispatch enum of this type. This is the type inside the #[versionize(MyTypeVersions)]
macro attribute. In general, this type has the same name as the base type with a Versions
suffix. You should find something like
Add a new variant to the enum to preserve the previous version of the type. You can simply copy and paste the previous definition of the type and add a version suffix:
Implement the Upgrade
trait to define how we should go from the previous version to the current version:
Fix the upgrade target of the previous version. In this example, impl Upgrade<MyType> for MyTypeV0 {
should simply be changed to impl Upgrade<MyTypeV1> for MyTypeV0 {
tfhe = { version = "~1.2.0", features = ["integer", "hpu-v80"] }
Linux
Supported
Unsupported
macOS
Unsupported
Unsupported
Windows
Unsupported
Unsupported
use tfhe::{ConfigBuilder, set_server_key, FheUint8, ClientKey, CompressedServerKey};
use tfhe::prelude::*;
use tfhe::tfhe_hpu_backend::prelude::*;
fn main() {
// Instantiate HpuDevice --------------------------------------------------
// HPU configuration knobs are retrieved from a TOML configuration file. Prebuilt configurations could be find in `backends/tfhe-hpu-backend/config_store`
// For ease of use a setup_hpu.sh script is available in repository root folder and it handle the required environment variables setup and driver initialisation
// More details are available in `backends/tfhe-hpu-backend/README.md`
let hpu_device = HpuDevice::from_config(ShellString::new("${HPU_BACKEND_DIR}/config_store/${HPU_CONFIG}/hpu_config.toml".to_string()).expand().as_str());
// Generate keys ----------------------------------------------------------
let config = Config::from_hpu_device(&hpu_device);
let client_key = ClientKey::generate(config);
let compressed_server_key = CompressedServerKey::new(&client_key);
// Register HpuDevice and key as thread-local engine
set_server_key((hpu_device, compressed_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);
// Server-side computation
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);
}
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
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);
let decrypted_result: u8 = result.decrypt(&client_key);
name
symbol
Enc
/Enc
Enc
/ Int
Add
+
✔️
✔️
Sub
-
✔️
✔️
Mul
*
✔️
✔️
BitAnd
&
✔️
✔️
BitOr
|
✔️
✔️
BitXor
^
✔️
✔️
Greater than
gt
✔️
✔️
Greater or equal than
ge
✔️
✔️
Lower than
lt
✔️
✔️
Lower or equal than
le
✔️
✔️
Equal
eq
✔️
✔️
Ternary operator
select
✔️
✔️
git checkout -b {feat|fix|docs|chore…}/short_description
git checkout -b feat/new_feature_X
make fmt
make clippy_all_targets
make pcc
make fpcc
make pcc_gpu
make test_*
#[derive(Serialize, Deserialize, Versionize)]
#[versionize(MyTypeVersions)]
struct MyType {
val: u64,
}
#[derive(Serialize, Deserialize, Versionize)]
#[versionize(MyTypeVersions)]
struct MyType {
val: u64,
other_val: i32
}
#[derive(VersionsDispatch)]
enum MyTypeVersions {
V0(MyTypeV0),
V1(MyType)
}
#[derive(Version)]
struct MyTypeV1 {
val: u64,
}
#[derive(VersionsDispatch)]
enum MyTypeVersions {
V0(MyTypeV0),
V1(MyTypeV1),
V2(MyType) // Here this points to your modified type
}
impl Upgrade<MyType> for MyTypeV1 {
type Error = Infallible;
fn upgrade(self) -> Result<MyType, Self::Error> {
Ok(MyType {
val: self.val,
other_val: 0
})
}
}
This document details the string operations supported by TFHE-rs.
eq
FheAsciiString
FheAsciiString or ClearString
ne
FheAsciiString
FheAsciiString or ClearString
le
FheAsciiString
FheAsciiString or ClearString
ge
FheAsciiString
FheAsciiString or ClearString
lt
FheAsciiString
FheAsciiString or ClearString
gt
FheAsciiString
FheAsciiString or ClearString
len
FheAsciiString
is_empty
FheAsciiString
eq_ignore_case
FheAsciiString
FheAsciiString or ClearString
to_lowercase
FheAsciiString
to_uppercase
FheAsciiString
contains
FheAsciiString
FheAsciiString or ClearString
ends_with
FheAsciiString
FheAsciiString or ClearString
starts_with
FheAsciiString
FheAsciiString or ClearString
find
FheAsciiString
FheAsciiString or ClearString
rfind
FheAsciiString
FheAsciiString or ClearString
strip_prefix
FheAsciiString
FheAsciiString or ClearString
strip_suffix
FheAsciiString
FheAsci---iString or ClearString
concat
FheAsciiString
FheAsciiString
repeat
FheAsciiString
u16 or u32 or i32 or usize or (FheUint16, u16)
trim_end
FheAsciiString
trim_start
FheAsciiString
trim
FheAsciiString
replace
FheAsciiString
FheAsciiString
replacen
FheAsciiString
FheAsciiString or ClearString
u16 or u32 or i32 or usize or (FheUint16, u16)
The following example shows how to perform string operations:
use tfhe::prelude::*;
use tfhe::{
generate_keys, set_server_key, ConfigBuilder, FheAsciiString, FheStringLen,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ConfigBuilder::default().build();
let (client_key, server_key) = generate_keys(config);
set_server_key(server_key);
let string1 = FheAsciiString::try_encrypt("tfhe-RS", &client_key).unwrap();
let string2 = FheAsciiString::try_encrypt("TFHE-rs", &client_key).unwrap();
let is_eq = string1.eq_ignore_case(&string2);
assert!(is_eq.decrypt(&client_key));
Ok(())
}
This tutorial shows how to build a small function that homomorphically computes a parity bit in 2 steps:
Write a non-generic function
Use generics to handle the case where the function inputs are both FheBool
s and clear bool
s.
The parity bit function processes two parameters:
A slice of Boolean
A mode (Odd
or Even
)
This function returns a Boolean (true
or false
) so that the total count of true
values across the input and the result matches with the specified parity mode (Odd
or Even
).
First, define the verification function.
The function initializes the parity bit to false
, then applies the XOR
operation across all bits, adding negation based on the requested mode.
The validation function also adds the number of the bits set in the input to the computed parity bit and checks whether the sum is even or odd, depending on the mode.
After configurations, call the function:
To enable the compute_parity_bit
function to operate with both encrypted FheBool
and plain bool, we introduce generics. This approach allows for validation using clear data and facilitates debugging.
Writing generic functions that incorporate operator overloading for our Fully Homomorphic Encryption (FHE) types is more complex than usual because FHE types do not implement the Copy
trait. Consequently, it is necessary to use references (&) with these types, unlike native types, which typically implement Copy
.
This complicates generic bounds at first.
The function has the following signature:
To make it generic, the first steps is:
Next, define the generic bounds with the where
clause.
In the function, you can use the following operators:
!
(trait: Not
)
^
(trait: BitXor
)
Adding them to where
, it gives:
However, the compiler will return an error:
fhe_bit
is a reference to a BoolType
(&BoolType
), because BoolType
is borrowed from the fhe_bits
slice during iteration. To fix the error, the first approach could be changing the BitXor
bounds to what the Compiler suggests, by requiring &BoolType
to implement BitXor
rather than BoolType
.
However, this approach still leads to an error:
To fix this error, use Higher-Rank Trait Bounds
:
The final code is as follows:
Here is a complete example that uses this function for both clear and FHE values:
# Cargo.toml
tfhe = { version = "~1.2.0", features = ["integer"] }
#![allow(dead_code)]
use tfhe::FheBool;
#[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),
}
}
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::default().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));
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: {decrypted_parity_bit} for mode: {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: {decrypted_parity_bit} for mode: {mode:?}");
assert!(is_parity_bit_valid);
}
fn check_parity_bit_validity(
fhe_bits: &[FheBool],
mode: ParityMode,
) -> bool
fn compute_parity_bit<BoolType>(
fhe_bits: &[BoolType],
mode: ParityMode,
) -> BoolType
where
BoolType: Clone + Not<Output = BoolType>,
BoolType: BitXor<BoolType, Output=BoolType>,
---- 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
where
BoolType: Clone + Not<Output = BoolType>,
&BoolType: BitXor<BoolType, Output=BoolType>,
---- 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,
|
where
BoolType: Clone + Not<Output = BoolType>,
for<'a> &'a BoolType: BitXor<BoolType, Output = BoolType>,
#![allow(dead_code)]
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,
}
}
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::default().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));
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: {decrypted_parity_bit} for mode: {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: {decrypted_parity_bit} for mode: {mode:?}");
assert!(is_parity_bit_valid);
assert_eq!(decrypted_parity_bit, clear_parity_bit);
}
This document outlines the GPU operations supported in TFHE-rs.
The GPU backend includes the following operations for both signed and unsigned encrypted integers:
name
symbol
Enc
/Enc
Enc
/ Int
Neg
-
✔️
N/A
Add
+
✔️
✔️
Sub
-
✔️
✔️
Mul
*
✔️
✔️
Div
/
✔️
✔️
Rem
%
✔️
✔️
Not
!
✔️
N/A
BitAnd
&
✔️
✔️
BitOr
|
✔️
✔️
BitXor
^
✔️
✔️
Shr
>>
✔️
✔️
Shl
<<
✔️
✔️
Rotate right
rotate_right
✔️
✔️
Rotate left
rotate_left
✔️
✔️
Min
min
✔️
✔️
Max
max
✔️
✔️
Greater than
gt
✔️
✔️
Greater or equal than
ge
✔️
✔️
Lower than
lt
✔️
✔️
Lower or equal than
le
✔️
✔️
Equal
eq
✔️
✔️
Not Equal
ne
✔️
✔️
Cast (into dest type)
cast_into
✔️
N/A
Cast (from src type)
cast_from
✔️
N/A
Ternary operator
select
✔️
✖️
Integer logarithm
ilog2
✔️
N/A
Count trailing/leading zeros/ones
count_leading_zeros
✔️
N/A
Oblivious Pseudo Random Generation
oprf
✔️
N/A
The structure and operations related to integers are described in this section.
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 Remainder Theorem) representation
The first possibility to represent a large integer is to use a Radix-based decomposition on the plaintexts. Let be a basis such that the size of is smaller than (or equal to) 4 bits. Then, an integer can be written as , where each is strictly smaller than . Each 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. These parameters are chosen at key generation. Below, the keys are dedicated to integers encrypting messages over 8 bits, using a basis over 2 bits (i.e., ) and 4 blocks.
In this representation, the correctness of operations requires the carries to be propagated throughout the ciphertext. This operation is costly, since it relies on the computation of many programmable bootstrapping operations over shortints.
The second approach to represent large integers is based on the Chinese Remainder Theorem. In this case, the basis is composed of several integers , such that there are pairwise coprime, and each has a size smaller than 4 bits. The CRT-based integer are defined modulus . For an integer , its CRT decomposition is simply defined as . 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 . The integer is defined modulus . 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.
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. Here, a keychain to the computations is required, but this may result in a performance improvement.
The list of operations available in integer
depends on the type of representation:
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. Some of those will require a mutable reference as input: this is because the inputs' carry might be cleaned, but this will not change the underlying encrypted value.
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.
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.
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:
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.
You must avoid cloning the inputs when calling smart
operations to preserve performance. For instance, you SHOULD NOT have these kind of patterns in the code:
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_KS_PBS;
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_KS_PBS, num_block);
}
use tfhe::integer::CrtClientKey;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
fn main() {
let basis = vec![2, 3, 5];
let cks = CrtClientKey::new(PARAM_MESSAGE_2_CARRY_2_KS_PBS, basis);
}
Negation
✔️
✔️
Addition
✔️
✔️
Scalar Addition
✔️
✔️
Subtraction
✔️
✔️
Scalar Subtraction
✔️
✔️
Multiplication
✔️
✔️
Scalar Multiplication
✔️
✔️
Bitwise OR, AND, XOR
✔️
✔️
Equality
✔️
✔️
Left/Right Shift
✔️
✖️
Comparisons <
,<=
,>
, >=
✔️
✖️
Min, Max
✔️
✖️
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
fn main() {
let num_block = 4;
let (client_key, server_key) = gen_keys_radix(PARAM_MESSAGE_2_CARRY_2_KS_PBS, 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);
// 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 - msg2) + msg3) % modulus);
}
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
fn main() {
let num_block = 2;
let (client_key, server_key) = gen_keys_radix(PARAM_MESSAGE_2_CARRY_2_KS_PBS, 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);
// 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);
server_key.checked_small_scalar_mul_assign(&mut ct_1, scalar).unwrap();
server_key.checked_sub_assign(&mut ct_1, &ct_2).unwrap();
let result = server_key.checked_add_assign(&mut ct_1, &ct_3);
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) - msg2) % modulus);
}
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
fn main() {
let num_block = 4;
let (client_key, server_key) = gen_keys_radix(PARAM_MESSAGE_2_CARRY_2_KS_PBS, 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);
// 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 - msg2) + msg3) % modulus);
}
sks.smart_add(&mut a.clone(), &mut b.clone());
use tfhe::integer::gen_keys_radix;
use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS_PBS;
fn main() {
let num_block = 4;
let (client_key, server_key) = gen_keys_radix(PARAM_MESSAGE_2_CARRY_2_KS_PBS, 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);
// 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);
server_key.scalar_mul_assign_parallelized(&mut ct_1, scalar);
server_key.sub_assign_parallelized(&mut ct_1, &ct_2);
server_key.add_assign_parallelized(&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);
assert_eq!(output, ((msg1 * scalar - msg2) + msg3) % modulus);
}
The structure and operations related to short integers are described in this section.
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 part of the message which exceeds the message modulus 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 an incorrect output.
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.
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 two 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 (a 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 to make the operation possible. Some of those will require a mutable reference as input: this is to allow the modification of the carry, but this will not change the underlying encrypted value;
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.
Let's try to do a circuit evaluation using the different flavors of operations that we have 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_KS_PBS);
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, 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_KS_PBS);
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);
}
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_KS_PBS);
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);
}
The main advantage of the default flavor is to ensure predictable timings as long as this is the only kind of operation which 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_KS_PBS);
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.scalar_mul_assign(&mut ct_1, scalar);
server_key.sub_assign(&mut ct_1, &ct_2);
server_key.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);
assert_eq!(output, ((msg1 * scalar as u64 - msg2) * msg2) % modulus);
}
#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
(*)
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_KS_PBS);
let pks = PublicKey::new(&cks);
let msg = 2;
// Encryption of one message:
let ct = pks.encrypt(msg);
// Decryption:
let dec = cks.decrypt(&ct);
assert_eq!(dec, msg);
}
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_KS_PBS);
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);
}
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_KS_PBS);
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);
}
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_KS_PBS);
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);
}
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_KS_PBS);
let msg1 = 3;
// We use the private client key to encrypt a message:
let ct_1 = client_key.encrypt(msg1);
// Compute the lookup table for the univariate function:
let acc = server_key.generate_lookup_table(|n| n.count_ones().into());
// Apply the table lookup on the input message:
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);
}
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_KS_PBS);
let msg1 = 3;
let msg2 = 2;
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);
// Compute the lookup table for the bivariate functions
let acc = server_key.generate_lookup_table_bivariate(|x,y| (x.count_ones()
+ y.count_ones()) as u64 % modulus );
let ct_res = server_key.apply_lookup_table_bivariate(&ct_1, &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);
}
This tutorial guides you to convert a regular SHA-256 function to its homomorphic version, with considerations of optimal performances. You will learn:
The basics of the SHA-256 function.
The steps to implement SHA-256 homomorphically.
First, you need to implement the SHA-256 function. You can find the official specification for SHA-256 here. We summarize the three key aspects of SHA-256 outlined in the document:
The SHA-256 function processes the input data in blocks or chunks of 512 bits. Before performing the hash computations, prepare the data as follows:
Append a single "1" bit
Append "0" bits until exactly 64 bits remain to make the message length a multiple of 512
Append the last 64 bits as a binary encoding of the original input length
In this diagram, the numbers on the top represent the length of the padded input at each position. The formula L+1+k+64 ensures that the length reaches a multiple of 512, matching the required length of the padded input.
We will use bitwise AND, XOR, NOT, addition modulo 2^32, the Rotate Right (ROTR) and Shift Right (SHR) operations as building blocks for functions inside the SHA-256 computation. These operations all use 32-bit words and produce new words.
We combine these operations inside the sigma (with 4 variations), Ch,
and Maj
functions. When changing SHA-256 to the homomorphic computation, we will mainly change the code of each operation.
Here is the definition of each function:
Ch(x, y, z) = (x AND y) XOR ((NOT x) AND z)
Maj(x, y, z) = (x AND y) XOR (x AND z) XOR (y AND z)
Σ0(x) = ROTR-2(x) XOR ROTR-13(x) XOR ROTR-22(x)
Σ1(x) = ROTR-6(x) XOR ROTR-11(x) XOR ROTR-25(x)
σ0(x) = ROTR-7(x) XOR ROTR-18(x) XOR SHR-3(x)
σ1(x) = ROTR-17(x) XOR ROTR-19(x) XOR SHR-10(x)
We simplify Maj
using the Boolean distributive law: (x AND y) XOR (x AND z) = x AND (y XOR z), as shown below:
Maj(x, y, z) = (x AND (y XOR z)) XOR (y AND z)
We simplify Ch
using a single bitwise multiplexer. Here's the truth table of the Ch
expression.
0
0
0
0
0
0
1
1
0
1
0
0
0
1
1
1
1
0
0
0
1
0
1
0
1
1
0
1
1
1
1
1
This table shows that the result equals to z
when x = 0
, and the result equals to y
when x = 1
, which means if x {y} else {z}
. Hence we can replace the 4 bitwise operations of Ch
by a single bitwise multiplexer.
All these operations can be evaluated homomorphically:
ROTR and SHR: They can be evaluated by changing the index of each ecrypted bit of the word without using any homomorphic operation.
Bitwise AND, XOR and multiplexer: They can be computed homomorphically
Addition modulo 2^32: It can be broken down into boolean homomorphic operations.
The SHA-256 function processes data in 512-bit chunks. Here is what happens during computation:
The 512-bit chunk is computed into 16 words, each containing 32 bits.
Another 48 words are computed using the previous function.
After computing the 64 words, within the same chunk, a compression loop will compute a hash value (8 32-bit words) using the previous functions and some constants to mix everything up.
This entire process iterate through each 512-bit chunk of your data.
When we finish the last chunk iteration, the resulting hash values will be the output of the SHA-256 function.
Here is an example of this function using arrays of 32 bools to represent words:
fn sha256(padded_input: Vec<bool>) -> [bool; 256] {
// Initialize hash values with constant values
let mut hash: [[bool; 32]; 8] = [
hex_to_bools(0x6a09e667), hex_to_bools(0xbb67ae85),
hex_to_bools(0x3c6ef372), hex_to_bools(0xa54ff53a),
hex_to_bools(0x510e527f), hex_to_bools(0x9b05688c),
hex_to_bools(0x1f83d9ab), hex_to_bools(0x5be0cd19),
];
let chunks = padded_input.chunks(512);
for chunk in chunks {
let mut w = [[false; 32]; 64];
// Copy first 16 words from current chunk
for i in 0..16 {
w[i].copy_from_slice(&chunk[i * 32..(i + 1) * 32]);
}
// Compute the other 48 words
for i in 16..64 {
w[i] = add(add(add(sigma1(&w[i - 2]), w[i - 7]), sigma0(&w[i - 15])), w[i - 16]);
}
let mut a = hash[0];
let mut b = hash[1];
let mut c = hash[2];
let mut d = hash[3];
let mut e = hash[4];
let mut f = hash[5];
let mut g = hash[6];
let mut h = hash[7];
// Compression loop, each iteration uses a specific constant from K
for i in 0..64 {
let temp1 = add(add(add(add(h, ch(&e, &f, &g)), w[i]), hex_to_bools(K[i])), sigma_upper_case_1(&e));
let temp2 = add(sigma_upper_case_0(&a), maj(&a, &b, &c));
h = g;
g = f;
f = e;
e = add(d, temp1);
d = c;
c = b;
b = a;
a = add(temp1, temp2);
}
hash[0] = add(hash[0], a);
hash[1] = add(hash[1], b);
hash[2] = add(hash[2], c);
hash[3] = add(hash[3], d);
hash[4] = add(hash[4], e);
hash[5] = add(hash[5], f);
hash[6] = add(hash[6], g);
hash[7] = add(hash[7], h);
}
// Concatenate the final hash values to produce a 256-bit hash
let mut output = [false; 256];
for i in 0..8 {
output[i * 32..(i + 1) * 32].copy_from_slice(&hash[i]);
}
output
}
To convert SHA-256 to a homomorphic version, you can replace each bit of padded_input
with a fully homomorphic encryption of the same bit value and operate on the encrypted value using homomorphic operations.
While the structure of the SHA-256 function remains the same, there are some important considerations in the code:
The function signature and the borrowing rules should adapt to the ciphertext type (representing the encrypted bits).
Implementing SHA-256 operations with homomorphic encryption uses homomorphic boolean operations internally.
Homomorphic operations on encrypted data can be very expensive. Consider these options for better speed:
Remove unnecessary use of homomorphic operations and maximize parallelization.
Simplify the code with Rayon crate that parallelizes iterators and manages threads efficiently.
The final code is available here.
Now let's dive into details of each SHA256 operation.
Rotate Right and Shift Right can be evaluated by changing the position of each encrypted bit in the word, requiring no homomorphic operations. Here is the implementation:
fn rotate_right(x: &[Ciphertext; 32], n: usize) -> [Ciphertext; 32] {
let mut result = x.clone();
result.rotate_right(n);
result
}
fn shift_right(x: &[Ciphertext; 32], n: usize, sk: &ServerKey) -> [Ciphertext; 32] {
let mut result = x.clone();
result.rotate_right(n);
result[..n].fill_with(|| sk.trivial_encrypt(false));
result
}
To implement these operations, we will use the xor
, and mux
methods from the TFHE-rs library to perform each boolean operation homomorphically.
For better efficiency, we can parallelize the homomorphic computations because we operate bitwise. It means that we can homomorphically XOR the bits at index 0 of two words using one thread while XORing the bits at index 1 using another thread, and so on. This approach allows for the computation of bitwise operations using up to 32 concurrent threads, corresponding to the 32-bit words used.
Here is the implementation of the bitwise homomorphic XOR operation. The par_iter
and par_iter_mut
methods create a parallel iterator that we use to compute each XOR efficiently. The other two bitwise operations are implemented in the same way.
fn xor(a: &[Ciphertext; 32], b: &[Ciphertext; 32], sk: &ServerKey) -> [Ciphertext; 32] {
let mut result = a.clone();
result.par_iter_mut()
.zip(a.par_iter().zip(b.par_iter()))
.for_each(|(dst, (lhs, rhs))| *dst = sk.xor(lhs, rhs));
result
}
This might be the trickiest operation to efficiently implement in a homomorphic manner. A naive implementation could use the Ripple Carry Adder algorithm, which is straightforward but cannot be parallelized because each step depends on the previous one.
A better choice is to use Carry Lookahead Adder, which allows us to use the parallelized AND and XOR bitwise operations. With this design, our adder is around 50% faster than the Ripple Carry Adder.
pub fn add(a: &[Ciphertext; 32], b: &[Ciphertext; 32], sk: &ServerKey) -> [Ciphertext; 32] {
let propagate = xor(a, b, sk); // Parallelized bitwise XOR
let generate = and(a, b, sk); // Parallelized bitwise AND
let carry = compute_carry(&propagate, &generate, sk);
let sum = xor(&propagate, &carry, sk); // Parallelized bitwise XOR
sum
}
fn compute_carry(propagate: &[Ciphertext; 32], generate: &[Ciphertext; 32], sk: &ServerKey) -> [Ciphertext; 32] {
let mut carry = trivial_bools(&[false; 32], sk);
carry[31] = sk.trivial_encrypt(false);
for i in (0..31).rev() {
carry[i] = sk.or(&generate[i + 1], &sk.and(&propagate[i + 1], &carry[i + 1]));
}
carry
}
To further optimize performance, we use parallel prefix algorithms to parallelize the function that computes the carry signals. These algorithms involve more (homomorphic) boolean operations and their parallel nature speeds up the processing. We have implemented the Brent-Kung and Ladner-Fischer algorithms with different tradeoffs:
Brent-Kung has the least amount of boolean operations we could find (140 when using grey cells, for 32-bit numbers), which makes it suitable when we can't process many operations concurrently and fast. Our results confirm that it's indeed faster than both the sequential algorithm and Ladner-Fischer when run on regular computers.
On the other hand, Ladner-Fischer performs more boolean operations (209 using grey cells) than Brent-Kung, but they are performed in larger batches. Hence we can compute more operations in parallel and finish earlier, but we need more fast threads available or they will slow down the carry signals computation. Ladner-Fischer can be suitable when using cloud-based computing services, which offer many high-speed threads.
Our implementation uses Brent-Kung by default, but you can enable Ladner-Fischer by using the --ladner-fischer
command line argument.
For more information about parallel prefix adders, you can read this paper or this other paper.
Finally, with all these SHA-256 operations working homomorphically, our functions will be homomomorphic as well along with the whole SHA-256 function (after adapting the code to work with the Ciphertext type).
Let's talk about other performance improvements we can make before we finish.
In the main sha256_fhe
, you can perform some functions in parallel. For example, in the compression loop, temp1
and temp2
can be computed in parallel by using the rayon::join()
function when there is a CPU available. The two temporary values in the compression loop are the result of multiple additions, so you can use nested calls to rayon::join()
to parallelize more operations.
Another way to speed up consecutive additions would be using the Carry Save Adder, a very efficient adder that takes 3 numbers and returns a sum and a carry sequence. If our inputs are A, B, and C, we can construct a CSA with our previously implemented Maj function and the bitwise XOR operation as follows:
Carry = Maj(A, B, C)
Sum = A XOR B XOR C
By chaining CSAs, we can input the sum and carry from a preceding stage along with another number into a new CSA. Finally, to get the result of the additions we add the sum and carry sequences using a conventional adder. In the end, we are performing the same number of additions, but some of them are now CSAs, speeding up the process. Below is the illustration of this process in the temp1
and temp2
computations.
let (temp1, temp2) = rayon::join(
|| {
let ((sum, carry), s1) = rayon::join(
|| {
let ((sum, carry), ch) = rayon::join(
|| csa(&h, &w[i], &trivial_bools(&hex_to_bools(K[i]), sk), sk),
|| ch(&e, &f, &g, sk),
);
csa(&sum, &carry, &ch, sk)
},
|| sigma_upper_case_1(&e, sk)
);
let (sum, carry) = csa(&sum, &carry, &s1, sk);
add(&sum, &carry, sk)
},
|| {
add(&sigma_upper_case_0(&a, sk), &maj(&a, &b, &c, sk), sk)
},
);
The first closure of the outer call to join will return temp1
and the second temp2
.
Inside the first outer closure, we call join recursively until we add the value h
, the current word w[i],
and the current constant K[i]
by using the CSA, while potentially computing the ch
function in parallel. Then we take the sum, carry, and ch values and add them again using the CSA.
All this is done while potentially computing the sigma_upper_case_1
function. Finally we input the previous sum, carry, and sigma values to the CSA and perform the final addition with add
. Once again, this is done while potentially computing sigma_upper_case_0
and maj
and adding them to get temp2
, in the second outer closure.
With these types of changes, we finally get a homomorphic SHA256 function that doesn't leave unused computational resources.
First, use the --release
flag when running the program. Considering the implementation of encrypt_bools
and decrypt_bools
, the use of SHA-256 will be as follows:
fn main() {
let matches = Command::new("Homomorphic sha256")
.arg(Arg::new("ladner_fischer")
.long("ladner-fischer")
.help("Use the Ladner Fischer parallel prefix algorithm for additions")
.action(ArgAction::SetTrue))
.get_matches();
// If set using the command line flag "--ladner-fischer" this algorithm will be used in additions
let ladner_fischer: bool = matches.get_flag("ladner_fischer");
// INTRODUCE INPUT FROM STDIN
let mut input = String::new();
println!("Write input to hash:");
io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
input = input.trim_end_matches('\n').to_string();
println!("You entered: \"{}\"", input);
// CLIENT PADS DATA AND ENCRYPTS IT
let (ck, sk) = gen_keys();
let padded_input = pad_sha256_input(&input);
let encrypted_input = encrypt_bools(&padded_input, &ck);
// SERVER COMPUTES OVER THE ENCRYPTED PADDED DATA
println!("Computing the hash");
let encrypted_output = sha256_fhe(encrypted_input, ladner_fischer, &sk);
// CLIENT DECRYPTS THE OUTPUT
let output = decrypt_bools(&encrypted_output, &ck);
let outhex = bools_to_hex(output);
println!("{}", outhex);
}
We can supply the data to hash using a file instead of the command line by using stdin
. For example, if the file input.txt
is in the same directory as the project, we can use the following shell command after building with cargo build --release
:
./target/release/examples/sha256_bool < input.txt
The program accepts hexadecimal inputs. The input must start with "0x" and contain only valid hex digits, otherwise it will be interpreted as text.
Finally, padding is performed on the client side. This has the advantage of hiding the exact length of the input content from the server, thus avoiding the server extracting information from the length, even though the content is fully encrypted.
It is also feasible to perform padding on the server side. The padding function would take the encrypted input and pad it with trivial bit encryptions. We can then integrate the padding function into the sha256_fhe
function computed by the server.