Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/tough-aliens-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/protect": patch
---

Update @cipherstash/protect-ffi to 0.19.0
64 changes: 64 additions & 0 deletions packages/protect/__tests__/backward-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dotenv/config'
import { csColumn, csTable } from '@cipherstash/schema'
import { describe, expect, it, beforeAll } from 'vitest'
import { protect } from '../src'

const users = csTable('users', {
email: csColumn('email'),
})

describe('k-field backward compatibility', () => {
let protectClient: Awaited<ReturnType<typeof protect>>

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

it('should encrypt new data WITHOUT k field (forward compatibility)', async () => {
const testData = 'test@example.com'

const result = await protectClient.encrypt(testData, {
column: users.email,
table: users,
})

if (result.failure) {
throw new Error(`Encryption failed: ${result.failure.message}`)
}

// Forward compatibility: new encryptions should NOT have k field
expect(result.data).not.toHaveProperty('k')
expect(result.data).toHaveProperty('c')
expect(result.data).toHaveProperty('v')
expect(result.data).toHaveProperty('i')
}, 30000)

it('should decrypt data with legacy k field (backward compatibility)', async () => {
// First encrypt some data
const testData = 'legacy@example.com'

const encrypted = await protectClient.encrypt(testData, {
column: users.email,
table: users,
})

if (encrypted.failure) {
throw new Error(`Encryption failed: ${encrypted.failure.message}`)
}

// Simulate legacy payload by adding k field to the encrypted data
const legacyPayload = {
...encrypted.data,
k: 'ct', // Legacy discriminant field - should be ignored during decryption
}

// Decrypt should succeed even with legacy k field present
const result = await protectClient.decrypt(legacyPayload)

if (result.failure) {
throw new Error(`Decryption failed: ${result.failure.message}`)
}

expect(result.data).toBe(testData)
}, 30000)
})
82 changes: 41 additions & 41 deletions packages/protect/__tests__/json-protect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('JSON encryption and decryption', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -107,7 +107,7 @@ describe('JSON encryption and decryption', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -149,7 +149,7 @@ describe('JSON encryption and decryption', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand All @@ -176,7 +176,7 @@ describe('JSON encryption and decryption', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -214,9 +214,9 @@ describe('JSON model encryption and decryption', () => {
}

// Verify encrypted fields
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.address).toHaveProperty('k')
expect(encryptedModel.data.json).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.address).not.toHaveProperty('k')
expect(encryptedModel.data.json).not.toHaveProperty('k')

// Verify non-encrypted fields remain unchanged
expect(encryptedModel.data.id).toBe('1')
Expand Down Expand Up @@ -254,8 +254,8 @@ describe('JSON model encryption and decryption', () => {
}

// Verify encrypted fields
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.address).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.address).not.toHaveProperty('k')
expect(encryptedModel.data.json).toBeNull()

const decryptedResult = await protectClient.decryptModel<User>(
Expand Down Expand Up @@ -289,8 +289,8 @@ describe('JSON model encryption and decryption', () => {
}

// Verify encrypted fields
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.address).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.address).not.toHaveProperty('k')
expect(encryptedModel.data.json).toBeUndefined()

const decryptedResult = await protectClient.decryptModel<User>(
Expand Down Expand Up @@ -326,13 +326,13 @@ describe('JSON bulk encryption and decryption', () => {
expect(encryptedData.data).toHaveLength(3)
expect(encryptedData.data[0]).toHaveProperty('id', 'user1')
expect(encryptedData.data[0]).toHaveProperty('data')
expect(encryptedData.data[0].data).toHaveProperty('k')
expect(encryptedData.data[0].data).not.toHaveProperty('k')
expect(encryptedData.data[1]).toHaveProperty('id', 'user2')
expect(encryptedData.data[1]).toHaveProperty('data')
expect(encryptedData.data[1].data).toHaveProperty('k')
expect(encryptedData.data[1].data).not.toHaveProperty('k')
expect(encryptedData.data[2]).toHaveProperty('id', 'user3')
expect(encryptedData.data[2]).toHaveProperty('data')
expect(encryptedData.data[2].data).toHaveProperty('k')
expect(encryptedData.data[2].data).not.toHaveProperty('k')

// Now decrypt the data
const decryptedData = await protectClient.bulkDecrypt(encryptedData.data)
Expand Down Expand Up @@ -380,13 +380,13 @@ describe('JSON bulk encryption and decryption', () => {
expect(encryptedData.data).toHaveLength(3)
expect(encryptedData.data[0]).toHaveProperty('id', 'user1')
expect(encryptedData.data[0]).toHaveProperty('data')
expect(encryptedData.data[0].data).toHaveProperty('k')
expect(encryptedData.data[0].data).not.toHaveProperty('k')
expect(encryptedData.data[1]).toHaveProperty('id', 'user2')
expect(encryptedData.data[1]).toHaveProperty('data')
expect(encryptedData.data[1].data).toBeNull()
expect(encryptedData.data[2]).toHaveProperty('id', 'user3')
expect(encryptedData.data[2]).toHaveProperty('data')
expect(encryptedData.data[2].data).toHaveProperty('k')
expect(encryptedData.data[2].data).not.toHaveProperty('k')

// Now decrypt the data
const decryptedData = await protectClient.bulkDecrypt(encryptedData.data)
Expand Down Expand Up @@ -447,12 +447,12 @@ describe('JSON bulk encryption and decryption', () => {
}

// Verify encrypted fields for each model
expect(encryptedModels.data[0].email).toHaveProperty('k')
expect(encryptedModels.data[0].address).toHaveProperty('k')
expect(encryptedModels.data[0].json).toHaveProperty('k')
expect(encryptedModels.data[1].email).toHaveProperty('k')
expect(encryptedModels.data[1].address).toHaveProperty('k')
expect(encryptedModels.data[1].json).toHaveProperty('k')
expect(encryptedModels.data[0].email).not.toHaveProperty('k')
expect(encryptedModels.data[0].address).not.toHaveProperty('k')
expect(encryptedModels.data[0].json).not.toHaveProperty('k')
expect(encryptedModels.data[1].email).not.toHaveProperty('k')
expect(encryptedModels.data[1].address).not.toHaveProperty('k')
expect(encryptedModels.data[1].json).not.toHaveProperty('k')

// Verify non-encrypted fields remain unchanged
expect(encryptedModels.data[0].id).toBe('1')
Expand Down Expand Up @@ -511,7 +511,7 @@ describe('JSON encryption with lock context', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient
.decrypt(ciphertext.data)
Expand Down Expand Up @@ -557,8 +557,8 @@ describe('JSON encryption with lock context', () => {
}

// Verify encrypted fields
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.json).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.json).not.toHaveProperty('k')

const decryptedResult = await protectClient
.decryptModel(encryptedModel.data)
Expand Down Expand Up @@ -606,10 +606,10 @@ describe('JSON encryption with lock context', () => {
expect(encryptedData.data).toHaveLength(2)
expect(encryptedData.data[0]).toHaveProperty('id', 'user1')
expect(encryptedData.data[0]).toHaveProperty('data')
expect(encryptedData.data[0].data).toHaveProperty('k')
expect(encryptedData.data[0].data).not.toHaveProperty('k')
expect(encryptedData.data[1]).toHaveProperty('id', 'user2')
expect(encryptedData.data[1]).toHaveProperty('data')
expect(encryptedData.data[1].data).toHaveProperty('k')
expect(encryptedData.data[1].data).not.toHaveProperty('k')

// Decrypt with lock context
const decryptedData = await protectClient
Expand Down Expand Up @@ -670,8 +670,8 @@ describe('JSON nested object encryption', () => {
}

// Verify encrypted fields
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.metadata?.profile).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.metadata?.profile).not.toHaveProperty('k')
expect(encryptedModel.data.metadata?.settings?.preferences).toHaveProperty(
'c',
)
Expand Down Expand Up @@ -714,7 +714,7 @@ describe('JSON nested object encryption', () => {
}

// Verify null fields are preserved
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.metadata?.profile).toBeNull()
expect(encryptedModel.data.metadata?.settings?.preferences).toBeNull()

Expand Down Expand Up @@ -753,7 +753,7 @@ describe('JSON nested object encryption', () => {
}

// Verify undefined fields are preserved
expect(encryptedModel.data.email).toHaveProperty('k')
expect(encryptedModel.data.email).not.toHaveProperty('k')
expect(encryptedModel.data.metadata?.profile).toBeUndefined()
expect(encryptedModel.data.metadata?.settings?.preferences).toBeUndefined()

Expand Down Expand Up @@ -799,7 +799,7 @@ describe('JSON edge cases and error handling', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -847,7 +847,7 @@ describe('JSON edge cases and error handling', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -948,7 +948,7 @@ describe('JSON advanced scenarios', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand All @@ -975,7 +975,7 @@ describe('JSON advanced scenarios', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -1010,7 +1010,7 @@ describe('JSON advanced scenarios', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -1045,7 +1045,7 @@ describe('JSON advanced scenarios', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -1081,7 +1081,7 @@ describe('JSON advanced scenarios', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -1140,7 +1140,7 @@ describe('JSON error handling and edge cases', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -1169,7 +1169,7 @@ describe('JSON error handling and edge cases', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down Expand Up @@ -1206,7 +1206,7 @@ describe('JSON error handling and edge cases', () => {
}

// Verify encrypted field
expect(ciphertext.data).toHaveProperty('k')
expect(ciphertext.data).not.toHaveProperty('k')

const plaintext = await protectClient.decrypt(ciphertext.data)

Expand Down
14 changes: 5 additions & 9 deletions packages/protect/__tests__/number-protect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,17 +304,13 @@ describe('Bulk encryption and decryption', () => {
expect(encryptedData.data[2]).toHaveProperty('data')
expect(encryptedData.data[2].data).toHaveProperty('c')

expect(encryptedData.data[0].data?.k).toBe('ct')
expect(encryptedData.data[1].data?.k).toBe('ct')
expect(encryptedData.data[2].data?.k).toBe('ct')
// Forward compatibility: new encryptions should NOT have k field
expect(encryptedData.data[0].data).not.toHaveProperty('k')
expect(encryptedData.data[1].data).not.toHaveProperty('k')
expect(encryptedData.data[2].data).not.toHaveProperty('k')

// Verify all encrypted values are different
const getCiphertext = (
data: { k?: string; c?: unknown } | null | undefined,
) => {
if (data?.k === 'ct') return data.c
return data?.c
}
const getCiphertext = (data: { c?: unknown } | null | undefined) => data?.c

expect(getCiphertext(encryptedData.data[0].data)).not.toBe(
getCiphertext(encryptedData.data[1].data),
Expand Down
2 changes: 1 addition & 1 deletion packages/protect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
},
"dependencies": {
"@byteslice/result": "^0.2.0",
"@cipherstash/protect-ffi": "0.18.1",
"@cipherstash/protect-ffi": "0.19.0",
"@cipherstash/schema": "workspace:*",
"zod": "^3.24.2"
},
Expand Down
Loading