Privacy-preserving mutual match: detect when two parties select each other without revealing unilateral selections to anyone.
  • TypeScript 98.7%
  • JavaScript 1.3%
Find a file
2026-03-24 00:19:30 -04:00
dist feat: barrel exports and build 2026-03-23 23:55:52 -04:00
examples feat: end-to-end example 2026-03-23 23:56:39 -04:00
src Update README.md 2026-03-24 00:15:47 -04:00
test feat: PsiService with owner-held key architecture 2026-03-23 23:50:32 -04:00
.gitignore chore: init matchlock repo 2026-03-23 23:24:35 -04:00
jest.config.js chore: init matchlock repo 2026-03-23 23:24:35 -04:00
package-lock.json feat: end-to-end example 2026-03-23 23:56:39 -04:00
package.json feat: end-to-end example 2026-03-23 23:56:39 -04:00
PROTOCOL.md docs: README and protocol specification 2026-03-23 23:57:30 -04:00
README.md Add 🗝️ Emoji as official Emoji for Matchlock 2026-03-24 00:19:30 -04:00
tsconfig.json chore: init matchlock repo 2026-03-23 23:24:35 -04:00

🗝️ Matchlock

Privacy-preserving mutual match: detect when two parties select each other without revealing unilateral selections to anyone.

The problem

Every existing matching platform operates as a trusted intermediary that sees all preferences. They know who you selected and who selected you, including rejections. This is a structural surveillance problem, not an implementation detail.

How Matchlock works

Matchlock composes two primitives:

1. DH token derivation

Two parties independently derive identical match tokens when they mutually select each other, using X25519 Diffie-Hellman:

Alice selects Bob:
  token = SHA256(DH(alice_priv, bob_pub) || poolId || "rendezvous-match-v1")

Bob selects Alice:
  token = SHA256(DH(bob_priv, alice_pub) || poolId || "rendezvous-match-v1")

// DH commutativity: same shared secret → same token

Tokens are derived locally. No server interaction required. The server never sees your selections — only the derived tokens you choose to submit.

2. Private Set Intersection (PSI)

PSI allows the server to detect overlapping tokens without learning which tokens each participant submitted. Built on OpenMined's PSI.js with an owner-held key architecture: the PSI server key is encrypted to the pool owner's public key, so the server cannot process queries without owner participation.

Together:

Party Learns Does NOT learn
Server Set sizes, timing Your selections or matches
You Your matches only Who rejected you, or who others selected
Pool owner Match token hashes Whose key belongs to whom

Installation

npm install matchlock

Usage

import { generateKeypair, deriveMatchToken, PsiService } from 'matchlock';

const alice = generateKeypair();
const bob = generateKeypair();

// Both derive the same token (DH commutativity)
const tokenA = deriveMatchToken(alice.privateKey, bob.publicKey, 'pool-1');
const tokenB = deriveMatchToken(bob.privateKey, alice.publicKey, 'pool-1');
console.log(tokenA === tokenB); // true — mutual match

See examples/mutual-match.ts for a complete end-to-end walkthrough.

Security properties

  • Zero server knowledge: The server sees only opaque hash values.
  • Unilateral privacy: If Alice selects Bob but Bob doesn't select Alice, Bob learns nothing about Alice's selection.
  • Pool isolation: Tokens are scoped to a pool ID. Cross-pool linkability requires breaking SHA-256.
  • Replay protection: Nullifiers prevent re-submission within a pool while remaining unlinkable across pools.
  • Owner-held PSI keys: The PSI server key is ECIES-encrypted to the pool owner's X25519 public key. The infrastructure operator cannot process PSI queries independently.

License

Apache-2.0