NuFHE, a GPU-powered Torus FHE implementation¶
Introduction¶
nufhe
implements the fully homomorphic encryption algorithm from TFHE using CUDA and OpenCL. For the theoretical background one may refer to the works TFHE is based on:
- Gentry, A. Sahai, and B. Waters, “Homomorphic encryption from learning with errors: Conceptually-simpler, asymptotically-faster, attribute-based.”, Crypto 75-92 (2013);
- Ducas and D. Micciancio, “FHEW: Bootstrapping homomorphic encryption in less than a second.”, Eurocrypt 617-640 (2015);
- Chillotti, N. Gama, M. Georgieva, and M. Izabachène. “Faster fully homomorphic encryption: Bootstrapping in less than 0.1 seconds”, Asiacrypt 3–33 (2016).
For more details check out this collection of papers on lattice cryptography.
Some additional performance improvements employed by nufhe
are described in Implementation details.
Installation¶
nufhe
supports two GPU backends, CUDA (via PyCUDA) and OpenCL (via PyOpenCL). Neither of the backend packages can be installed by default, because, depending on the videocard, one of the platforms may be unavailable. Therefore, the user must pick one or more backends they want to use and request them explicitly during installation. A simple rule of thumb is to pick CUDA if you have an nVidia videocard, and OpenCL otherwise (although OpenCL will work with nVidia cards as well). Then nufhe
can be installed using PyPi specifying the required extras.
For the CUDA backend:
$ pip install nufhe[pycuda]
For the OpenCL backend:
$ pip install nufhe[pyopencl]
For both CUDA and OpenCL backends:
$ pip install nufhe[pycuda,pyopencl]
A short example¶
import random
import nufhe
ctx = nufhe.Context()
secret_key, cloud_key = ctx.make_key_pair()
size = 32
bits1 = [random.choice([False, True]) for i in range(size)]
bits2 = [random.choice([False, True]) for i in range(size)]
ciphertext1 = ctx.encrypt(secret_key, bits1)
ciphertext2 = ctx.encrypt(secret_key, bits2)
reference = [not (b1 and b2) for b1, b2 in zip(bits1, bits2)]
vm = ctx.make_virtual_machine(cloud_key)
result = vm.gate_nand(ciphertext1, ciphertext2)
result_bits = ctx.decrypt(secret_key, result)
assert all(result_bits == reference)
Context¶
ctx = nufhe.Context()
A context object represents an execution environment on a GPU (akin to a process), and is tied to a specific GPU device (if there are several available).
The target device can be either selected interactively, or picked automaticall based on various filters; see the Context
constructor for details.
Similar to a process, each context has its own memory space, and objects (keys and ciphertexts) from one context cannot be used in another one directly.
One can transfer them between contexts via serialization/deserialization, see NuFHESecretKey.dump()
, NuFHECloudKey.dump()
and LweSampleArray.dump()
for details.
Key pair¶
The next step is the creation of a secret and a cloud key. The former is used to encrypt plaintexts or decrypt cyphertexts; the latter is required to apply gates to encrypted data. Note that the cloud key can be rather large (of the order of 100Mb).
secret_key, cloud_key = ctx.make_key_pair()
make_key_pair()
takes some keyword parameters that affect the security of the algorithm; the default values correspond to about 110 bits of security.
Encryption¶
Using the secret key we can encrypt some data with encrypt()
. nufhe
gates operate on bit arrays (either lists or numpy
arrays):
size = 32
bits1 = [random.choice([False, True]) for i in range(size)]
bits2 = [random.choice([False, True]) for i in range(size)]
ciphertext1 = ctx.encrypt(secret_key, bits1)
ciphertext2 = ctx.encrypt(secret_key, bits2)
In this example we will test the NAND gate, so the reference result would be
reference = [not (b1 and b2) for b1, b2 in zip(bits1, bits2)]
Processing¶
Calculations are performed on a fully encrypted virtual machine created out of a cloud key:
vm = ctx.make_virtual_machine(cloud_key)
result = vm.gate_nand(ciphertext1, ciphertext2)
The output of a gate can be pre-initialized with empty_ciphertext()
and passed to any gate function as a dest
keyword parameter.
Decryption¶
After the processing, the person in possession of the secret key can decrypt the result with decrypt()
and verify that the gate was applied correctly:
result_bits = ctx.decrypt(secret_key, result)
assert all(result_bits == reference)
GPU threads for the low-level API¶
nufhe
uses Reikna as a backend for GPU operations, and all the low-level nufhe
calls require a reikna
Thread object, encapsulating a GPU context and a serialization queue for GPU kernel calls. It can be created interactively:
from reikna.cluda import cuda_api
thr = cuda_api().Thread.create(interactive=True)
where the user will be offered to choose between available platforms and videocards. Alternatively, one can pick an arbitrary available platform/device:
thr = cuda_api().Thread.create()
It is also possible to create a Thread
using a known device, or an existing PyCUDA or PyOpenCL context. This is advanced usage, for those who plan to integrate nufhe
into a larger GPU-based program. See the documentation for Thread and Thread.create() for details.
If one wants to use OpenCL instead of CUDA, cuda_api
should be replaced with ocl_api
. Alternatively, one can use any_api
to select an arbitrary available backend.