@cipherstash/protect
Version:
CipherStash Protect for JavaScript
473 lines (405 loc) • 13.8 kB
text/typescript
import 'dotenv/config'
import { describe, expect, it, beforeAll } from 'vitest'
import { csTable, csColumn } from '@cipherstash/schema'
import { LockContext, protect } from '../src'
const users = csTable('users', {
auditable: csColumn('auditable'),
email: csColumn('email').freeTextSearch().equality().orderAndRange(),
address: csColumn('address').freeTextSearch(),
})
type User = {
id: string
email?: string | null
address?: string | null
auditable?: string | null
createdAt?: Date
updatedAt?: Date
number?: number
}
let protectClient: Awaited<ReturnType<typeof protect>>
beforeAll(async () => {
protectClient = await protect({
schemas: [users],
})
})
describe('encryption and decryption with audit', () => {
it('should encrypt and decrypt a payload with audit metadata', async () => {
const email = 'very_secret_data'
const ciphertext = await protectClient
.encrypt(email, {
column: users.auditable,
table: users,
})
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'encrypt',
},
})
if (ciphertext.failure) {
throw new Error(`[protect]: ${ciphertext.failure.message}`)
}
// Verify encrypted field
expect(ciphertext.data).toHaveProperty('c')
const plaintext = await protectClient.decrypt(ciphertext.data).audit({
metadata: {
sub: 'cj@cjb.io',
type: 'decrypt',
},
})
expect(plaintext).toEqual({
data: email,
})
}, 30000)
it('should encrypt and decrypt a model with audit metadata', async () => {
// Create a model with decrypted values
const decryptedModel: User = {
id: '1',
email: 'test@example.com',
address: '123 Main St',
auditable: 'sensitive_data',
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 1,
}
// Encrypt the model with audit
const encryptedModel = await protectClient
.encryptModel<User>(decryptedModel, users)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'encrypt_model',
},
})
if (encryptedModel.failure) {
throw new Error(`[protect]: ${encryptedModel.failure.message}`)
}
// Verify encrypted fields
expect(encryptedModel.data.email).toHaveProperty('c')
expect(encryptedModel.data.address).toHaveProperty('c')
expect(encryptedModel.data.auditable).toHaveProperty('c')
// Verify non-encrypted fields remain unchanged
expect(encryptedModel.data.id).toBe('1')
expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01'))
expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01'))
expect(encryptedModel.data.number).toBe(1)
// Decrypt the model with audit
const decryptedResult = await protectClient
.decryptModel<User>(encryptedModel.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'decrypt_model',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual(decryptedModel)
}, 30000)
it('should handle null values in a model with audit metadata', async () => {
// Create a model with null values
const decryptedModel: User = {
id: '1',
email: null,
address: null,
auditable: null,
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 1,
}
// Encrypt the model with audit
const encryptedModel = await protectClient
.encryptModel<User>(decryptedModel, users)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'encrypt_model_nulls',
},
})
if (encryptedModel.failure) {
throw new Error(`[protect]: ${encryptedModel.failure.message}`)
}
// Verify null fields are preserved
expect(encryptedModel.data.email).toBeNull()
expect(encryptedModel.data.address).toBeNull()
expect(encryptedModel.data.auditable).toBeNull()
// Verify non-encrypted fields remain unchanged
expect(encryptedModel.data.id).toBe('1')
expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01'))
expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01'))
expect(encryptedModel.data.number).toBe(1)
// Decrypt the model with audit
const decryptedResult = await protectClient
.decryptModel<User>(encryptedModel.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'decrypt_model_nulls',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual(decryptedModel)
}, 30000)
})
describe('bulk encryption with audit', () => {
it('should bulk encrypt and decrypt models with audit metadata', async () => {
// Create models with decrypted values
const decryptedModels: User[] = [
{
id: '1',
email: 'test1@example.com',
address: '123 Main St',
auditable: 'sensitive_data_1',
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 1,
},
{
id: '2',
email: 'test2@example.com',
address: '456 Oak St',
auditable: 'sensitive_data_2',
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 2,
},
]
// Encrypt the models with audit
const encryptedModels = await protectClient
.bulkEncryptModels<User>(decryptedModels, users)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_encrypt_models',
},
})
if (encryptedModels.failure) {
throw new Error(`[protect]: ${encryptedModels.failure.message}`)
}
// Verify encrypted fields for each model
expect(encryptedModels.data[0].email).toHaveProperty('c')
expect(encryptedModels.data[0].address).toHaveProperty('c')
expect(encryptedModels.data[0].auditable).toHaveProperty('c')
expect(encryptedModels.data[1].email).toHaveProperty('c')
expect(encryptedModels.data[1].address).toHaveProperty('c')
expect(encryptedModels.data[1].auditable).toHaveProperty('c')
// Verify non-encrypted fields remain unchanged
expect(encryptedModels.data[0].id).toBe('1')
expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01'))
expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01'))
expect(encryptedModels.data[0].number).toBe(1)
expect(encryptedModels.data[1].id).toBe('2')
expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01'))
expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01'))
expect(encryptedModels.data[1].number).toBe(2)
// Decrypt the models with audit
const decryptedResult = await protectClient
.bulkDecryptModels<User>(encryptedModels.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_decrypt_models',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual(decryptedModels)
}, 30000)
it('should handle mixed null and non-null values in bulk operations with audit', async () => {
const decryptedModels: User[] = [
{
id: '1',
email: 'test1@example.com',
address: null,
auditable: 'sensitive_data_1',
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 1,
},
{
id: '2',
email: null,
address: '123 Main St',
auditable: null,
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 2,
},
{
id: '3',
email: 'test3@example.com',
address: '456 Oak St',
auditable: 'sensitive_data_3',
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
number: 3,
},
]
// Encrypt the models with audit
const encryptedModels = await protectClient
.bulkEncryptModels<User>(decryptedModels, users)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_encrypt_mixed_nulls',
},
})
if (encryptedModels.failure) {
throw new Error(`[protect]: ${encryptedModels.failure.message}`)
}
// Verify encrypted fields for each model
expect(encryptedModels.data[0].email).toHaveProperty('c')
expect(encryptedModels.data[0].address).toBeNull()
expect(encryptedModels.data[0].auditable).toHaveProperty('c')
expect(encryptedModels.data[1].email).toBeNull()
expect(encryptedModels.data[1].address).toHaveProperty('c')
expect(encryptedModels.data[1].auditable).toBeNull()
expect(encryptedModels.data[2].email).toHaveProperty('c')
expect(encryptedModels.data[2].address).toHaveProperty('c')
expect(encryptedModels.data[2].auditable).toHaveProperty('c')
// Decrypt the models with audit
const decryptedResult = await protectClient
.bulkDecryptModels<User>(decryptedModels)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_decrypt_mixed_nulls',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual(decryptedModels)
}, 30000)
it('should return empty array if models is empty with audit', async () => {
// Encrypt empty array of models with audit
const encryptedModels = await protectClient
.bulkEncryptModels<User>([], users)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_encrypt_empty',
},
})
if (encryptedModels.failure) {
throw new Error(`[protect]: ${encryptedModels.failure.message}`)
}
expect(encryptedModels.data).toEqual([])
// Decrypt empty array of models with audit
const decryptedResult = await protectClient
.bulkDecryptModels<User>([])
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_decrypt_empty',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual([])
}, 30000)
})
describe('audit with lock context', () => {
it('should encrypt and decrypt a model with both audit and 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}`)
}
// Create a model with decrypted values
const decryptedModel: User = {
id: '1',
email: 'test@example.com',
auditable: 'sensitive_with_context',
}
// Encrypt the model with both audit and lock context
const encryptedModel = await protectClient
.encryptModel(decryptedModel, users)
.withLockContext(lockContext.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'encrypt_with_context',
},
})
if (encryptedModel.failure) {
throw new Error(`[protect]: ${encryptedModel.failure.message}`)
}
// Decrypt the model with both audit and lock context
const decryptedResult = await protectClient
.decryptModel(encryptedModel.data)
.withLockContext(lockContext.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'decrypt_with_context',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual(decryptedModel)
}, 30000)
it('should bulk encrypt and decrypt models with both audit and 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}`)
}
// Create models with decrypted values
const decryptedModels: User[] = [
{
id: '1',
email: 'test1@example.com',
auditable: 'bulk_sensitive_1',
},
{
id: '2',
email: 'test2@example.com',
auditable: 'bulk_sensitive_2',
},
]
// Encrypt the models with both audit and lock context
const encryptedModels = await protectClient
.bulkEncryptModels(decryptedModels, users)
.withLockContext(lockContext.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_encrypt_with_context',
},
})
if (encryptedModels.failure) {
throw new Error(`[protect]: ${encryptedModels.failure.message}`)
}
// Decrypt the models with both audit and lock context
const decryptedResult = await protectClient
.bulkDecryptModels(encryptedModels.data)
.withLockContext(lockContext.data)
.audit({
metadata: {
sub: 'cj@cjb.io',
type: 'bulk_decrypt_with_context',
},
})
if (decryptedResult.failure) {
throw new Error(`[protect]: ${decryptedResult.failure.message}`)
}
expect(decryptedResult.data).toEqual(decryptedModels)
}, 30000)
})