Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/fresh-windows-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@cipherstash/jseql": major
---

Enforced lock context to be called as a proto function rather than an optional argument for crypto functions.
There was a bug that caused the lock context to be interpreted as undefined when the users intention was to use it causing the encryption/decryption to fail.
This is a breaking change for users who were using the lock context as an optional argument.
To use the lock context, call the `withLockContext` method on the encrypt, decrypt, and bulk encrypt/decrypt functions, passing the lock context as a parameter rather than as an optional argument.
5 changes: 5 additions & 0 deletions .changeset/strange-bees-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/nextjs": major
---

Implemented better error handling for fetching CTS tokens and accessing them in the Next.js application.
77 changes: 51 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ The `decrypt` function returns a string with the plaintext data.

### Lock context

> [!CAUTION]
> If you use a lock context to encrypt data, you must also use the same lock context to decrypt the data.
> Otherwise, you will receive a `400` error from ZeroKMS indicating that the request was unable to generate a data key, and you will be unable to decrypt the data.

`jseql` supports lock contexts to ensure that only the intended users can access sensitive data.
To use a lock context, initialize a `LockContext` object with the identity claims.

Expand All @@ -162,7 +166,34 @@ const lc = new LockContext()
```

> [!NOTE]
> At the time of this writing, the default LockContext is set to use the `sub` Identity Claim, as this is the only Identity Claim that is currently supported.
> When initializing a `LockContext` the default context is set to use the `sub` Identity Claim.

**Custom context**

If you want to override the default context, you can pass a custom context to the `LockContext` constructor.

```typescript
import { LockContext } from '@cipherstash/jseql/identify'

// eqlClient from the previous steps
const lc = new LockContext({
context: {
identityClaim: ['sub'], // this is the default context
},
})
```

**Context and identity claim options**

The context object contains an `identityClaim` property.
The `identityClaim` property must be an array of strings that correspond to the Identity Claim(s) you want to lock the encryption operation to.

Currently supported Identity Claims are:

| Identity Claim | Description |
| -------------- | ----------- |
| `sub` | The user's subject identifier. |
| `scopes` | The user's scopes set by your IDP policy. |

#### Identifying the user

Expand Down Expand Up @@ -200,6 +231,8 @@ export default clerkMiddleware(async (auth, req: NextRequest) => {
})
```

#### Retrieving the CTS token in Next.js

You can then use the `getCtsToken` function to retrieve the CTS token for the current user session.

```typescript
Expand Down Expand Up @@ -240,24 +273,21 @@ export default async function Page() {

### Encrypting data with a lock context

To encrypt data with a lock context, pass the lock context object as a parameter to the `encrypt` function.
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.

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

### Decrypting data with a lock context

To decrypt data with a lock context, pass the lock context object as a parameter to the `decrypt` function.
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.

```typescript
const plaintext = await eqlClient.decrypt(ciphertext, {
lockContext,
})
const plaintext = await eqlClient.decrypt(ciphertext).withLockContext(lockContext)
```

### Storing encrypted data in a database
Expand All @@ -283,8 +313,14 @@ If you have a large list of items to encrypt or decrypt, you can use the **`bulk
const encryptedResults = await eqlClient.bulkEncrypt(plaintextsToEncrypt, {
column: 'email',
table: 'Users',
// lockContext: someLockContext, // if you have one
})

// or with lock context

const encryptedResults = await eqlClient.bulkEncrypt(plaintextsToEncrypt, {
column: 'email',
table: 'Users',
}).withLockContext(lockContext)
```

**Parameters**
Expand All @@ -306,11 +342,6 @@ const encryptedResults = await eqlClient.bulkEncrypt(plaintextsToEncrypt, {
- **Description**:
The name of the table you’re encrypting data in (e.g., "Users").

4. **`lockContext`** (optional)
- **Type**: `LockContext`
- **Description**:
Additional metadata and tokens for secure encryption/decryption. If not provided, encryption proceeds without a lock context.

### Return Value

- **Type**: `Promise<Array<{ c: string; id: string }> | null>`
Expand Down Expand Up @@ -338,7 +369,6 @@ const plaintextsToEncrypt = users.map((user) => ({
const encryptedResults = await bulkEncrypt(plaintextsToEncrypt, {
column: 'email',
table: 'Users',
// lockContext: someLockContext, // if you have one
})

// encryptedResults might look like:
Expand All @@ -362,9 +392,11 @@ if (encryptedResults) {
#### bulkDecrypt

```ts
const decryptedResults = await eqlClient.bulkDecrypt(encryptedPayloads, {
// lockContext: someLockContext, // if needed
})
const decryptedResults = await eqlClient.bulkDecrypt(encryptedPayloads)

// or with lock context

