Skip to content

Encrypt and protect data using industry standard algorithms, field level encryption, a unique data key per record, bulk encryption operations, and decryption level identity verification.

License

Notifications You must be signed in to change notification settings

cipherstash/protectjs

Repository files navigation

CipherStash Logo
Protect.js

Implement robust data security without sacrificing performance or usability


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

Every value you encrypt with Protect.js has a unique key, made possible by CipherStash ZeroKMS's blazing fast bulk key operations, and backed by a root key in AWS KMS.

The encrypted data is structured as an EQL JSON payload, and can be stored in any database that supports JSONB.

Important

Searching, sorting, and filtering on encrypted data is only supported in PostgreSQL at the moment. Read more about searching encrypted data.

Table of contents

For more specific documentation, refer to the docs.

Features

Protect.js protects data in 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.
  • Audit trail: Every decryption event will be logged in ZeroKMS to help you prove compliance.
  • Searchable encryption: Protect.js supports searching encrypted data in PostgreSQL.
  • 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: meet 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.

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:

Note

You need to opt-out of bundling when using Protect.js.

Protect.js uses Node.js specific features and requires the use of the native Node.js require.

You need to opt-out of bundling for tools like Webpack, esbuild, or Next.js.

Read more about building and bundling with Protect.js.

Getting started

If you are following this getting started guide with an existing app, you can skip to the next step.

If you are following this getting started guide with a clean slate, check out the dedicated getting started guide

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 haven't 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

Don't commit cipherstash.secret.toml to git; it contains sensitive credentials. The stash setup command will attempt to append to your .gitignore file with the cipherstash.secret.toml file.

Read more about configuration via TOML file or environment variables.

Basic file structure

This is the basic file structure of the project. In the src/protect/ directory, we have the table definition in schema.ts and the protect client in index.ts.

📦 <project root>
 ├ 📂 src
 │   ├ 📂 protect
 │   │  ├ 📜 index.ts
 │   │  └ 📜 schema.ts
 │   └ 📜 index.ts
 ├ 📜 .env
 ├ 📜 cipherstash.toml
 ├ 📜 cipherstash.secret.toml
 ├ 📜 package.json
 â”” đź“ś tsconfig.json

Define your schema

Protect.js uses a schema to define the tables and columns that you want to encrypt and decrypt.

Define your tables and columns by adding this to src/protect/schema.ts:

import { csTable, csColumn } from '@cipherstash/protect'

export const users = csTable('users', {
  email: csColumn('email'),
})

export const orders = csTable('orders', {
  address: csColumn('address'),
})

Searchable encryption:

If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in src/protect/schema.ts:

import { csTable, csColumn } from '@cipherstash/protect'

export const users = csTable('users', {
  email: csColumn('email').freeTextSearch().equality().orderAndRange(),
})

export const orders = csTable('orders', {
  address: csColumn('address'),
})

Read more about defining your schema.

Initialize the Protect client

Import the protect function and initialize a client with your defined schema, by adding this to src/protect/index.ts:

import { protect } from '@cipherstash/protect'
import { users } from './schema'

// Pass all your tables to the protect function to initialize the client
export const protectClient = await protect(users, orders)

The protect function requires at least one csTable be provided.

Encrypt data

Protect.js provides the encrypt function on protectClient to encrypt data. encrypt takes a plaintext string, and an object with the table and column as parameters.

Start encrypting data by adding this to src/index.ts:

import { users } from './protect/schema'
import { protectClient } from './protect'

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

if (encryptResult.failure) {
  // Handle the failure
  console.log("error when encrypting:", encryptResult.failure.type, encryptResult.failure.message)
}

const ciphertext = encryptResult.data
console.log("ciphertext:", ciphertext)

The encrypt function will return a Result object with either a data key, or a failure key. The encryptResult will return one of the following:

// Success
{
  data: {
    c: '\\\\\\\\\\\\\\\\x61202020202020472aaf602219d48c4a...'
  }
}

