Skip to content

Latest commit

 

History

History
402 lines (281 loc) · 15.4 KB

README.md

File metadata and controls

402 lines (281 loc) · 15.4 KB

Protect.js

Tests Built by CipherStash

Protect.js is a JavaScript/TypeScript package for encrypting and decrypting data in PostgreSQL databases. Encryption operations happen directly in your app, and the ciphertext is stored in your PostgreSQL database.

Every value you encrypt with Protect.js has a unique key, made possible by CipherStash ZeroKMS's blazing fast bulk key operations. Under the hood Protect.js uses CipherStash Encrypt Query Language (EQL), and all ZeroKMS data keys are backed by a root key in AWS KMS.

Table of contents

Features

Protect.js protects data in PostgreSQL databases using industry-standard AES encryption. Protect.js uses ZeroKMS for bulk encryption and decryption operations. This enables every encrypted value, in every column, in every row in your database to have a unique key — without sacrificing performance.

Features:

  • Bulk encryption and decryption: Protect.js uses ZeroKMS for encrypting and decrypting thousands of records at once, while using a unique key for every value.
  • Single item encryption and decryption: Just looking for a way to encrypt and decrypt single values? Protect.js has you covered.
  • Really fast: ZeroKMS's performance makes using millions of unique keys feasible and performant for real-world applications built with Protect.js.
  • Identity-aware encryption: Lock down access to sensitive data by requiring a valid JWT to perform a decryption.
  • TypeScript support: Strongly typed with TypeScript interfaces and types.

Use cases:

  • Trusted data access: make sure only your end-users can access their sensitive data stored in your product.
  • Meet compliance requirements faster: achieve and exceed the data encryption requirements of SOC2 and ISO27001.
  • Reduce the blast radius of data breaches: limit the impact of exploited vulnerabilities to only the data your end-users can decrypt.

Example applications

New to Protect.js? Check out the example applications:

@cipherstash/protect can be used with most ORMs that support PostgreSQL. If you're interested in using @cipherstash/protect with a specific ORM, please create an issue.

Installing Protect.js

Install the @cipherstash/protect package with your package manager of choice:

npm install @cipherstash/protect
# or
yarn add @cipherstash/protect
# or
pnpm add @cipherstash/protect

Tip

Bun is not currently supported due to a lack of Node-API compatibility. Under the hood, Protect.js uses CipherStash Client which is written in Rust and embedded using Neon.

Lastly, install the CipherStash CLI:

  • On macOS:

    brew install cipherstash/tap/stash
  • On Linux, download the binary for your platform, and put it on your PATH:

Getting started

Configuration

Important

Make sure you have installed the CipherStash CLI before following these steps.

To set up all the configuration and credentials required for Protect.js:

stash setup

If you have not already signed up for a CipherStash account, this will prompt you to do so along the way.

At the end of stash setup, you will have two files in your project:

  • cipherstash.toml which contains the configuration for Protect.js
  • cipherstash.secret.toml: which contains the credentials for Protect.js

Warning

cipherstash.secret.toml should not be committed to git, because it contains sensitive credentials.

Initializing the EQL client

In your application, import the protect function from the @cipherstash/protect package, and initialize a client with your CipherStash credentials.

const { protect } = require('@cipherstash/protect')
const protectClient = await protect()

If you are using ES6:

import { protect } from '@cipherstash/protect'
const protectClient = await protect()

Encrypting data

Use the encrypt function to encrypt data. encrypt takes a plaintext string, and an object with the table and column name as parameters.

const ciphertext = await protectClient.encrypt('secret@squirrel.example', {
  column: 'email',
  table: 'users',
})

The encrypt function returns an object with a c key, and the value is the encrypted data.

{
  c: '\\\\\\\\\\\\\\\\x61202020202020472aaf602219d48c4a...'
}

Tip

Get significantly better encryption performance by using the bulkEncrypt function.

Decrypting data

Use the decrypt function to decrypt data. decrypt takes an encrypted data object, and an object with the lock context as parameters.

const plaintext = await protectClient.decrypt(ciphertext)

The decrypt function returns a string containing the plaintext data.

'secret@squirrel.example'

Tip

Get significantly better decryption performance by using the bulkDecrypt function.

Storing encrypted data in a database

To store the encrypted data in PostgreSQL, you will need to specify the column type as jsonb.

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email jsonb NOT NULL,
);

Identity-aware encryption

Protect.js can add an additional layer of protection to your data by requiring a valid JWT to perform a decryption.

This ensures that only the user who encrypted data is able to decrypt it.

Protect.js does this through a mechanism called a lock context.

Lock context

Lock contexts ensure that only specific users can access sensitive data.

Caution

You must use the same lock context to encrypt and decrypt data. If you use different lock contexts, you will be unable to decrypt the data.

To use a lock context, initialize a LockContext object with the identity claims.

import { LockContext } from '@cipherstash/protect/identify'

// protectClient from the previous steps
const lc = new LockContext()

Note

When initializing a LockContext, the default context is set to use the sub Identity Claim.

Identifying a user for a lock context

A lock context needs to be locked to a user. To identify the user, call the identify method on the lock context object, and pass a valid JWT from a user's session:

const lockContext = await lc.identify(jwt)

Encrypting data with a lock context

To encrypt data with a lock context, call the optional withLockContext method on the encrypt function and pass the lock context object as a parameter:

