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.
- Features
- Example applications
- Installing Protect.js
- Getting started
- Identity-aware encryption
- Bulk encryption and decryption
- Supported data types
- Searchable encryption
- Logging
- CipherStash Client
- Builds and bundling
- Contributing
- License
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.
New to 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 that support PostgreSQL.
If you're interested in using @cipherstash/protect
with a specific ORM, please create an issue.
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
:
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.jscipherstash.secret.toml
: which contains the credentials for Protect.js
Warning
cipherstash.secret.toml
should not be committed to git, because it contains sensitive credentials.
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()
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.
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.
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,
);
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 lockContext = await lc.identify(jwt)
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)
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)
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',
})
// 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
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
@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:
- Read about how these data types work in EQL
- Express interest in this feature by adding a 👍 on this GitHub Issue.
@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:
- Read about how searchable encryption works in EQL
- Express interest in this feature by adding a 👍 on this GitHub Issue.
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.
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 theCS_ZEROKMS_HOST
environment variable to the appropriate region. For example, if you are using ZeroKMS in theeu-central-1
region, you need to set theCS_ZEROKMS_HOST
variable tohttps://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. SettingCS_CONFIG_PATH
to/tmp/.cipherstash
will work in most cases, and has been tested on Vercel, AWS Lambda, and other hosting environments.
@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.
Please read the contribution guide.
@cipherstash/protect
is MIT licensed.