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 psi
build with the wasm
targeting
the node
environment.
The deep import structure is as follows: <package name> / <psi>_<wasm>_<node|web|worker>
Example:
import PSI from '@openmined/psi.js/psi_wasm_node.js'
import PSI from '@openmined/psi.js/psi_wasm_web.js'
import PSI from '@openmined/psi.js/psi_wasm_worker.js'
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/psi_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:
Now, install the rest of the dev dependencies
npm install
To compile the client, server, or psi (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
Ensure we start with a clean build
npm run clean
Build the client, server, and psi (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.