The simplest way of encrypting plaintexts into ciphertexts in Concrete is via **LWE** encryption.

LWE stands for Learning With Errors and refers to a computational problem conjectured to be hard to solve. With LWE, every computation is performed in a modular integer ring such as $\mathbb{Z}/q\mathbb{Z}$ where $q$ is the modulus (typically 64).

For simplicity, we use "LWE" and "LWE ciphertext" interchangeably.

An LWE is composed of a vector of integers called a **mask** and an integer called a **body.** The size of the mask vector is called the **dimension.** To achieve a strong level of security, we need to add random gaussian noise to the body. The combination of the mask's dimension and standard deviation of the noise distribution is what we call the **LWE security parameters. **

Choosing wrong security parameters can lead to weak security, slow computations or insufficient precision to carry the computations.

Parameters are stored in a `LWEParams`

struct that takes the dimension and standard deviation as parameters. Concrete also comes with predefined sets of parameters for 80 or 128 bits of security. These parameters were secure as of **September 15th 2020**, and estimated using the LWE estimator.

// manually chosen parameters for 80 and 128 bits of securitylet lwe80 = LWEParams::new(256, -19);let lwe128 = LWEParams::new(630, -14);â€‹// Predefined consts with parameters for 80 bits of security:LWE80_256; // dimension = 256, noise = 2^-9LWE80_512; // dimension = 512, noise = 2^-19LWE80_630; // dimension = 630, noise = 2^-24LWE80_650; // dimension = 650, noise = 2^-25LWE80_688; // dimension = 690, noise = 2^-26LWE80_1024; // dimension = 1024, noise = 2^-30â€‹// Predefined consts with parameters for 128 bits of security:LWE128_256; // dimension = 256, noise = 2^-5LWE128_512; // dimension = 512, noise = 2^-11LWE128_630; // dimension = 630, noise = 2^-14LWE128_650; // dimension = 650, noise = 2^-15LWE128_688; // dimension = 690, noise = 2^-16LWE128_1024; // dimension = 1024, noise = 2^-25

We can see above that for a given security level, **the larger the dimension is, the smaller the noise standard deviation has to be**. A larger dimension will lead to more computation and larger ciphertexts, while a larger standard deviation will lead to more noisy ciphertexts and less precise messages. Hence, there is a tradeoff between performance and precision, which is inherent to any FHE program.

A good rule of thumb is to pick secure parameters with the largest possible noise that supports your program's desired precision. By not provisioning unnecessary precision, you can use a smaller mask dimension and thus get better runtime performance.

Once appropriate security parameters have been chosen, we can generate a secret key that will be used to encrypt and decrypt ciphertexts. Concrete currently only implements symmetric uniformly random binary secret keys. Future versions will offer support for public-key cryptography and other key generation methods.

Creating a secret key in Concrete is as easy as using the `new`

function from the `LWESecretKey`

struct, and passing it the chosen security parameters:

// pick a set of LWE parameterslet lwe_params = LWE128_630;â€‹// generate a fresh secret keylet secret_key = LWESecretKey::new(&lwe_params);

Secret keys can be saved into json files with the `save`

method, and recovered using the `load`

method:

// save secret keysecret_key.save("my_very_secret_key.json").unwrap();â€‹â€‹// load secret keylet recovered_secret_key = LWESecretKey::load("my_very_secret_key.json").unwrap();

Encrypting messages can be done by either:

using the

`LWE`

struct's factory method`encrypt`

, which takes a plaintext and a secret key as a parameter.using the

`LWE`

struct's factory method`encode_encrypt`

, which takes a message, an encoder and a secret key. This method will encode the messages before encrypting them.

The following code shows how to create an LWE by encoding and encrypting in one step:

// generate a secret keylet secret_key = LWESecretKey::new(&LWE128_630);â€‹// encoderlet encoder = Encoder::new(-10., 10., 8, 0)?;â€‹// message to encryptlet message: f64 = -6.276;â€‹// encode and encrypt the message in one steplet c1 = LWE::encode_encrypt(&secret_key, message, &encoder)?;

In FHE, it is often necessary to manipulate a vector of encrypted values, rather than a single value. Concrete has a convenience struct to simplify working with vectors of LWEs called `VectorLWE`

, which has the same methods as the LWE struct, but taking a vector of message as input:

// a vector of messageslet messages: Vec<f64> = vec![-6.276, 4.3, 0.12, -1.1, 7.78];â€‹// encode and encrypt a vector of messages into a single VectorLWElet c2 = VectorLWE::encode_encrypt(&secret_key, &messages, &encoder)?;

Decryption turns a ciphertext into a plaintext and decodes it to yield the final message. The same secret key that was used for encryption must be used for decryption. The `decrypt_decode`

method exists both for LWE and VectorLWE ciphertexts:

// decrypt an LWE into a single valuelet o1: f64 = c1.decrypt_decode(&secret_key)?;â€‹// decrypt a VectorLWE into a vector of valueslet o2: Vec<f64> = c2.decrypt_decode(&secret_key)?;

The `decrypt_decode`

method performs two separate operations:

the

**decryption**, which decrypts the ciphertext using the secret keythe

**decoding**, which removes the noise and decodes the result back to the domain of the original message.

Here is a complete example using a vector of messages:

/// file: main.rsuse concrete::*;â€‹fn main() -> Result<(), CryptoAPIError> {// generate a secret key and save itlet secret_key = LWESecretKey::new(&LWE128_630);secret_key.save("my_very_secret_key.json");â€‹// create an encoderlet encoder = Encoder::new(-10., 10., 8, 0)?;â€‹// a list of messageslet messages: Vec<f64> = vec![-6.276, 4.3, 0.12, -1.1, 7.78];â€‹// encode and encrypt message vectorlet ciphertext = VectorLWE::encode_encrypt(&secret_key, &messages, &encoder)?;â€‹// decryptlet outputs: Vec<f64> = ciphertext.decrypt_decode(&secret_key)?;â€‹println!("{:?}", outputs);Ok(())}