Skip to content

Commit

Permalink
Merge pull request #32 from cipherstash/buld-operations
Browse files Browse the repository at this point in the history
feat: bulk encryptions
  • Loading branch information
calvinbrewer authored Jan 17, 2025
2 parents f3d06e9 + 71e780b commit df2c6d7
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-pets-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/jseql": minor
---

Implemented bulk encryption and decryptions.
6 changes: 6 additions & 0 deletions .changeset/tough-cats-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cipherstash/nextjs": minor
"@cipherstash/jseql": minor
---

Fixed the logtape peer dependency version.
154 changes: 154 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,160 @@ CREATE TABLE users (
);
```

### Buld Encryption/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.
These methods are more efficient and perform better than the single-item encryption/decryption methods.

#### bulkEncrypt

```ts
const encryptedResults = await eqlClient.bulkEncrypt(plaintextsToEncrypt, {
column: 'email',
table: 'Users',
// lockContext: someLockContext, // if you have one
})
```

**Parameters**

1. **`plaintexts`**
- **Type**: `{ plaintext: string; id: string }[]`
- **Description**:
An array of objects containing the **plaintext** and an **id**.
- **plaintext**: The string you want encrypted.
- **id**: A unique identifier you can use to map the returned ciphertext back to its source. For example, if you have a `User` with `id: 1`, you might pass `id: '1'`.

2. **`column`**
- **Type**: `string`
- **Description**:
The name of the column you’re encrypting (e.g., "email"). This is typically used in logging or contextual purposes when constructing the payload for the encryption engine.

3. **`table`**
- **Type**: `string`
- **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>`
- Returns an array of objects, where:
- **`c`** is the ciphertext.
- **`id`** is the same **id** you passed in, so you can correlate which ciphertext matches which original plaintext.
- If `plaintexts` is an empty array, it returns `null`.

### Example Usage

```ts
// 1) Gather your data. For example, a list of users with plaintext fields.
const users = [
{ id: '1', name: 'CJ', email: '[email protected]' },
{ id: '2', name: 'Alex', email: '[email protected]' },
]

// 2) Prepare the array for bulk encryption (only encrypting the "email" field here).
const plaintextsToEncrypt = users.map((user) => ({
plaintext: user.email, // The data to encrypt
id: user.id, // Keep track by user ID
}))

// 3) Call bulkEncrypt
const encryptedResults = await bulkEncrypt(plaintextsToEncrypt, {
column: 'email',
table: 'Users',
// lockContext: someLockContext, // if you have one
})

// encryptedResults might look like:
// [
// { c: 'ENCRYPTED_VALUE_1', id: '1' },
// { c: 'ENCRYPTED_VALUE_2', id: '2' },
// ]

// 4) Reassemble data by matching IDs
if (encryptedResults) {
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
}
})
}
```

---

#### bulkDecrypt

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

**Parameters**

1. **`encryptedPayloads`**
- **Type**: `Array<{ c: string; id: string }> | null`
- **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>`
- Returns an array of objects, where:
- **`plaintext`** is the decrypted value.
- **`id`** is the same **id** you passed in, so you can correlate which plaintext matches which original ciphertext.
- Returns `null` if the provided `encryptedPayloads` is empty or `null`.

### Example Usage

```ts
// Suppose you've retrieved an array of users where their email fields are ciphertext:
const users = [
{ id: '1', name: 'CJ', email: 'ENCRYPTED_VALUE_1' },
{ id: '2', name: 'Alex', email: 'ENCRYPTED_VALUE_2' },
]

// 1) Prepare the array for bulk decryption
const encryptedPayloads = users.map((user) => ({
c: user.email,
id: user.id,
}))

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

// decryptedResults might look like:
// [
// { plaintext: '[email protected]', id: '1' },
// { plaintext: '[email protected]', id: '2' },
// ]

// 3) Reassemble data by matching IDs
if (decryptedResults) {
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
}
})
}
```

## Searchable encrypted data

`jseql` does not currently support searching encrypted data.
Expand Down
60 changes: 59 additions & 1 deletion packages/jseql/__tests__/jseql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ await configure({
loggers: [
{
category: ['jseql'],
level: 'debug',
level: 'info',
sinks: ['console'],
},
],
Expand Down Expand Up @@ -202,3 +202,61 @@ describe('jseql-ffi', () => {
}
}, 30000)
})

describe('bulk encryption', () => {
it('should bulk encrypt and decrypt a payload', async () => {
const eqlClient = await eql()

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

console.log('ct', ciphertexts)

const plaintexts = await eqlClient.bulkDecrypt(ciphertexts)

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

it('should return null if plaintexts is empty', async () => {
const eqlClient = await eql()

const ciphertexts = await eqlClient.bulkEncrypt([], {
table: 'users',
column: 'column_name',
})

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

it('should return null if decrypting empty ciphertexts', async () => {
const eqlClient = await eql()

const ciphertexts = null
const plaintexts = await eqlClient.bulkDecrypt(ciphertexts)

expect(plaintexts).toEqual(null)
}, 30000)
})
2 changes: 1 addition & 1 deletion packages/jseql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"peerDependencies": {
"typescript": "^5.0.0",
"@logtape/logtape": "^0.7.1"
"@logtape/logtape": "^0"
},
"publishConfig": {
"access": "public"
Expand Down
Loading

0 comments on commit df2c6d7

Please sign in to comment.