Encrypted Nearest Neighbor Search with Homomorphic Encryption

Nearest neighbor search is everywhere. Face recognition compares a captured face against a gallery of known people. Recommendation systems find the items closest to a user’s taste. Semantic search finds the documents closest to a question. All of them turn their inputs into vectors, called embeddings, and ask the same question: which stored vectors are closest to this one? The trouble is that those vectors are deeply personal. A face embedding is biometric data, a preference vector is a profile of your habits, and a question embedding can be translated back into something close to the original text. In this post, we’ll find the nearest neighbors without revealing the query, using partially homomorphic encryption with LightPHE in Python, Go, Java and TypeScript.

A stunning aerial view of a peaceful suburban neighborhood at sunset with lined houses and streets by pexels

Our running example is face recognition, where embeddings typically come from a model such as DeepFace. Real face embeddings have 128 to 512 dimensions. We’ll use 4-dimensional ones to keep the examples readable, but the code works the same way for any dimension.


🙋‍♂️ You may consider to enroll my top-rated cryptography course on Udemy

Cryptography Basiscs From Scratch In Python

Two Settings, Two Different Problems

Encrypted nearest neighbor search comes in two flavours, depending on who owns the gallery of stored vectors, and they need very different cryptography:

  • A server owns the gallery in plain, and a client has a private query. A building’s server holds the face embeddings of its employees, and a visitor’s phone wants to know whether the visitor is one of them, without showing its face to the server. The server must not learn the query or the result, and the client must not learn anyone else in the gallery—only who the nearest people are.
  • An owner stores its encrypted gallery in an untrusted cloud. A company keeps the face embeddings of its employees in a public cloud, and searches them with encrypted queries. The cloud stores and computes, but must learn nothing: neither the gallery, nor the queries, nor the results.

Both measure closeness with the squared euclidean distance:

d(q, x) = (q_1 - x_1)^2 + (q_2 - x_2)^2 + ... + (q_n - x_n)^2

The square root is skipped on purpose: it is monotonic, so it doesn’t change which vector is the nearest, and it would be a nightmare under encryption.

Setting 1: Plain Gallery, Encrypted Query

Required Homomorphic Features

This setting follows the protocol of Erkin et al. in Privacy-Preserving Face Recognition (PETS 2009), and it needs five things.

1. Additive homomorphism and scalar multiplication to compute distances. The query is encrypted, but each gallery vector is the server’s own plain data. Expanding the square makes the distance linear in the encrypted query:

d(q, x) = ||q||^2 - 2 (q . x) + ||x||^2

E(d) = E(||q||^2) * E(q_1)^(-2 x_1) * ... * E(q_n)^(-2 x_n) * E(||x||^2)

The only term the server cannot compute is ||q||^2, since squaring an encrypted value needs a multiplication of two ciphertexts. So the client simply computes it in plain and sends it encrypted along with the query. Everything else is additions and multiplications with the server’s plain numbers.

2. Integers. Embeddings are floats, so they are quantized: scaled, e.g. by 100, and rounded. Both the query and the gallery must be quantized the same way. Squared distances must be less than 2^bit_length, which you pass to the search.

3. Encrypted argmin. The server now holds an encrypted distance for every gallery vector, and it cannot decrypt them. Sending them to the client would leak far too much: the distances to every person in the gallery. Instead, the server turns each distance into 2^bit_length - 1 - d, so the nearest vector gets the greatest value, and runs the encrypted argmax of Damgard-Geisler-Kroigaard, which compares encrypted values with a few masked round trips to the client. The result is the encrypted index of the nearest vector, and neither side learns anything else. We covered encrypted argmax in detail in the post on encrypted logistic regression and linear SVM.

