Skip to content

Commit 99aea91

Browse files
authored
Merge pull request #153 from cipherstash/search-docs
docs: searchable encryption
2 parents 08bc22f + c216588 commit 99aea91

File tree

5 files changed

+318
-21
lines changed

5 files changed

+318
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ CREATE TABLE users (
583583
> The `eql_v2_encrypted` type is a [composite type](https://www.postgresql.org/docs/current/rowtypes.html) and each ORM/client has a different way of handling inserts and selects.
584584
> We've documented how to handle inserts and selects for the different ORMs/clients in the [docs](./docs/reference/working-with-composite-types.md).
585585
586-
Read more about [how to search encrypted data](./docs/how-to/searchable-encryption.md) in the docs.
586+
Read more about [how to search encrypted data](./docs/reference/searchable-encryption.md) in the docs.
587587

588588
## Identity-aware encryption
589589

docs/concepts/searchable-encryption.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Searchable encryption
22

3+
> [!NOTE]
4+
> If you are looking for a reference guide on how to search encrypted data with Protect.js, [click here](../reference/searchable-encryption.md).
5+
36
Protect.js supports searching encrypted data, which enables trusted data access so that you can:
47

58
1. Prove to your customers that you can track exactly what data is being accessed in your application.

docs/how-to/searchable-encryption.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

docs/reference/searchable-encryption-postgres.md

Lines changed: 313 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,330 @@ This reference guide outlines the different query patterns you can use to search
44

55
## Table of contents
66

7-
- [Before you start](#before-you-start)
8-
- [Query examples](#query-examples)
7+
- [Prerequisites](#prerequisites)
8+
- [What is EQL?](#what-is-eql)
9+
- [Setting up your schema](#setting-up-your-schema)
10+
- [Search capabilities](#search-capabilities)
11+
- [Exact matching](#exact-matching)
12+
- [Free text search](#free-text-search)
13+
- [Sorting and range queries](#sorting-and-range-queries)
14+
- [Implementation examples](#implementation-examples)
15+
- [Using Raw PostgreSQL Client (pg)](#using-raw-postgresql-client-pg)
16+
- [Using Supabase SDK](#using-supabase-sdk)
17+
- [Best practices](#best-practices)
18+
- [Common use cases](#common-use-cases)
919

10-
## Before you start
20+
## Prerequisites
1121

12-
You will have needed to [define your schema and initialized the protect client](../../README.md#defining-your-schema), and have [installed the EQL custom types and functions](../../README.md#searchable-encryption-in-postgresql).
22+
Before you can use searchable encryption with PostgreSQL, you need to:
1323

14-
The below examples assume you have a schema defined:
24+
1. Install the [EQL custom types and functions](https://github.com/cipherstash/encrypt-query-language?tab=readme-ov-file#installation)
25+
2. Set up your Protect.js schema with the appropriate search capabilities
1526

16-
```ts
17-
import { csTable, csColumn } from '@cipherstash/protect'
27+
> [!WARNING]
28+
> The formal EQL repo documentation is heavily focused on the underlying custom function implementation.
29+
> It also has a bias towards the [CipherStash Proxy](https://github.com/cipherstash/proxy) product, so this guide is the best place to get started when using Protect.js.
1830
19-
export const protectedUsers = csTable('users', {
20-
email: csColumn('email').equality().freeTextSearch().orderAndRange(),
31+
## What is EQL?
32+
33+
EQL (Encrypt Query Language) is a set of PostgreSQL extensions that enable searching and sorting on encrypted data. It provides:
34+
35+
- Custom data types for storing encrypted data
36+
- Functions for comparing and searching encrypted values
37+
- Support for range queries and sorting on encrypted data
38+
39+
When you install EQL, it adds these capabilities to your PostgreSQL database, allowing Protect.js to perform operations on encrypted data without decrypting it first.
40+
41+
> [!IMPORTANT]
42+
> Any column that is encrypted with EQL must be of type `eql_v2_encrypted` which is included in the EQL extension.
43+
44+
## Setting up your schema
45+
46+
Define your Protect.js schema using `csTable` and `csColumn` to specify how each field should be encrypted and searched:
47+
48+
```typescript
49+
import { protect, csTable, csColumn } from '@cipherstash/protect'
50+
51+
const schema = csTable('users', {
52+
email: csColumn('email_encrypted')
53+
.equality() // Enables exact matching
54+
.freeTextSearch() // Enables text search
55+
.orderAndRange(), // Enables sorting and range queries
56+
phone: csColumn('phone_encrypted')
57+
.equality(), // Only exact matching
58+
age: csColumn('age_encrypted')
59+
.orderAndRange() // Only sorting and range queries
60+
})
61+
```
62+
63+
## The `createSearchTerms` function
64+
65+
The `createSearchTerms` function is used to create search terms used in the SQL query.
66+
67+
The function takes an array of objects, each with the following properties:
68+
69+
| Property | Description |
70+
|----------|-------------|
71+
| `value` | The value to search for |
72+
| `column` | The column to search in |
73+
| `table` | The table to search in |
74+
| `returnType` | The type of return value to expect from the SQL query. Required for PostgreSQL composite types. |
75+
76+
**Return types:**
77+
78+
- `eql` (default) - EQL encrypted payload
79+
- `composite-literal` - EQL encrypted payload wrapped in a composite literal
80+
- `escaped-composite-literal` - EQL encrypted payload wrapped in an escaped composite literal
81+
82+
Example:
83+
84+
```typescript
85+
const term = await protectClient.createSearchTerms([{
86+
value: 'user@example.com',
87+
column: schema.email,
88+
table: schema,
89+
returnType: 'composite-literal'
90+
}, {
91+
value: '18',
92+
column: schema.age,
93+
table: schema,
94+
returnType: 'composite-literal'
95+
}])
96+
97+
if (term.failure) {
98+
// Handle the error
99+
}
100+
101+
console.log(term.data) // array of search terms
102+
```
103+
104+
> [!NOTE]
105+
> As a developer, you must track the index of the search term in the array when using the `createSearchTerms` function.
106+
107+
## Search capabilities
108+
109+
### Exact matching
110+
111+
Use `.equality()` when you need to find exact matches:
112+
113+
```typescript
114+
// Find user with specific email
115+
const term = await protectClient.createSearchTerms([{
116+
value: 'user@example.com',
117+
column: schema.email,
118+
table: schema,
119+
returnType: 'composite-literal' // Required for PostgreSQL composite types
120+
}])
121+
122+
if (term.failure) {
123+
// Handle the error
124+
}
125+
126+
// SQL query
127+
const result = await client.query(
128+
'SELECT * FROM users WHERE email_encrypted = $1',
129+
[term.data[0]]
130+
)
131+
```
132+
133+
### Free text search
134+
135+
Use `.freeTextSearch()` for text-based searches:
136+
137+
```typescript
138+
// Search for users with emails containing "example"
139+
const term = await protectClient.createSearchTerms([{
140+
value: 'example',
141+
column: schema.email,
142+
table: schema,
143+
returnType: 'composite-literal'
144+
}])
145+
146+
if (term.failure) {
147+
// Handle the error
148+
}
149+
150+
// SQL query
151+
const result = await client.query(
152+
'SELECT * FROM users WHERE email_encrypted LIKE $1',
153+
[term.data[0]]
154+
)
155+
```
156+
157+
### Sorting and range queries
158+
159+
Use `.orderAndRange()` for sorting and range operations:
160+
161+
> [!NOTE]
162+
> When using ORDER BY with encrypted columns, you need to use the EQL v2 functions if your PostgreSQL database doesn't support EQL Operator families. For databases that support EQL Operator families, you can use ORDER BY directly with encrypted column names.
163+
164+
```typescript
165+
// Get users sorted by age
166+
const result = await client.query(
167+
'SELECT * FROM users ORDER BY eql_v2.ore_block_u64_8_256(age_encrypted) ASC'
168+
)
169+
```
170+
171+
## Implementation examples
172+
173+
### Using Raw PostgreSQL Client (pg)
174+
175+
```typescript
176+
import { Client } from 'pg'
177+
import { protect, csTable, csColumn } from '@cipherstash/protect'
178+
179+
const schema = csTable('users', {
180+
email: csColumn('email_encrypted')
181+
.equality()
182+
.freeTextSearch()
183+
.orderAndRange()
21184
})
185+
186+
const client = new Client({
187+
// your connection details
188+
})
189+
190+
const protectClient = await protect({
191+
schemas: [schema]
192+
})
193+
194+
// Insert encrypted data
195+
const encryptedData = await protectClient.encryptModel({
196+
email: 'user@example.com'
197+
}, schema)
198+
199+
if (encryptedData.failure) {
200+
// Handle the error
201+
}
202+
203+
await client.query(
204+
'INSERT INTO users (email_encrypted) VALUES ($1::jsonb)',
205+
[encryptedData.data.email_encrypted]
206+
)
207+
208+
// Search encrypted data
209+
const searchTerm = await protectClient.createSearchTerms([{
210+
value: 'example.com',
211+
column: schema.email,
212+
table: schema,
213+
returnType: 'composite-literal'
214+
}])
215+
216+
if (searchTerm.failure) {
217+
// Handle the error
218+
}
219+
220+
const result = await client.query(
221+
'SELECT * FROM users WHERE email_encrypted LIKE $1',
222+
[searchTerm.data[0]]
223+
)
224+
225+
// Decrypt results
226+
const decryptedData = await protectClient.bulkDecryptModels(result.rows)
22227
```
23228

24-
> [!TIP]
25-
> To see an example using the [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) see the example [here](../../examples/drizzle/src/select.ts).
229+
### Using Supabase SDK
230+
231+
For Supabase users, we provide a specific implementation guide. [Read more about using Protect.js with Supabase](./supabase-sdk.md).
232+
233+
## Best practices
234+
235+
1. **Schema Design**
236+
- Choose the right search capabilities for each field:
237+
- Use `.equality()` for exact matches (most efficient)
238+
- Use `.freeTextSearch()` for text-based searches (more expensive)
239+
- Use `.orderAndRange()` for numerical data and sorting (most expensive)
240+
- Only enable features you need to minimize performance impact
241+
- Use `eql_v2_encrypted` column type in your database schema for encrypted columns
242+
243+
2. **Security Considerations**
244+
- Never store unencrypted sensitive data
245+
- Keep your CipherStash secrets secure
246+
- Use parameterized queries to prevent SQL injection
247+
248+
3. **Performance**
249+
- Index your encrypted columns appropriately
250+
- Monitor query performance
251+
- Consider the impact of search operations on your database
252+
- Use bulk operations when possible
253+
- Cache frequently accessed data
254+
255+
4. **Error Handling**
256+
- Always check for failures with any Protect.js method
257+
- Handle encryption errors aggressively
258+
- Handle decryption errors gracefully
259+
260+
## Common use cases
261+
262+
### Combining multiple search conditions
263+
264+
```typescript
265+
// Search for users with specific email domain and age range
266+
const terms = await protectClient.createSearchTerms([
267+
{
268+
value: 'example.com',
269+
column: schema.email,
270+
table: schema,
271+
returnType: 'composite-literal'
272+
},
273+
{
274+
value: '18',
275+
column: schema.age,
276+
table: schema,
277+
returnType: 'composite-literal'
278+
}
279+
])
280+
281+
if (terms.failure) {
282+
// Handle the error
283+
}
284+
285+
const result = await client.query(
286+
'SELECT * FROM users WHERE email_encrypted LIKE $1 AND eql_v2.ore_block_u64_8_256(age_encrypted) > $2',
287+
[terms.data[0], terms.data[1]]
288+
)
289+
```
290+
291+
### Performance optimization
292+
293+
1. **Use appropriate indexes**
294+
```sql
295+
CREATE INDEX idx_users_email ON users USING btree (email_encrypted);
296+
CREATE INDEX idx_users_age ON users USING btree (eql_v2.ore_block_u64_8_256(age_encrypted));
297+
```
298+
299+
2. **Cache frequently accessed data**
300+
```typescript
301+
// Example using Redis
302+
const cacheKey = `user:${userId}`
303+
let user = await redis.get(cacheKey)
304+
305+
if (!user) {
306+
const result = await client.query('SELECT * FROM users WHERE id = $1', [userId])
307+
user = result.rows[0]
308+
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600)
309+
}
310+
```
26311

27-
## Query examples
312+
### Common pitfalls to avoid
28313

29-
TODO: flesh this out (sorry it's not done yet!)
314+
1. **Don't mix encrypted and unencrypted data when data is encrypted**
315+
```sql
316+
-- ❌ Wrong
317+
SELECT * FROM users WHERE email = 'user@example.com'
318+
319+
-- ✅ Correct
320+
SELECT * FROM users WHERE email_encrypted = $1
321+
```
30322

31-
---
323+
2. **Don't use ORDER BY directly on encrypted columns**
324+
```sql
325+
-- ❌ Wrong
326+
SELECT * FROM users ORDER BY email_encrypted
327+
328+
-- ✅ Correct
329+
SELECT * FROM users ORDER BY eql_v2.ore_block_u64_8_256(age_encrypted)
330+
```
32331

33332
### Didn't find what you wanted?
34333

packages/protect/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ CREATE TABLE users (
583583
> The `eql_v2_encrypted` type is a [composite type](https://www.postgresql.org/docs/current/rowtypes.html) and each ORM/client has a different way of handling inserts and selects.
584584
> We've documented how to handle inserts and selects for the different ORMs/clients in the [docs](./docs/reference/working-with-composite-types.md).
585585
586-
Read more about [how to search encrypted data](./docs/how-to/searchable-encryption.md) in the docs.
586+
Read more about [how to search encrypted data](./docs/reference/searchable-encryption.md) in the docs.
587587

588588
## Identity-aware encryption
589589

0 commit comments

Comments
 (0)