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 currently only supported when storing encrypted data in PostgreSQL. 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:

Opt-out of bundling

Important

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.

When using Protect.js, 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

🆕 Existing app? Skip to the next step.

🌱 Clean slate? Check out the getting started tutorial.

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

The following 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

To import the protect function and initialize a client with your defined schema, add the following to src/protect/index.ts:

import { protect } from "@cipherstash/protect";
import { users, orders } 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.

To start encrypting data, add the following to src/index.ts:

import { users } from "./protect/schema";
import { protectClient } from "./protect";

const encryptResult = await protectClient.encrypt("[email protected]", {
  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: 'mBbKmsMMkbKBSN}s1THy_NfQN892!dercyd0s...'
  }
}

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

Decrypt data

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

To start decrypting data, add the following 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: '[email protected]'
}

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

Working with models and objects

Protect.js provides model-level encryption methods that make it easy to encrypt and decrypt entire objects. These methods automatically handle the encryption of fields defined in your schema.

If you are working with a large data set, the model operations are significantly faster than encrypting and decrypting individual objects as they are able to perform bulk operations.

Tip

CipherStash ZeroKMS is optimized for bulk operations.

All the model operations are able to take advantage of this performance for real-world use cases by only making a single call to ZeroKMS regardless of the number of objects you are encrypting or decrypting while still using a unique key for each record.

Encrypting a model

Use the encryptModel method to encrypt a model's fields that are defined in your schema:

import { protectClient } from "./protect";
import { users } from "./protect/schema";

// Your model with plaintext values
const user = {
  id: "1",
  email: "[email protected]",
  address: "123 Main St",
  createdAt: new Date("2024-01-01"),
};

const encryptedResult = await protectClient.encryptModel(user, users);

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

const encryptedUser = encryptedResult.data;
console.log("encrypted user:", encryptedUser);

The encryptModel function will only encrypt fields that are defined in your schema. Other fields (like id and createdAt in the example above) will remain unchanged.

Type safety with models

Protect.js provides strong TypeScript support for model operations. You can specify your model's type to ensure end-to-end type safety:

import { protectClient } from "./protect";
import { users } from "./protect/schema";

// Define your model type
type User = {
  id: string;
  email: string | null;
  address: string | null;
  createdAt: Date;
  updatedAt: Date;
  metadata?: {
    preferences?: {
      notifications: boolean;
      theme: string;
    };
  };
};

// The encryptModel method will ensure type safety
const encryptedResult = await protectClient.encryptModel<User>(user, users);

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

const encryptedUser = encryptedResult.data;
// TypeScript knows that encryptedUser matches the User type structure
// but with encrypted fields for those defined in the schema

// Decryption maintains type safety
const decryptedResult = await protectClient.decryptModel<User>(encryptedUser);

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

const decryptedUser = decryptedResult.data;
// decryptedUser is fully typed as User

// Bulk operations also support type safety
const bulkEncryptedResult = await protectClient.bulkEncryptModels<User>(
  userModels,
  users
);

const bulkDecryptedResult = await protectClient.bulkDecryptModels<User>(
  bulkEncryptedResult.data
);

The type system ensures that:

  • Input models match your defined type structure
  • Only fields defined in your schema are encrypted
  • Encrypted and decrypted results maintain the correct type structure
  • Optional and nullable fields are properly handled
  • Nested object structures are preserved
  • Additional properties not defined in the schema remain unchanged

This type safety helps catch potential issues at compile time and provides better IDE support with autocompletion and type hints.

Tip

When using TypeScript with an ORM, you can reuse your ORM's model types directly with Protect.js's model operations.

Example with Drizzle infered types:

import { protectClient } from "./protect";
import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core";
import { csTable, csColumn } from "@cipherstash/protect";

const protectUsers = csTable("users", {
  email: csColumn("email"),
});

const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: jsonb("email").notNull(),
});

type User = InferSelectModel<typeof users>;

const user = {
  id: "1",
  email: "[email protected]",
};

// Drizzle User type works directly with model operations
const encryptedResult = await protectClient.encryptModel<User>(
  user,
  protectUsers
);

Decrypting a model

Use the decryptModel method to decrypt a model's encrypted fields:

import { protectClient } from "./protect";

const decryptedResult = await protectClient.decryptModel(encryptedUser);

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

const decryptedUser = decryptedResult.data;
console.log("decrypted user:", decryptedUser);

Bulk model operations

For better performance when working with multiple models, use the bulkEncryptModels and bulkDecryptModels methods:

import { protectClient } from "./protect";
import { users } from "./protect/schema";

// Array of models with plaintext values
const userModels = [
  {
    id: "1",
    email: "[email protected]",
    address: "123 Main St",
  },
  {
    id: "2",
    email: "[email protected]",
    address: "456 Oak Ave",
  },
];

// Encrypt multiple models at once
const encryptedResult = await protectClient.bulkEncryptModels(
  userModels,
  users
);

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

const encryptedUsers = encryptedResult.data;

// Decrypt multiple models at once
const decryptedResult = await protectClient.bulkDecryptModels(encryptedUsers);

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

const decryptedUsers = decryptedResult.data;

The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object. They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema.

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, 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, 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;

Model encryption with lock context

All model operations support lock contexts for identity-aware encryption:

import { protectClient } from "./protect";
import { users } from "./protect/schema";

const myUsers = [
  {
    id: "1",
    email: "[email protected]",
    address: "123 Main St",
    createdAt: new Date("2024-01-01"),
  },
  {
    id: "2",
    email: "[email protected]",
    address: "456 Oak Ave",
  },
];

// Encrypt a model with lock context
const encryptedResult = await protectClient
  .encryptModel(myUsers[0], users)
  .withLockContext(lockContext);

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

// Decrypt a model with lock context
const decryptedResult = await protectClient
  .decryptModel(encryptedResult.data)
  .withLockContext(lockContext);

// Bulk operations also support lock contexts
const bulkEncryptedResult = await protectClient
  .bulkEncryptModels(myUsers, users)
  .withLockContext(lockContext);

const bulkDecryptedResult = await protectClient
  .bulkDecryptModels(bulkEncryptedResult.data)
  .withLockContext(lockContext);

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.


Didn't find what you wanted?

Click here to let us know what was missing from our docs.

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