@striae-org/striae 4.3.4 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.env.example +9 -2
  2. package/app/components/actions/case-export/download-handlers.ts +66 -11
  3. package/app/components/actions/case-import/confirmation-import.ts +50 -7
  4. package/app/components/actions/case-import/confirmation-package.ts +99 -22
  5. package/app/components/actions/case-import/orchestrator.ts +116 -13
  6. package/app/components/actions/case-import/validation.ts +171 -7
  7. package/app/components/actions/case-import/zip-processing.ts +224 -127
  8. package/app/components/actions/case-manage.ts +74 -15
  9. package/app/components/actions/confirm-export.ts +32 -3
  10. package/app/components/actions/generate-pdf.ts +43 -1
  11. package/app/components/actions/image-manage.ts +13 -45
  12. package/app/components/navbar/navbar.module.css +0 -10
  13. package/app/components/navbar/navbar.tsx +0 -22
  14. package/app/components/sidebar/case-import/case-import.module.css +7 -131
  15. package/app/components/sidebar/case-import/case-import.tsx +7 -14
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
  19. package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
  20. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
  21. package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
  22. package/app/config-example/config.json +5 -0
  23. package/app/routes/auth/login.tsx +1 -1
  24. package/app/routes/striae/hooks/use-striae-reset-helpers.ts +4 -0
  25. package/app/routes/striae/striae.tsx +15 -4
  26. package/app/utils/data/operations/case-operations.ts +13 -1
  27. package/app/utils/data/operations/confirmation-summary-operations.ts +38 -1
  28. package/app/utils/data/operations/file-annotation-operations.ts +13 -1
  29. package/app/utils/data/operations/signing-operations.ts +93 -0
  30. package/app/utils/data/operations/types.ts +6 -0
  31. package/app/utils/forensics/export-encryption.ts +316 -0
  32. package/app/utils/forensics/export-verification.ts +1 -409
  33. package/app/utils/forensics/index.ts +1 -0
  34. package/app/utils/ui/case-messages.ts +5 -2
  35. package/package.json +2 -2
  36. package/scripts/deploy-config.sh +244 -7
  37. package/scripts/deploy-pages-secrets.sh +0 -6
  38. package/scripts/deploy-worker-secrets.sh +66 -5
  39. package/scripts/encrypt-r2-backfill.mjs +376 -0
  40. package/worker-configuration.d.ts +13 -7
  41. package/workers/audit-worker/package.json +1 -4
  42. package/workers/audit-worker/src/audit-worker.example.ts +522 -61
  43. package/workers/audit-worker/wrangler.jsonc.example +6 -1
  44. package/workers/data-worker/package.json +1 -4
  45. package/workers/data-worker/src/data-worker.example.ts +409 -1
  46. package/workers/data-worker/src/encryption-utils.ts +269 -0
  47. package/workers/data-worker/worker-configuration.d.ts +1 -1
  48. package/workers/data-worker/wrangler.jsonc.example +6 -2
  49. package/workers/image-worker/package.json +1 -4
  50. package/workers/image-worker/src/encryption-utils.ts +217 -0
  51. package/workers/image-worker/src/image-worker.example.ts +196 -127
  52. package/workers/image-worker/wrangler.jsonc.example +8 -1
  53. package/workers/keys-worker/package.json +1 -4
  54. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  55. package/workers/pdf-worker/package.json +1 -4
  56. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  57. package/workers/user-worker/package.json +1 -4
  58. package/workers/user-worker/wrangler.jsonc.example +1 -1
  59. package/wrangler.toml.example +1 -1
  60. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
  61. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
