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.
- Features
- Installing Protect.js
- Getting started
- Identity-aware encryption
- Bulk encryption and decryption
- Supported data types
- Searchable encryption
- Logging
- CipherStash Client
- Example applications
- Builds and bundling
- Contributing
- License
For more specific documentation, refer to the docs.
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.
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.
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
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.jscipherstash.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.
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
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.
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.
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.
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.
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,
);
To enable searchable encryption in PostgreSQL, you need to install the EQL custom types and functions.
-
Download the latest EQL install script:
curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql
-
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
);
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 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.
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
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
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
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.
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
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
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.
Read more about searching encrypted data in the docs.
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
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.
Looking for examples of how to use Protect.js? Check out the example applications:
- Basic example demonstrates how to perform encryption operations
- Drizzle example demonstrates how to use Protect.js with an ORM
- Next.js and lock contexts example using Clerk demonstrates how to protect data with identity-aware encryption
@cipherstash/protect
can be used with most ORMs.
If you're interested in using @cipherstash/protect
with a specific ORM, please create an issue.
@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:
Please read the contribution guide.
Protect.js is MIT licensed.