diff --git a/.changeset/tough-aliens-type.md b/.changeset/tough-aliens-type.md new file mode 100644 index 00000000..54e8f1fc --- /dev/null +++ b/.changeset/tough-aliens-type.md @@ -0,0 +1,5 @@ +--- +"@cipherstash/protect": patch +--- + +Update @cipherstash/protect-ffi to 0.19.0 diff --git a/packages/protect/__tests__/backward-compat.test.ts b/packages/protect/__tests__/backward-compat.test.ts new file mode 100644 index 00000000..128ab872 --- /dev/null +++ b/packages/protect/__tests__/backward-compat.test.ts @@ -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> + + 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) +}) diff --git a/packages/protect/__tests__/json-protect.test.ts b/packages/protect/__tests__/json-protect.test.ts index 24841d21..66604400 100644 --- a/packages/protect/__tests__/json-protect.test.ts +++ b/packages/protect/__tests__/json-protect.test.ts @@ -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) @@ -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) @@ -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) @@ -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) @@ -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') @@ -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( @@ -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( @@ -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) @@ -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) @@ -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') @@ -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) @@ -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) @@ -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 @@ -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', ) @@ -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() @@ -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() @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/packages/protect/__tests__/number-protect.test.ts b/packages/protect/__tests__/number-protect.test.ts index 710f2d99..3ade327a 100644 --- a/packages/protect/__tests__/number-protect.test.ts +++ b/packages/protect/__tests__/number-protect.test.ts @@ -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), diff --git a/packages/protect/package.json b/packages/protect/package.json index fe609404..fa31870c 100644 --- a/packages/protect/package.json +++ b/packages/protect/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12db4963..3231b7d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -512,8 +512,8 @@ importers: specifier: ^0.2.0 version: 0.2.2 '@cipherstash/protect-ffi': - specifier: 0.18.1 - version: 0.18.1 + specifier: 0.19.0 + version: 0.19.0 '@cipherstash/schema': specifier: workspace:* version: link:../schema @@ -1058,38 +1058,38 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cipherstash/protect-ffi-darwin-arm64@0.18.1': - resolution: {integrity: sha512-kTkshWuGG07X9m4Ug0mPXIglfZopVFCJy55/B2yy9Z+hhNP95d4Zjq6AXlRYAHsnxgcp0SnynC8LaVTcXsEx8w==} + '@cipherstash/protect-ffi-darwin-arm64@0.19.0': + resolution: {integrity: sha512-0U/paHskpD2SCiy4T4s5Ery112n5MfT3cXudCZ0m82x03SiK5sU9SNtD2tI0tJhcUlDP9TsFUKnYEEZPFR8pUA==} cpu: [arm64] os: [darwin] - '@cipherstash/protect-ffi-darwin-x64@0.18.1': - resolution: {integrity: sha512-VdoTOIF5hmS7sQUR/w6FQKAj7aJOhOD2lo2wlV3hxG0PNd2pWiB1HvE9wvrGY2VAPqBU97Cp+S8IFkf5ckqQ2A==} + '@cipherstash/protect-ffi-darwin-x64@0.19.0': + resolution: {integrity: sha512-gbPomTjvBCO7eZsMLGzMVv0Al/TZQ3SOfLWCRzRdWzff3BIC+wPrqJJBbpxIb/WRG7Ak8ceRSdMkrnhQnlsYHA==} cpu: [x64] os: [darwin] - '@cipherstash/protect-ffi-linux-arm64-gnu@0.18.1': - resolution: {integrity: sha512-DcGIgoTkRDaBqqXs3X4fB7XCOMtleSLd+kMAStS1uEq6agVHp2P/9Y+ag30MQ7Ypm0xTJBZVwl8ClDXDSklQcw==} + '@cipherstash/protect-ffi-linux-arm64-gnu@0.19.0': + resolution: {integrity: sha512-z4ZFJGrmxlsZM5arFyLeeiod8z5SONPYLYnQnO+HG9CH+ra2jRhCvA5qvPjF1+/7vL/zpuV+9MhVJGTt7Vo38A==} cpu: [arm64] os: [linux] - '@cipherstash/protect-ffi-linux-x64-gnu@0.18.1': - resolution: {integrity: sha512-I5Cg7vlkVX2qL3hKYiYEx1C1lTQ+uGt2YrAxaqSs09gpvpa6xC2/2QMpV9UvKnJ9uLlxVly8jycnzp7TyvS9pA==} + '@cipherstash/protect-ffi-linux-x64-gnu@0.19.0': + resolution: {integrity: sha512-ZD3YSzGdgtN7Elsp4rKGBREvbhsYNIt5ywnme8JEgVID7UFENQK5WmsLr20ZbMT1C37TaMRS5ZuIS+loSZvE5Q==} cpu: [x64] os: [linux] - '@cipherstash/protect-ffi-linux-x64-musl@0.18.1': - resolution: {integrity: sha512-Ol9dYVNXIaoYrhpsJv90PX5pHg1mmWKqnpnhh2ofA+NNZqEAdolo4BeTsQc10sCE6WogcM8FFJ9OcOrk79N8lA==} + '@cipherstash/protect-ffi-linux-x64-musl@0.19.0': + resolution: {integrity: sha512-dngMn6EP2016fwJMg8yeZiJJ/lDOiZ5lkA8fMrVxkr/pv6t7x8m1pdbh4TuLA4OSozm2MLXFu/SZInPwdWZu/w==} cpu: [x64] os: [linux] - '@cipherstash/protect-ffi-win32-x64-msvc@0.18.1': - resolution: {integrity: sha512-j9mqXQDHu3vdLJx93g2lAuKDeOh+XuC1FBvsrr2FTjgzBqrZAHH60QFPXjvFT1/U1oWjV8nAgTZQ4cUMJSDa9Q==} + '@cipherstash/protect-ffi-win32-x64-msvc@0.19.0': + resolution: {integrity: sha512-A0WaKj+8WtO+synaMUbOy4a34/s7urJemXj5nC/8EKS8ppGcAJR5pZqV4+RV57j0pQSSR52BAvAenuQEGyKZPA==} cpu: [x64] os: [win32] - '@cipherstash/protect-ffi@0.18.1': - resolution: {integrity: sha512-rDYIRIWo7EvJ+ytGEwzeb0D/4j3tQ8w+5Kpz089IEhxIVxOzavsR+f5HyMXohAKYkH+19RpD+2X+v1P8rwmiUA==} + '@cipherstash/protect-ffi@0.19.0': + resolution: {integrity: sha512-UfPwO2axmi4O18Wwv87wDg1aGU1RHIEZoWtb/nEYWQgXDOhYtKmWcKQic0MMednBeHAF972pNsrw9Dxhs0ZxXw==} '@clerk/backend@2.28.0': resolution: {integrity: sha512-rd0hWrU7VES/CEYwnyaXDDHzDXYIaSzI5G03KLUfxLyOQSChU0ZUeViDYyXEsjZgAQqiUP1TFykh9JU2YlaNYg==} @@ -7426,34 +7426,34 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@cipherstash/protect-ffi-darwin-arm64@0.18.1': + '@cipherstash/protect-ffi-darwin-arm64@0.19.0': optional: true - '@cipherstash/protect-ffi-darwin-x64@0.18.1': + '@cipherstash/protect-ffi-darwin-x64@0.19.0': optional: true - '@cipherstash/protect-ffi-linux-arm64-gnu@0.18.1': + '@cipherstash/protect-ffi-linux-arm64-gnu@0.19.0': optional: true - '@cipherstash/protect-ffi-linux-x64-gnu@0.18.1': + '@cipherstash/protect-ffi-linux-x64-gnu@0.19.0': optional: true - '@cipherstash/protect-ffi-linux-x64-musl@0.18.1': + '@cipherstash/protect-ffi-linux-x64-musl@0.19.0': optional: true - '@cipherstash/protect-ffi-win32-x64-msvc@0.18.1': + '@cipherstash/protect-ffi-win32-x64-msvc@0.19.0': optional: true - '@cipherstash/protect-ffi@0.18.1': + '@cipherstash/protect-ffi@0.19.0': dependencies: '@neon-rs/load': 0.1.82 optionalDependencies: - '@cipherstash/protect-ffi-darwin-arm64': 0.18.1 - '@cipherstash/protect-ffi-darwin-x64': 0.18.1 - '@cipherstash/protect-ffi-linux-arm64-gnu': 0.18.1 - '@cipherstash/protect-ffi-linux-x64-gnu': 0.18.1 - '@cipherstash/protect-ffi-linux-x64-musl': 0.18.1 - '@cipherstash/protect-ffi-win32-x64-msvc': 0.18.1 + '@cipherstash/protect-ffi-darwin-arm64': 0.19.0 + '@cipherstash/protect-ffi-darwin-x64': 0.19.0 + '@cipherstash/protect-ffi-linux-arm64-gnu': 0.19.0 + '@cipherstash/protect-ffi-linux-x64-gnu': 0.19.0 + '@cipherstash/protect-ffi-linux-x64-musl': 0.19.0 + '@cipherstash/protect-ffi-win32-x64-msvc': 0.19.0 '@clerk/backend@2.28.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: