Private Set Intersection for JavaScript
npm install @openmined/psi.js
!License
!OpenCollective
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:
``bash`
npm install @openmined/psi.jsor with yarn
yarn add @openmined/psi.js
Then import or require the package:
`javascript`
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 targetingnode
the environment.
The deep import structure is as follows:
Example:
`javascript`
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-nativenpm install
doesn't support the crypto builtin. It can be fixed by
react-native-get-random-values which provides access to this global whiledocument
supporting a CSPRNG. The library also needs to have the browser whichglobal.document = {}
is an artifact from the build system. Simply provide .
Finally, it requires the following deep import structure:
`javascript
// 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:
1. Initialize the server and create the server setup to be sent laterrequest
2. Initialize the client and create a client to be sent to the serverrequest
3. Process the and send both the server setup and response to thesetup
client
4. Client receives the and response and computes the intersection
`javascript
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.false
//
// Ex: const server = psi.server.createWithNewKey(true)
const server = psi.server.createWithNewKey(revealIntersection)
// Specifying no parameters is equivalent to passing in
// 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.false
//
// Ex: const server = psi.client.createWithNewKey(true)
const client = psi.client.createWithNewKey(revealIntersection)
// Specifying no parameters is equivalent to passing in
// 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.getIntersection
// Any calls made to 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
`bash`
npm install
To compile the client, server, or psi (both client and server) for WebAssembly
and pure JS
`bash`
npm run build
Now, build the JS protobufs for TypeScript.
`bash`
npm run build:proto
Compile TypeScript to JS (sanity checking)
`bash`
npm run compile
Run the tests or generate coverage reports. Note tests are run using the
WASM variant.
`bash`
npm run test
npm run coverage
Build the benchmark for WebAssembly and pure JS
`bash`
npm run compile
npm run build:benchmark
Finally, run the benchmark for WebAssembly
`bash`
npm run benchmark
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
`bashTest TS
npm run test
Then, bundle all the files
npm run rollupNow, we can test publish
npm run publish:testFinally, publish the bundle
npm run publishNote: 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.Changes
See
JS` CHANGES.md.