Skip to content

Commit 2d71807

Browse files
authored
Merge pull request #163 from cipherstash/bulk-fallible
feat(protect): init raw bulk interfaces
2 parents 9008990 + 1cc4772 commit 2d71807

File tree

12 files changed

+1706
-192
lines changed

12 files changed

+1706
-192
lines changed

.changeset/fuzzy-memes-slide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cipherstash/protect": minor
3+
---
4+
5+
Released support for bulk encryption and decryption.

README.md

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,229 @@ const decryptedUsers = decryptedResult.data;
528528
The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object.
529529
They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema.
530530

531+
### Bulk operations
532+
533+
Protect.js provides direct access to ZeroKMS bulk operations through the `bulkEncrypt` and `bulkDecrypt` methods. These methods are ideal when you need maximum performance and want to handle the correlation between encrypted/decrypted values and your application data manually.
534+
535+
> [!TIP]
536+
> The bulk operations provide the most direct interface to ZeroKMS's blazing fast bulk encryption and decryption capabilities. Each value gets a unique key while maintaining optimal performance through a single call to ZeroKMS.
537+
538+
#### Bulk encryption
539+
540+
Use the `bulkEncrypt` method to encrypt multiple plaintext values at once:
541+
542+
```typescript
543+
import { protectClient } from "./protect";
544+
import { users } from "./protect/schema";
545+
546+
// Array of plaintext values with optional IDs for correlation
547+
const plaintexts = [
548+
{ id: "user1", plaintext: "alice@example.com" },
549+
{ id: "user2", plaintext: "bob@example.com" },
550+
{ id: "user3", plaintext: "charlie@example.com" },
551+
];
552+
553+
const encryptedResult = await protectClient.bulkEncrypt(plaintexts, {
554+
column: users.email,
555+
table: users,
556+
});
557+
558+
if (encryptedResult.failure) {
559+
// Handle the failure
560+
console.log(
561+
"error when bulk encrypting:",
562+
encryptedResult.failure.type,
563+
encryptedResult.failure.message
564+
);
565+
}
566+
567+
const encryptedData = encryptedResult.data;
568+
console.log("encrypted data:", encryptedData);
569+
```
570+
571+
The `bulkEncrypt` method returns an array of objects with the following structure:
572+
573+
```typescript
574+
[
575+
{ id: "user1", data: EncryptedPayload },
576+
{ id: "user2", data: EncryptedPayload },
577+
{ id: "user3", data: EncryptedPayload },
578+
]
579+
```
580+
581+
You can also encrypt without IDs if you don't need correlation:
582+
583+
```typescript
584+
const plaintexts = [
585+
{ plaintext: "alice@example.com" },
586+
{ plaintext: "bob@example.com" },
587+
{ plaintext: "charlie@example.com" },
588+
];
589+
590+
const encryptedResult = await protectClient.bulkEncrypt(plaintexts, {
591+
column: users.email,
592+
table: users,
593+
});
594+
```
595+
596+
#### Bulk decryption
597+
598+
Use the `bulkDecrypt` method to decrypt multiple encrypted values at once:
599+
600+
```typescript
601+
import { protectClient } from "./protect";
602+
603+
// encryptedData is the result from bulkEncrypt
604+
const decryptedResult = await protectClient.bulkDecrypt(encryptedData);
605+
606+
if (decryptedResult.failure) {
607+
// Handle the failure
608+
console.log(
609+
"error when bulk decrypting:",
610+
decryptedResult.failure.type,
611+
decryptedResult.failure.message
612+
);
613+
}
614+
615+
const decryptedData = decryptedResult.data;
616+
console.log("decrypted data:", decryptedData);
617+
```
618+
619+
The `bulkDecrypt` method returns an array of objects with the following structure:
620+
621+
```typescript
622+
[
623+
{ id: "user1", data: "alice@example.com" },
624+
{ id: "user2", data: "bob@example.com" },
625+
{ id: "user3", data: "charlie@example.com" },
626+
]
627+
```
628+
629+
#### Response structure
630+
631+
The `bulkDecrypt` method returns a `Result` object that represents the overall operation status. When successful from an HTTP and execution perspective, the `data` field contains an array where each item can have one of two outcomes:
632+
633+
- **Success**: The item has a `data` field containing the decrypted plaintext
634+
- **Failure**: The item has an `error` field containing a specific error message explaining why that particular decryption failed
635+
636+
```typescript
637+
// Example response structure
638+
{
639+
data: [
640+
{ id: "user1", data: "alice@example.com" }, // Success
641+
{ id: "user2", error: "Invalid ciphertext format" }, // Failure
642+
{ id: "user3", data: "charlie@example.com" }, // Success
643+
]
644+
}
645+
```
646+
647+
> [!NOTE]
648+
> The underlying ZeroKMS response uses HTTP status code 207 (Multi-Status) to indicate that the bulk operation completed, but individual items within the batch may have succeeded or failed. This allows you to handle partial failures gracefully while still processing the successful decryptions.
649+
650+
You can handle mixed results by checking each item:
651+
652+
```typescript
653+
const decryptedResult = await protectClient.bulkDecrypt(encryptedData);
654+
655+
if (decryptedResult.failure) {
656+
// Handle overall operation failure
657+
console.log("Bulk decryption failed:", decryptedResult.failure.message);
658+
return;
659+
}
660+
661+
// Process individual results
662+
decryptedResult.data.forEach((item) => {
663+
if ('data' in item) {
664+
// Success - item.data contains the decrypted plaintext
665+
console.log(`Decrypted ${item.id}:`, item.data);
666+
} else if ('error' in item) {
667+
// Failure - item.error contains the specific error message
668+
console.log(`Failed to decrypt ${item.id}:`, item.error);
669+
}
670+
});
671+
```
672+
673+
#### Handling null values
674+
675+
Bulk operations properly handle null values in both encryption and decryption:
676+
677+
```typescript
678+
const plaintexts = [
679+
{ id: "user1", plaintext: "alice@example.com" },
680+
{ id: "user2", plaintext: null },
681+
{ id: "user3", plaintext: "charlie@example.com" },
682+
];
683+
684+
const encryptedResult = await protectClient.bulkEncrypt(plaintexts, {
685+
column: users.email,
686+
table: users,
687+
});
688+
689+
// Null values are preserved in the encrypted result
690+
// encryptedResult.data[1].data will be null
691+
692+
const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data);
693+
694+
// Null values are preserved in the decrypted result
695+
// decryptedResult.data[1].data will be null
696+
```
697+
698+
#### Using bulk operations with lock contexts
699+
700+
Bulk operations support identity-aware encryption through lock contexts:
701+
702+
```typescript
703+
import { LockContext } from "@cipherstash/protect/identify";
704+
705+
const lc = new LockContext();
706+
const lockContext = await lc.identify(userJwt);
707+
708+
if (lockContext.failure) {
709+
// Handle the failure
710+
}
711+
712+
const plaintexts = [
713+
{ id: "user1", plaintext: "alice@example.com" },
714+
{ id: "user2", plaintext: "bob@example.com" },
715+
];
716+
717+
// Encrypt with lock context
718+
const encryptedResult = await protectClient
719+
.bulkEncrypt(plaintexts, {
720+
column: users.email,
721+
table: users,
722+
})
723+
.withLockContext(lockContext.data);
724+
725+
// Decrypt with lock context
726+
const decryptedResult = await protectClient
727+
.bulkDecrypt(encryptedResult.data)
728+
.withLockContext(lockContext.data);
729+
```
730+
731+
#### Performance considerations
732+
733+
Bulk operations are optimized for performance and can handle thousands of values efficiently:
734+
735+
```typescript
736+
// Create a large array of values
737+
const plaintexts = Array.from({ length: 1000 }, (_, i) => ({
738+
id: `user${i}`,
739+
plaintext: `user${i}@example.com`,
740+
}));
741+
742+
// Single call to ZeroKMS for all 1000 values
743+
const encryptedResult = await protectClient.bulkEncrypt(plaintexts, {
744+
column: users.email,
745+
table: users,
746+
});
747+
748+
// Single call to ZeroKMS for all 1000 values
749+
const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data);
750+
```
751+
752+
The bulk operations maintain the same security guarantees as individual operations - each value gets a unique key - while providing optimal performance through ZeroKMS's bulk processing capabilities.
753+
531754
### Store encrypted data in a database
532755

