Skip to content

Latest commit

 

History

History
344 lines (258 loc) · 9.46 KB

File metadata and controls

344 lines (258 loc) · 9.46 KB

om-logo

CD License OpenCollective

PSI - JavaScript

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

Installation

To install, run:

npm install @openmined/psi.js
# or with yarn
yarn add @openmined/psi.js

Usage

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'

React-Native

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()
  //...
})()

Example

We show an example following the steps:

  1. Initialize the server and create the server setup to be sent later
  2. Initialize the client and create a client request to be sent to the server
  3. Process the request and send both the server setup and response to the client
  4. Client receives the setup and response 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 ]
})()

Contributors

See CONTRIBUTORS.md.

Contributing

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.

Requirements

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

Benchmarks

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

Publishing

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.

Changes

See JS CHANGES.md.

License

Apache License 2.0