Skip to content

Commit 3ca6418

Browse files
authored
Merge pull request #126 from cipherstash/eql-v2
feat(protect): implement eql v2
2 parents ccd2a40 + 53024ec commit 3ca6418

File tree

14 files changed

+102
-145
lines changed

14 files changed

+102
-145
lines changed

.changeset/shiny-jeans-clean.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@cipherstash/protect": major
3+
---
4+
5+
Implement EQL v2 data structure.
6+
7+
- Support for Protect.js searchable encryption when using Supabase.
8+
- Encrypted payloads are now composite types which support searchable encryption with EQL v2 functions.
9+
- The `data` property is an object that matches the EQL v2 data structure.

apps/drizzle/src/db/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {
99

1010
// Custom types will be implemented in the future - this is an example for now
1111
// ---
12-
// const cs_encrypted_v1 = <TData>(name: string) =>
12+
// const cs_encrypted_v2 = <TData>(name: string) =>
1313
// customType<{ data: TData; driverData: string }>({
1414
// dataType() {
15-
// return 'cs_encrypted_v1'
15+
// return 'cs_encrypted_v2'
1616
// },
1717
// toDriver(value: TData): string {
1818
// return JSON.stringify(value)

apps/drizzle/src/select.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,19 @@ const fnForOp: (op: string) => BinaryOperator = (op) => {
4444
}
4545

4646
const csEq: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => {
47-
return sql`cs_unique_v1(${left}) = cs_unique_v1(${bindIfParam(right, left)})`
47+
return sql`cs_unique_v2(${left}) = cs_unique_v2(${bindIfParam(right, left)})`
4848
}
4949

5050
const csGt: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => {
51-
return sql`cs_ore_64_8_v1(${left}) > cs_ore_64_8_v1(${bindIfParam(right, left)})`
51+
return sql`cs_ore_64_8_v2(${left}) > cs_ore_64_8_v2(${bindIfParam(right, left)})`
5252
}
5353

5454
const csLt: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => {
55-
return sql`cs_ore_64_8_v1(${left}) < cs_ore_64_8_v1(${bindIfParam(right, left)})`
55+
return sql`cs_ore_64_8_v2(${left}) < cs_ore_64_8_v2(${bindIfParam(right, left)})`
5656
}
5757

5858
const csMatch: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => {
59-
return sql`cs_match_v1(${left}) @> cs_match_v1(${bindIfParam(right, left)})`
59+
return sql`cs_match_v2(${left}) @> cs_match_v2(${bindIfParam(right, left)})`
6060
}
6161

6262
const filterInput = await protectClient.encrypt(filter, {
@@ -76,7 +76,7 @@ const query = db
7676
})
7777
.from(users)
7878
.where(filterFn(users.email_encrypted, filterInput.data))
79-
.orderBy(sql`cs_ore_64_8_v1(users.email_encrypted)`)
79+
.orderBy(sql`cs_ore_64_8_v2(users.email_encrypted)`)
8080

8181
const sqlResult = query.toSQL()
8282
console.log('[INFO] SQL statement:', sqlResult)

docs/concepts/searchable-encryption.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ if (encryptedParam.failure) {
8282
const equalitySQL = `
8383
SELECT email
8484
FROM users
85-
WHERE cs_unique_v1($1) = cs_unique_v1($2)
85+
WHERE cs_unique_v2($1) = cs_unique_v2($2)
8686
`
8787

8888
// 3) Execute the query, passing in the Postgres column name
@@ -96,7 +96,7 @@ Using the above approach, Protect.js is generating the EQL payloads and which me
9696
So does this solve the original problem of searching on encrypted data?
9797

9898
```sql
99-
# SELECT * FROM users WHERE WHERE cs_unique_v1(email) = cs_unique_v1(eql_payload_created_by_protect);
99+
# SELECT * FROM users WHERE WHERE cs_unique_v2(email) = cs_unique_v2(eql_payload_created_by_protect);
100100
id | name | email
101101
----+----------------+----------------------------
102102
1 | Alice Johnson | mBbKmsMMkbKBSN...
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# How to search encrypted data
2+
3+
TODO: flesh this out (sorry it's not done yet!)
4+
5+
In the meantime checkout the [EQL repo](https://github.com/cipherstash/encrypt-query-language) which is where these docs will get their inspiration from specifically for JavaScript/TypeScript implementations.

docs/reference/searchable-encryption-postgres.md

Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ This reference guide outlines the different query patterns you can use to search
55
## Table of contents
66

77
- [Before you start](#before-you-start)
8-
- [Equality query](#equality-query)
9-
- [Free-text search](#free-text-search)
10-
- [Sorting data](#sorting-data)
11-
- [Range queries](#range-queries)
8+
- [Query examples](#query-examples)
129

1310
## Before you start
1411

@@ -27,96 +24,7 @@ export const protectedUsers = csTable('users', {
2724
> [!TIP]
2825
> To see an example using the [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) see the example [here](../../apps/drizzle/src/select.ts).
2926
30-
## Equality query
31-
32-
For an equality query, use the `cs_unique_v1` function.
33-
34-
```ts
35-
// 1) Encrypt the search term
36-
const searchTerm = 'alice@example.com'
37-
38-
const encryptedParam = await protectClient.encrypt(searchTerm, {
39-
column: protectedUsers.email, // Your Protect column definition
40-
table: protectedUsers, // Reference to the table schema
41-
})
42-
43-
if (encryptedParam.failure) {
44-
// Handle the failure
45-
}
46-
47-
// 2) Build an equality query (cs_unique_v1)
48-
const equalitySQL = `
49-
SELECT email
50-
FROM users
51-
WHERE cs_unique_v1($1) = cs_unique_v1($2)
52-
`
53-
54-
// Use your flavor or ORM to execute the query. `client` is a PostgreSQL client.
55-
const result = await client.query(equalitySQL, [ protectedUser.email.getName(), encryptedParam.data ])
56-
```
57-
58-
**Explanation:**
59-
60-
`WHERE cs_unique_v1($1) = cs_unique_v1($2)`
61-
62-
The first `$1` is the Postgres column name, and the second `$2` is the encrypted search term.
63-
64-
## Free-text search
65-
66-
For partial matches or full-text searches, use the `cs_match_v1` function.
67-
68-
```ts
69-
// Suppose you're searching for emails containing "alice"
70-
const searchTerm = 'alice'
71-
72-
const encryptedParam = await protectClient.encrypt(searchTerm, {
73-
column: protectedUsers.email,
74-
table: protectedUsers,
75-
})
76-
77-
if (encryptedParam.failure) {
78-
// Handle the failure
79-
}
80-
81-
// Build and execute a "match" query (cs_match_v1)
82-
const matchSQL = `
83-
SELECT email
84-
FROM users
85-
WHERE cs_match_v1($1) @> cs_match_v1($2)
86-
`
87-
88-
// Use your flavor or ORM to execute the query. `client` is a PostgreSQL client.
89-
const result = await client.query(matchSQL, [ protectedUser.email.getName(), encryptedParam.data ])
90-
```
91-
92-
**Explanation:**
93-
94-
`WHERE cs_match_v1($1) @> cs_match_v1($2)`
95-
96-
The first `$1` is the Postgres column name, and the second `$2` is the encrypted search term.
97-
98-
## Sorting data
99-
100-
For `order by` queries, use the `cs_ore_64_8_v1` function.
101-
102-
```ts
103-
// Suppose you're sorting by email address
104-
const orderSQL = `
105-
SELECT email
106-
FROM users
107-
ORDER BY cs_ore_64_8_v1(email) ASC
108-
`
109-
const orderedResult = await client.query(orderSQL)
110-
111-
// Use your flavor or ORM to execute the query. `client` is a PostgreSQL client.
112-
const result = await client.query(orderSQL)
113-
```
114-
115-
### Explanation
116-
117-
The `cs_ore_64_8_v1` function doesn't require any encrypted parameters, but rather the name of the Postgres column you want to sort by.
118-
119-
## Range queries
27+
## Query examples
12028

12129
TODO: flesh this out (sorry it's not done yet!)
12230

packages/protect/README.md

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ yarn add @cipherstash/protect
9494
pnpm add @cipherstash/protect
9595
```
9696

97-
> [!TIP]
97+
> [!TIP]
9898
> [Bun](https://bun.sh/) is not currently supported due to a lack of [Node-API compatibility](https://github.com/oven-sh/bun/issues/158). Under the hood, Protect.js uses [CipherStash Client](#cipherstash-client) which is written in Rust and embedded using [Neon](https://github.com/neon-bindings/neon).
9999
100100
Lastly, install the CipherStash CLI:
@@ -111,7 +111,7 @@ Lastly, install the CipherStash CLI:
111111

112112
### Opt-out of bundling
113113

114-
> [!IMPORTANT]
114+
> [!IMPORTANT]
115115
> **You need to opt-out of bundling when using Protect.js.**
116116
117117
Protect.js uses Node.js specific features and requires the use of the [native Node.js `require`](https://nodejs.org/api/modules.html#requireid).
@@ -122,9 +122,8 @@ Read more about [building and bundling with Protect.js](#builds-and-bundling).
122122

123123
## Getting started
124124

125-
🆕 **Existing app?** Skip to [the next step](#configuration).
126-
127-
🌱 **Clean slate?** Check out the [getting started tutorial](./docs/getting-started.md).
125+
- 🆕 **Existing app?** Skip to [the next step](#configuration).
126+
- 🌱 **Clean slate?** Check out the [getting started tutorial](./docs/getting-started.md).
128127

129128
### Configuration
130129

@@ -244,8 +243,7 @@ if (encryptResult.failure) {
244243
);
245244
}
246245

247-
const ciphertext = encryptResult.data;
248-
console.log("ciphertext:", ciphertext);
246+
console.log("EQL Payload containing ciphertexts:", encryptResult.data);
249247
```
250248

251249
The `encrypt` function will return a `Result` object with either a `data` key, or a `failure` key.
@@ -254,9 +252,7 @@ The `encryptResult` will return one of the following:
254252
```typescript
255253
// Success
256254
{
257-
data: {
258-
c: 'mBbKmsMMkbKBSN}s1THy_NfQN892!dercyd0s...'
259-
}
255+
data: EncryptedPayload
260256
}
261257

262258
// Failure
@@ -278,7 +274,8 @@ To start decrypting data, add the following to `src/index.ts`:
278274
```typescript
279275
import { protectClient } from "./protect";
280276

281-
const decryptResult = await protectClient.decrypt(ciphertext);
277+
// encryptResult is the EQL payload from the previous step
278+
const decryptResult = await protectClient.decrypt(encryptResult.data);
282279

283280
if (decryptResult.failure) {
284281
// Handle the failure
@@ -313,7 +310,7 @@ The `decryptResult` will return one of the following:
313310

314311
### Working with models and objects
315312

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

319316
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.
@@ -354,12 +351,12 @@ const encryptedUser = encryptedResult.data;
354351
console.log("encrypted user:", encryptedUser);
355352
```
356353

357-
The `encryptModel` function will only encrypt fields that are defined in your schema.
354+
The `encryptModel` function will only encrypt fields that are defined in your schema.
358355
Other fields (like `id` and `createdAt` in the example above) will remain unchanged.
359356

360357
#### Type safety with models
361358

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

365362
```typescript
@@ -523,7 +520,7 @@ if (decryptedResult.failure) {
523520
const decryptedUsers = decryptedResult.data;
524521
```
525522

526-
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.
523+
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.
527524
They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema.
528525

529526
### Store encrypted data in a database
@@ -549,21 +546,36 @@ To enable searchable encryption in PostgreSQL, [install the EQL custom types and
549546
curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql
550547
```
551548

549+
Using [Supabase](https://supabase.com/)? We ship an EQL release specifically for Supabase.
550+
Download the latest EQL install script:
551+
552+
```sh
553+
curl -sLo cipherstash-encrypt-supabase.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt-supabase.sql
554+
```
555+
552556
2. Run this command to install the custom types and functions:
553557

554558
```sh
555559
psql -f cipherstash-encrypt.sql
556560
```
557561

558-
EQL is now installed in your database and you can enable searchable encryption by adding the `cs_encrypted_v1` type to a column.
562+
or with Supabase:
563+
564+
```sh
565+
psql -f cipherstash-encrypt-supabase.sql
566+
```
567+
568+
EQL is now installed in your database and you can enable searchable encryption by adding the `eql_v2_encrypted` type to a column.
559569

560570
```sql
561571
CREATE TABLE users (
562572
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
563-
email cs_encrypted_v1
573+
email eql_v2_encrypted
564574
);
565575
```
566576

577+
Read more about [how to search encrypted data](./docs/how-to/searchable-encryption.md) in the docs.
578+
567579
## Identity-aware encryption
568580

569581
> [!IMPORTANT]
@@ -631,7 +643,7 @@ if (encryptResult.failure) {
631643
// Handle the failure
632644
}
633645

634-
const ciphertext = encryptResult.data;
646+
console.log("EQL Payload containing ciphertexts:", encryptResult.data);
635647
```
636648

637649
### Decrypting data with a lock context
@@ -642,7 +654,7 @@ To decrypt data with a lock context, call the optional `withLockContext` method
642654
import { protectClient } from "./protect";
643655

644656
const decryptResult = await protectClient
645-
.decrypt(ciphertext)
657+
.decrypt(encryptResult.data)
646658
.withLockContext(lockContext);
647659

648660
if (decryptResult.failure) {

packages/protect/__tests__/protect.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,8 @@ describe('performance', () => {
467467

468468
// ------------------------
469469
// TODO get LockContext working in CI.
470-
// These tests pass locally, given you provide a valid JWT.
471470
// To manually test locally, uncomment the following lines and provide a valid JWT in the userJwt variable.
471+
// Last successful local test was 2025-05-20 by cj@cipherstash.com
472472
// ------------------------
473473
// const userJwt = ''
474474
// describe('encryption and decryption with lock context', () => {

packages/protect/generateEqlSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execa } from 'execa'
33

44
async function main() {
55
const url =
6-
'https://raw.githubusercontent.com/cipherstash/encrypt-query-language/main/sql/schemas/cs_encrypted_storage_v1.schema.json'
6+
'https://raw.githubusercontent.com/cipherstash/encrypt-query-language/main/sql/schemas/cs_encrypted_storage_v2.schema.json'
77

88
const response = await fetch(url)
99

0 commit comments

Comments
 (0)