533756
Encrypted data can be stored in any database that supports JSONB, noting that searchable encryption is only supported in PostgreSQL at the moment.

examples/basic/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ async function main() {
4343
console.log('Decrypting the ciphertext...')
4444
console.log('The plaintext is:', plaintext)
4545

46+
// Demonstrate bulk encryption
47+
console.log('\n--- Bulk Encryption Demo ---')
48+
49+
const bulkPlaintexts = [
50+
{ id: '1', plaintext: 'Alice' },
51+
{ id: '2', plaintext: 'Bob' },
52+
{ id: '3', plaintext: 'Charlie' },
53+
{ id: '4', plaintext: null },
54+
]
55+
56+
console.log(
57+
'Bulk encrypting names:',
58+
bulkPlaintexts.map((p) => p.plaintext),
59+
)
60+
61+
const bulkEncryptResult = await protectClient.bulkEncrypt(bulkPlaintexts, {
62+
column: users.name,
63+
table: users,
64+
})
65+
66+
if (bulkEncryptResult.failure) {
67+
throw new Error(`[protect]: ${bulkEncryptResult.failure.message}`)
68+
}
69+
70+
console.log('Bulk encrypted data:', bulkEncryptResult.data)
71+
4672
rl.close()
4773
}
4874

0 commit comments

Comments
 (0)