Skip to content

Commit 7fd8216

Browse files
committed
feat(protect): hanlde bulk decrypt fallible response correctly
1 parent 38555cc commit 7fd8216

File tree

8 files changed

+929
-539
lines changed

8 files changed

+929
-539
lines changed

packages/protect/__tests__/bulk-protect.test.ts

Lines changed: 526 additions & 0 deletions
Large diffs are not rendered by default.

packages/protect/__tests__/protect.test.ts

Lines changed: 171 additions & 225 deletions
Large diffs are not rendered by default.

packages/protect/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
},
5454
"dependencies": {
5555
"@byteslice/result": "^0.2.0",
56-
"@cipherstash/protect-ffi": "0.15.0",
56+
"@cipherstash/protect-ffi": "0.16.0-0",
5757
"zod": "^3.24.2"
5858
},
5959
"optionalDependencies": {

packages/protect/src/ffi/index.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
EncryptOptions,
1111
EncryptPayload,
1212
SearchTerm,
13+
BulkEncryptPayload,
14+
BulkDecryptPayload,
1315
} from '../types'
1416
import { EncryptModelOperation } from './operations/encrypt-model'
1517
import { DecryptModelOperation } from './operations/decrypt-model'
@@ -18,10 +20,8 @@ import { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models'
1820
import { EncryptOperation } from './operations/encrypt'
1921
import { DecryptOperation } from './operations/decrypt'
2022
import { SearchTermsOperation } from './operations/search-terms'
21-
import {
22-
BulkEncryptOperation,
23-
type BulkEncryptPayload,
24-
} from './operations/bulk-encrypt'
23+
import { BulkEncryptOperation } from './operations/bulk-encrypt'
24+
import { BulkDecryptOperation } from './operations/bulk-decrypt'
2525
import {
2626
type EncryptConfig,
2727
encryptConfigSchema,
@@ -170,6 +170,16 @@ export class ProtectClient {
170170
return new BulkEncryptOperation(this.client, plaintexts, opts)
171171
}
172172

173+
/**
174+
* Bulk decryption - returns a thenable object.
175+
* Usage:
176+
* await eqlClient.bulkDecrypt(encryptedPayloads)
177+
* await eqlClient.bulkDecrypt(encryptedPayloads).withLockContext(lockContext)
178+
*/
179+
bulkDecrypt(encryptedPayloads: BulkDecryptPayload): BulkDecryptOperation {
180+
return new BulkDecryptOperation(this.client, encryptedPayloads)
181+
}
182+
173183
/**
174184
* Create search terms to use in a query searching encrypted data
175185
* Usage:

packages/protect/src/ffi/operations/bulk-decrypt.ts

Lines changed: 88 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,71 @@
1-
import { decryptBulk } from '@cipherstash/protect-ffi'
1+
import {
2+
decryptBulkFallible,
3+
type DecryptResult,
4+
} from '@cipherstash/protect-ffi'
25
import { withResult, type Result } from '@byteslice/result'
36
import { noClientError } from '../index'
47
import { type ProtectError, ProtectErrorTypes } from '../..'
58
import { logger } from '../../../../utils/logger'
6-
import type { LockContext } from '../../identify'
7-
import type { Client, EncryptedPayload } from '../../types'
9+
import type { LockContext, Context } from '../../identify'
10+
import type { Client, BulkDecryptPayload, BulkDecryptedData } from '../../types'
811
import { ProtectOperation } from './base-operation'
912

10-
export type BulkDecryptPayload =
11-
| Array<{ id?: string; c: EncryptedPayload }>
12-
| Array<EncryptedPayload | null>
13-
export type BulkDecryptedData =
14-
| Array<{ id?: string; plaintext: string | null }>
15-
| Array<string | null>
13+
// Helper functions for better composability
14+
const createDecryptPayloads = (
15+
encryptedPayloads: BulkDecryptPayload,
16+
lockContext?: Context,
17+
) => {
18+
return encryptedPayloads
19+
.map((item, index) => ({ ...item, originalIndex: index }))
20+
.filter(({ data }) => data !== null)
21+
.map(({ id, data, originalIndex }) => ({
22+
id,
23+
ciphertext: (typeof data === 'object' && data !== null
24+
? data.c
25+
: data) as string,
26+
originalIndex,
27+
...(lockContext && { lockContext }),
28+
}))
29+
}
30+
31+
const createNullResult = (
32+
encryptedPayloads: BulkDecryptPayload,
33+
): BulkDecryptedData => {
34+
return encryptedPayloads.map(({ id }) => ({
35+
id,
36+
data: null,
37+
}))
38+
}
39+
40+
const mapDecryptedDataToResult = (
41+
encryptedPayloads: BulkDecryptPayload,
42+
decryptedData: DecryptResult[],
43+
): BulkDecryptedData => {
44+
const result: BulkDecryptedData = new Array(encryptedPayloads.length)
45+
let decryptedIndex = 0
46+
47+
for (let i = 0; i < encryptedPayloads.length; i++) {
48+
if (encryptedPayloads[i].data === null) {
49+
result[i] = { id: encryptedPayloads[i].id, data: null }
50+
} else {
51+
const decryptResult = decryptedData[decryptedIndex]
52+
if ('error' in decryptResult) {
53+
result[i] = {
54+
id: encryptedPayloads[i].id,
55+
error: decryptResult.error,
56+
}
57+
} else {
58+
result[i] = {
59+
id: encryptedPayloads[i].id,
60+
data: decryptResult.data,
61+
}
62+
}
63+
decryptedIndex++
64+
}
65+
}
66+
67+
return result
68+
}
1669

1770
export class BulkDecryptOperation extends ProtectOperation<BulkDecryptedData> {
1871
private client: Client
@@ -38,70 +91,17 @@ export class BulkDecryptOperation extends ProtectOperation<BulkDecryptedData> {
3891
if (!this.encryptedPayloads || this.encryptedPayloads.length === 0)
3992
return []
4093

41-
const isSimpleArray =
42-
typeof this.encryptedPayloads[0] !== 'object' ||
43-
this.encryptedPayloads[0] === null ||
44-
!('c' in (this.encryptedPayloads[0] || {}))
45-
if (isSimpleArray) {
46-
const simplePayloads = this
47-
.encryptedPayloads as Array<EncryptedPayload | null>
48-
const nonNullPayloads = simplePayloads
49-
.map((c, index) => ({ c, index }))
50-
.filter(({ c }) => c !== null)
51-
.map(({ c, index }) => ({
52-
ciphertext: (typeof c === 'object' && c !== null
53-
? c.c
54-
: c) as string,
55-
originalIndex: index,
56-
}))
57-
if (nonNullPayloads.length === 0)
58-
return simplePayloads.map(() => null)
59-
const decryptedData = await decryptBulk(this.client, nonNullPayloads)
60-
const result: Array<string | null> = new Array(simplePayloads.length)
61-
let decryptedIndex = 0
62-
for (let i = 0; i < simplePayloads.length; i++) {
63-
if (simplePayloads[i] === null) {
64-
result[i] = null
65-
} else {
66-
result[i] = decryptedData[decryptedIndex]
67-
decryptedIndex++
68-
}
69-
}
70-
return result
71-
}
72-
// Array of objects (with or without id)
73-
const objPayloads = this.encryptedPayloads as Array<{
74-
id?: string
75-
c: EncryptedPayload
76-
}>
77-
const nonNullPayloads = objPayloads
78-
.map((item, index) => ({ ...item, originalIndex: index }))
79-
.filter(({ c }) => c !== null)
80-
.map(({ id, c, originalIndex }) => ({
81-
id,
82-
ciphertext: (typeof c === 'object' && c !== null
83-
? c.c
84-
: c) as string,
85-
originalIndex,
86-
}))
87-
if (nonNullPayloads.length === 0)
88-
return objPayloads.map(({ id }) => ({ id, plaintext: null }))
89-
const decryptedData = await decryptBulk(this.client, nonNullPayloads)
90-
const result: Array<{ id?: string; plaintext: string | null }> =
91-
new Array(objPayloads.length)
92-
let decryptedIndex = 0
93-
for (let i = 0; i < objPayloads.length; i++) {
94-
if (objPayloads[i].c === null) {
95-
result[i] = { id: objPayloads[i].id, plaintext: null }
96-
} else {
97-
result[i] = {
98-
id: objPayloads[i].id,
99-
plaintext: decryptedData[decryptedIndex],
100-
}
101-
decryptedIndex++
102-
}
94+
const nonNullPayloads = createDecryptPayloads(this.encryptedPayloads)
95+
96+
if (nonNullPayloads.length === 0) {
97+
return createNullResult(this.encryptedPayloads)
10398
}
104-
return result
99+
100+
const decryptedData = await decryptBulkFallible(
101+
this.client,
102+
nonNullPayloads,
103+
)
104+
return mapDecryptedDataToResult(this.encryptedPayloads, decryptedData)
105105
},
106106
(error: unknown) => ({
107107
type: ProtectErrorTypes.DecryptionError,
@@ -136,85 +136,31 @@ export class BulkDecryptOperationWithLockContext extends ProtectOperation<BulkDe
136136
async () => {
137137
const { client, encryptedPayloads } = this.operation.getOperation()
138138
logger.debug('Bulk decrypting data WITH a lock context')
139+
139140
if (!client) throw noClientError()
140141
if (!encryptedPayloads || encryptedPayloads.length === 0) return []
142+
141143
const context = await this.lockContext.getLockContext()
142-
if (context.failure)
144+
if (context.failure) {
143145
throw new Error(`[protect]: ${context.failure.message}`)
144-
const isSimpleArray =
145-
typeof encryptedPayloads[0] !== 'object' ||
146-
encryptedPayloads[0] === null ||
147-
!('c' in (encryptedPayloads[0] || {}))
148-
if (isSimpleArray) {
149-
const simplePayloads =
150-
encryptedPayloads as Array<EncryptedPayload | null>
151-
const nonNullPayloads = simplePayloads
152-
.map((c, index) => ({ c, index }))
153-
.filter(({ c }) => c !== null)
154-
.map(({ c, index }) => ({
155-
ciphertext: (typeof c === 'object' && c !== null
156-
? c.c
157-
: c) as string,
158-
originalIndex: index,
159-
lockContext: context.data.context,
160-
}))
161-
if (nonNullPayloads.length === 0)
162-
return simplePayloads.map(() => null)
163-
const decryptedData = await decryptBulk(
164-
client,
165-
nonNullPayloads,
166-
context.data.ctsToken,
167-
)
168-
const result: Array<string | null> = new Array(simplePayloads.length)
169-
let decryptedIndex = 0
170-
for (let i = 0; i < simplePayloads.length; i++) {
171-
if (simplePayloads[i] === null) {
172-
result[i] = null
173-
} else {
174-
result[i] = decryptedData[decryptedIndex]
175-
decryptedIndex++
176-
}
177-
}
178-
return result
179146
}
180-
// Array of objects (with or without id)
181-
const objPayloads = encryptedPayloads as Array<{
182-
id?: string
183-
c: EncryptedPayload
184-
}>
185-
const nonNullPayloads = objPayloads
186-
.map((item, index) => ({ ...item, originalIndex: index }))
187-
.filter(({ c }) => c !== null)
188-
.map(({ id, c, originalIndex }) => ({
189-
id,
190-
ciphertext: (typeof c === 'object' && c !== null
191-
? c.c
192-
: c) as string,
193-
originalIndex,
194-
lockContext: context.data.context,
195-
}))
196-
if (nonNullPayloads.length === 0)
197-
return objPayloads.map(({ id }) => ({ id, plaintext: null }))
198-
const decryptedData = await decryptBulk(
147+
148+
const nonNullPayloads = createDecryptPayloads(
149+
encryptedPayloads,
150+
context.data.context,
151+
)
152+
153+
if (nonNullPayloads.length === 0) {
154+
return createNullResult(encryptedPayloads)
155+
}
156+
157+
const decryptedData = await decryptBulkFallible(
199158
client,
200159
nonNullPayloads,
201160
context.data.ctsToken,
202161
)
203-
const result: Array<{ id?: string; plaintext: string | null }> =
204-
new Array(objPayloads.length)
205-
let decryptedIndex = 0
206-
for (let i = 0; i < objPayloads.length; i++) {
207-
if (objPayloads[i].c === null) {
208-
result[i] = { id: objPayloads[i].id, plaintext: null }
209-
} else {
210-
result[i] = {
211-
id: objPayloads[i].id,
212-
plaintext: decryptedData[decryptedIndex],
213-
}
214-
decryptedIndex++
215-
}
216-
}
217-
return result
162+
163+
return mapDecryptedDataToResult(encryptedPayloads, decryptedData)
218164
},
219165
(error: unknown) => ({
220166
type: ProtectErrorTypes.DecryptionError,

0 commit comments

Comments
 (0)