4. An encrypted equality test, to find the next nearest vector. For k > 1, the server must exclude the vector it just found, but it only has its index in encrypted form. So it turns the encrypted index into an encrypted one-hot vector with the client’s help: for every position i, it computes E(r_i (index - i)) with a fresh random r_i, shuffles them, and sends them to the client. Exactly one of them decrypts to zero, at a random position, and every other one is a random number. The client returns an encrypted 1 for the zero and an encrypted 0 for the others, without learning which vector it was. The server then lifts every other value by 2^bit_length homomorphically, so the next argmax finds the next nearest vector. The same one-hot vector also selects the label of the found vector, e.g. an employee ID, in encrypted form.

5. A small plaintext space with a cheap zero test. Both the argmax comparisons and the equality test rely on checking many blinded values for zero, which Damgard-Geisler-Kroigaard does with a single exponentiation, thanks to its plaintext space of about 32 bits.

Example: Unlocking a Door with Your Face

A visitor walks up to an office building. Their phone computes a face embedding from a selfie and encrypts it. The building’s server holds the embeddings of its employees, labeled with employee IDs, and finds the 2 nearest employees. The phone learns only the IDs of the 2 nearest employees, and the server learns nothing.

After quantization, the squared distances from the visitor to employees 101, 102 and 103 are 18, 4585 and 7, so the phone gets 103 first and 101 second. It never sees these distances, and it never learns that employee 102 exists. In this run, the server sent the phone 14 messages: 12 for the two argmax rounds, 3 round trips per comparison of 3 candidates, and 2 equality rounds, one for each found label. In each equality round, the phone saw exactly one zero among 3 values, in a shuffled position.

The cost grows with both the gallery size and k, since every round compares all vectors. In Python with 1024-bit keys, it takes about 0.6 seconds per gallery vector and per nearest vector to find; Go is about four times faster. That’s fine for a building with a few hundred employees, and it’s the price of hiding everything from both sides. Java snippets use assert for brevity, so run them with -ea to enable assertions.





Setting 2: Encrypted Gallery in an Untrusted Cloud

Required Homomorphic Features

Now the gallery is encrypted as well, so the trick of the first setting stops working: q . x is a product of two ciphertexts. Squared distance is a degree-2 function, so we need exactly one multiplication of ciphertexts, followed by additions:

E(d) = E(q_1 - x_1) * E(q_1 - x_1) + ... + E(q_n - x_n) * E(q_n - x_n)

Additively homomorphic cryptosystems cannot multiply two ciphertexts on their own. The Catalano-Fiore transform boosts them with exactly one multiplication, without changing the underlying cryptosystem. A multiplied ciphertext can still be added to others and multiplied by constants, but not multiplied again—which is all a squared distance needs.

There is no argmin under encryption here, and no interaction: the cloud computes every encrypted distance with the public key alone, and the owner decrypts them and sorts them locally. That’s fine, because the owner owns both the gallery and the query, so the distances reveal nothing it doesn’t already know. The cloud sees ciphertexts only.

Example: Searching an Encrypted Employee Gallery in the Cloud

A company encrypts the face embeddings of its employees once and uploads them to a public cloud. Whenever a camera captures a face, the company encrypts its embedding, the cloud computes the encrypted distances to the whole gallery, and the company decrypts them to find the nearest employees.

The company gets indices 2 and 0, with squared distances 7 and 18, the same answer as in the first setting. This setting is much cheaper, since there are no round trips and no comparisons under encryption. The price is size: each multiplied ciphertext keeps a pair of ciphertexts for every product, so an encrypted distance over 512 dimensions is far larger than a plain Paillier ciphertext.

Which Algorithms Can Be Used, and Why

The two settings need different features, so they accept different cryptosystems. I ran both examples against every candidate in LightPHE, and here is what works:

