@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.
- package/.env.example +9 -2
- package/app/components/actions/case-export/download-handlers.ts +66 -11
- package/app/components/actions/case-import/confirmation-import.ts +50 -7
- package/app/components/actions/case-import/confirmation-package.ts +99 -22
- package/app/components/actions/case-import/orchestrator.ts +116 -13
- package/app/components/actions/case-import/validation.ts +171 -7
- package/app/components/actions/case-import/zip-processing.ts +224 -127
- package/app/components/actions/case-manage.ts +74 -15
- package/app/components/actions/confirm-export.ts +32 -3
- package/app/components/actions/generate-pdf.ts +43 -1
- package/app/components/actions/image-manage.ts +13 -45
- package/app/components/navbar/navbar.module.css +0 -10
- package/app/components/navbar/navbar.tsx +0 -22
- package/app/components/sidebar/case-import/case-import.module.css +7 -131
- package/app/components/sidebar/case-import/case-import.tsx +7 -14
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
- package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
- package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
- package/app/config-example/config.json +5 -0
- package/app/routes/auth/login.tsx +1 -1
- package/app/routes/striae/hooks/use-striae-reset-helpers.ts +4 -0
- package/app/routes/striae/striae.tsx +15 -4
- package/app/utils/data/operations/case-operations.ts +13 -1
- package/app/utils/data/operations/confirmation-summary-operations.ts +38 -1
- package/app/utils/data/operations/file-annotation-operations.ts +13 -1
- package/app/utils/data/operations/signing-operations.ts +93 -0
- package/app/utils/data/operations/types.ts +6 -0
- package/app/utils/forensics/export-encryption.ts +316 -0
- package/app/utils/forensics/export-verification.ts +1 -409
- package/app/utils/forensics/index.ts +1 -0
- package/app/utils/ui/case-messages.ts +5 -2
- package/package.json +2 -2
- package/scripts/deploy-config.sh +244 -7
- package/scripts/deploy-pages-secrets.sh +0 -6
- package/scripts/deploy-worker-secrets.sh +66 -5
- package/scripts/encrypt-r2-backfill.mjs +376 -0
- package/worker-configuration.d.ts +13 -7
- package/workers/audit-worker/package.json +1 -4
- package/workers/audit-worker/src/audit-worker.example.ts +522 -61
- package/workers/audit-worker/wrangler.jsonc.example +6 -1
- package/workers/data-worker/package.json +1 -4
- package/workers/data-worker/src/data-worker.example.ts +409 -1
- package/workers/data-worker/src/encryption-utils.ts +269 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +6 -2
- package/workers/image-worker/package.json +1 -4
- package/workers/image-worker/src/encryption-utils.ts +217 -0
- package/workers/image-worker/src/image-worker.example.ts +196 -127
- package/workers/image-worker/wrangler.jsonc.example +8 -1
- package/workers/keys-worker/package.json +1 -4
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -4
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -4
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
// Required secrets: R2_KEY_SECRET
|
|
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)
|
|
2
7
|
"name": "AUDIT_WORKER_NAME",
|
|
3
8
|
"account_id": "ACCOUNT_ID",
|
|
4
9
|
"main": "src/audit-worker.ts",
|
|
5
|
-
"compatibility_date": "2026-03-
|
|
10
|
+
"compatibility_date": "2026-03-24",
|
|
6
11
|
"compatibility_flags": [
|
|
7
12
|
"nodejs_compat"
|
|
8
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": {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { signPayload as signWithWorkerKey } from './signature-utils';
|
|
2
|
+
import {
|
|
3
|
+
decryptExportData,
|
|
4
|
+
decryptImageBlob,
|
|
5
|
+
decryptJsonFromStorage,
|
|
6
|
+
encryptJsonForStorage,
|
|
7
|
+
type DataAtRestEnvelope
|
|
8
|
+
} from './encryption-utils';
|
|
2
9
|
import {
|
|
3
10
|
AUDIT_EXPORT_SIGNATURE_VERSION,
|
|
4
11
|
CONFIRMATION_SIGNATURE_VERSION,
|
|
@@ -20,6 +27,12 @@ interface Env {
|
|
|
20
27
|
STRIAE_DATA: R2Bucket;
|
|
21
28
|
MANIFEST_SIGNING_PRIVATE_KEY: string;
|
|
22
29
|
MANIFEST_SIGNING_KEY_ID: string;
|
|
30
|
+
EXPORT_ENCRYPTION_PRIVATE_KEY?: string;
|
|
31
|
+
EXPORT_ENCRYPTION_KEY_ID?: string;
|
|
32
|
+
DATA_AT_REST_ENCRYPTION_ENABLED?: string;
|
|
33
|
+
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
|
|
34
|
+
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY?: string;
|
|
35
|
+
DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
interface SuccessResponse {
|
|
@@ -50,6 +63,204 @@ const hasValidHeader = (request: Request, env: Env): boolean =>
|
|
|
50
63
|
const SIGN_MANIFEST_PATH = '/api/forensic/sign-manifest';
|
|
51
64
|
const SIGN_CONFIRMATION_PATH = '/api/forensic/sign-confirmation';
|
|
52
65
|
const SIGN_AUDIT_EXPORT_PATH = '/api/forensic/sign-audit-export';
|
|
66
|
+
const DECRYPT_EXPORT_PATH = '/api/forensic/decrypt-export';
|
|
67
|
+
const DATA_AT_REST_BACKFILL_PATH = '/api/admin/data-at-rest-backfill';
|
|
68
|
+
const DATA_AT_REST_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM';
|
|
69
|
+
const DATA_AT_REST_ENCRYPTION_VERSION = '1.0';
|
|
70
|
+
|
|
71
|
+
function isDataAtRestEncryptionEnabled(env: Env): boolean {
|
|
72
|
+
const value = env.DATA_AT_REST_ENCRYPTION_ENABLED;
|
|
73
|
+
if (!value) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
78
|
+
return normalizedValue === '1' || normalizedValue === 'true' || normalizedValue === 'yes' || normalizedValue === 'on';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractDataAtRestEnvelope(file: R2ObjectBody): DataAtRestEnvelope | null {
|
|
82
|
+
const metadata = file.customMetadata;
|
|
83
|
+
if (!metadata) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
algorithm,
|
|
89
|
+
encryptionVersion,
|
|
90
|
+
keyId,
|
|
91
|
+
dataIv,
|
|
92
|
+
wrappedKey
|
|
93
|
+
} = metadata;
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
typeof algorithm !== 'string' ||
|
|
97
|
+
typeof encryptionVersion !== 'string' ||
|
|
98
|
+
typeof keyId !== 'string' ||
|
|
99
|
+
typeof dataIv !== 'string' ||
|
|
100
|
+
typeof wrappedKey !== 'string'
|
|
101
|
+
) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
algorithm,
|
|
107
|
+
encryptionVersion,
|
|
108
|
+
keyId,
|
|
109
|
+
dataIv,
|
|
110
|
+
wrappedKey
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasDataAtRestMetadata(metadata: Record<string, string> | undefined): boolean {
|
|
115
|
+
if (!metadata) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
typeof metadata.algorithm === 'string' &&
|
|
121
|
+
typeof metadata.encryptionVersion === 'string' &&
|
|
122
|
+
typeof metadata.keyId === 'string' &&
|
|
123
|
+
typeof metadata.dataIv === 'string' &&
|
|
124
|
+
typeof metadata.wrappedKey === 'string'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function clampBackfillBatchSize(size: number | undefined): number {
|
|
129
|
+
if (typeof size !== 'number' || !Number.isFinite(size)) {
|
|
130
|
+
return 100;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const normalized = Math.floor(size);
|
|
134
|
+
if (normalized < 1) {
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (normalized > 1000) {
|
|
139
|
+
return 1000;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function handleDataAtRestBackfill(request: Request, env: Env): Promise<Response> {
|
|
146
|
+
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
147
|
+
return createResponse(
|
|
148
|
+
{ error: 'Data-at-rest encryption is not configured for backfill writes' },
|
|
149
|
+
400
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const requestBody = await request.json().catch(() => ({})) as {
|
|
154
|
+
dryRun?: boolean;
|
|
155
|
+
prefix?: string;
|
|
156
|
+
cursor?: string;
|
|
157
|
+
batchSize?: number;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const dryRun = requestBody.dryRun === true;
|
|
161
|
+
const prefix = typeof requestBody.prefix === 'string' ? requestBody.prefix : '';
|
|
162
|
+
const cursor = typeof requestBody.cursor === 'string' && requestBody.cursor.length > 0
|
|
163
|
+
? requestBody.cursor
|
|
164
|
+
: undefined;
|
|
165
|
+
const batchSize = clampBackfillBatchSize(requestBody.batchSize);
|
|
166
|
+
|
|
167
|
+
const bucket = env.STRIAE_DATA;
|
|
168
|
+
const listed = await bucket.list({
|
|
169
|
+
prefix: prefix.length > 0 ? prefix : undefined,
|
|
170
|
+
cursor,
|
|
171
|
+
limit: batchSize
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
let scanned = 0;
|
|
175
|
+
let eligible = 0;
|
|
176
|
+
let encrypted = 0;
|
|
177
|
+
let skippedEncrypted = 0;
|
|
178
|
+
let skippedNonJson = 0;
|
|
179
|
+
let failed = 0;
|
|
180
|
+
const failures: Array<{ key: string; error: string }> = [];
|
|
181
|
+
|
|
182
|
+
for (const object of listed.objects) {
|
|
183
|
+
scanned += 1;
|
|
184
|
+
const key = object.key;
|
|
185
|
+
|
|
186
|
+
if (!key.endsWith('.json')) {
|
|
187
|
+
skippedNonJson += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const objectHead = await bucket.head(key);
|
|
192
|
+
if (!objectHead) {
|
|
193
|
+
failed += 1;
|
|
194
|
+
if (failures.length < 20) {
|
|
195
|
+
failures.push({ key, error: 'Object not found during metadata check' });
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (hasDataAtRestMetadata(objectHead.customMetadata)) {
|
|
201
|
+
skippedEncrypted += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
eligible += 1;
|
|
206
|
+
|
|
207
|
+
if (dryRun) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const existingObject = await bucket.get(key);
|
|
213
|
+
if (!existingObject) {
|
|
214
|
+
failed += 1;
|
|
215
|
+
if (failures.length < 20) {
|
|
216
|
+
failures.push({ key, error: 'Object disappeared before processing' });
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const plaintext = await existingObject.text();
|
|
222
|
+
const encryptedPayload = await encryptJsonForStorage(
|
|
223
|
+
plaintext,
|
|
224
|
+
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
225
|
+
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await bucket.put(key, encryptedPayload.ciphertext, {
|
|
229
|
+
customMetadata: {
|
|
230
|
+
algorithm: encryptedPayload.envelope.algorithm,
|
|
231
|
+
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
232
|
+
keyId: encryptedPayload.envelope.keyId,
|
|
233
|
+
dataIv: encryptedPayload.envelope.dataIv,
|
|
234
|
+
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
encrypted += 1;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
failed += 1;
|
|
241
|
+
if (failures.length < 20) {
|
|
242
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown backfill failure';
|
|
243
|
+
failures.push({ key, error: errorMessage });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return createResponse({
|
|
249
|
+
success: failed === 0,
|
|
250
|
+
dryRun,
|
|
251
|
+
prefix: prefix.length > 0 ? prefix : null,
|
|
252
|
+
batchSize,
|
|
253
|
+
scanned,
|
|
254
|
+
eligible,
|
|
255
|
+
encrypted,
|
|
256
|
+
skippedEncrypted,
|
|
257
|
+
skippedNonJson,
|
|
258
|
+
failed,
|
|
259
|
+
failures,
|
|
260
|
+
hasMore: listed.truncated,
|
|
261
|
+
nextCursor: listed.truncated ? listed.cursor : null
|
|
262
|
+
});
|
|
263
|
+
}
|
|
53
264
|
|
|
54
265
|
async function signPayloadWithWorkerKey(payload: string, env: Env): Promise<{
|
|
55
266
|
algorithm: string;
|
|
@@ -196,6 +407,128 @@ async function handleSignAuditExport(request: Request, env: Env): Promise<Respon
|
|
|
196
407
|
}
|
|
197
408
|
}
|
|
198
409
|
|
|
410
|
+
async function handleDecryptExport(request: Request, env: Env): Promise<Response> {
|
|
411
|
+
try {
|
|
412
|
+
// Check if encryption is configured
|
|
413
|
+
if (!env.EXPORT_ENCRYPTION_PRIVATE_KEY || !env.EXPORT_ENCRYPTION_KEY_ID) {
|
|
414
|
+
return createResponse(
|
|
415
|
+
{ error: 'Export decryption is not configured on this server' },
|
|
416
|
+
400
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const requestBody = await request.json() as {
|
|
421
|
+
wrappedKey?: string;
|
|
422
|
+
dataIv?: string;
|
|
423
|
+
encryptedData?: string;
|
|
424
|
+
encryptedImages?: Array<{ filename: string; encryptedData: string; iv?: string }>;
|
|
425
|
+
keyId?: string;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const { wrappedKey, dataIv, encryptedData, encryptedImages, keyId } = requestBody;
|
|
429
|
+
|
|
430
|
+
// Validate required fields
|
|
431
|
+
if (
|
|
432
|
+
!wrappedKey ||
|
|
433
|
+
typeof wrappedKey !== 'string' ||
|
|
434
|
+
!dataIv ||
|
|
435
|
+
typeof dataIv !== 'string' ||
|
|
436
|
+
!encryptedData ||
|
|
437
|
+
typeof encryptedData !== 'string' ||
|
|
438
|
+
!keyId ||
|
|
439
|
+
typeof keyId !== 'string'
|
|
440
|
+
) {
|
|
441
|
+
return createResponse(
|
|
442
|
+
{ error: 'Missing or invalid required fields: wrappedKey, dataIv, encryptedData, keyId' },
|
|
443
|
+
400
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Validate keyId matches configured key
|
|
448
|
+
if (keyId !== env.EXPORT_ENCRYPTION_KEY_ID) {
|
|
449
|
+
return createResponse(
|
|
450
|
+
{ error: `Key ID mismatch: expected ${env.EXPORT_ENCRYPTION_KEY_ID}, got ${keyId}` },
|
|
451
|
+
400
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Decrypt data file
|
|
456
|
+
let plaintextData: string;
|
|
457
|
+
try {
|
|
458
|
+
plaintextData = await decryptExportData(
|
|
459
|
+
encryptedData,
|
|
460
|
+
wrappedKey,
|
|
461
|
+
dataIv,
|
|
462
|
+
env.EXPORT_ENCRYPTION_PRIVATE_KEY
|
|
463
|
+
);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error('Data file decryption failed:', error);
|
|
466
|
+
const errorMessage = error instanceof Error ? error.message : 'Decryption failed';
|
|
467
|
+
return createResponse(
|
|
468
|
+
{ error: `Failed to decrypt data file: ${errorMessage}` },
|
|
469
|
+
500
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Decrypt images if provided
|
|
474
|
+
const decryptedImages: Array<{ filename: string; data: string }> = [];
|
|
475
|
+
if (Array.isArray(encryptedImages) && encryptedImages.length > 0) {
|
|
476
|
+
for (const imageEntry of encryptedImages) {
|
|
477
|
+
try {
|
|
478
|
+
if (!imageEntry.iv || typeof imageEntry.iv !== 'string') {
|
|
479
|
+
return createResponse(
|
|
480
|
+
{ error: `Missing IV for image ${imageEntry.filename}` },
|
|
481
|
+
400
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const imageBlob = await decryptImageBlob(
|
|
486
|
+
imageEntry.encryptedData,
|
|
487
|
+
wrappedKey,
|
|
488
|
+
imageEntry.iv,
|
|
489
|
+
env.EXPORT_ENCRYPTION_PRIVATE_KEY
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// Convert blob to base64 for transport
|
|
493
|
+
const arrayBuffer = await imageBlob.arrayBuffer();
|
|
494
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
495
|
+
const chunkSize = 8192;
|
|
496
|
+
let binary = '';
|
|
497
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
498
|
+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
499
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
500
|
+
binary += String.fromCharCode(chunk[j]);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const base64Data = btoa(binary);
|
|
504
|
+
|
|
505
|
+
decryptedImages.push({
|
|
506
|
+
filename: imageEntry.filename,
|
|
507
|
+
data: base64Data
|
|
508
|
+
});
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error(`Image decryption failed for ${imageEntry.filename}:`, error);
|
|
511
|
+
const errorMessage = error instanceof Error ? error.message : 'Decryption failed';
|
|
512
|
+
return createResponse(
|
|
513
|
+
{ error: `Failed to decrypt image ${imageEntry.filename}: ${errorMessage}` },
|
|
514
|
+
500
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return createResponse({
|
|
521
|
+
success: true,
|
|
522
|
+
plaintext: plaintextData,
|
|
523
|
+
decryptedImages
|
|
524
|
+
});
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error('Export decryption request failed:', error);
|
|
527
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
528
|
+
return createResponse({ error: errorMessage }, 500);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
199
532
|
export default {
|
|
200
533
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
201
534
|
if (request.method === 'OPTIONS') {
|
|
@@ -223,6 +556,14 @@ export default {
|
|
|
223
556
|
return await handleSignAuditExport(request, env);
|
|
224
557
|
}
|
|
225
558
|
|
|
559
|
+
if (request.method === 'POST' && pathname === DECRYPT_EXPORT_PATH) {
|
|
560
|
+
return await handleDecryptExport(request, env);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (request.method === 'POST' && pathname === DATA_AT_REST_BACKFILL_PATH) {
|
|
564
|
+
return await handleDataAtRestBackfill(request, env);
|
|
565
|
+
}
|
|
566
|
+
|
|
226
567
|
const filename = pathname.slice(1) || 'data.json';
|
|
227
568
|
|
|
228
569
|
if (!filename.endsWith('.json')) {
|
|
@@ -235,6 +576,39 @@ export default {
|
|
|
235
576
|
if (!file) {
|
|
236
577
|
return createResponse([], 200);
|
|
237
578
|
}
|
|
579
|
+
|
|
580
|
+
const atRestEnvelope = extractDataAtRestEnvelope(file);
|
|
581
|
+
if (atRestEnvelope) {
|
|
582
|
+
if (atRestEnvelope.algorithm !== DATA_AT_REST_ENCRYPTION_ALGORITHM) {
|
|
583
|
+
return createResponse({ error: 'Unsupported data-at-rest encryption algorithm' }, 500);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (atRestEnvelope.encryptionVersion !== DATA_AT_REST_ENCRYPTION_VERSION) {
|
|
587
|
+
return createResponse({ error: 'Unsupported data-at-rest encryption version' }, 500);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY) {
|
|
591
|
+
return createResponse(
|
|
592
|
+
{ error: 'Data-at-rest decryption is not configured on this server' },
|
|
593
|
+
500
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const encryptedData = await file.arrayBuffer();
|
|
599
|
+
const plaintext = await decryptJsonFromStorage(
|
|
600
|
+
encryptedData,
|
|
601
|
+
atRestEnvelope,
|
|
602
|
+
env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY
|
|
603
|
+
);
|
|
604
|
+
const decryptedPayload = JSON.parse(plaintext);
|
|
605
|
+
return createResponse(decryptedPayload);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
console.error('Data-at-rest decryption failed:', error);
|
|
608
|
+
return createResponse({ error: 'Failed to decrypt stored data' }, 500);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
238
612
|
const fileText = await file.text();
|
|
239
613
|
const data = JSON.parse(fileText);
|
|
240
614
|
return createResponse(data);
|
|
@@ -242,7 +616,41 @@ export default {
|
|
|
242
616
|
|
|
243
617
|
case 'PUT': {
|
|
244
618
|
const newData = await request.json();
|
|
245
|
-
|
|
619
|
+
const serializedData = JSON.stringify(newData);
|
|
620
|
+
|
|
621
|
+
if (!isDataAtRestEncryptionEnabled(env)) {
|
|
622
|
+
await bucket.put(filename, serializedData);
|
|
623
|
+
return createResponse({ success: true });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
627
|
+
return createResponse(
|
|
628
|
+
{ error: 'Data-at-rest encryption is enabled but not fully configured' },
|
|
629
|
+
500
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
const encryptedPayload = await encryptJsonForStorage(
|
|
635
|
+
serializedData,
|
|
636
|
+
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
637
|
+
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
await bucket.put(filename, encryptedPayload.ciphertext, {
|
|
641
|
+
customMetadata: {
|
|
642
|
+
algorithm: encryptedPayload.envelope.algorithm,
|
|
643
|
+
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
644
|
+
keyId: encryptedPayload.envelope.keyId,
|
|
645
|
+
dataIv: encryptedPayload.envelope.dataIv,
|
|
646
|
+
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.error('Data-at-rest encryption failed:', error);
|
|
651
|
+
return createResponse({ error: 'Failed to encrypt data for storage' }, 500);
|
|
652
|
+
}
|
|
653
|
+
|
|
246
654
|
return createResponse({ success: true });
|
|
247
655
|
}
|
|
248
656
|
|