Skip to content

Commit 3bac078

Browse files
authored
Merge pull request #134 from cipherstash/spike/dynamo-db
Add DynamoDB examples
2 parents d003709 + 563dbe3 commit 3bac078

27 files changed

+2579
-30
lines changed

.changeset/fifty-toes-carry.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cipherstash/protect-dynamodb": minor
3+
"@cipherstash/protect": minor
4+
"@cipherstash/dynamo-example": minor
5+
---
6+
7+
Released initial version of the DynamoDB helper interface.

examples/dynamo/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
docker
2+
sql/cipherstash-encrypt.sql

examples/dynamo/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# DynamoDB Examples
2+
3+
Examples of using Protect.js with DynamoDB.
4+
5+
## Prereqs
6+
- [Node.js](https://nodejs.org/en) (tested with v22.11.0)
7+
- [pnpm](https://pnpm.io/) (tested with v9.15.3)
8+
- [Docker](https://www.docker.com/)
9+
- a CipherStash account and [credentials configured](../../README.md#configuration)
10+
11+
## Setup
12+
13+
Install the workspace dependencies and build Protect.js:
14+
```
15+
# change to the workspace root directory
16+
cd ../..
17+
18+
pnpm install
19+
pnpm run build
20+
```
21+
22+
Switch back to the DynamoDB examples
23+
```
24+
cd examples/dynamo
25+
```
26+
27+
Start Docker services used by the DynamoDB examples:
28+
```
29+
docker compose up --detach
30+
```
31+
32+
Download [EQL](https://github.com/cipherstash/encrypt-query-language) and install it into the PG DB (this is optional and only necessary for running the `export-to-pg` example):
33+
```
34+
pnpm run eql:download
35+
pnpm run eql:install
36+
```
37+
38+
## Examples
39+
40+
All examples run as scripts from [`package.json`](./package.json).
41+
You can run an example with the command `pnpm run [example_name]`.
42+
43+
Each example runs against local DynamoDB in Docker.
44+
45+
- `simple`
46+
- `pnpm run simple`
47+
- Round trip encryption/decryption through DynamoDB (no search on encrypted attributes).
48+
- `encrypted-partition-key`
49+
- `pnpm run encrypted-partition-key`
50+
- Uses an encrypted attribute as a partition key.
51+
- `encrypted-sort-key`
52+
- `pnpm run encrypted-sort-key`
53+
- Similar to the `encrypted-partition-key` example, but uses an encrypted attribute as a sort key instead.
54+
- `encrypted-key-in-gsi`
55+
- `pnpm run encrypted-key-in-gsi`
56+
- Uses an encrypted attribute as the partition key in a global secondary index.
57+
The source ciphertext is projected into the index for decryption after querying the index.
58+
- `export-to-pg`
59+
- `pnpm run export-to-pg`
60+
- Encrypts an item, puts it in Dynamo, exports it to Postgres, and decrypts a result from Postgres.

examples/dynamo/docker-compose.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
services:
3+
dynamodb-local:
4+
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
5+
image: "amazon/dynamodb-local:latest"
6+
container_name: dynamodb-local
7+
ports:
8+
- "8000:8000"
9+
volumes:
10+
- "./docker/dynamodb:/home/dynamodblocal/data"
11+
working_dir: /home/dynamodblocal
12+
13+
dynamodb-admin:
14+
image: aaronshaf/dynamodb-admin
15+
ports:
16+
- 8001:8001
17+
environment:
18+
DYNAMO_ENDPOINT: http://dynamodb-local:8000
19+
20+
# used by export-to-pg example
21+
postgres:
22+
image: postgres:latest
23+
environment:
24+
PGPORT: 5432
25+
POSTGRES_DB: "cipherstash"
26+
POSTGRES_USER: "cipherstash"
27+
PGUSER: "cipherstash"
28+
POSTGRES_PASSWORD: password
29+
ports:
30+
- 5433:5432
31+
deploy:
32+
resources:
33+
limits:
34+
cpus: "${CPU_LIMIT:-2}"
35+
memory: 2048mb
36+
restart: always
37+
healthcheck:
38+
test: [ "CMD-SHELL", "pg_isready" ]
39+
interval: 1s
40+
timeout: 5s
41+
retries: 10

examples/dynamo/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@cipherstash/dynamo-example",
3+
"private": true,
4+
"version": "0.1.0",
5+
"type": "module",
6+
"scripts": {
7+
"simple": "tsx src/simple.ts",
8+
"bulk-operations": "tsx src/bulk-operations.ts",
9+
"encrypted-partition-key": "tsx src/encrypted-partition-key.ts",
10+
"encrypted-sort-key": "tsx src/encrypted-sort-key.ts",
11+
"encrypted-key-in-gsi": "tsx src/encrypted-key-in-gsi.ts",
12+
"export-to-pg": "tsx src/export-to-pg.ts",
13+
"eql:download": "curl -sLo sql/cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/download/eql-2.0.2/cipherstash-encrypt.sql",
14+
"eql:install": "cat sql/cipherstash-encrypt.sql | docker exec -i dynamo-postgres-1 psql postgresql://cipherstash:password@postgres:5432/cipherstash -f-"
15+
},
16+
"keywords": [],
17+
"author": "",
18+
"license": "ISC",
19+
"description": "",
20+
"dependencies": {
21+
"@aws-sdk/client-dynamodb": "^3.817.0",
22+
"@aws-sdk/lib-dynamodb": "^3.817.0",
23+
"@aws-sdk/util-dynamodb": "^3.817.0",
24+
"@cipherstash/protect": "workspace:*",
25+
"@cipherstash/protect-dynamodb": "workspace:*",
26+
"pg": "^8.13.1"
27+
},
28+
"devDependencies": {
29+
"@types/pg": "^8.11.10",
30+
"tsx": "catalog:repo",
31+
"typescript": "catalog:repo"
32+
}
33+
}

examples/dynamo/sql/.gitkeep

Whitespace-only changes.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { dynamoClient, docClient, createTable } from './common/dynamo'
2+
import { log } from './common/log'
3+
import { users, protectClient } from './common/protect'
4+
import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb'
5+
import { protectDynamoDB } from '@cipherstash/protect-dynamodb'
6+
7+
const tableName = 'UsersBulkOperations'
8+
9+
type User = {
10+
pk: string
11+
email: string
12+
}
13+
14+
const main = async () => {
15+
await createTable({
16+
TableName: tableName,
17+
AttributeDefinitions: [
18+
{
19+
AttributeName: 'pk',
20+
AttributeType: 'S',
21+
},
22+
],
23+
KeySchema: [
24+
{
25+
AttributeName: 'pk',
26+
KeyType: 'HASH',
27+
},
28+
],
29+
})
30+
31+
const protectDynamo = protectDynamoDB({
32+
protectClient,
33+
})
34+
35+
const items = [
36+
{
37+
// `pk` won't be encrypted because it's not included in the `users` protected table schema.
38+
pk: 'user#1',
39+
// `email` will be encrypted because it's included in the `users` protected table schema.
40+
email: 'abc@example.com',
41+
},
42+
{
43+
pk: 'user#2',
44+
email: 'def@example.com',
45+
},
46+
]
47+
48+
const encryptResult = await protectDynamo.bulkEncryptModels(items, users)
49+
50+
if (encryptResult.failure) {
51+
throw new Error(`Failed to encrypt items: ${encryptResult.failure.message}`)
52+
}
53+
54+
const putRequests = encryptResult.data.map(
55+
(item: Record<string, unknown>) => ({
56+
PutRequest: {
57+
Item: item,
58+
},
59+
}),
60+
)
61+
62+
log('encrypted items', encryptResult)
63+
64+
const batchWriteCommand = new BatchWriteCommand({
65+
RequestItems: {
66+
[tableName]: putRequests,
67+
},
68+
})
69+
70+
await dynamoClient.send(batchWriteCommand)
71+
72+
const batchGetCommand = new BatchGetCommand({
73+
RequestItems: {
74+
[tableName]: {
75+
Keys: [{ pk: 'user#1' }, { pk: 'user#2' }],
76+
},
77+
},
78+
})
79+
80+
const getResult = await docClient.send(batchGetCommand)
81+
82+
const decryptedItems = await protectDynamo.bulkDecryptModels<User>(
83+
getResult.Responses?.[tableName],
84+
users,
85+
)
86+
87+
log('decrypted items', decryptedItems)
88+
}
89+
90+
main()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
CreateTableCommand,
3+
DynamoDBClient,
4+
type CreateTableCommandInput,
5+
} from '@aws-sdk/client-dynamodb'
6+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
7+
8+
export const dynamoClient = new DynamoDBClient({
9+
credentials: {
10+
accessKeyId: 'fakeAccessKeyId',
11+
secretAccessKey: 'fakeSecretAccessKey',
12+
},
13+
endpoint: 'http://localhost:8000',
14+
})
15+
16+
export const docClient = DynamoDBDocumentClient.from(dynamoClient)
17+
18+
// Creates a table with provisioned throughput set to 5 RCU and 5 WCU.
19+
// Ignores `ResourceInUseException`s if the table already exists.
20+
export async function createTable(
21+
input: Omit<CreateTableCommandInput, 'ProvisionedThroughPut'>,
22+
) {
23+
const command = new CreateTableCommand({
24+
ProvisionedThroughput: {
25+
ReadCapacityUnits: 5,
26+
WriteCapacityUnits: 5,
27+
},
28+
...input,
29+
})
30+
31+
try {
32+
await docClient.send(command)
33+
} catch (err) {
34+
if (err?.name! !== 'ResourceInUseException') {
35+
throw err
36+
}
37+
}
38+
}

examples/dynamo/src/common/log.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function log(description: string, data: unknown) {
2+
console.log(`\n${description}:\n${JSON.stringify(data, null, 2)}`)
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { protect, csColumn, csTable } from '@cipherstash/protect'
2+
3+
export const users = csTable('users', {
4+
email: csColumn('email').equality(),
5+
})
6+
7+
export const protectClient = await protect({
8+
schemas: [users],
9+
})

0 commit comments

Comments
 (0)