AlgorithmsPlain gallery, encrypted queryEncrypted gallery in the cloudWhy
Damgard-Geisler-Kroigaard✅✅Small plaintext space, cheap zero test and encrypted argmax for the first setting, and additive, so Catalano-Fiore can boost it for the second
Paillier, Damgard-Jurik, Benaloh, Naccache-Stern, Joye-Libert, Bresson-Catalano-Pointcheval, Camenisch-Shoup, Castagnos-Laguillaumie, Elliptic Curve Paillier❌✅Additive with a public plaintext space and direct decryption, which is what Catalano-Fiore needs. Their plaintext spaces are too large for the comparisons of the first setting, so LightPHE refuses them there and points to Damgard-Geisler-Kroigaard

For the cloud setting, Paillier is the practical default. In my tests, Paillier, Joye-Libert and Damgard-Geisler-Kroigaard ran the cloud example in about a second, key generation included, while Elliptic Curve Paillier, Benaloh, Castagnos-Laguillaumie and Naccache-Stern took between 12 and 46 seconds, mostly in key generation.

Comparing the Two Settings

Plain gallery, encrypted queryEncrypted gallery in the cloud
Gallery ownerServerSame party as the query owner
CryptosystemDamgard-Geisler-KroigaardAdditive scheme boosted with Catalano-Fiore, e.g. Paillier
Homomorphic featuresAddition, scalar multiplication, encrypted argmax, encrypted equalityAddition, one multiplication of ciphertexts
InteractionSeveral round trips per nearest vectorNone
Who learns the distancesNobodyThe owner only
Client learnsIndices or labels of the k nearest vectors onlyEverything, as it owns the data
CostGrows with gallery size times kOne pass over the gallery, larger ciphertexts

More Scenarios

  • Sanctions screening: A bank checks whether a new customer’s face matches anyone on a sanctions list held by a screening provider. With a plain gallery and an encrypted query, the provider doesn’t learn who the bank’s customer is, and the bank doesn’t get the list.
  • Private semantic search: A user asks a question over a company’s document collection, as in retrieval-augmented generation. The question embedding is encrypted, so the search service returns the IDs of the most relevant documents without learning what the user asked.
  • Product recommendations: A retailer finds the nearest items to a user’s encrypted preference vector, so the server recommends products without learning the user’s taste.
  • Encrypted photo library: A photo app stores the face embeddings of a user’s photos in the cloud, encrypted, and searches them for a person. A breach of the cloud reveals neither the faces nor the searches.
  • Duplicate detection: An insurer checks whether a new claim’s photo embedding is close to photos of earlier claims stored by another insurer, to catch reused photos, without either insurer sharing its claims.

Caveats

  • Nearest is not the same as a match: A face recognition system also checks that the nearest face is close enough, i.e. that its distance is below a threshold. Otherwise, a stranger is simply matched to the most similar employee. The protocol in the first setting returns the nearest vectors, not whether they are within a threshold, so a verification threshold needs an extra secure comparison on top.
  • Semi-honest parties: Both settings assume every party follows the protocol and is only curious. A malicious server could, for example, search a different gallery than it claims.
  • Repeated queries: In the first setting, the client learns the k nearest labels for every query. With many queries, an attacker can learn who is in the gallery, so rate-limit queries per client.
  • Quantization: Rounding embeddings can swap two neighbors that are almost equally close. Pick a scale that keeps the ranking stable on your data, while keeping squared distances within 2^bit_length.

Conclusion

Encrypted nearest neighbor search is really two problems, depending on who owns the gallery. When a server owns it in plain, the distances are linear in the encrypted query, so additive encryption computes them, and the hard part is finding the nearest vectors without revealing any distance. That takes an encrypted argmin and an encrypted equality test, which is why it runs on Damgard-Geisler-Kroigaard. When the gallery itself is encrypted in an untrusted cloud, the hard part moves to the distances, which need one multiplication of ciphertexts, and an additive cryptosystem boosted with Catalano-Fiore provides exactly that.

Either way, faces, preferences and questions stay encrypted while they are being searched, and with LightPHE, both settings are only a few lines of Python, Go, Java or TypeScript.


Support this blog financially if you do like!

Buy me a coffee      Buy me a coffee


Leave a Reply