const ciphertext = await protectClient.encrypt('plaintext', {
  table: 'users',
  column: 'email',
}).withLockContext(lockContext)

Decrypting data with a lock context

To decrypt data with a lock context, call the optional withLockContext method on the decrypt function and pass the lock context object as a parameter:

const plaintext = await protectClient.decrypt(ciphertext).withLockContext(lockContext)

Bulk encryption and decryption

If you have a large list of items to encrypt or decrypt, you can use the bulkEncrypt and bulkDecrypt methods to batch encryption/decryption. bulkEncrypt and bulkDecrypt give your app significantly better throughput than the single-item encrypt and decrypt methods.

Bulk encrypting data

Build a list of records to encrypt:

const users = [
  { id: '1', name: 'CJ', email: 'cj@example.com' },
  { id: '2', name: 'Alex', email: 'alex@example.com' },
]

Prepare the array for bulk encryption:

const plaintextsToEncrypt = users.map((user) => ({
  plaintext: user.email, // The data to encrypt
  id: user.id,           // Keep track by user ID
}))

Perform the bulk encryption:

const encryptedResults = await bulkEncrypt(plaintextsToEncrypt, {
  column: 'email',
  table: 'Users',
})

// encryptedResults might look like:
// [
//   { c: 'ENCRYPTED_VALUE_1', id: '1' },
//   { c: 'ENCRYPTED_VALUE_2', id: '2' },
// ]

Reassemble data by matching IDs:

encryptedResults.forEach((result) => {
  // Find the corresponding user
  const user = users.find((u) => u.id === result.id)
  if (user) {
    user.email = result.c  // Store ciphertext back into the user object
  }
})

Learn more about bulk encryption

Bulk decrypting data

Build an array of records to decrypt:

const users = [
  { id: '1', name: 'CJ', email: 'ENCRYPTED_VALUE_1' },
  { id: '2', name: 'Alex', email: 'ENCRYPTED_VALUE_2' },
]

Prepare the array for bulk decryption:

const encryptedPayloads = users.map((user) => ({
  c: user.email,
  id: user.id,
}))

Perform the bulk decryption:

const decryptedResults = await bulkDecrypt(encryptedPayloads)

// decryptedResults might look like:
// [
//   { plaintext: 'cj@example.com', id: '1' },
//   { plaintext: 'alex@example.com', id: '2' },
// ]

Reassemble data by matching IDs:

decryptedResults.forEach((result) => {
  const user = users.find((u) => u.id === result.id)
  if (user) {
    user.email = result.plaintext  // Put the decrypted value back in place
  }
})

Learn more about bulk decryption

Supported data types

@cipherstash/protect currently supports encrypting and decrypting text. Other data types like booleans, dates, ints, floats, and JSON are extremely well supported in other CipherStash products, and will be coming to @cipherstash/protect. Until support for other data types are available in @cipherstash/protect, you can:

Searchable encryption

@cipherstash/protect does not currently support searching encrypted data. Searchable encryption is an extremely well supported capability in other CipherStash products, and will be coming to @cipherstash/protect. Until searchable encryption support is released in @cipherstash/protect, you can:

Logging

Important

@cipherstash/protect will NEVER log plaintext data. This is by design to prevent sensitive data from leaking into logs.

@cipherstash/protect and @cipherstash/nextjs will log to the console with a log level of info by default. To enable the logger, configure the following environment variable:

PROTECT_LOG_LEVEL=debug  # Enable debug logging
PROTECT_LOG_LEVEL=info   # Enable info logging
PROTECT_LOG_LEVEL=error  # Enable error logging

CipherStash Client

Protect.js is built on top of the CipherStash Client Rust SDK which is embedded with the @cipherstash/protect-ffi package. The @cipherstash/protect-ffi source code is available on GitHub.

The Cipherstash Client is configured by environment variables, which are used to initialize the client when the protect function is called:

Variable name Description Required Default
CS_CLIENT_ID The client ID for your CipherStash account. Yes
CS_CLIENT_KEY The client key for your CipherStash account. Yes
CS_WORKSPACE_ID The workspace ID for your CipherStash account. Yes
CS_CLIENT_ACCESS_KEY The access key for your CipherStash account. Yes
CS_ZEROKMS_HOST The host for the ZeroKMS server. No https://ap-southeast-2.aws.viturhosted.net
CS_CONFIG_PATH A temporary path to store the CipherStash client configuration. No /home/{username}/.cipherstash

Tip

There are some configuration details you should take note of when deploying @cipherstash/protect in your production apps.

  • If you've created a Workspace in a region other than ap-southeast-2, you will need to set the CS_ZEROKMS_HOST environment variable to the appropriate region. For example, if you are using ZeroKMS in the eu-central-1 region, you need to set the CS_ZEROKMS_HOST variable to https://eu-central-1.aws.viturhosted.net. This is a known usability issue that will be addressed.
  • In most hosting environments, the CS_CONFIG_PATH environment variable will need to be set to a path that the user running the application has permission to write to. Setting CS_CONFIG_PATH to /tmp/.cipherstash will work in most cases, and has been tested on Vercel, AWS Lambda, and other hosting environments.

Builds and bundling

@cipherstash/protect is a native Node.js module, and relies on native Node.js require to load the package.

If you are using @cipherstash/protect with Next.js, you must opt out from the Server Components bundling and use native Node.js require instead.

Contributing

Please read the contribution guide.

License

@cipherstash/protect is MIT licensed.