// Failure
{
  failure: {
    type: 'EncryptionError',
    message: 'A message about the error'
  }
}

Tip

Get significantly better encryption performance by using the bulkEncrypt function for large payloads.

Decrypt data

Protect.js provides the decrypt function on protectClient to decrypt data. decrypt takes an encrypted data object as a parameter.

Start decrypting data by adding this to src/index.ts:

import { protectClient } from './protect'

const decryptResult = await protectClient.decrypt(ciphertext)

if (decryptResult.failure) {
  // Handle the failure
  console.log("error when decrypting:", decryptResult.failure.type, decryptResult.failure.message)
}

const plaintext = decryptResult.data
console.log("plaintext:", plaintext)

The decrypt function returns a Result object with either a data key, or a failure key. The decryptResult will return one of the following:

// Success
{
  data: 'secret@squirrel.example'
}

// Failure
{
  failure: {
    type: 'DecryptionError',
    message: 'A message about the error'
  }
}

Tip

Get significantly better decryption performance by using the bulkDecrypt function for large payloads.

Store encrypted data in a database

Encrypted data can be stored in any database that supports JSONB, noting that searchable encryption is only supported in PostgreSQL at the moment.

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

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

Searchable encryption in PostgreSQL

To enable searchable encryption in PostgreSQL, you need to install the EQL custom types and functions.

  1. Download the latest EQL install script:

    curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql
  2. Run this command to install the custom types and functions:

    psql -f cipherstash-encrypt.sql

EQL is now installed in your database and you can enable searchable encryption by adding the cs_encrypted_v1 type to a column.

CREATE TABLE users (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email cs_encrypted_v1
);

Identity-aware encryption

Important

Right now identity-aware encryption is only supported if you are using Clerk as your identity provider. Read more about lock contexts with Clerk and Next.js.

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 identifyResult = await lc.identify(jwt)

// The identify method returns the same Result pattern as the encrypt and decrypt methods.
if (identifyResult.failure) {
  // Hanlde the failure
}

const lockContext = identifyResult.data

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:

import { protectClient } from './protect'
import { users } from './protect/schema'

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

if (encryptResult.failure) {
  // Handle the failure
}

const ciphertext = encryptResult.data

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:

import { protectClient } from './protect'

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

if (decryptResult.failure) {
  // Handle the failure
}

const plaintext = decryptResult.data

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',
})

if (encryptedResults.failure) {
  // Handle the failure
}

const encryptedValues = encryptedResults.data

// encryptedValues might look like:
// [
//   { encryptedData: { c: 'ENCRYPTED_VALUE_1', k: 'ct' }, id: '1' },
//   { encryptedData: { c: 'ENCRYPTED_VALUE_2', k: 'ct' }, id: '2' },
// ]

Reassemble data by matching IDs:

encryptedValues.forEach((result) => {
  // Find the corresponding user
  const user = users.find((u) => u.id === result.id)
  if (user) {
    user.email = result.encryptedData  // Store the encrypted data 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)

if (decryptedResults.failure) {
  // Handle the failure
}

const decryptedValues = decryptedResults.data

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

Reassemble data by matching IDs:

decryptedValues.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

Protect.js currently supports encrypting and decrypting text. Other data types like booleans, dates, ints, floats, and JSON are well supported in other CipherStash products, and will be coming to Protect.js soon.

Until support for other data types are available, you can express interest in this feature by adding a đź‘Ť on this GitHub Issue.

Searchable encryption

Read more about searching encrypted data in the docs.

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.

Read more about configuring the CipherStash client in the configuration docs.

Example applications

Looking for examples of how to use Protect.js? Check out the example applications:

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

Builds and bundling

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

Here are a few resources to help based on your tool set:

Contributing

Please read the contribution guide.

License

Protect.js is MIT licensed.

About

Encrypt and protect data using industry standard algorithms, field level encryption, a unique data key per record, bulk encryption operations, and decryption level identity verification.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published