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.
The mutual number is added to both contact lists automatically.
What just happened, step by step
Section titled “What just happened, step by step”The playground runs the exact pipeline from client/src/crypto.ts and
server/src/vouch/matching.py:
- 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. - 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. - Round 1 — publish
P·k. Each contact is hashed to a curve pointP(ristretto255_from_hash(SHA-512("vouch/v1" ‖ number))), then the device’s key is applied. The device uploadsP·kfor every contact, plusown_token = P(own_number)·k. The server stores only these opaque points. - Pair and relay. The crypto-blind server pairs preference-compatible users and hands each person the other’s published points to work on.
- 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. - Compare. Scalar multiplication commutes, so
P·k_A·k_BequalsP·k_B·k_Abyte 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 ismutual_count, and the aligned indices tell each device which of its contacts matched, without the server ever seeing a number.
Things to try
Section titled “Things to try”- 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_countclimb 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 asdirectso it isn’t surfaced as a match.
Where this lives in the code
Section titled “Where this lives in the code”- Primitives (
normalize,hash_to_point,generate_key,apply_key) —server/src/vouch/protocol.pyandclient/src/crypto.ts, kept byte-compatible byvectors/protocol_vectors.json. - Round-2 re-encryption —
client/src/sync.ts. - The compare and direct-acquaintance logic —
compare_pairinserver/src/vouch/matching.py.
For the why behind the design, read The matching protocol.