Skip to content

Commit 517253d

Browse files
authored
Merge pull request #158 from cipherstash/bug/dynamo-missing-fields
2 parents 9d49155 + 2b63ee1 commit 517253d

File tree

7 files changed

+441
-37
lines changed

7 files changed

+441
-37
lines changed

.changeset/eighty-items-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cipherstash/protect-dynamodb": minor
3+
---
4+
5+
Support nested protect schema in dynamodb helper functions.

.changeset/kind-symbols-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cipherstash/protect-dynamodb": minor
3+
---
4+
5+
Fixed bug when handling schema definitions without an equality flag.

.github/workflows/tests.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ jobs:
4141
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> ./packages/protect/.env
4242
echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> ./packages/protect/.env
4343
44+
- name: Create .env file in ./packages/protect-dynamodb/
45+
run: |
46+
touch ./packages/protect-dynamodb/.env
47+
echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect-dynamodb/.env
48+
echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect-dynamodb/.env
49+
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect-dynamodb/.env
50+
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect-dynamodb/.env
51+
4452
# Run TurboRepo tests
4553
- name: Run tests
4654
run: pnpm run test
Lines changed: 292 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,296 @@
1-
import { describe, expect, it } from 'vitest'
1+
import 'dotenv/config'
2+
import { describe, expect, it, beforeAll } from 'vitest'
3+
import { protectDynamoDB } from '../src'
4+
import { protect, csColumn, csTable, csValue } from '@cipherstash/protect'
5+
6+
const schema = csTable('dynamo_cipherstash_test', {
7+
email: csColumn('email').equality(),
8+
firstName: csColumn('firstName').equality(),
9+
lastName: csColumn('lastName').equality(),
10+
phoneNumber: csColumn('phoneNumber'),
11+
example: {
12+
protected: csValue('example.protected'),
13+
deep: {
14+
protected: csValue('example.deep.protected'),
15+
},
16+
},
17+
})
218

