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.
Setup
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");
}
Encrypting inputs
Once the server key is available on the server side, it is possible to perform some homomorphic computations. The client needs to encrypt some data and send it to the server. Again, the Ciphertext type implements the Serialize and the Deserialize traits, so that any serializer and communication tool suiting your use case can be employed:
use tfhe::boolean::prelude::*;
fn main() {
// Don't consider the following line; you should follow the procedure above.
let (client_key, _) = gen_keys();
//---------------------------- 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
// ...
}
Encrypting inputs using a public key
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)
// ...
}
Executing a Boolean circuit
Once the encrypted inputs are on the server side, the server_key can be used to homomorphically execute the desired Boolean circuit:
use std::fs::File;
use std::io::{Write, Read};
use tfhe::boolean::prelude::*;
fn main() {
// Don't consider the following lines; you should follow the procedure above.
let (client_key, server_key) = gen_keys();
let ct_1 = client_key.encrypt(true);
let ct_2 = client_key.encrypt(false);
let encoded_1: Vec<u8> = bincode::serialize(&ct_1).unwrap();
let encoded_2: Vec<u8> = bincode::serialize(&ct_2).unwrap();
//---------------------------- ON SERVER SIDE ----------------------------
// We deserialize the ciphertexts:
let ct_1: Ciphertext = bincode::deserialize(&encoded_1[..])
.expect("failed to deserialize");
let ct_2: Ciphertext = bincode::deserialize(&encoded_2[..])
.expect("failed to deserialize");
// We use the server key to execute the boolean circuit:
// if ((NOT ct_2) NAND (ct_1 AND ct_2)) then (NOT ct_2) else (ct_1 AND ct_2)
let ct_3 = server_key.not(&ct_2);
let ct_4 = server_key.and(&ct_1, &ct_2);
let ct_5 = server_key.nand(&ct_3, &ct_4);
let ct_6 = server_key.mux(&ct_5, &ct_3, &ct_4);
// Then we serialize the output of the circuit:
let encoded_output: Vec<u8> = bincode::serialize(&ct_6)
.expect("failed to serialize output");
// ...
// And we send the output to the client
// ...
}
Decrypting the output
Once the encrypted output is on the client side, the client_key can be used to decrypt it:
use std::fs::File;
use std::io::{Write, Read};
use tfhe::boolean::prelude::*;
fn main() {
// Don't consider the following lines; you should follow the procedure above.
let (client_key, server_key) = gen_keys();
let ct_6 = client_key.encrypt(true);
let encoded_output: Vec<u8> = bincode::serialize(&ct_6).unwrap();
//---------------------------- ON CLIENT SIDE
// We deserialize the output ciphertext:
let output: Ciphertext = bincode::deserialize(&encoded_output[..])
.expect("failed to deserialize");
// Finally, we decrypt the output:
let output = client_key.decrypt(&output);
// And check that the result is the expected one:
assert_eq!(output, true);
}
Operations
This contains the operations available in tfhe::boolean, along with code examples.
The NOT unary gate
use tfhe::boolean::prelude::*;
fn main() {
// We generate a set of client/server keys, using the default parameters:
let (client_key, server_key) = gen_keys();
// We use the client secret key to encrypt a message:
let ct_1 = client_key.encrypt(true);
// We use the server public key to execute the NOT gate:
let ct_not = server_key.not(&ct_1);
// We use the client key to decrypt the output of the circuit:
let output = client_key.decrypt(&ct_not);
assert_eq!(output, false);
}
Binary gates
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);
}
The MUX ternary gate
Let ct_1, ct_2, ct_3 be three Boolean ciphertexts. Then, the MUX gate (abbreviation of MUltipleXer) is equivalent to the operation:
if ct_1 {
return ct_2
} else {
return ct_3
}
This example shows how to use the MUX ternary gate:
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});
}
Cryptographic parameters
Default parameters
The TFHE cryptographic scheme relies on a variant of Regev cryptosystem and is based on a problem so difficult that it is even post-quantum resistant.
Some cryptographic parameters will require tuning to ensure both the correctness of the result and the security of the computation.
To make it simpler, we've provided two sets of parameters, which ensure correct computations for a certain probability with the standard security of 128 bits. There exists an error probability due to the probabilistic nature of the encryption, which requires adding randomness (noise) following a Gaussian distribution. If this noise is too large, the decryption will not give a correct result. There is a trade-off between efficiency and correctness: generally, using a less efficient parameter set (in terms of computation time) leads to a smaller risk of having an error during homomorphic evaluation.
In the two proposed sets of parameters, the only difference lies in this error probability. The default parameter set ensures an error probability of at most 2−40 when computing a programmable bootstrapping (i.e., any gates but the not). The other one is closer to the error probability claimed in the original TFHE paper, namely 2−165, but it is up-to-date regarding security requirements.
The following array summarizes this:
Parameter set
Error probability
DEFAULT_PARAMETERS
2−40
TFHE_LIB_PARAMETERS
2−165
User-defined parameters
You can also create your own set of parameters. This is an unsafe operation as failing to properly fix the parameters will result in an incorrect and/or insecure computation:
use tfhe::boolean::prelude::*;
fn main() {
// WARNING: might be insecure and/or incorrect
// You can create your own set of parameters
let parameters = unsafe {
BooleanParameters::new(
LweDimension(586),
GlweDimension(2),
PolynomialSize(512),
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,
)
};
}
Serialization/Deserialization
Since the ServerKey and ClientKey types both implement the Serialize and Deserialize traits, you are free to use any serializer that suits you to save and load the keys to disk.
Here is an example using the bincode serialization library, which serializes to a binary format:
use 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_eq!(false, loaded_client_key.decrypt(&ct_1));
}