Skip to content

See the matching algorithm work

This tutorial makes the matching protocol concrete. You enter a phone number that two people both have saved, and the page runs the actual Private Set Intersection protocol — the same libsodium ristretto255 calls the production client uses — showing every value at every step. Nothing here is a mock-up: the bytes below are computed live in your browser.

The scenario: Alice and Bob don’t know each other, but they both have the same person in their phone book. The protocol has to discover that shared contact without either the server or the other person learning any phone number.

Alice
Bob
The mutual number is added to both contact lists automatically.

The playground runs the exact pipeline from client/src/crypto.ts and server/src/vouch/matching.py:

  1. Normalise. Every number is stripped of separators and validated as E.164 (+ and 7–15 digits). +32 470 12 34 56+32470123456. Two devices must agree on the exact string, or the hashes won’t line up.
  2. Key per device. Each person’s device generates a random ristretto255 scalar k. It is the one secret that makes the scheme work and it never leaves the device.
  3. Round 1 — publish P·k. Each contact is hashed to a curve point P (ristretto255_from_hash(SHA-512("vouch/v1" ‖ number))), then the device’s key is applied. The device uploads P·k for every contact, plus own_token = P(own_number)·k. The server stores only these opaque points.
  4. Pair and relay. The crypto-blind server pairs preference-compatible users and hands each person the other’s published points to work on.
  5. Round 2 — re-encrypt. Each person applies their own key on top of the other’s already-encrypted points, giving P·k_other·k_self. Order is preserved, so an index still maps back to a specific contact.
  6. Compare. Scalar multiplication commutes, so P·k_A·k_B equals P·k_B·k_A byte for byte. A contact both people share therefore produces identical 32-byte values in the two doubly-encrypted lists; everything else differs. The server intersects the two sets — the size of the intersection is mutual_count, and the aligned indices tell each device which of its contacts matched, without the server ever seeing a number.
  • Break the match: clear the mutual-contact box at the top. Nothing is forced into both lists any more, so with the default (non-overlapping) contacts the intersection goes empty — no match.
  • Add a second shared contact: put the same extra number in both lists and watch mutual_count climb to 2.
  • Trigger direct-acquaintance suppression: put Bob’s own number into Alice’s contacts. The protocol detects that they already know each other (via the re-encrypted own_token) and flags the pairing as direct so it isn’t surfaced as a match.
  • Primitives (normalize, hash_to_point, generate_key, apply_key) — server/src/vouch/protocol.py and client/src/crypto.ts, kept byte-compatible by vectors/protocol_vectors.json.
  • Round-2 re-encryption — client/src/sync.ts.
  • The compare and direct-acquaintance logic — compare_pair in server/src/vouch/matching.py.

For the why behind the design, read The matching protocol.