UNPKG

@cipherstash/protect

Version:
645 lines (533 loc) 22.4 kB
import 'dotenv/config' import { describe, expect, it, beforeAll } from 'vitest' import { csTable, csColumn } from '@cipherstash/schema' import { LockContext, protect, type EncryptedPayload } from '../src' const users = csTable('users', { email: csColumn('email').freeTextSearch().equality().orderAndRange(), address: csColumn('address').freeTextSearch(), }) type User = { id: string email?: string | null createdAt?: Date updatedAt?: Date address?: string | null number?: number } let protectClient: Awaited<ReturnType<typeof protect>> beforeAll(async () => { protectClient = await protect({ schemas: [users], }) }) describe('bulk encryption and decryption', () => { describe('bulk encrypt', () => { it('should bulk encrypt an array of plaintexts with IDs', async () => { const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: 'bob@example.com' }, { id: 'user3', plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Verify structure expect(encryptedData.data).toHaveLength(3) expect(encryptedData.data[0]).toHaveProperty('id', 'user1') expect(encryptedData.data[0]).toHaveProperty('data') expect(encryptedData.data[0].data).toHaveProperty('c') expect(encryptedData.data[1]).toHaveProperty('id', 'user2') expect(encryptedData.data[1]).toHaveProperty('data') expect(encryptedData.data[2]).toHaveProperty('id', 'user3') expect(encryptedData.data[2]).toHaveProperty('data') // Verify all encrypted values are different expect(encryptedData.data[0].data?.c).not.toBe( encryptedData.data[1].data?.c, ) expect(encryptedData.data[1].data?.c).not.toBe( encryptedData.data[2].data?.c, ) expect(encryptedData.data[0].data?.c).not.toBe( encryptedData.data[2].data?.c, ) }, 30000) it('should bulk encrypt an array of plaintexts without IDs', async () => { const plaintexts = [ { plaintext: 'alice@example.com' }, { plaintext: 'bob@example.com' }, { plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Verify structure expect(encryptedData.data).toHaveLength(3) expect(encryptedData.data[0]).toHaveProperty('id', undefined) expect(encryptedData.data[0]).toHaveProperty('data') expect(encryptedData.data[0].data).toHaveProperty('c') expect(encryptedData.data[1]).toHaveProperty('id', undefined) expect(encryptedData.data[1]).toHaveProperty('data') expect(encryptedData.data[1].data).toHaveProperty('c') expect(encryptedData.data[2]).toHaveProperty('id', undefined) expect(encryptedData.data[2]).toHaveProperty('data') expect(encryptedData.data[2].data).toHaveProperty('c') }, 30000) it('should handle null values in bulk encrypt', async () => { const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: null }, { id: 'user3', plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Verify structure expect(encryptedData.data).toHaveLength(3) expect(encryptedData.data[0]).toHaveProperty('id', 'user1') expect(encryptedData.data[0]).toHaveProperty('data') expect(encryptedData.data[0].data).toHaveProperty('c') 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('c') }, 30000) it('should handle all null values in bulk encrypt', async () => { const plaintexts = [ { id: 'user1', plaintext: null }, { id: 'user2', plaintext: null }, { id: 'user3', plaintext: null }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Verify structure expect(encryptedData.data).toHaveLength(3) expect(encryptedData.data[0]).toHaveProperty('id', 'user1') expect(encryptedData.data[0]).toHaveProperty('data') expect(encryptedData.data[0].data).toBeNull() 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).toBeNull() }, 30000) it('should handle empty array in bulk encrypt', async () => { const plaintexts: Array<{ id?: string; plaintext: string | null }> = [] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } expect(encryptedData.data).toHaveLength(0) }, 30000) }) describe('bulk decrypt', () => { it('should bulk decrypt an array of encrypted payloads with IDs', async () => { // First encrypt some data const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: 'bob@example.com' }, { id: 'user3', plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify structure expect(decryptedData.data).toHaveLength(3) expect(decryptedData.data[0]).toHaveProperty('id', 'user1') expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') expect(decryptedData.data[1]).toHaveProperty('id', 'user2') expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') expect(decryptedData.data[2]).toHaveProperty('id', 'user3') expect(decryptedData.data[2]).toHaveProperty( 'data', 'charlie@example.com', ) }, 30000) it('should bulk decrypt an array of encrypted payloads without IDs', async () => { // First encrypt some data const plaintexts = [ { plaintext: 'alice@example.com' }, { plaintext: 'bob@example.com' }, { plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify structure expect(decryptedData.data).toHaveLength(3) expect(decryptedData.data[0]).toHaveProperty('id', undefined) expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') expect(decryptedData.data[1]).toHaveProperty('id', undefined) expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') expect(decryptedData.data[2]).toHaveProperty('id', undefined) expect(decryptedData.data[2]).toHaveProperty( 'data', 'charlie@example.com', ) }, 30000) it('should handle null values in bulk decrypt', async () => { // First encrypt some data with nulls const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: null }, { id: 'user3', plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify structure expect(decryptedData.data).toHaveLength(3) expect(decryptedData.data[0]).toHaveProperty('id', 'user1') expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') expect(decryptedData.data[1]).toHaveProperty('id', 'user2') expect(decryptedData.data[1]).toHaveProperty('data', null) expect(decryptedData.data[2]).toHaveProperty('id', 'user3') expect(decryptedData.data[2]).toHaveProperty( 'data', 'charlie@example.com', ) }, 30000) it('should handle all null values in bulk decrypt', async () => { // First encrypt some data with all nulls const plaintexts = [ { id: 'user1', plaintext: null }, { id: 'user2', plaintext: null }, { id: 'user3', plaintext: null }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify structure expect(decryptedData.data).toHaveLength(3) expect(decryptedData.data[0]).toHaveProperty('id', 'user1') expect(decryptedData.data[0]).toHaveProperty('data', null) expect(decryptedData.data[1]).toHaveProperty('id', 'user2') expect(decryptedData.data[1]).toHaveProperty('data', null) expect(decryptedData.data[2]).toHaveProperty('id', 'user3') expect(decryptedData.data[2]).toHaveProperty('data', null) }, 30000) it('should handle empty array in bulk decrypt', async () => { const encryptedPayloads: Array<{ id?: string; data: EncryptedPayload }> = [] const decryptedData = await protectClient.bulkDecrypt(encryptedPayloads) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } expect(decryptedData.data).toHaveLength(0) }, 30000) it('should handle encrypted payloads with only "c" key', async () => { // First encrypt some data const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: 'bob@example.com' }, { id: 'user3', plaintext: 'charlie@example.com' }, ] const encryptedData = await protectClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Remove all keys except "c" from each encrypted payload const modifiedEncryptedData = encryptedData.data.map((item) => ({ id: item.id, data: { c: item.data?.c, } as EncryptedPayload, })) // Now decrypt the modified data const decryptedData = await protectClient.bulkDecrypt( modifiedEncryptedData, ) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify structure expect(decryptedData.data).toHaveLength(3) expect(decryptedData.data[0]).toHaveProperty('id', 'user1') expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') expect(decryptedData.data[1]).toHaveProperty('id', 'user2') expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') expect(decryptedData.data[2]).toHaveProperty('id', 'user3') expect(decryptedData.data[2]).toHaveProperty( 'data', 'charlie@example.com', ) }, 30000) }) describe('bulk operations with lock context', () => { it('should bulk encrypt and decrypt with lock context', async () => { // This test requires a valid JWT token, so we'll skip it in CI // TODO: Add proper JWT token handling for CI const userJwt = process.env.USER_JWT if (!userJwt) { console.log('Skipping lock context test - no USER_JWT provided') return } const lc = new LockContext() const lockContext = await lc.identify(userJwt) if (lockContext.failure) { throw new Error(`[protect]: ${lockContext.failure.message}`) } const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: 'bob@example.com' }, { id: 'user3', plaintext: 'charlie@example.com' }, ] // Encrypt with lock context const encryptedData = await protectClient .bulkEncrypt(plaintexts, { column: users.email, table: users, }) .withLockContext(lockContext.data) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Verify structure expect(encryptedData.data).toHaveLength(3) expect(encryptedData.data[0]).toHaveProperty('id', 'user1') expect(encryptedData.data[0]).toHaveProperty('data') expect(encryptedData.data[0].data).toHaveProperty('c') expect(encryptedData.data[1]).toHaveProperty('id', 'user2') expect(encryptedData.data[1]).toHaveProperty('data') expect(encryptedData.data[1].data).toHaveProperty('c') expect(encryptedData.data[2]).toHaveProperty('id', 'user3') expect(encryptedData.data[2]).toHaveProperty('data') expect(encryptedData.data[2].data).toHaveProperty('c') // Decrypt with lock context const decryptedData = await protectClient .bulkDecrypt(encryptedData.data) .withLockContext(lockContext.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify decrypted data expect(decryptedData.data).toHaveLength(3) expect(decryptedData.data[0]).toHaveProperty('id', 'user1') expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') expect(decryptedData.data[1]).toHaveProperty('id', 'user2') expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') expect(decryptedData.data[2]).toHaveProperty('id', 'user3') expect(decryptedData.data[2]).toHaveProperty( 'data', 'charlie@example.com', ) }, 30000) it('should handle null values with lock context', async () => { const userJwt = process.env.USER_JWT if (!userJwt) { console.log('Skipping lock context test - no USER_JWT provided') return } const lc = new LockContext() const lockContext = await lc.identify(userJwt) if (lockContext.failure) { throw new Error(`[protect]: ${lockContext.failure.message}`) } const plaintexts = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: null }, { id: 'user3', plaintext: 'charlie@example.com' }, ] // Encrypt with lock context const encryptedData = await protectClient .bulkEncrypt(plaintexts, { column: users.email, table: users, }) .withLockContext(lockContext.data) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Verify null is preserved expect(encryptedData.data[1]).toHaveProperty('data') expect(encryptedData.data[1].data).toBeNull() // Decrypt with lock context const decryptedData = await protectClient .bulkDecrypt(encryptedData.data) .withLockContext(lockContext.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify null is preserved expect(decryptedData.data[1]).toHaveProperty('data') expect(decryptedData.data[1].data).toBeNull() }, 30000) it('should decrypt mixed lock context payloads with specific lock context', async () => { const userJwt = process.env.USER_JWT const user2Jwt = process.env.USER_2_JWT if (!userJwt || !user2Jwt) { console.log( 'Skipping mixed lock context test - missing USER_JWT or USER_2_JWT', ) return } const lc = new LockContext() const lc2 = new LockContext() const lockContext1 = await lc.identify(userJwt) const lockContext2 = await lc2.identify(user2Jwt) if (lockContext1.failure) { throw new Error(`[protect]: ${lockContext1.failure.message}`) } if (lockContext2.failure) { throw new Error(`[protect]: ${lockContext2.failure.message}`) } // Encrypt first value with USER_JWT lock context const encryptedData1 = await protectClient .bulkEncrypt([{ id: 'user1', plaintext: 'alice@example.com' }], { column: users.email, table: users, }) .withLockContext(lockContext1.data) if (encryptedData1.failure) { throw new Error(`[protect]: ${encryptedData1.failure.message}`) } // Encrypt second value with USER_2_JWT lock context const encryptedData2 = await protectClient .bulkEncrypt([{ id: 'user2', plaintext: 'bob@example.com' }], { column: users.email, table: users, }) .withLockContext(lockContext2.data) if (encryptedData2.failure) { throw new Error(`[protect]: ${encryptedData2.failure.message}`) } // Combine both encrypted payloads const combinedEncryptedData = [ ...encryptedData1.data, ...encryptedData2.data, ] // Decrypt with USER_2_JWT lock context const decryptedData = await protectClient .bulkDecrypt(combinedEncryptedData) .withLockContext(lockContext2.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify both payloads are returned expect(decryptedData.data).toHaveLength(2) // First payload should fail to decrypt since it was encrypted with different lock context expect(decryptedData.data[0]).toHaveProperty('id', 'user1') expect(decryptedData.data[0]).toHaveProperty('error') expect(decryptedData.data[0]).not.toHaveProperty('data') // Second payload should be decrypted since it was encrypted with the same lock context expect(decryptedData.data[1]).toHaveProperty('id', 'user2') expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') expect(decryptedData.data[1]).not.toHaveProperty('error') }, 30000) }) describe('bulk operations round-trip', () => { it('should maintain data integrity through encrypt/decrypt cycle', async () => { const originalData = [ { id: 'user1', plaintext: 'alice@example.com' }, { id: 'user2', plaintext: 'bob@example.com' }, { id: 'user3', plaintext: null }, { id: 'user4', plaintext: 'dave@example.com' }, ] // Encrypt const encryptedData = await protectClient.bulkEncrypt(originalData, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Decrypt const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify round-trip integrity expect(decryptedData.data).toHaveLength(originalData.length) for (let i = 0; i < originalData.length; i++) { expect(decryptedData.data[i].id).toBe(originalData[i].id) expect(decryptedData.data[i].data).toBe(originalData[i].plaintext) } }, 30000) it('should handle large arrays efficiently', async () => { const originalData = Array.from({ length: 100 }, (_, i) => ({ id: `user${i}`, plaintext: `user${i}@example.com`, })) // Encrypt const encryptedData = await protectClient.bulkEncrypt(originalData, { column: users.email, table: users, }) if (encryptedData.failure) { throw new Error(`[protect]: ${encryptedData.failure.message}`) } // Decrypt const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[protect]: ${decryptedData.failure.message}`) } // Verify all data is preserved expect(decryptedData.data).toHaveLength(100) for (let i = 0; i < 100; i++) { expect(decryptedData.data[i].id).toBe(`user${i}`) expect(decryptedData.data[i].data).toBe(`user${i}@example.com`) } }, 30000) }) })