Private Set Intersection protocol based on ECDH, Bloom Filters, and Golomb Compressed Sets. The goal of this library is to allow a server to compute and return the intersection or intersection size (cardinality) from a set on the server and a set from a client without the server learning anything about the client's set.
- 💾 Low memory footprint
- 🚀 Fastest implementation using WebAssembly
- 🔥 Works in any client / server configuration, even React Native!
- 😎 Privacy preserving
To install, run:
npm install @openmined/psi.js
# or with yarn
yarn add @openmined/psi.js
Then import
or require
the package:
import PSI from '@openmined/psi.js'
// or
const PSI = require('@openmined/psi.js')
By default, the package will use the combined
build with the wasm
targeting the node
environment. This includes both client
and server
implementations, but often only one is used. We offer deep import links to only load what is needed for your specific environment.
The deep import structure is as follows:
<package name> / <client|server|combined>_<wasm|js>_<node|web|worker>
Example:
import PSI from '@openmined/psi.js/combined_wasm_node'
To only load the client
:
// Using a deep import link for client_wasm_node
import PSI from '@openmined/psi.js/client_wasm_node'
;(async () => {
// Wait for the library to initialize
const psi = await PSI()
const client = psi.client.createWithNewKey()
// psi.server is not implemented
//...
})()
To only load the server
:
// Using a deep import link for server_wasm_node
import PSI from '@openmined/psi.js/server_wasm_node'
;(async () => {
// Wait for the library to initialize
const psi = await PSI()
const server = psi.server.createWithNewKey()
// psi.client is not implemented
//...
})()
To manually override the combined
default import:
// Using a deep import link for combined_wasm_node
import PSI from '@openmined/psi.js/combined_wasm_node'
;(async () => {
// Wait for the library to initialize
const psi = await PSI()
const client = psi.client.createWithNewKey()
const server = psi.server.createWithNewKey()
//...
})()
The bundle needs a bit of extra work. Specifically, it expects the browser crypto.getRandomValues
which it will not find by default as react-native doesn't support the crypto builtin. It can be fixed by npm install react-native-get-random-values
which provides access to this global while supporting a CSPRNG. The library also needs to have the browser document
which is an artifact from the build system. Simply provide global.document = {}
. Finally, it requires the following deep import structure:
// Provide a CSPRNG mapping to crypto.getRandomValues()
import 'react-native-get-random-values'
import PSI from '@openmined/psi.js/combined_wasm_web'
;(async () => {
// Spoof the browser document
global.document = {}
// Wait for the library to initialize
const psi = await PSI()
const client = psi.client.createWithNewKey()
const server = psi.server.createWithNewKey()
//...
})()
We show an example following the steps:
- Initialize the server and create the
server setup
to be sent later - Initialize the client and create a client
request
to be sent to the server - Process the
request
and send both theserver setup
andresponse
to the client - Client receives the
setup
andresponse
and computes the intersection
const PSI = require('@openmined/psi.js')
;(async () => {
const psi = await PSI()
// Define mutually agreed upon parameters for both client and server
const fpr = 0.001 // false positive rate (0.1%)
const numClientElements = 10 // Size of the client set to check
const numTotalElements = 100 // Maximum size of the server set
const revealIntersection = false // Allows to reveal the intersection (true)
/******************
*** 1. Server ****
******************/
// Create new server instance
//
// By default, the server will only support calculating
// the intersection size (cardinality). To obtain the
// intersection of the two sets, pass in the boolean `true`.
//
// Ex: const server = psi.server.createWithNewKey(true)
const server = psi.server.createWithNewKey(revealIntersection)
// Specifying no parameters is equivalent to passing in `false`
// const server = psi.server.createWithNewKey()
// Example server set of data
const serverInputs = Array.from(
{ length: numTotalElements },
(_, i) => `Element ${i * 2}`
)
// Create the setup message that will later
// be used to compute the intersection. By default,
// this function will use Golomb Compressed Sets (GCS).
// You may pass in an extra argument at the end to pick
// the bloom filter option.
const serverSetup = server.createSetupMessage(
fpr,
numClientElements,
serverInputs
// psi.dataStructure.GCS // This is the default and can omitted
)
// Example with regular bloom filter
// const serverSetup = server.createSetupMessage(
// fpr,
// numClientElements,
// serverInputs,
// psi.dataStructure.BloomFilter
// )
/******************
*** 2. Client ****
******************/
// Create new client instance
//
// By default, the client will only support calculating
// the intersection size (cardinality). To obtain the
// intersection of the two sets, pass in the boolean `true`.
//
// Ex: const server = psi.client.createWithNewKey(true)
const client = psi.client.createWithNewKey(revealIntersection)
// Specifying no parameters is equivalent to passing in `false`
// const client = psi.client.createWithNewKey()
// Example client set of data to check
const clientInputs = Array.from(
{ length: numClientElements },
(_, i) => `Element ${i}`
)
// Create a client request to send to the server
const clientRequest = client.createRequest(clientInputs)
// Serialize the client request. Will be a Uint8Array.
const serializedClientRequest = clientRequest.serializeBinary()
// ... send the serialized client request from client -> server
/******************
*** 3. Server ****
******************/
// Deserialize the client request for the server
const deserializedClientRequest = psi.request.deserializeBinary(
serializedClientRequest
)
// Process the client's request and return to the client
const serverResponse = server.processRequest(deserializedClientRequest)
// Serialize the server response. Will be a Uint8Array.
const serializedServerResponse = serverResponse.serializeBinary()
// Serialize the server setup. Will be an Uint8Array.
const serializedServerSetup = serverSetup.serializeBinary()
// ... send the serialized server setup _and_ server response from server -> client
/******************
*** 4. Client ****
******************/
// Deserialize the server response
const deserializedServerResponse = psi.response.deserializeBinary(
serializedServerResponse
)
// Deserialize the server setup
const deserializedServerSetup = psi.serverSetup.deserializeBinary(
serializedServerSetup
)
// NOTE:
// A client can always compute the intersection size (cardinality), but by
// default does not reveal the actual intersection between the two arrays.
// This is dependent on whether or not _both_ client/server were initialized
// with the same boolean `true` value which sets an internal `reveal_intersection` flag.
// Any calls made to `getIntersection` will throw an error if this flag was not set to true.
// Reveal the cardinality
const intersectionSize = client.getIntersectionSize(
deserializedServerSetup,
deserializedServerResponse
)
console.log('intersectionSize', intersectionSize)
// intsersectionSize 5
// Reveal the intersection (only if `revealIntersection` was set to true)
// const intersection = client.getIntersection(
// deserializedServerSetup,
// deserializedServerResponse
// )
// console.log('intersection', intersection)
// intersection [ 0, 2, 4, 6, 8 ]
})()
See CONTRIBUTORS.md.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
Ensure your environment has the following global dependencies:
Next, ensure you have updated submodules
npm run submodule:update
Then, update and initialize emsdk
npm run em:update
npm run em:init
Now, install the rest of the dev dependencies
npm install
To compile the client, server, or combined (both client and server) for WebAssembly and pure JS
npm run build
Now, build the JS protobufs for TypeScript.
npm run build:proto
Compile TypeScript to JS (sanity checking)
npm run compile
Run the tests or generate coverage reports. Note tests are run using the WASM variant.
npm run test
npm run coverage
Build the benchmark for WebAssembly and pure JS
npm run compile
npm run build:benchmark
Finally, run the benchmark for WebAssembly or pure JS
npm run benchmark:wasm
npm run benchmark:js
Ensure we start with a clean build
npm run clean
Build the client, server, and combined (client and server)
npm run build
Build protobufs
npm run build:proto
Compile typescript (sanity check)
npm run compile
Test your changes and check code coverage
# Test TS
npm run test
# Generate coverage report
npm run coverage
Then, bundle all the files
npm run rollup
Now, we can test publish
npm run publish:test
Finally, publish the bundle
npm run publish
Note: The default npm publish
has been disabled to prevent publishing of the entire project files.
Instead, we have a custom override which will publish the npm package from a specific directory.
This allows us to publish a single package with shortened deep import links that specify the
different targets listed above.
See JS
CHANGES.md.