const decryptedResults = await eqlClient.bulkDecrypt(encryptedPayloads).withLockContext(lockContext)
```

**Parameters**
Expand All @@ -374,11 +406,6 @@ const decryptedResults = await eqlClient.bulkDecrypt(encryptedPayloads, {
- **Description**:
An array of objects containing the **ciphertext** (`c`) and the **id**. If this array is empty or `null`, the function returns `null`.

2. **`lockContext`** (optional)
- **Type**: `LockContext`
- **Description**:
Additional metadata used to securely unlock ciphertext. If not provided, decryption proceeds without it.

### Return Value

- **Type**: `Promise<Array<{ plaintext: string; id: string }> | null>`
Expand All @@ -403,9 +430,7 @@ const encryptedPayloads = users.map((user) => ({
}))

// 2) Call bulkDecrypt
const decryptedResults = await bulkDecrypt(encryptedPayloads, {
// lockContext: someLockContext, // if needed
})
const decryptedResults = await bulkDecrypt(encryptedPayloads)

// decryptedResults might look like:
// [
Expand Down
7 changes: 7 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,12 @@
"quoteStyle": "single",
"semicolons": "asNeeded"
}
},
"linter": {
"rules": {
"suspicious": {
"noThenProperty": "off"
}
}
}
}
134 changes: 91 additions & 43 deletions packages/jseql/__tests__/jseql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe('getPlaintext', () => {
})
})

describe('jseql-ffi', () => {
describe('encryption and decryption', () => {
it('should have all required environment variables defined', () => {
expect(process.env.CS_CLIENT_ID).toBeDefined()
expect(process.env.CS_CLIENT_KEY).toBeDefined()
Expand Down Expand Up @@ -144,48 +144,6 @@ describe('jseql-ffi', () => {

expect(plaintext).toEqual(null)
}, 30000)

it('should encrypt and decrypt a payload with lock context', async () => {
const eqlClient = await eql()

const lc = new LockContext()

// TODO: implement lockContext when CTS v2 is deployed
// const lockContext = await lc.identify('users_1_jwt')

const ciphertext = await eqlClient.encrypt('plaintext', {
column: 'column_name',
table: 'users',
})

const plaintext = await eqlClient.decrypt(ciphertext)

expect(plaintext).toEqual('plaintext')
}, 30000)

it('should encrypt with context and be unable to decrypt without correct context', async () => {
const eqlClient = await eql()

const lc = new LockContext()

// const lockContext = await lc.identify('users_1_jwt')

const ciphertext = await eqlClient.encrypt('plaintext', {
column: 'column_name',
table: 'users',
})

const incorrectLc = new LockContext()

// const badLockContext = await incorrectLc.identify('users_2_jwt')

try {
await eqlClient.decrypt(ciphertext)
} catch (error) {
const e = error as Error
expect(e.message.startsWith('Failed to retrieve key')).toEqual(true)
}
}, 30000)
})

describe('bulk encryption', () => {
Expand Down Expand Up @@ -238,3 +196,93 @@ describe('bulk encryption', () => {
expect(plaintexts).toEqual(null)
}, 30000)
})

// ------------------------
// TODO get bulk Encryption/Decryption working in CI.
// These tests pass locally, given you provide a valid JWT.
// To manually test locally, uncomment the following lines and provide a valid JWT in the userJwt variable.
// ------------------------
// const userJwt = ''
// describe('encryption and decryption with lock context', () => {
// it('should encrypt and decrypt a payload with lock context', async () => {
// const eqlClient = await eql()

// const lc = new LockContext()
// const lockContext = await lc.identify(userJwt)

// const ciphertext = await eqlClient
// .encrypt('plaintext', {
// column: 'column_name',
// table: 'users',
// })
// .withLockContext(lockContext)

// const plaintext = await eqlClient
// .decrypt(ciphertext)
// .withLockContext(lockContext)

// expect(plaintext).toEqual('plaintext')
// }, 30000)

// it('should encrypt with context and be unable to decrypt without context', async () => {
// const eqlClient = await eql()

// const lc = new LockContext()
// const lockContext = await lc.identify(userJwt)

// const ciphertext = await eqlClient
// .encrypt('plaintext', {
// column: 'column_name',
// table: 'users',
// })
// .withLockContext(lockContext)

// try {
// await eqlClient.decrypt(ciphertext)
// } catch (error) {
// const e = error as Error
// expect(e.message.startsWith('Failed to retrieve key')).toEqual(true)
// }
// }, 30000)

// it('should bulk encrypt and decrypt a payload with lock context', async () => {
// const eqlClient = await eql()

// const lc = new LockContext()
// const lockContext = await lc.identify(userJwt)

// const ciphertexts = await eqlClient
// .bulkEncrypt(
// [
// {
// plaintext: 'test',
// id: '1',
// },
// {
// plaintext: 'test2',
// id: '2',
// },
// ],
// {
// table: 'users',
// column: 'column_name',
// },
// )
// .withLockContext(lockContext)

// const plaintexts = await eqlClient
// .bulkDecrypt(ciphertexts)
// .withLockContext(lockContext)

// expect(plaintexts).toEqual([
// {
// plaintext: 'test',
// id: '1',
// },
// {
// plaintext: 'test2',
// id: '2',
// },
// ])
// }, 30000)
// })
Loading
Loading