Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/kind-symbols-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/protect-dynamodb": minor
---

Fixed bug when handling schema definitions without an equality flag.
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ jobs:
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> ./packages/protect/.env
echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> ./packages/protect/.env
- name: Create .env file in ./packages/protect-dynamodb/
run: |
touch ./packages/protect-dynamodb/.env
echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect-dynamodb/.env
echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect-dynamodb/.env
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect-dynamodb/.env
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect-dynamodb/.env
# Run TurboRepo tests
- name: Run tests
run: pnpm run test
247 changes: 244 additions & 3 deletions packages/protect-dynamodb/__tests__/dynamodb.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,248 @@
import { describe, expect, it } from 'vitest'
import 'dotenv/config'
import { describe, expect, it, beforeAll } from 'vitest'
import { protectDynamoDB } from '../src'
import { protect, csColumn, csTable } from '@cipherstash/protect'

const schema = csTable('dynamo_cipherstash_test', {
email: csColumn('email').equality(),
firstName: csColumn('firstName').equality(),
lastName: csColumn('lastName').equality(),
phoneNumber: csColumn('phoneNumber'),
})

describe('protect dynamodb helpers', () => {
it('should say hello', () => {
expect(true).toBe(true)
let protectClient: Awaited<ReturnType<typeof protect>>
let protectDynamo: ReturnType<typeof protectDynamoDB>

beforeAll(async () => {
protectClient = await protect({
schemas: [schema],
})

protectDynamo = protectDynamoDB({
protectClient,
})
})

it('should encrypt columns', async () => {
const testData = {
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
email: 'test.user@example.com',
address: '123 Main Street',
createdAt: '2024-08-15T22:14:49.948Z',
firstName: 'John',
lastName: 'Smith',
phoneNumber: '555-555-5555',
companyName: 'Acme Corp',
batteryBrands: ['Brand1', 'Brand2'],
metadata: { role: 'admin' },
}

const result = await protectDynamo.encryptModel(testData, schema)
if (result.failure) {
throw new Error(`Encryption failed: ${result.failure.message}`)
}

const encryptedData = result.data

// Verify equality columns are encrypted
expect(encryptedData).toHaveProperty('email__source')
expect(encryptedData).toHaveProperty('email__hmac')
expect(encryptedData).toHaveProperty('firstName__source')
expect(encryptedData).toHaveProperty('firstName__hmac')
expect(encryptedData).toHaveProperty('lastName__source')
expect(encryptedData).toHaveProperty('lastName__hmac')
expect(encryptedData).toHaveProperty('phoneNumber__source')

// Verify other fields remain unchanged
expect(encryptedData.id).toBe('01ABCDEFGHIJKLMNOPQRSTUVWX')
expect(encryptedData.address).toBe('123 Main Street')
expect(encryptedData.createdAt).toBe('2024-08-15T22:14:49.948Z')
expect(encryptedData.companyName).toBe('Acme Corp')
expect(encryptedData.batteryBrands).toEqual(['Brand1', 'Brand2'])
expect(encryptedData.metadata).toEqual({ role: 'admin' })
})

it('should handle null and undefined values', async () => {
const testData = {
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
email: null,
firstName: undefined,
lastName: 'Smith',
phoneNumber: null,
metadata: { role: null },
}

const result = await protectDynamo.encryptModel(testData, schema)
if (result.failure) {
throw new Error(`Encryption failed: ${result.failure.message}`)
}

const encryptedData = result.data

// Verify null/undefined equality columns are handled
expect(encryptedData).toHaveProperty('lastName__source')
expect(encryptedData).toHaveProperty('lastName__hmac')

// Verify other fields remain unchanged
expect(encryptedData.id).toBe('01ABCDEFGHIJKLMNOPQRSTUVWX')
expect(encryptedData.phoneNumber).toBeNull()
expect(encryptedData.email).toBeNull()
expect(encryptedData.firstName).toBeUndefined()
expect(encryptedData.metadata).toEqual({ role: null })
})

it('should handle empty strings and special characters', async () => {
const testData = {
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
email: '',
firstName: 'John!@#$%^&*()',
lastName: 'Smith ',
phoneNumber: '',
metadata: { role: 'admin!@#$%^&*()' },
}

const result = await protectDynamo.encryptModel(testData, schema)
if (result.failure) {
throw new Error(`Encryption failed: ${result.failure.message}`)
}

const encryptedData = result.data

// Verify equality columns are encrypted
expect(encryptedData).toHaveProperty('email__source')
expect(encryptedData).toHaveProperty('email__hmac')
expect(encryptedData).toHaveProperty('firstName__source')
expect(encryptedData).toHaveProperty('firstName__hmac')
expect(encryptedData).toHaveProperty('lastName__source')
expect(encryptedData).toHaveProperty('lastName__hmac')
expect(encryptedData).toHaveProperty('phoneNumber__source')

// Verify other fields remain unchanged
expect(encryptedData.id).toBe('01ABCDEFGHIJKLMNOPQRSTUVWX')
expect(encryptedData.metadata).toEqual({ role: 'admin!@#$%^&*()' })
})

it('should handle bulk encryption', async () => {
const testData = [
{
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
email: 'test1@example.com',
firstName: 'John',
lastName: 'Smith',
phoneNumber: '555-555-5555',
},
{
id: '02ABCDEFGHIJKLMNOPQRSTUVWX',
email: 'test2@example.com',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '555-555-5556',
},
]

const result = await protectDynamo.bulkEncryptModels(testData, schema)
if (result.failure) {
throw new Error(`Bulk encryption failed: ${result.failure.message}`)
}

const encryptedData = result.data

// Verify both items are encrypted
expect(encryptedData).toHaveLength(2)

// Verify first item
expect(encryptedData[0]).toHaveProperty('email__source')
expect(encryptedData[0]).toHaveProperty('email__hmac')
expect(encryptedData[0]).toHaveProperty('firstName__source')
expect(encryptedData[0]).toHaveProperty('firstName__hmac')
expect(encryptedData[0]).toHaveProperty('lastName__source')
expect(encryptedData[0]).toHaveProperty('lastName__hmac')
expect(encryptedData[0]).toHaveProperty('phoneNumber__source')

// Verify second item
expect(encryptedData[1]).toHaveProperty('email__source')
expect(encryptedData[1]).toHaveProperty('email__hmac')
expect(encryptedData[1]).toHaveProperty('firstName__source')
expect(encryptedData[1]).toHaveProperty('firstName__hmac')
expect(encryptedData[1]).toHaveProperty('lastName__source')
expect(encryptedData[1]).toHaveProperty('lastName__hmac')
expect(encryptedData[1]).toHaveProperty('phoneNumber__source')
})

it('should handle decryption of encrypted data', async () => {
const originalData = {
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
email: 'test.user@example.com',
firstName: 'John',
lastName: 'Smith',
phoneNumber: '555-555-5555',
}

// First encrypt
const encryptResult = await protectDynamo.encryptModel(originalData, schema)
if (encryptResult.failure) {
throw new Error(`Encryption failed: ${encryptResult.failure.message}`)
}

// Then decrypt
const decryptResult = await protectDynamo.decryptModel(
encryptResult.data,
schema,
)
if (decryptResult.failure) {
throw new Error(`Decryption failed: ${decryptResult.failure.message}`)
}

const decryptedData = decryptResult.data

// Verify all fields match original data
expect(decryptedData).toEqual(originalData)
})

it('should handle decryption of bulk encrypted data', async () => {
const originalData = [
{
id: '01ABCDEFGHIJKLMNOPQRSTUVWX',
email: 'test1@example.com',
firstName: 'John',
lastName: 'Smith',
phoneNumber: '555-555-5555',
},
{
id: '02ABCDEFGHIJKLMNOPQRSTUVWX',
email: 'test2@example.com',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '555-555-5556',
},
]

// First encrypt
const encryptResult = await protectDynamo.bulkEncryptModels(
originalData,
schema,
)
if (encryptResult.failure) {
throw new Error(
`Bulk encryption failed: ${encryptResult.failure.message}`,
)
}

// Then decrypt
const decryptResult = await protectDynamo.bulkDecryptModels(
encryptResult.data,
schema,
)
if (decryptResult.failure) {
throw new Error(
`Bulk decryption failed: ${decryptResult.failure.message}`,
)
}

const decryptedData = decryptResult.data

// Verify all items match original data
expect(decryptedData).toEqual(originalData)
})
})
3 changes: 3 additions & 0 deletions packages/protect-dynamodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"typescript": "catalog:repo",
"vitest": "catalog:repo"
},
"peerDependencies": {
"@cipherstash/protect": "workspace:*"
},
"publishConfig": {
"access": "public"
},
Expand Down
14 changes: 10 additions & 4 deletions packages/protect-dynamodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ function toEncryptedDynamoItem(
return Object.entries(encrypted).reduce(
(putItem, [attrName, attrValue]) => {
if (encryptedAttrs.includes(attrName)) {
const encryptPayload = attrValue as EncryptedPayload
if (encryptPayload?.hm && encryptPayload?.c) {
putItem[`${attrName}${searchTermAttrSuffix}`] = encryptPayload.hm
putItem[`${attrName}${ciphertextAttrSuffix}`] = encryptPayload.c
if (attrValue === null || attrValue === undefined) {
putItem[attrName] = attrValue
} else {
const encryptPayload = attrValue as EncryptedPayload
if (encryptPayload?.c) {
if (encryptPayload.hm) {
putItem[`${attrName}${searchTermAttrSuffix}`] = encryptPayload.hm
}
putItem[`${attrName}${ciphertextAttrSuffix}`] = encryptPayload.c
}
}
} else {
putItem[attrName] = attrValue
Expand Down