Quick start

Functional encryption lets a key holder learn a chosen function of encrypted data – here an inner product – and nothing else. Every scheme in PyFE4AI has two objects: a key generator, which sets up the public parameters and issues keys, and a crypto class, whose objects encrypt (with the public parameters and a client’s key) or decrypt (with a functional key).

A first example

import pyfe4ai

x, y = [2, 1, 3], [4, 5, 6]

# sec_param = bit length of the DDH modulus (NOT a security level);
# 128 keeps the demo fast but is insecure -- for real use replace it with
# "security_profile": "standard" (a 3072-bit group)
kg, SIFEDDH = pyfe4ai.create("sife/ddh", {"sec_param": 128, "eta": len(x)})
kg.setup()
pp, sk = kg.get_public_parameters(), kg.get_private_keys()
dk = kg.get_decryption_keys("sid_0", credentials={"fusion_weight": y})

ct = SIFEDDH({"precision": 3, "keys": {"pp": pp, "sk": sk}}).encrypt(x)
result = SIFEDDH({"precision": 3, "keys": {"pp": pp}}).decrypt(ct, dk, y)
print(result)  # <x, y> = 31

pyfe4ai.create takes a scheme id "family/variant" and a config, and returns the scheme’s key generator – built, not yet set up – with its crypto class. Options can also be passed as keywords: pyfe4ai.create("sife/ddh", eta=3). The classes are importable by name too (from pyfe4ai.schemes.sife import SIFEDDH, SIFEDDHKeyGenerator), named after the id.

The key generator’s methods are the same in every family:

  • setup() draws the parameters and the master key;

  • get_public_parameters() returns pp, for everyone;

  • get_private_keys(nid) returns a client’s encryption key (sk);

  • get_decryption_keys(sid, credentials={"fusion_weight": y, ...}) returns a functional key for the weights y (in the threshold schemes, one server’s key share).

The crypto objects take {"keys": {"pp": pp, "sk": sk}} to encrypt and {"keys": {"pp": pp}} to decrypt; their encrypt and decrypt arguments depend on the family (a vector, a vector and a label, …). The module of each scheme documents them (API Reference), and the Glossary lists the configuration keys.

Several clients

In multi-client FE (MCFE) every client encrypts its own vector under a shared label, such as a training round; the functional key reveals only the weighted sum over the clients, and only for ciphertexts of one label:

import pyfe4ai

x = {"alice": [1, 2], "bob": [3, -1], "carol": [0, 4]}
y = {"alice": [2, 1], "bob": [1, 1], "carol": [-1, 3]}

kg, MCFEDDH = pyfe4ai.create("mcfe/ddh", {"eta": 2, "n": 3, "lst_nid": list(x)})
kg.setup()
pp = kg.get_public_parameters()

label = "round-1"
cts = {nid: MCFEDDH({"id": nid, "keys": {"pp": pp, "sk": kg.get_private_keys(nid)}}).encrypt(x[nid], label)
       for nid in x}

dk = kg.get_decryption_keys("aggregator", credentials={"fusion_weight": y})
print(MCFEDDH({"id": "aggregator", "keys": {"pp": pp}}).decrypt(cts, dk, y, label))  # 18

A client must encrypt at most one vector per label; the MCFE schemes assume it, and the ML adapter below enforces it.

Federated learning

pyfe4ai.ml wraps a scheme for gradient aggregation:

import numpy as np
from pyfe4ai.ml import FESchemeWrapper, aggregate_gradients, encrypt_gradient

clients = ["c0", "c1", "c2"]
wrapper = FESchemeWrapper("mcfe", "ring_lwe",
                          {"eta": 1, "n": 3, "lst_nid": clients, "bound_x": 10**4, "bound_y": 10},
                          precision=3)
grads = {nid: np.random.default_rng(i).uniform(-1, 1, 1000) for i, nid in enumerate(clients)}
enc = {nid: encrypt_gradient(g, wrapper, nid=nid, label="round-1") for nid, g in grads.items()}
total = aggregate_gradients(enc, wrapper, {nid: 1 for nid in clients}, label="round-1")
print(np.allclose(total, sum(grads.values()), atol=1e-2))  # True

The wrapper runs the key generator, so it holds the master key and plays every party: it is meant for experiments. Threshold decryption and the ML adapter covers the adapter’s policy checks, the threshold MCFE whose decryption servers it runs, and the separate messages of a deployment.

Real parameters

The default security_profile="toy" keeps parameters small: fast, and not secure. "standard" aims at 128-bit security and refuses explicit values below it:

import pyfe4ai

kg, SIFEDDH = pyfe4ai.create("sife/ddh", {"eta": 3, "security_profile": "standard"})  # 3072-bit group

The pairing-based schemes stay at about 120 to 126 bits (BLS12-381, with a FESecurityWarning), and four schemes have no parameter set that is both secure and usable, so standard refuses them: see Security & Status and Configuration options.

Finding a scheme

import pyfe4ai

pyfe4ai.list_schemes()                                  # all 42 ids
pyfe4ai.list_schemes("mcfe", capabilities="threshold")  # the threshold MCFEs
scheme = pyfe4ai.get_scheme("mcfe/ring_lwe")
print(scheme.kind, sorted(scheme.capabilities), scheme.status.status)

Choosing a scheme explains the families, their capabilities and review status, and which scheme fits which task.