319
describe('protect dynamodb helpers', () => {
4-
it('should say hello', () => {
5-
expect(true).toBe(true)
20+
let protectClient: Awaited<ReturnType<typeof protect>>
21+
let protectDynamo: ReturnType<typeof protectDynamoDB>
22+
23+
beforeAll(async () => {
24+
protectClient = await protect({
25+
schemas: [schema],
26+
})
27+
28+
protectDynamo = protectDynamoDB({
29+
protectClient,
30+
})
31+
})
32+
33+
it('should encrypt columns', async () => {
34+
const testData = {
35+
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
36+
email: 'test.user@example.com',
37+
address: '123 Main Street',
38+
createdAt: '2024-08-15T22:14:49.948Z',
39+
firstName: 'John',
40+
lastName: 'Smith',
41+
phoneNumber: '555-555-5555',
42+
companyName: 'Acme Corp',
43+
batteryBrands: ['Brand1', 'Brand2'],
44+
metadata: { role: 'admin' },
45+
example: {
46+
protected: 'hello world',
47+
notProtected: 'I am not protected',
48+
deep: {
49+
protected: 'deep protected',
50+
notProtected: 'deep not protected',
51+
},
52+
},
53+
}
54+
55+
const result = await protectDynamo.encryptModel(testData, schema)
56+
if (result.failure) {
57+
throw new Error(`Encryption failed: ${result.failure.message}`)
58+
}
59+
60+
const encryptedData = result.data
61+
62+
// Verify equality columns are encrypted
63+
expect(encryptedData).toHaveProperty('email__source')
64+
expect(encryptedData).toHaveProperty('email__hmac')
65+
expect(encryptedData).toHaveProperty('firstName__source')
66+
expect(encryptedData).toHaveProperty('firstName__hmac')
67+
expect(encryptedData).toHaveProperty('lastName__source')
68+
expect(encryptedData).toHaveProperty('lastName__hmac')
69+
expect(encryptedData).toHaveProperty('phoneNumber__source')
70+
expect(encryptedData).not.toHaveProperty('phoneNumber__hmac')
71+
expect(encryptedData.example).toHaveProperty('protected__source')
72+
expect(encryptedData.example.deep).toHaveProperty('protected__source')
73+
74+
// Verify other fields remain unchanged
75+
expect(encryptedData.id).toBe('01ABCDEFGHIJKLMNOPQRSTUVWX')
76+
expect(encryptedData.address).toBe('123 Main Street')
77+
expect(encryptedData.createdAt).toBe('2024-08-15T22:14:49.948Z')
78+
expect(encryptedData.companyName).toBe('Acme Corp')
79+
expect(encryptedData.batteryBrands).toEqual(['Brand1', 'Brand2'])
80+
expect(encryptedData.example.notProtected).toBe('I am not protected')
81+
expect(encryptedData.example.deep.notProtected).toBe('deep not protected')
82+
expect(encryptedData.metadata).toEqual({ role: 'admin' })
83+
})
84+
85+
it('should handle null and undefined values', async () => {
86+
const testData = {
87+
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
88+
email: null,
89+
firstName: undefined,
90+
lastName: 'Smith',
91+
phoneNumber: null,
92+
metadata: { role: null },
93+
example: {
94+
protected: null,
95+
notProtected: 'I am not protected',
96+
deep: {
97+
protected: undefined,
98+
notProtected: 'deep not protected',
99+
},
100+
},
101+
}
102+
103+
const result = await protectDynamo.encryptModel(testData, schema)
104+
if (result.failure) {
105+
throw new Error(`Encryption failed: ${result.failure.message}`)
106+
}
107+
108+
const encryptedData = result.data
109+
110+
// Verify null/undefined equality columns are handled
111+
expect(encryptedData).toHaveProperty('lastName__source')
112+
expect(encryptedData).toHaveProperty('lastName__hmac')
113+
114+
// Verify other fields remain unchanged
115+
expect(encryptedData.id).toBe('01ABCDEFGHIJKLMNOPQRSTUVWX')
116+
expect(encryptedData.phoneNumber).toBeNull()
117+
expect(encryptedData.email).toBeNull()
118+
expect(encryptedData.firstName).toBeUndefined()
119+
expect(encryptedData.metadata).toEqual({ role: null })
120+
expect(encryptedData.example.protected).toBeNull()
121+
expect(encryptedData.example.deep.protected).toBeUndefined()
122+
expect(encryptedData.example.deep.notProtected).toBe('deep not protected')
123+
})
124+
125+
it('should handle empty strings and special characters', async () => {
126+
const testData = {
127+
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
128+
email: '',
129+
firstName: 'John!@#$%^&*()',
130+
lastName: 'Smith ',
131+
phoneNumber: '',
132+
metadata: { role: 'admin!@#$%^&*()' },
133+
}
134+
135+
const result = await protectDynamo.encryptModel(testData, schema)
136+
if (result.failure) {
137+
throw new Error(`Encryption failed: ${result.failure.message}`)
138+
}
139+
140+
const encryptedData = result.data
141+
142+
// Verify equality columns are encrypted
143+
expect(encryptedData).toHaveProperty('email__source')
144+
expect(encryptedData).toHaveProperty('email__hmac')
145+
expect(encryptedData).toHaveProperty('firstName__source')
146+
expect(encryptedData).toHaveProperty('firstName__hmac')
147+
expect(encryptedData).toHaveProperty('lastName__source')
148+
expect(encryptedData).toHaveProperty('lastName__hmac')
149+
expect(encryptedData).toHaveProperty('phoneNumber__source')
150+
expect(encryptedData).not.toHaveProperty('phoneNumber__hmac')
151+
152+
// Verify other fields remain unchanged
153+
expect(encryptedData.id).toBe('01ABCDEFGHIJKLMNOPQRSTUVWX')
154+
expect(encryptedData.metadata).toEqual({ role: 'admin!@#$%^&*()' })
155+
})
156+
157+
it('should handle bulk encryption', async () => {
158+
const testData = [
159+
{
160+
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
161+
email: 'test1@example.com',
162+
firstName: 'John',
163+
lastName: 'Smith',
164+
phoneNumber: '555-555-5555',
165+
},
166+
{
167+
id: '02ABCDEFGHIJKLMNOPQRSTUVWX',
168+
email: 'test2@example.com',
169+
firstName: 'Jane',
170+
lastName: 'Doe',
171+
phoneNumber: '555-555-5556',
172+
},
173+
]
174+
175+
const result = await protectDynamo.bulkEncryptModels(testData, schema)
176+
if (result.failure) {
177+
throw new Error(`Bulk encryption failed: ${result.failure.message}`)
178+
}
179+
180+
const encryptedData = result.data
181+
182+
// Verify both items are encrypted
183+
expect(encryptedData).toHaveLength(2)
184+
185+
// Verify first item
186+
expect(encryptedData[0]).toHaveProperty('email__source')
187+
expect(encryptedData[0]).toHaveProperty('email__hmac')
188+
expect(encryptedData[0]).toHaveProperty('firstName__source')
189+
expect(encryptedData[0]).toHaveProperty('firstName__hmac')
190+
expect(encryptedData[0]).toHaveProperty('lastName__source')
191+
expect(encryptedData[0]).toHaveProperty('lastName__hmac')
192+
expect(encryptedData[0]).toHaveProperty('phoneNumber__source')
193+
194+
// Verify second item
195+
expect(encryptedData[1]).toHaveProperty('email__source')
196+
expect(encryptedData[1]).toHaveProperty('email__hmac')
197+
expect(encryptedData[1]).toHaveProperty('firstName__source')
198+
expect(encryptedData[1]).toHaveProperty('firstName__hmac')
199+
expect(encryptedData[1]).toHaveProperty('lastName__source')
200+
expect(encryptedData[1]).toHaveProperty('lastName__hmac')
201+
expect(encryptedData[1]).toHaveProperty('phoneNumber__source')
202+
})
203+
204+
it('should handle decryption of encrypted data', async () => {
205+
const originalData = {
206+
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
207+
email: 'test.user@example.com',
208+
firstName: 'John',
209+
lastName: 'Smith',
210+
phoneNumber: '555-555-5555',
211+
example: {
212+
protected: 'hello world',
213+
notProtected: 'I am not protected',
214+
deep: {
215+
protected: 'deep protected',
216+
notProtected: 'deep not protected',
217+
},
218+
},
219+
}
220+
221+
// First encrypt
222+
const encryptResult = await protectDynamo.encryptModel(originalData, schema)
223+
224+
if (encryptResult.failure) {
225+
throw new Error(`Encryption failed: ${encryptResult.failure.message}`)
226+
}
227+
228+
// Then decrypt
229+
const decryptResult = await protectDynamo.decryptModel(
230+
encryptResult.data,
231+
schema,
232+
)
233+
if (decryptResult.failure) {
234+
throw new Error(`Decryption failed: ${decryptResult.failure.message}`)
235+
}
236+
237+
const decryptedData = decryptResult.data
238+
239+
// Verify all fields match original data
240+
expect(decryptedData).toEqual(originalData)
241+
})
242+
243+
it('should handle decryption of bulk encrypted data', async () => {
244+
const originalData = [
245+
{
246+
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
247+
email: 'test1@example.com',
248+
firstName: 'John',
249+
lastName: 'Smith',
250+
phoneNumber: '555-555-5555',
251+
},
252+
{
253+
id: '02ABCDEFGHIJKLMNOPQRSTUVWX',
254+
email: 'test2@example.com',
255+
firstName: 'Jane',
256+
lastName: 'Doe',
257+
phoneNumber: '555-555-5556',
258+
example: {
259+
protected: 'hello world',
260+
notProtected: 'I am not protected',
261+
deep: {
262+
protected: 'deep protected',
263+
notProtected: 'deep not protected',
264+
},
265+
},
266+
},
267+
]
268+
269+
// First encrypt
270+
const encryptResult = await protectDynamo.bulkEncryptModels(
271+
originalData,
272+
schema,
273+
)
274+
if (encryptResult.failure) {
275+
throw new Error(
276+
`Bulk encryption failed: ${encryptResult.failure.message}`,
277+
)
278+
}
279+
280+
// Then decrypt
281+
const decryptResult = await protectDynamo.bulkDecryptModels(
282+
encryptResult.data,
283+
schema,
284+
)
285+
if (decryptResult.failure) {
286+
throw new Error(
287+
`Bulk decryption failed: ${decryptResult.failure.message}`,
288+
)
289+
}
290+
291+
const decryptedData = decryptResult.data
292+
293+
// Verify all items match original data
294+
expect(decryptedData).toEqual(originalData)
6295
})
7296
})

packages/protect-dynamodb/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
"typescript": "catalog:repo",
4242
"vitest": "catalog:repo"
4343
},
44+
"peerDependencies": {
45+
"@cipherstash/protect": "workspace:*"
46+
},
4447
"publishConfig": {
4548
"access": "public"
4649
},

0 commit comments

Comments
 (0)