@@ -0,0 +1,269 @@
1
+ export function base64UrlDecode(value: string): Uint8Array {
2
+ const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
3
+ const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
4
+ const decoded = atob(normalized + padding);
5
+ const bytes = new Uint8Array(decoded.length);
6
+
7
+ for (let i = 0; i < decoded.length; i += 1) {
8
+ bytes[i] = decoded.charCodeAt(i);
9
+ }
10
+
11
+ return bytes;
12
+ }
13
+
14
+ export function base64UrlEncode(value: Uint8Array): string {
15
+ let binary = '';
16
+ const chunkSize = 8192;
17
+
18
+ for (let i = 0; i < value.length; i += chunkSize) {
19
+ const chunk = value.subarray(i, Math.min(i + chunkSize, value.length));
20
+ for (let j = 0; j < chunk.length; j += 1) {
21
+ binary += String.fromCharCode(chunk[j]);
22
+ }
23
+ }
24
+
25
+ return btoa(binary)
26
+ .replace(/\+/g, '-')
27
+ .replace(/\//g, '_')
28
+ .replace(/=+$/g, '');
29
+ }
30
+
31
+ const DATA_AT_REST_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM';
32
+ const DATA_AT_REST_ENCRYPTION_VERSION = '1.0';
33
+
34
+ export interface DataAtRestEnvelope {
35
+ algorithm: string;
36
+ encryptionVersion: string;
37
+ keyId: string;
38
+ dataIv: string;
39
+ wrappedKey: string;
40
+ }
41
+
42
+ interface EncryptJsonAtRestResult {
43
+ ciphertext: Uint8Array;
44
+ envelope: DataAtRestEnvelope;
45
+ }
46
+
47
+ function parseSpkiPublicKey(publicKey: string): ArrayBuffer {
48
+ const normalizedKey = publicKey
49
+ .trim()
50
+ .replace(/^['"]|['"]$/g, '')
51
+ .replace(/\\n/g, '\n');
52
+
53
+ const pemBody = normalizedKey
54
+ .replace('-----BEGIN PUBLIC KEY-----', '')
55
+ .replace('-----END PUBLIC KEY-----', '')
56
+ .replace(/\s+/g, '');
57
+
58
+ if (!pemBody) {
59
+ throw new Error('Encryption public key is invalid');
60
+ }
61
+
62
+ const binary = atob(pemBody);
63
+ const bytes = new Uint8Array(binary.length);
64
+
65
+ for (let index = 0; index < binary.length; index += 1) {
66
+ bytes[index] = binary.charCodeAt(index);
67
+ }
68
+
69
+ return bytes.buffer;
70
+ }
71
+
72
+ function parsePkcs8PrivateKey(privateKey: string): ArrayBuffer {
73
+ const normalizedKey = privateKey
74
+ .trim()
75
+ .replace(/^['"]|['"]$/g, '')
76
+ .replace(/\\n/g, '\n');
77
+
78
+ const pemBody = normalizedKey
79
+ .replace('-----BEGIN PRIVATE KEY-----', '')
80
+ .replace('-----END PRIVATE KEY-----', '')
81
+ .replace(/\s+/g, '');
82
+
83
+ if (!pemBody) {
84
+ throw new Error('Encryption private key is invalid');
85
+ }
86
+
87
+ const binary = atob(pemBody);
88
+ const bytes = new Uint8Array(binary.length);
89
+
90
+ for (let index = 0; index < binary.length; index += 1) {
91
+ bytes[index] = binary.charCodeAt(index);
92
+ }
93
+
94
+ return bytes.buffer;
95
+ }
96
+
97
+ /**
98
+ * Import RSA private key from PKCS8 PEM format
99
+ */
100
+ async function importRsaOaepPrivateKey(privateKeyPem: string): Promise<CryptoKey> {
101
+ const key = await crypto.subtle.importKey(
102
+ 'pkcs8',
103
+ parsePkcs8PrivateKey(privateKeyPem),
104
+ {
105
+ name: 'RSA-OAEP',
106
+ hash: 'SHA-256'
107
+ },
108
+ false,
109
+ ['decrypt']
110
+ );
111
+
112
+ return key;
113
+ }
114
+
115
+ async function importRsaOaepPublicKey(publicKeyPem: string): Promise<CryptoKey> {
116
+ const key = await crypto.subtle.importKey(
117
+ 'spki',
118
+ parseSpkiPublicKey(publicKeyPem),
119
+ {
120
+ name: 'RSA-OAEP',
121
+ hash: 'SHA-256'
122
+ },
123
+ false,
124
+ ['encrypt']
125
+ );
126
+
127
+ return key;
128
+ }
129
+
130
+ async function createAesGcmKey(usages: KeyUsage[]): Promise<CryptoKey> {
131
+ return crypto.subtle.generateKey(
132
+ {
133
+ name: 'AES-GCM',
134
+ length: 256
135
+ },
136
+ true,
137
+ usages
138
+ ) as Promise<CryptoKey>;
139
+ }
140
+
141
+ async function wrapAesKey(
142
+ aesKey: CryptoKey,
143
+ publicKeyPem: string
144
+ ): Promise<string> {
145
+ const rsaPublicKey = await importRsaOaepPublicKey(publicKeyPem);
146
+ const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
147
+ const wrappedKey = await crypto.subtle.encrypt(
148
+ { name: 'RSA-OAEP' },
149
+ rsaPublicKey,
150
+ rawAesKey as BufferSource
151
+ );
152
+
153
+ return base64UrlEncode(new Uint8Array(wrappedKey));
154
+ }
155
+
156
+ /**
157
+ * Decrypt AES key from RSA-OAEP wrapped form
158
+ */
159
+ async function unwrapAesKey(
160
+ wrappedKeyBase64: string,
161
+ privateKeyPem: string
162
+ ): Promise<CryptoKey> {
163
+ const rsaPrivateKey = await importRsaOaepPrivateKey(privateKeyPem);
164
+ const wrappedKeyBytes = base64UrlDecode(wrappedKeyBase64);
165
+
166
+ const rawAesKey = await crypto.subtle.decrypt(
167
+ { name: 'RSA-OAEP' },
168
+ rsaPrivateKey,
169
+ wrappedKeyBytes as BufferSource
170
+ );
171
+
172
+ return crypto.subtle.importKey(
173
+ 'raw',
174
+ rawAesKey,
175
+ { name: 'AES-GCM' },
176
+ false,
177
+ ['encrypt', 'decrypt']
178
+ );
179
+ }
180
+
181
+ export async function encryptJsonForStorage(
182
+ plaintextJson: string,
183
+ publicKeyPem: string,
184
+ keyId: string
185
+ ): Promise<EncryptJsonAtRestResult> {
186
+ const aesKey = await createAesGcmKey(['encrypt', 'decrypt']);
187
+ const wrappedKey = await wrapAesKey(aesKey, publicKeyPem);
188
+ const iv = crypto.getRandomValues(new Uint8Array(12));
189
+
190
+ const plaintextBytes = new TextEncoder().encode(plaintextJson);
191
+ const encryptedBuffer = await crypto.subtle.encrypt(
192
+ { name: 'AES-GCM', iv: iv as BufferSource },
193
+ aesKey,
194
+ plaintextBytes as BufferSource
195
+ );
196
+
197
+ return {
198
+ ciphertext: new Uint8Array(encryptedBuffer),
199
+ envelope: {
200
+ algorithm: DATA_AT_REST_ENCRYPTION_ALGORITHM,
201
+ encryptionVersion: DATA_AT_REST_ENCRYPTION_VERSION,
202
+ keyId,
203
+ dataIv: base64UrlEncode(iv),
204
+ wrappedKey
205
+ }
206
+ };
207
+ }
208
+
209
+ export async function decryptJsonFromStorage(
210
+ ciphertext: ArrayBuffer,
211
+ envelope: DataAtRestEnvelope,
212
+ privateKeyPem: string
213
+ ): Promise<string> {
214
+ const aesKey = await unwrapAesKey(envelope.wrappedKey, privateKeyPem);
215
+ const iv = base64UrlDecode(envelope.dataIv);
216
+
217
+ const plaintext = await crypto.subtle.decrypt(
218
+ { name: 'AES-GCM', iv: iv as BufferSource },
219
+ aesKey,
220
+ ciphertext as BufferSource
221
+ );
222
+
223
+ return new TextDecoder().decode(plaintext);
224
+ }
225
+
226
+ /**
227
+ * Decrypt data file (plaintext JSON/CSV)
228
+ */
229
+ export async function decryptExportData(
230
+ encryptedDataBase64: string,
231
+ wrappedKeyBase64: string,
232
+ ivBase64: string,
233
+ privateKeyPem: string
234
+ ): Promise<string> {
235
+ const aesKey = await unwrapAesKey(wrappedKeyBase64, privateKeyPem);
236
+ const iv = base64UrlDecode(ivBase64);
237
+ const ciphertext = base64UrlDecode(encryptedDataBase64);
238
+
239
+ const plaintext = await crypto.subtle.decrypt(
240
+ { name: 'AES-GCM', iv: iv as BufferSource },
241
+ aesKey,
242
+ ciphertext as BufferSource
243
+ );
244
+
245
+ return new TextDecoder().decode(plaintext);
246
+ }
247
+
248
+ /**
249
+ * Decrypt a single image blob
250
+ */
251
+ export async function decryptImageBlob(
252
+ encryptedImageBase64: string,
253
+ wrappedKeyBase64: string,
254
+ ivBase64: string,
255
+ privateKeyPem: string
256
+ ): Promise<Blob> {
257
+ const aesKey = await unwrapAesKey(wrappedKeyBase64, privateKeyPem);
258
+ const iv = base64UrlDecode(ivBase64);
259
+ const ciphertext = base64UrlDecode(encryptedImageBase64);
260
+
261
+ const plaintext = await crypto.subtle.decrypt(
262
+ { name: 'AES-GCM', iv: iv as BufferSource },
263
+ aesKey,
264
+ ciphertext as BufferSource
265
+ );
266
+
267
+ // Return as blob (caller can determine MIME type from context)
268
+ return new Blob([plaintext]);
269
+ }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable */
2
2
  // Generated by Wrangler by running `wrangler types` (hash: 4ccb8b314830f4c7bb743cb9b033a6cb)
3
- // Runtime types generated with workerd@1.20250823.0 2026-03-20 nodejs_compat
3
+ // Runtime types generated with workerd@1.20250823.0 2026-03-23 nodejs_compat
4
4
  declare namespace Cloudflare {
5
5
  interface Env {
6
6
  STRIAE_DATA: R2Bucket;
@@ -1,9 +1,13 @@
1
1
  {
2
- // Required secrets: R2_KEY_SECRET, MANIFEST_SIGNING_PRIVATE_KEY, MANIFEST_SIGNING_KEY_ID
2
+ // Required secrets: R2_KEY_SECRET, MANIFEST_SIGNING_PRIVATE_KEY, MANIFEST_SIGNING_KEY_ID, EXPORT_ENCRYPTION_PRIVATE_KEY, EXPORT_ENCRYPTION_KEY_ID
3
+ // Optional data-at-rest secrets/vars:
4
+ // - DATA_AT_REST_ENCRYPTION_ENABLED=true
5
+ // - DATA_AT_REST_ENCRYPTION_PRIVATE_KEY (required for decrypting encrypted records)
6
+ // - DATA_AT_REST_ENCRYPTION_PUBLIC_KEY and DATA_AT_REST_ENCRYPTION_KEY_ID (required when encrypt-on-write is enabled)
3
7
  "name": "DATA_WORKER_NAME",
4
8
  "account_id": "ACCOUNT_ID",
5
9
  "main": "src/data-worker.ts",
6
- "compatibility_date": "2026-03-23",
10
+ "compatibility_date": "2026-03-24",
7
11
  "compatibility_flags": [
8
12
  "nodejs_compat"
9
13
  ],
@@ -5,13 +5,10 @@
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
7
7
  "dev": "wrangler dev",
8
- "start": "wrangler dev",
9
- "test": "vitest"
8
+ "start": "wrangler dev"
10
9
  },
11
10
  "devDependencies": {
12
11
  "@cloudflare/puppeteer": "^1.0.6",
13
- "@cloudflare/vitest-pool-workers": "^0.13.0",
14
- "vitest": "~4.1.0",
15
12
  "wrangler": "^4.76.0"
16
13
  },
17
14
  "overrides": {
@@ -0,0 +1,217 @@
1
+ export function base64UrlDecode(value: string): Uint8Array {
2
+ const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
3
+ const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
4
+ const decoded = atob(normalized + padding);
5
+ const bytes = new Uint8Array(decoded.length);
6
+
7
+ for (let i = 0; i < decoded.length; i += 1) {
8
+ bytes[i] = decoded.charCodeAt(i);
9
+ }
10
+
11
+ return bytes;
12
+ }
13
+
14
+ export function base64UrlEncode(value: Uint8Array): string {
15
+ let binary = '';
16
+ const chunkSize = 8192;
17
+
18
+ for (let i = 0; i < value.length; i += chunkSize) {
19
+ const chunk = value.subarray(i, Math.min(i + chunkSize, value.length));
20
+ for (let j = 0; j < chunk.length; j += 1) {
21
+ binary += String.fromCharCode(chunk[j]);
22
+ }
23
+ }
24
+
25
+ return btoa(binary)
26
+ .replace(/\+/g, '-')
27
+ .replace(/\//g, '_')
28
+ .replace(/=+$/g, '');
29
+ }
30
+
31
+ const DATA_AT_REST_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM';
32
+ const DATA_AT_REST_ENCRYPTION_VERSION = '1.0';
33
+
34
+ export interface DataAtRestEnvelope {
35
+ algorithm: string;
36
+ encryptionVersion: string;
37
+ keyId: string;
38
+ dataIv: string;
39
+ wrappedKey: string;
40
+ }
41
+
42
+ interface EncryptBinaryAtRestResult {
43
+ ciphertext: Uint8Array;
44
+ envelope: DataAtRestEnvelope;
45
+ }
46
+
47
+ function parseSpkiPublicKey(publicKey: string): ArrayBuffer {
48
+ const normalizedKey = publicKey
49
+ .trim()
50
+ .replace(/^['"]|['"]$/g, '')
51
+ .replace(/\\n/g, '\n');
52
+
53
+ const pemBody = normalizedKey
54
+ .replace('-----BEGIN PUBLIC KEY-----', '')
55
+ .replace('-----END PUBLIC KEY-----', '')
56
+ .replace(/\s+/g, '');
57
+
58
+ if (!pemBody) {
59
+ throw new Error('Encryption public key is invalid');
60
+ }
61
+
62
+ const binary = atob(pemBody);
63
+ const bytes = new Uint8Array(binary.length);
64
+
65
+ for (let index = 0; index < binary.length; index += 1) {
66
+ bytes[index] = binary.charCodeAt(index);
67
+ }
68
+
69
+ return bytes.buffer;
70
+ }
71
+
72
+ function parsePkcs8PrivateKey(privateKey: string): ArrayBuffer {
73
+ const normalizedKey = privateKey
74
+ .trim()
75
+ .replace(/^['"]|['"]$/g, '')
76
+ .replace(/\\n/g, '\n');
77
+
78
+ const pemBody = normalizedKey
79
+ .replace('-----BEGIN PRIVATE KEY-----', '')
80
+ .replace('-----END PRIVATE KEY-----', '')
81
+ .replace(/\s+/g, '');
82
+
83
+ if (!pemBody) {
84
+ throw new Error('Encryption private key is invalid');
85
+ }
86
+
87
+ const binary = atob(pemBody);
88
+ const bytes = new Uint8Array(binary.length);
89
+
90
+ for (let index = 0; index < binary.length; index += 1) {
91
+ bytes[index] = binary.charCodeAt(index);
92
+ }
93
+
94
+ return bytes.buffer;
95
+ }
96
+
97
+ async function importRsaOaepPrivateKey(privateKeyPem: string): Promise<CryptoKey> {
98
+ return crypto.subtle.importKey(
99
+ 'pkcs8',
100
+ parsePkcs8PrivateKey(privateKeyPem),
101
+ {
102
+ name: 'RSA-OAEP',
103
+ hash: 'SHA-256'
104
+ },
105
+ false,
106
+ ['decrypt']
107
+ );
108
+ }
109
+
110
+ async function importRsaOaepPublicKey(publicKeyPem: string): Promise<CryptoKey> {
111
+ return crypto.subtle.importKey(
112
+ 'spki',
113
+ parseSpkiPublicKey(publicKeyPem),
114
+ {
115
+ name: 'RSA-OAEP',
116
+ hash: 'SHA-256'
117
+ },
118
+ false,
119
+ ['encrypt']
120
+ );
121
+ }
122
+
123
+ async function createAesGcmKey(usages: KeyUsage[]): Promise<CryptoKey> {
124
+ return crypto.subtle.generateKey(
125
+ {
126
+ name: 'AES-GCM',
127
+ length: 256
128
+ },
129
+ true,
130
+ usages
131
+ ) as Promise<CryptoKey>;
132
+ }
133
+
134
+ async function wrapAesKey(aesKey: CryptoKey, publicKeyPem: string): Promise<string> {
135
+ const rsaPublicKey = await importRsaOaepPublicKey(publicKeyPem);
136
+ const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
137
+ const wrappedKey = await crypto.subtle.encrypt(
138
+ { name: 'RSA-OAEP' },
139
+ rsaPublicKey,
140
+ rawAesKey as BufferSource
141
+ );
142
+
143
+ return base64UrlEncode(new Uint8Array(wrappedKey));
144
+ }
145
+
146
+ async function unwrapAesKey(wrappedKeyBase64: string, privateKeyPem: string): Promise<CryptoKey> {
147
+ const rsaPrivateKey = await importRsaOaepPrivateKey(privateKeyPem);
148
+ const wrappedKeyBytes = base64UrlDecode(wrappedKeyBase64);
149
+
150
+ const rawAesKey = await crypto.subtle.decrypt(
151
+ { name: 'RSA-OAEP' },
152
+ rsaPrivateKey,
153
+ wrappedKeyBytes as BufferSource
154
+ );
155
+
156
+ return crypto.subtle.importKey(
157
+ 'raw',
158
+ rawAesKey,
159
+ { name: 'AES-GCM' },
160
+ false,
161
+ ['encrypt', 'decrypt']
162
+ );
163
+ }
164
+
165
+ export function validateEnvelope(envelope: DataAtRestEnvelope): void {
166
+ if (envelope.algorithm !== DATA_AT_REST_ENCRYPTION_ALGORITHM) {
167
+ throw new Error('Unsupported data-at-rest encryption algorithm');
168
+ }
169
+
170
+ if (envelope.encryptionVersion !== DATA_AT_REST_ENCRYPTION_VERSION) {
171
+ throw new Error('Unsupported data-at-rest encryption version');
172
+ }
173
+ }
174
+
175
+ export async function encryptBinaryForStorage(
176
+ plaintextBytes: ArrayBuffer,
177
+ publicKeyPem: string,
178
+ keyId: string
179
+ ): Promise<EncryptBinaryAtRestResult> {
180
+ const aesKey = await createAesGcmKey(['encrypt', 'decrypt']);
181
+ const wrappedKey = await wrapAesKey(aesKey, publicKeyPem);
182
+ const iv = crypto.getRandomValues(new Uint8Array(12));
183
+
184
+ const encryptedBuffer = await crypto.subtle.encrypt(
185
+ { name: 'AES-GCM', iv: iv as BufferSource },
186
+ aesKey,
187
+ plaintextBytes as BufferSource
188
+ );
189
+
190
+ return {
191
+ ciphertext: new Uint8Array(encryptedBuffer),
192
+ envelope: {
193
+ algorithm: DATA_AT_REST_ENCRYPTION_ALGORITHM,
194
+ encryptionVersion: DATA_AT_REST_ENCRYPTION_VERSION,
195
+ keyId,
196
+ dataIv: base64UrlEncode(iv),
197
+ wrappedKey
198
+ }
199
+ };
200
+ }
201
+
202
+ export async function decryptBinaryFromStorage(
203
+ ciphertext: ArrayBuffer,
204
+ envelope: DataAtRestEnvelope,
205
+ privateKeyPem: string
206
+ ): Promise<ArrayBuffer> {
207
+ validateEnvelope(envelope);
208
+
209
+ const aesKey = await unwrapAesKey(envelope.wrappedKey, privateKeyPem);
210
+ const iv = base64UrlDecode(envelope.dataIv);
211
+
212
+ return crypto.subtle.decrypt(
213
+ { name: 'AES-GCM', iv: iv as BufferSource },
214
+ aesKey,
215
+ ciphertext as BufferSource
216
+ );
217
+ }