cuhepy
GPU-accelerated homomorphic encryption in Python.
Paillier and BFV, with encrypted nearest-neighbour search as the worked example.
[!WARNING]
**Experimental research code. Not audited, not constant-time, and not suitable
for protecting real data.** Both schemes are implemented from scratch for
study and measurement. We publish the attacks we found against our own code in
attacks/anddocs/research/— read those
before trusting anything here.
What this is
Two homomorphic encryption schemes, each with a CPU reference implementation in Python and optional compiled backends:
| | Scheme | Backends | Homomorphic over |
|---|---|---|---|
| cuhepy.paillier | Paillier, plus an α-subgroup variant with precomputed tables | pure Python/GMP · CUDA | addition |
| cuhepy.bfv | Leveled BFV over Z_q[X]/(X^N+1), SIMD batching | pure Python/GMP · cached GMP · C++ RNS/NTT | addition, multiplication, rotation |
cuhepy.paillier and cuhepy.bfv are the primitives — key generation,
encryption, decryption and the homomorphic operations, over arbitrary integers.
cuhepy.hamming is an application built on top: **encrypted k-nearest-neighbour
search**, where an evaluator holding only the public key ranks encrypted vectors
by Hamming distance without learning the vectors, the query, or the distances.
It is the workload the compiled backends were originally written for, and it is
kept separate from the schemes so neither depends on the other.
Install
pip install cuhepy
Requires Python 3.11+. The CPU path has four dependencies (gmpy2, msgpack,
numpy, pycryptodome) and needs no GPU. Compiled backends are opt-in — see
Compiled backends.
Quick start
Encrypt two vectors, let an untrusted evaluator compute their Hamming distance on the ciphertexts, and decrypt the result:
from cuhepy.hamming.paillier_lookup import PaillierLookupClient
client = PaillierLookupClient(embed_len=512, key_len=1024)
a = [i % 2 for i in range(512)]
b = [(i // 2) % 2 for i in range(512)]
Client side: encrypt. Only these ciphertexts leave.
ct_a = client.encrypt_vec_one(a)
ct_b = client.encrypt_vec_one(b)
Evaluator side: public key only, one modular multiply per chunk.
ct_distance = client.encode_hamming_server(ct_a, ct_b)
Client side: decrypt.
assert client.decode_hamming_client_one(ct_distance) == sum(x ^ y for x, y in zip(a, b))
Persist the keypair with cuhepy.keys (writes the secret key in plaintext at
mode 0600 — key management is deliberately out of scope):
from cuhepy import keys
keys.save(client, "key.json", include_tables=True)
restored = keys.load("key.json")
The schemes are also usable directly, without the Hamming layer:
from cuhepy.paillier.scheme import Paillier
kp = Paillier.key_gen(1024)
c = Paillier.add(Paillier.encrypt(7, kp["pk"]), Paillier.encrypt(35, kp["pk"]), kp["pk"])
assert Paillier.decrypt(c, kp) == 42
Cost
Per 512-dimensional binary vector, single CPU thread, key_len=1024. The two
schemes put the cost in different places, which is the whole reason both exist:
| | encrypt | evaluate | decrypt | response bytes | |---|---|---|---|---| | Paillier | 6.9 ms | 3 µs | 4.6 ms | 512 B / vector | | Paillier-Lookup (α=280) | 0.5 ms | 3 µs | 0.66 ms | 512 B / vector | | BFV, packed | — | ~2.4 ms | ~constant per query | ~100–200 KB total |
Paillier is cheap for the evaluator and expensive for the client, whose work
grows linearly with the corpus. BFV inverts that: the evaluator does real work,
but packs many distances into one response ciphertext, so the client decrypts
once and the response stops growing. Raw numbers and the harness are in
benchmarks/ and docs/research/.
Threat model
The evaluator receives the public key, the encrypted index and encrypted queries. It never receives the secret key.
Hidden: vector contents, query contents, the computed distances. Visible: how many vectors exist, which one was queried, when, and any plaintext metadata the caller chooses to attach.
These schemes are secure against an honest-but-curious evaluator — one that follows the protocol and only observes. Against a malicious evaluator that crafts ciphertexts and watches how the client reacts, both are broken, and we demonstrate it:
- PL-01 — the α-subgroup variant's secret key is recoverable from the public
- PL-04 — a malicious evaluator recovers stored bits by watching which
- BFV-01 — a decryption oracle recovers the BFV secret key from accept/reject
cuhepy.hamming.bfv_guarded, bfv_verified and bfv_attested are experimental
protocol layers that address BFV-01 at a trust and compute cost.
Full write-ups: docs/research/paillier-security.md
and docs/research/native-bfv-security.md.
Compiled backends
Both are optional; the pure-Python path works without them.
CUDA (Paillier) — builds inside a pinned nvidia/cuda container, so the
build host needs only Docker, not a GPU or a local CUDA toolkit:
./build_gpu_binaries.sh # -> src/cuhepy/paillier/_{,lookup_}gpu_ext/*.so
Runtime needs an NVIDIA driver ≥ 550 and a GPU of compute capability 7.0–9.0.
Clients probe for the extension at construction; device="cpu" or device="gpu"
forces a backend.
C++ RNS/NTT (BFV) — make -C src/cuhepy/bfv/_cpu_ext. Select it with
BFVClient(server_backend="rns" | "native" | "residue").
Verify it yourself
Everything runs offline, no account or service required:
uv sync --all-groups
uv run pytest tests/ # full offline suite
uv run python attacks/pl01_alpha_recovery.py # recover a key from a public key
Repository layout
src/cuhepy/ ← primitives
paillier/ scheme.py · lookup.py · _gpu_ext/ · _lookup_gpu_ext/
bfv/ scheme.py · evaluator.py · rns.py · native.py · private.py · _cpu_ext/
hamming/ ← application
base.py · paillier.py · paillier_lookup.py · bfv.py
bfv_security.py · bfv_assurance.py
bfv_guarded.py · bfv_verified.py · bfv_attested.py · bfv_nitro.py
base.py device.py keys.py types.py bench.py
attacks/ runnable demonstrations of the findings above
benchmarks/ measurement harness and recorded results
docs/research/ security reviews and optimisation reports
experiments/ SEAL cross-check, Nitro enclave evaluator
Contributing
See CONTRIBUTING.md. Findings against the schemes are especially welcome — that is what this repository is for.
License
Apache 2.0 — see LICENSE.