@striae-org/striae 5.2.0 → 5.3.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 +36 -33
- package/README.md +5 -46
- package/app/components/actions/case-export/core-export.ts +2 -174
- package/app/components/actions/case-export/download-handlers.ts +83 -750
- package/app/components/actions/case-export/index.ts +6 -30
- package/app/components/actions/case-export/metadata-helpers.ts +0 -78
- package/app/components/actions/case-export/types-constants.ts +0 -43
- package/app/components/actions/case-import/confirmation-import.ts +13 -14
- package/app/components/actions/case-import/zip-processing.ts +92 -12
- package/app/components/actions/generate-pdf.ts +3 -2
- package/app/components/audit/user-audit-viewer.tsx +0 -19
- package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
- package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/case-import/case-import.module.css +35 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
- package/app/components/sidebar/notes/class-details-shared.ts +2 -2
- package/app/components/toast/toast.module.css +36 -0
- package/app/components/toast/toast.tsx +6 -2
- package/app/components/user/manage-profile.tsx +4 -3
- package/app/config-example/config.json +1 -2
- package/app/root.tsx +0 -7
- package/app/routes/_index.tsx +1 -1
- package/app/routes/auth/login.example.tsx +22 -103
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +53 -59
- package/app/services/firebase/index.ts +0 -3
- package/app/types/export.ts +1 -2
- package/app/utils/auth/index.ts +0 -1
- package/app/utils/data/permissions.ts +3 -2
- package/package.json +10 -17
- package/public/_headers +0 -4
- package/public/_routes.json +0 -1
- package/worker-configuration.d.ts +20 -17
- package/workers/audit-worker/src/audit-worker.example.ts +9 -806
- package/workers/audit-worker/src/config.ts +7 -0
- package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
- package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
- package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
- package/workers/audit-worker/src/types.ts +56 -0
- package/workers/audit-worker/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/config.ts +11 -0
- package/workers/data-worker/src/data-worker.example.ts +21 -942
- package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
- package/workers/data-worker/src/handlers/signing.ts +174 -0
- package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
- package/workers/data-worker/src/registry/key-registry.ts +368 -0
- package/workers/data-worker/src/types.ts +46 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/worker-configuration.d.ts +2 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/auth.ts +30 -0
- package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
- package/workers/user-worker/src/config.ts +4 -0
- package/workers/user-worker/src/encryption-utils.ts +25 -0
- package/workers/user-worker/src/firebase/admin.ts +152 -0
- package/workers/user-worker/src/handlers/user-routes.ts +242 -0
- package/workers/user-worker/src/registry/user-kv.ts +172 -0
- package/workers/user-worker/src/storage/user-records.ts +34 -0
- package/workers/user-worker/src/types.ts +106 -0
- package/workers/user-worker/src/user-worker.example.ts +18 -964
- package/workers/user-worker/worker-configuration.d.ts +4 -2
- package/workers/user-worker/wrangler.jsonc.example +12 -1
- package/wrangler.toml.example +1 -1
- package/app/components/actions/case-export/data-processing.ts +0 -223
- package/app/components/sidebar/case-export/case-export.module.css +0 -418
- package/app/components/sidebar/case-export/case-export.tsx +0 -310
- package/app/types/exceljs-bare.d.ts +0 -9
- package/app/utils/auth/auth.ts +0 -11
- package/public/.well-known/security.txt +0 -6
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +0 -39
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/vendor/exceljs.LICENSE +0 -22
- package/public/vendor/exceljs.bare.min.js +0 -45
- package/scripts/deploy-all.sh +0 -166
- package/scripts/deploy-config/modules/env-utils.sh +0 -322
- package/scripts/deploy-config/modules/keys.sh +0 -404
- package/scripts/deploy-config/modules/prompt.sh +0 -372
- package/scripts/deploy-config/modules/scaffolding.sh +0 -336
- package/scripts/deploy-config/modules/validation.sh +0 -365
- package/scripts/deploy-config.sh +0 -236
- package/scripts/deploy-pages-secrets.sh +0 -231
- package/scripts/deploy-pages.sh +0 -34
- package/scripts/deploy-primershear-emails.sh +0 -167
- package/scripts/deploy-worker-secrets.sh +0 -374
- package/scripts/dev.cjs +0 -23
- package/scripts/install-workers.sh +0 -88
- package/scripts/run-eslint.cjs +0 -43
- package/scripts/update-compatibility-dates.cjs +0 -124
- package/scripts/update-markdown-versions.cjs +0 -43
- package/workers/keys-worker/package.json +0 -18
- package/workers/keys-worker/src/keys.example.ts +0 -67
- package/workers/keys-worker/src/keys.ts +0 -67
- package/workers/keys-worker/worker-configuration.d.ts +0 -7447
- package/workers/keys-worker/wrangler.jsonc.example +0 -15
|
@@ -1,71 +1,18 @@
|
|
|
1
|
-
import { signPayload as signWithWorkerKey } from './signature-utils';
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from './
|
|
2
|
+
DECRYPT_EXPORT_PATH,
|
|
3
|
+
SIGN_AUDIT_EXPORT_PATH,
|
|
4
|
+
SIGN_CONFIRMATION_PATH,
|
|
5
|
+
SIGN_MANIFEST_PATH,
|
|
6
|
+
hasValidHeader
|
|
7
|
+
} from './config';
|
|
8
|
+
import { handleDecryptExport } from './handlers/decrypt-export';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type ForensicManifestPayload,
|
|
17
|
-
createAuditExportSigningPayload,
|
|
18
|
-
createConfirmationSigningPayload,
|
|
19
|
-
createManifestSigningPayload,
|
|
20
|
-
isValidAuditExportPayload,
|
|
21
|
-
isValidConfirmationPayload,
|
|
22
|
-
isValidManifestPayload
|
|
23
|
-
} from './signing-payload-utils';
|
|
24
|
-
|
|
25
|
-
interface Env {
|
|
26
|
-
R2_KEY_SECRET: string;
|
|
27
|
-
STRIAE_DATA: R2Bucket;
|
|
28
|
-
MANIFEST_SIGNING_PRIVATE_KEY: string;
|
|
29
|
-
MANIFEST_SIGNING_KEY_ID: string;
|
|
30
|
-
EXPORT_ENCRYPTION_PRIVATE_KEY?: string;
|
|
31
|
-
EXPORT_ENCRYPTION_KEY_ID?: string;
|
|
32
|
-
EXPORT_ENCRYPTION_KEYS_JSON?: string;
|
|
33
|
-
EXPORT_ENCRYPTION_ACTIVE_KEY_ID?: string;
|
|
34
|
-
DATA_AT_REST_ENCRYPTION_ENABLED?: string;
|
|
35
|
-
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
|
|
36
|
-
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY?: string;
|
|
37
|
-
DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
|
|
38
|
-
DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
|
|
39
|
-
DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface KeyRegistryPayload {
|
|
43
|
-
activeKeyId?: unknown;
|
|
44
|
-
keys?: unknown;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface PrivateKeyRegistry {
|
|
48
|
-
activeKeyId: string | null;
|
|
49
|
-
keys: Record<string, string>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface ExportDecryptionContext {
|
|
53
|
-
recordKeyId: string | null;
|
|
54
|
-
candidates: Array<{ keyId: string; privateKeyPem: string }>;
|
|
55
|
-
primaryKeyId: string | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
|
|
59
|
-
|
|
60
|
-
interface SuccessResponse {
|
|
61
|
-
success: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface ErrorResponse {
|
|
65
|
-
error: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type APIResponse = SuccessResponse | ErrorResponse | unknown[] | Record<string, unknown>;
|
|
10
|
+
handleSignAuditExport,
|
|
11
|
+
handleSignConfirmation,
|
|
12
|
+
handleSignManifest
|
|
13
|
+
} from './handlers/signing';
|
|
14
|
+
import { handleStorageRequest } from './handlers/storage-routes';
|
|
15
|
+
import type { CreateResponse, Env } from './types';
|
|
69
16
|
|
|
70
17
|
const corsHeaders: Record<string, string> = {
|
|
71
18
|
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
@@ -74,780 +21,11 @@ const corsHeaders: Record<string, string> = {
|
|
|
74
21
|
'Content-Type': 'application/json'
|
|
75
22
|
};
|
|
76
23
|
|
|
77
|
-
const
|
|
24
|
+
const createWorkerResponse: CreateResponse = (data, status: number = 200): Response => new Response(
|
|
78
25
|
JSON.stringify(data),
|
|
79
26
|
{ status, headers: corsHeaders }
|
|
80
27
|
);
|
|
81
28
|
|
|
82
|
-
const hasValidHeader = (request: Request, env: Env): boolean =>
|
|
83
|
-
request.headers.get('X-Custom-Auth-Key') === env.R2_KEY_SECRET;
|
|
84
|
-
|
|
85
|
-
const SIGN_MANIFEST_PATH = '/api/forensic/sign-manifest';
|
|
86
|
-
const SIGN_CONFIRMATION_PATH = '/api/forensic/sign-confirmation';
|
|
87
|
-
const SIGN_AUDIT_EXPORT_PATH = '/api/forensic/sign-audit-export';
|
|
88
|
-
const DECRYPT_EXPORT_PATH = '/api/forensic/decrypt-export';
|
|
89
|
-
const DATA_AT_REST_BACKFILL_PATH = '/api/admin/data-at-rest-backfill';
|
|
90
|
-
const DATA_AT_REST_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM';
|
|
91
|
-
const DATA_AT_REST_ENCRYPTION_VERSION = '1.0';
|
|
92
|
-
|
|
93
|
-
function normalizePrivateKeyPem(rawValue: string): string {
|
|
94
|
-
return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function getNonEmptyString(value: unknown): string | null {
|
|
98
|
-
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function parsePrivateKeyRegistry(input: {
|
|
102
|
-
registryJson: string | undefined;
|
|
103
|
-
activeKeyId: string | undefined;
|
|
104
|
-
legacyKeyId: string | undefined;
|
|
105
|
-
legacyPrivateKey: string | undefined;
|
|
106
|
-
context: string;
|
|
107
|
-
}): PrivateKeyRegistry {
|
|
108
|
-
const keys: Record<string, string> = {};
|
|
109
|
-
const configuredActiveKeyId = getNonEmptyString(input.activeKeyId);
|
|
110
|
-
const registryJson = getNonEmptyString(input.registryJson);
|
|
111
|
-
|
|
112
|
-
if (registryJson) {
|
|
113
|
-
let parsedRegistry: unknown;
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
parsedRegistry = JSON.parse(registryJson) as unknown;
|
|
117
|
-
} catch {
|
|
118
|
-
throw new Error(`${input.context} registry JSON is invalid`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!parsedRegistry || typeof parsedRegistry !== 'object') {
|
|
122
|
-
throw new Error(`${input.context} registry JSON must be an object`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const payload = parsedRegistry as KeyRegistryPayload;
|
|
126
|
-
const payloadActiveKeyId = getNonEmptyString(payload.activeKeyId);
|
|
127
|
-
const rawKeys = payload.keys && typeof payload.keys === 'object'
|
|
128
|
-
? payload.keys as Record<string, unknown>
|
|
129
|
-
: parsedRegistry as Record<string, unknown>;
|
|
130
|
-
|
|
131
|
-
for (const [keyId, pemValue] of Object.entries(rawKeys)) {
|
|
132
|
-
if (keyId === 'activeKeyId' || keyId === 'keys') {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const normalizedKeyId = getNonEmptyString(keyId);
|
|
137
|
-
const normalizedPem = getNonEmptyString(pemValue);
|
|
138
|
-
|
|
139
|
-
if (!normalizedKeyId || !normalizedPem) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
keys[normalizedKeyId] = normalizePrivateKeyPem(normalizedPem);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const resolvedActiveKeyId = configuredActiveKeyId ?? payloadActiveKeyId;
|
|
147
|
-
|
|
148
|
-
if (Object.keys(keys).length === 0) {
|
|
149
|
-
throw new Error(`${input.context} registry does not contain any usable keys`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (resolvedActiveKeyId && !keys[resolvedActiveKeyId]) {
|
|
153
|
-
throw new Error(`${input.context} active key ID is not present in registry`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
activeKeyId: resolvedActiveKeyId ?? null,
|
|
158
|
-
keys
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const legacyKeyId = getNonEmptyString(input.legacyKeyId);
|
|
163
|
-
const legacyPrivateKey = getNonEmptyString(input.legacyPrivateKey);
|
|
164
|
-
|
|
165
|
-
if (!legacyKeyId || !legacyPrivateKey) {
|
|
166
|
-
throw new Error(`${input.context} private key registry is not configured`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
keys[legacyKeyId] = normalizePrivateKeyPem(legacyPrivateKey);
|
|
170
|
-
const resolvedActiveKeyId = configuredActiveKeyId ?? legacyKeyId;
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
activeKeyId: resolvedActiveKeyId,
|
|
174
|
-
keys
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function buildPrivateKeyCandidates(
|
|
179
|
-
recordKeyId: string | null,
|
|
180
|
-
registry: PrivateKeyRegistry
|
|
181
|
-
): Array<{ keyId: string; privateKeyPem: string }> {
|
|
182
|
-
const candidates: Array<{ keyId: string; privateKeyPem: string }> = [];
|
|
183
|
-
const seen = new Set<string>();
|
|
184
|
-
|
|
185
|
-
const appendCandidate = (candidateKeyId: string | null): void => {
|
|
186
|
-
if (!candidateKeyId || seen.has(candidateKeyId)) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const privateKeyPem = registry.keys[candidateKeyId];
|
|
191
|
-
if (!privateKeyPem) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
seen.add(candidateKeyId);
|
|
196
|
-
candidates.push({ keyId: candidateKeyId, privateKeyPem });
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
appendCandidate(recordKeyId);
|
|
200
|
-
appendCandidate(registry.activeKeyId);
|
|
201
|
-
|
|
202
|
-
for (const keyId of Object.keys(registry.keys)) {
|
|
203
|
-
appendCandidate(keyId);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return candidates;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function logRegistryDecryptionTelemetry(input: {
|
|
210
|
-
scope: 'data-at-rest' | 'export-data' | 'export-image';
|
|
211
|
-
recordKeyId: string | null;
|
|
212
|
-
selectedKeyId: string | null;
|
|
213
|
-
attemptCount: number;
|
|
214
|
-
outcome: DecryptionTelemetryOutcome;
|
|
215
|
-
reason?: string;
|
|
216
|
-
}): void {
|
|
217
|
-
const details = {
|
|
218
|
-
scope: input.scope,
|
|
219
|
-
recordKeyId: input.recordKeyId,
|
|
220
|
-
selectedKeyId: input.selectedKeyId,
|
|
221
|
-
attemptCount: input.attemptCount,
|
|
222
|
-
fallbackUsed: input.outcome === 'fallback-hit',
|
|
223
|
-
outcome: input.outcome,
|
|
224
|
-
reason: input.reason ?? null
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
if (input.outcome === 'all-failed') {
|
|
228
|
-
console.warn('Key registry decryption failed', details);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
console.info('Key registry decryption resolved', details);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function getDataAtRestPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
|
|
236
|
-
return parsePrivateKeyRegistry({
|
|
237
|
-
registryJson: env.DATA_AT_REST_ENCRYPTION_KEYS_JSON,
|
|
238
|
-
activeKeyId: env.DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID,
|
|
239
|
-
legacyKeyId: env.DATA_AT_REST_ENCRYPTION_KEY_ID,
|
|
240
|
-
legacyPrivateKey: env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY,
|
|
241
|
-
context: 'Data-at-rest decryption'
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function getExportPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
|
|
246
|
-
return parsePrivateKeyRegistry({
|
|
247
|
-
registryJson: env.EXPORT_ENCRYPTION_KEYS_JSON,
|
|
248
|
-
activeKeyId: env.EXPORT_ENCRYPTION_ACTIVE_KEY_ID,
|
|
249
|
-
legacyKeyId: env.EXPORT_ENCRYPTION_KEY_ID,
|
|
250
|
-
legacyPrivateKey: env.EXPORT_ENCRYPTION_PRIVATE_KEY,
|
|
251
|
-
context: 'Export decryption'
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function buildExportDecryptionContext(keyId: string | null, env: Env): ExportDecryptionContext {
|
|
256
|
-
const keyRegistry = getExportPrivateKeyRegistry(env);
|
|
257
|
-
const candidates = buildPrivateKeyCandidates(keyId, keyRegistry);
|
|
258
|
-
|
|
259
|
-
if (candidates.length === 0) {
|
|
260
|
-
throw new Error('Export decryption key registry does not contain any usable keys');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
recordKeyId: keyId,
|
|
265
|
-
candidates,
|
|
266
|
-
primaryKeyId: candidates[0]?.keyId ?? null
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function decryptJsonFromStorageWithRegistry(
|
|
271
|
-
ciphertext: ArrayBuffer,
|
|
272
|
-
envelope: DataAtRestEnvelope,
|
|
273
|
-
env: Env
|
|
274
|
-
): Promise<string> {
|
|
275
|
-
const keyRegistry = getDataAtRestPrivateKeyRegistry(env);
|
|
276
|
-
const candidates = buildPrivateKeyCandidates(getNonEmptyString(envelope.keyId), keyRegistry);
|
|
277
|
-
const primaryKeyId = candidates[0]?.keyId ?? null;
|
|
278
|
-
let lastError: unknown;
|
|
279
|
-
|
|
280
|
-
for (let index = 0; index < candidates.length; index += 1) {
|
|
281
|
-
const candidate = candidates[index];
|
|
282
|
-
try {
|
|
283
|
-
const plaintext = await decryptJsonFromStorage(ciphertext, envelope, candidate.privateKeyPem);
|
|
284
|
-
logRegistryDecryptionTelemetry({
|
|
285
|
-
scope: 'data-at-rest',
|
|
286
|
-
recordKeyId: getNonEmptyString(envelope.keyId),
|
|
287
|
-
selectedKeyId: candidate.keyId,
|
|
288
|
-
attemptCount: index + 1,
|
|
289
|
-
outcome: candidate.keyId === primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
290
|
-
});
|
|
291
|
-
return plaintext;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
lastError = error;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
logRegistryDecryptionTelemetry({
|
|
298
|
-
scope: 'data-at-rest',
|
|
299
|
-
recordKeyId: getNonEmptyString(envelope.keyId),
|
|
300
|
-
selectedKeyId: null,
|
|
301
|
-
attemptCount: candidates.length,
|
|
302
|
-
outcome: 'all-failed',
|
|
303
|
-
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
throw new Error(
|
|
307
|
-
`Failed to decrypt stored data after ${candidates.length} key attempt(s): ${
|
|
308
|
-
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
309
|
-
}`
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async function decryptExportDataWithRegistry(
|
|
314
|
-
encryptedDataBase64: string,
|
|
315
|
-
wrappedKeyBase64: string,
|
|
316
|
-
ivBase64: string,
|
|
317
|
-
context: ExportDecryptionContext
|
|
318
|
-
): Promise<string> {
|
|
319
|
-
let lastError: unknown;
|
|
320
|
-
|
|
321
|
-
for (let index = 0; index < context.candidates.length; index += 1) {
|
|
322
|
-
const candidate = context.candidates[index];
|
|
323
|
-
try {
|
|
324
|
-
const plaintext = await decryptExportData(
|
|
325
|
-
encryptedDataBase64,
|
|
326
|
-
wrappedKeyBase64,
|
|
327
|
-
ivBase64,
|
|
328
|
-
candidate.privateKeyPem
|
|
329
|
-
);
|
|
330
|
-
logRegistryDecryptionTelemetry({
|
|
331
|
-
scope: 'export-data',
|
|
332
|
-
recordKeyId: context.recordKeyId,
|
|
333
|
-
selectedKeyId: candidate.keyId,
|
|
334
|
-
attemptCount: index + 1,
|
|
335
|
-
outcome: candidate.keyId === context.primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
336
|
-
});
|
|
337
|
-
return plaintext;
|
|
338
|
-
} catch (error) {
|
|
339
|
-
lastError = error;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
logRegistryDecryptionTelemetry({
|
|
344
|
-
scope: 'export-data',
|
|
345
|
-
recordKeyId: context.recordKeyId,
|
|
346
|
-
selectedKeyId: null,
|
|
347
|
-
attemptCount: context.candidates.length,
|
|
348
|
-
outcome: 'all-failed',
|
|
349
|
-
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
throw new Error(
|
|
353
|
-
`Failed to decrypt export payload after ${context.candidates.length} key attempt(s): ${
|
|
354
|
-
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
355
|
-
}`
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function decryptExportImageWithRegistry(
|
|
360
|
-
encryptedImageBase64: string,
|
|
361
|
-
wrappedKeyBase64: string,
|
|
362
|
-
ivBase64: string,
|
|
363
|
-
context: ExportDecryptionContext
|
|
364
|
-
): Promise<Blob> {
|
|
365
|
-
let lastError: unknown;
|
|
366
|
-
|
|
367
|
-
for (let index = 0; index < context.candidates.length; index += 1) {
|
|
368
|
-
const candidate = context.candidates[index];
|
|
369
|
-
try {
|
|
370
|
-
const imageBlob = await decryptImageBlob(
|
|
371
|
-
encryptedImageBase64,
|
|
372
|
-
wrappedKeyBase64,
|
|
373
|
-
ivBase64,
|
|
374
|
-
candidate.privateKeyPem
|
|
375
|
-
);
|
|
376
|
-
logRegistryDecryptionTelemetry({
|
|
377
|
-
scope: 'export-image',
|
|
378
|
-
recordKeyId: context.recordKeyId,
|
|
379
|
-
selectedKeyId: candidate.keyId,
|
|
380
|
-
attemptCount: index + 1,
|
|
381
|
-
outcome: candidate.keyId === context.primaryKeyId ? 'primary-hit' : 'fallback-hit'
|
|
382
|
-
});
|
|
383
|
-
return imageBlob;
|
|
384
|
-
} catch (error) {
|
|
385
|
-
lastError = error;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
logRegistryDecryptionTelemetry({
|
|
390
|
-
scope: 'export-image',
|
|
391
|
-
recordKeyId: context.recordKeyId,
|
|
392
|
-
selectedKeyId: null,
|
|
393
|
-
attemptCount: context.candidates.length,
|
|
394
|
-
outcome: 'all-failed',
|
|
395
|
-
reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
throw new Error(
|
|
399
|
-
`Failed to decrypt export image after ${context.candidates.length} key attempt(s): ${
|
|
400
|
-
lastError instanceof Error ? lastError.message : 'unknown decryption error'
|
|
401
|
-
}`
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function isDataAtRestEncryptionEnabled(env: Env): boolean {
|
|
406
|
-
const value = env.DATA_AT_REST_ENCRYPTION_ENABLED;
|
|
407
|
-
if (!value) {
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const normalizedValue = value.trim().toLowerCase();
|
|
412
|
-
return normalizedValue === '1' || normalizedValue === 'true' || normalizedValue === 'yes' || normalizedValue === 'on';
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function extractDataAtRestEnvelope(file: R2ObjectBody): DataAtRestEnvelope | null {
|
|
416
|
-
const metadata = file.customMetadata;
|
|
417
|
-
if (!metadata) {
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const {
|
|
422
|
-
algorithm,
|
|
423
|
-
encryptionVersion,
|
|
424
|
-
keyId,
|
|
425
|
-
dataIv,
|
|
426
|
-
wrappedKey
|
|
427
|
-
} = metadata;
|
|
428
|
-
|
|
429
|
-
if (
|
|
430
|
-
typeof algorithm !== 'string' ||
|
|
431
|
-
typeof encryptionVersion !== 'string' ||
|
|
432
|
-
typeof keyId !== 'string' ||
|
|
433
|
-
typeof dataIv !== 'string' ||
|
|
434
|
-
typeof wrappedKey !== 'string'
|
|
435
|
-
) {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return {
|
|
440
|
-
algorithm,
|
|
441
|
-
encryptionVersion,
|
|
442
|
-
keyId,
|
|
443
|
-
dataIv,
|
|
444
|
-
wrappedKey
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function hasDataAtRestMetadata(metadata: Record<string, string> | undefined): boolean {
|
|
449
|
-
if (!metadata) {
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return (
|
|
454
|
-
typeof metadata.algorithm === 'string' &&
|
|
455
|
-
typeof metadata.encryptionVersion === 'string' &&
|
|
456
|
-
typeof metadata.keyId === 'string' &&
|
|
457
|
-
typeof metadata.dataIv === 'string' &&
|
|
458
|
-
typeof metadata.wrappedKey === 'string'
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function clampBackfillBatchSize(size: number | undefined): number {
|
|
463
|
-
if (typeof size !== 'number' || !Number.isFinite(size)) {
|
|
464
|
-
return 100;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const normalized = Math.floor(size);
|
|
468
|
-
if (normalized < 1) {
|
|
469
|
-
return 1;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (normalized > 1000) {
|
|
473
|
-
return 1000;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return normalized;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async function handleDataAtRestBackfill(request: Request, env: Env): Promise<Response> {
|
|
480
|
-
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
481
|
-
return createResponse(
|
|
482
|
-
{ error: 'Data-at-rest encryption is not configured for backfill writes' },
|
|
483
|
-
400
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const requestBody = await request.json().catch(() => ({})) as {
|
|
488
|
-
dryRun?: boolean;
|
|
489
|
-
prefix?: string;
|
|
490
|
-
cursor?: string;
|
|
491
|
-
batchSize?: number;
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
const dryRun = requestBody.dryRun === true;
|
|
495
|
-
const prefix = typeof requestBody.prefix === 'string' ? requestBody.prefix : '';
|
|
496
|
-
const cursor = typeof requestBody.cursor === 'string' && requestBody.cursor.length > 0
|
|
497
|
-
? requestBody.cursor
|
|
498
|
-
: undefined;
|
|
499
|
-
const batchSize = clampBackfillBatchSize(requestBody.batchSize);
|
|
500
|
-
|
|
501
|
-
const bucket = env.STRIAE_DATA;
|
|
502
|
-
const listed = await bucket.list({
|
|
503
|
-
prefix: prefix.length > 0 ? prefix : undefined,
|
|
504
|
-
cursor,
|
|
505
|
-
limit: batchSize
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
let scanned = 0;
|
|
509
|
-
let eligible = 0;
|
|
510
|
-
let encrypted = 0;
|
|
511
|
-
let skippedEncrypted = 0;
|
|
512
|
-
let skippedNonJson = 0;
|
|
513
|
-
let failed = 0;
|
|
514
|
-
const failures: Array<{ key: string; error: string }> = [];
|
|
515
|
-
|
|
516
|
-
for (const object of listed.objects) {
|
|
517
|
-
scanned += 1;
|
|
518
|
-
const key = object.key;
|
|
519
|
-
|
|
520
|
-
if (!key.endsWith('.json')) {
|
|
521
|
-
skippedNonJson += 1;
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const objectHead = await bucket.head(key);
|
|
526
|
-
if (!objectHead) {
|
|
527
|
-
failed += 1;
|
|
528
|
-
if (failures.length < 20) {
|
|
529
|
-
failures.push({ key, error: 'Object not found during metadata check' });
|
|
530
|
-
}
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (hasDataAtRestMetadata(objectHead.customMetadata)) {
|
|
535
|
-
skippedEncrypted += 1;
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
eligible += 1;
|
|
540
|
-
|
|
541
|
-
if (dryRun) {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const existingObject = await bucket.get(key);
|
|
547
|
-
if (!existingObject) {
|
|
548
|
-
failed += 1;
|
|
549
|
-
if (failures.length < 20) {
|
|
550
|
-
failures.push({ key, error: 'Object disappeared before processing' });
|
|
551
|
-
}
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const plaintext = await existingObject.text();
|
|
556
|
-
const encryptedPayload = await encryptJsonForStorage(
|
|
557
|
-
plaintext,
|
|
558
|
-
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
559
|
-
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
await bucket.put(key, encryptedPayload.ciphertext, {
|
|
563
|
-
customMetadata: {
|
|
564
|
-
algorithm: encryptedPayload.envelope.algorithm,
|
|
565
|
-
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
566
|
-
keyId: encryptedPayload.envelope.keyId,
|
|
567
|
-
dataIv: encryptedPayload.envelope.dataIv,
|
|
568
|
-
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
encrypted += 1;
|
|
573
|
-
} catch (error) {
|
|
574
|
-
failed += 1;
|
|
575
|
-
if (failures.length < 20) {
|
|
576
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown backfill failure';
|
|
577
|
-
failures.push({ key, error: errorMessage });
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return createResponse({
|
|
583
|
-
success: failed === 0,
|
|
584
|
-
dryRun,
|
|
585
|
-
prefix: prefix.length > 0 ? prefix : null,
|
|
586
|
-
batchSize,
|
|
587
|
-
scanned,
|
|
588
|
-
eligible,
|
|
589
|
-
encrypted,
|
|
590
|
-
skippedEncrypted,
|
|
591
|
-
skippedNonJson,
|
|
592
|
-
failed,
|
|
593
|
-
failures,
|
|
594
|
-
hasMore: listed.truncated,
|
|
595
|
-
nextCursor: listed.truncated ? listed.cursor : null
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
async function signPayloadWithWorkerKey(payload: string, env: Env): Promise<{
|
|
600
|
-
algorithm: string;
|
|
601
|
-
keyId: string;
|
|
602
|
-
signedAt: string;
|
|
603
|
-
value: string;
|
|
604
|
-
}> {
|
|
605
|
-
return signWithWorkerKey(
|
|
606
|
-
payload,
|
|
607
|
-
env.MANIFEST_SIGNING_PRIVATE_KEY,
|
|
608
|
-
env.MANIFEST_SIGNING_KEY_ID,
|
|
609
|
-
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async function signManifest(manifest: ForensicManifestPayload, env: Env): Promise<{
|
|
614
|
-
algorithm: string;
|
|
615
|
-
keyId: string;
|
|
616
|
-
signedAt: string;
|
|
617
|
-
value: string;
|
|
618
|
-
}> {
|
|
619
|
-
const payload = createManifestSigningPayload(manifest);
|
|
620
|
-
return signPayloadWithWorkerKey(payload, env);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
async function signConfirmation(confirmationData: ConfirmationSigningPayload, env: Env): Promise<{
|
|
624
|
-
algorithm: string;
|
|
625
|
-
keyId: string;
|
|
626
|
-
signedAt: string;
|
|
627
|
-
value: string;
|
|
628
|
-
}> {
|
|
629
|
-
const payload = createConfirmationSigningPayload(confirmationData);
|
|
630
|
-
return signPayloadWithWorkerKey(payload, env);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
async function signAuditExport(auditExportData: AuditExportSigningPayload, env: Env): Promise<{
|
|
634
|
-
algorithm: string;
|
|
635
|
-
keyId: string;
|
|
636
|
-
signedAt: string;
|
|
637
|
-
value: string;
|
|
638
|
-
}> {
|
|
639
|
-
const payload = createAuditExportSigningPayload(auditExportData);
|
|
640
|
-
return signPayloadWithWorkerKey(payload, env);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
async function handleSignManifest(request: Request, env: Env): Promise<Response> {
|
|
644
|
-
try {
|
|
645
|
-
const requestBody = await request.json() as { manifest?: Partial<ForensicManifestPayload> } & Partial<ForensicManifestPayload>;
|
|
646
|
-
const manifestCandidate: Partial<ForensicManifestPayload> = requestBody.manifest ?? requestBody;
|
|
647
|
-
|
|
648
|
-
if (!manifestCandidate || !isValidManifestPayload(manifestCandidate)) {
|
|
649
|
-
return createResponse({ error: 'Invalid manifest payload' }, 400);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const signature = await signManifest(manifestCandidate, env);
|
|
653
|
-
|
|
654
|
-
return createResponse({
|
|
655
|
-
success: true,
|
|
656
|
-
manifestVersion: FORENSIC_MANIFEST_VERSION,
|
|
657
|
-
signature
|
|
658
|
-
});
|
|
659
|
-
} catch (error) {
|
|
660
|
-
console.error('Manifest signing failed:', error);
|
|
661
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
662
|
-
return createResponse({ error: errorMessage }, 500);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
async function handleSignConfirmation(request: Request, env: Env): Promise<Response> {
|
|
667
|
-
try {
|
|
668
|
-
const requestBody = await request.json() as {
|
|
669
|
-
confirmationData?: Partial<ConfirmationSigningPayload>;
|
|
670
|
-
signatureVersion?: string;
|
|
671
|
-
} & Partial<ConfirmationSigningPayload>;
|
|
672
|
-
|
|
673
|
-
const requestedSignatureVersion =
|
|
674
|
-
typeof requestBody.signatureVersion === 'string' && requestBody.signatureVersion.trim().length > 0
|
|
675
|
-
? requestBody.signatureVersion
|
|
676
|
-
: CONFIRMATION_SIGNATURE_VERSION;
|
|
677
|
-
|
|
678
|
-
if (requestedSignatureVersion !== CONFIRMATION_SIGNATURE_VERSION) {
|
|
679
|
-
return createResponse(
|
|
680
|
-
{ error: `Unsupported confirmation signature version: ${requestedSignatureVersion}` },
|
|
681
|
-
400
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const confirmationCandidate: Partial<ConfirmationSigningPayload> = requestBody.confirmationData ?? requestBody;
|
|
686
|
-
|
|
687
|
-
if (!confirmationCandidate || !isValidConfirmationPayload(confirmationCandidate)) {
|
|
688
|
-
return createResponse({ error: 'Invalid confirmation payload' }, 400);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const signature = await signConfirmation(confirmationCandidate, env);
|
|
692
|
-
|
|
693
|
-
return createResponse({
|
|
694
|
-
success: true,
|
|
695
|
-
signatureVersion: CONFIRMATION_SIGNATURE_VERSION,
|
|
696
|
-
signature
|
|
697
|
-
});
|
|
698
|
-
} catch (error) {
|
|
699
|
-
console.error('Confirmation signing failed:', error);
|
|
700
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
701
|
-
return createResponse({ error: errorMessage }, 500);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
async function handleSignAuditExport(request: Request, env: Env): Promise<Response> {
|
|
706
|
-
try {
|
|
707
|
-
const requestBody = await request.json() as {
|
|
708
|
-
auditExport?: Partial<AuditExportSigningPayload>;
|
|
709
|
-
signatureVersion?: string;
|
|
710
|
-
} & Partial<AuditExportSigningPayload>;
|
|
711
|
-
|
|
712
|
-
const requestedSignatureVersion =
|
|
713
|
-
typeof requestBody.signatureVersion === 'string' && requestBody.signatureVersion.trim().length > 0
|
|
714
|
-
? requestBody.signatureVersion
|
|
715
|
-
: AUDIT_EXPORT_SIGNATURE_VERSION;
|
|
716
|
-
|
|
717
|
-
if (requestedSignatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
|
|
718
|
-
return createResponse(
|
|
719
|
-
{ error: `Unsupported audit export signature version: ${requestedSignatureVersion}` },
|
|
720
|
-
400
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const auditExportCandidate: Partial<AuditExportSigningPayload> = requestBody.auditExport ?? requestBody;
|
|
725
|
-
|
|
726
|
-
if (!auditExportCandidate || !isValidAuditExportPayload(auditExportCandidate)) {
|
|
727
|
-
return createResponse({ error: 'Invalid audit export payload' }, 400);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const signature = await signAuditExport(auditExportCandidate, env);
|
|
731
|
-
|
|
732
|
-
return createResponse({
|
|
733
|
-
success: true,
|
|
734
|
-
signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION,
|
|
735
|
-
signature
|
|
736
|
-
});
|
|
737
|
-
} catch (error) {
|
|
738
|
-
console.error('Audit export signing failed:', error);
|
|
739
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
740
|
-
return createResponse({ error: errorMessage }, 500);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
async function handleDecryptExport(request: Request, env: Env): Promise<Response> {
|
|
745
|
-
try {
|
|
746
|
-
const requestBody = await request.json() as {
|
|
747
|
-
wrappedKey?: string;
|
|
748
|
-
dataIv?: string;
|
|
749
|
-
encryptedData?: string;
|
|
750
|
-
encryptedImages?: Array<{ filename: string; encryptedData: string; iv?: string }>;
|
|
751
|
-
keyId?: string;
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
const { wrappedKey, dataIv, encryptedData, encryptedImages, keyId } = requestBody;
|
|
755
|
-
|
|
756
|
-
// Validate required fields
|
|
757
|
-
if (
|
|
758
|
-
!wrappedKey ||
|
|
759
|
-
typeof wrappedKey !== 'string' ||
|
|
760
|
-
!dataIv ||
|
|
761
|
-
typeof dataIv !== 'string' ||
|
|
762
|
-
!encryptedData ||
|
|
763
|
-
typeof encryptedData !== 'string'
|
|
764
|
-
) {
|
|
765
|
-
return createResponse(
|
|
766
|
-
{ error: 'Missing or invalid required fields: wrappedKey, dataIv, encryptedData' },
|
|
767
|
-
400
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const recordKeyId = getNonEmptyString(keyId);
|
|
772
|
-
const decryptionContext = buildExportDecryptionContext(recordKeyId, env);
|
|
773
|
-
|
|
774
|
-
// Decrypt data file
|
|
775
|
-
let plaintextData: string;
|
|
776
|
-
try {
|
|
777
|
-
plaintextData = await decryptExportDataWithRegistry(
|
|
778
|
-
encryptedData,
|
|
779
|
-
wrappedKey,
|
|
780
|
-
dataIv,
|
|
781
|
-
decryptionContext
|
|
782
|
-
);
|
|
783
|
-
} catch (error) {
|
|
784
|
-
console.error('Data file decryption failed:', error);
|
|
785
|
-
const errorMessage = error instanceof Error ? error.message : 'Decryption failed';
|
|
786
|
-
return createResponse(
|
|
787
|
-
{ error: `Failed to decrypt data file: ${errorMessage}` },
|
|
788
|
-
500
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Decrypt images if provided
|
|
793
|
-
const decryptedImages: Array<{ filename: string; data: string }> = [];
|
|
794
|
-
if (Array.isArray(encryptedImages) && encryptedImages.length > 0) {
|
|
795
|
-
for (const imageEntry of encryptedImages) {
|
|
796
|
-
try {
|
|
797
|
-
if (!imageEntry.iv || typeof imageEntry.iv !== 'string') {
|
|
798
|
-
return createResponse(
|
|
799
|
-
{ error: `Missing IV for image ${imageEntry.filename}` },
|
|
800
|
-
400
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const imageBlob = await decryptExportImageWithRegistry(
|
|
805
|
-
imageEntry.encryptedData,
|
|
806
|
-
wrappedKey,
|
|
807
|
-
imageEntry.iv,
|
|
808
|
-
decryptionContext
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
// Convert blob to base64 for transport
|
|
812
|
-
const arrayBuffer = await imageBlob.arrayBuffer();
|
|
813
|
-
const bytes = new Uint8Array(arrayBuffer);
|
|
814
|
-
const chunkSize = 8192;
|
|
815
|
-
let binary = '';
|
|
816
|
-
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
817
|
-
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
818
|
-
for (let j = 0; j < chunk.length; j++) {
|
|
819
|
-
binary += String.fromCharCode(chunk[j]);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
const base64Data = btoa(binary);
|
|
823
|
-
|
|
824
|
-
decryptedImages.push({
|
|
825
|
-
filename: imageEntry.filename,
|
|
826
|
-
data: base64Data
|
|
827
|
-
});
|
|
828
|
-
} catch (error) {
|
|
829
|
-
console.error(`Image decryption failed for ${imageEntry.filename}:`, error);
|
|
830
|
-
const errorMessage = error instanceof Error ? error.message : 'Decryption failed';
|
|
831
|
-
return createResponse(
|
|
832
|
-
{ error: `Failed to decrypt image ${imageEntry.filename}: ${errorMessage}` },
|
|
833
|
-
500
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return createResponse({
|
|
840
|
-
success: true,
|
|
841
|
-
plaintext: plaintextData,
|
|
842
|
-
decryptedImages
|
|
843
|
-
});
|
|
844
|
-
} catch (error) {
|
|
845
|
-
console.error('Export decryption request failed:', error);
|
|
846
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
847
|
-
return createResponse({ error: errorMessage }, 500);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
29
|
export default {
|
|
852
30
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
853
31
|
if (request.method === 'OPTIONS') {
|
|
@@ -855,133 +33,34 @@ export default {
|
|
|
855
33
|
}
|
|
856
34
|
|
|
857
35
|
if (!hasValidHeader(request, env)) {
|
|
858
|
-
return
|
|
36
|
+
return createWorkerResponse({ error: 'Forbidden' }, 403);
|
|
859
37
|
}
|
|
860
38
|
|
|
861
39
|
try {
|
|
862
40
|
const url = new URL(request.url);
|
|
863
41
|
const pathname = url.pathname;
|
|
864
|
-
const bucket = env.STRIAE_DATA;
|
|
865
42
|
|
|
866
43
|
if (request.method === 'POST' && pathname === SIGN_MANIFEST_PATH) {
|
|
867
|
-
return await handleSignManifest(request, env);
|
|
44
|
+
return await handleSignManifest(request, env, createWorkerResponse);
|
|
868
45
|
}
|
|
869
46
|
|
|
870
47
|
if (request.method === 'POST' && pathname === SIGN_CONFIRMATION_PATH) {
|
|
871
|
-
return await handleSignConfirmation(request, env);
|
|
48
|
+
return await handleSignConfirmation(request, env, createWorkerResponse);
|
|
872
49
|
}
|
|
873
50
|
|
|
874
51
|
if (request.method === 'POST' && pathname === SIGN_AUDIT_EXPORT_PATH) {
|
|
875
|
-
return await handleSignAuditExport(request, env);
|
|
52
|
+
return await handleSignAuditExport(request, env, createWorkerResponse);
|
|
876
53
|
}
|
|
877
54
|
|
|
878
55
|
if (request.method === 'POST' && pathname === DECRYPT_EXPORT_PATH) {
|
|
879
|
-
return await handleDecryptExport(request, env);
|
|
56
|
+
return await handleDecryptExport(request, env, createWorkerResponse);
|
|
880
57
|
}
|
|
881
58
|
|
|
882
|
-
|
|
883
|
-
return await handleDataAtRestBackfill(request, env);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const filename = pathname.slice(1) || 'data.json';
|
|
887
|
-
|
|
888
|
-
if (!filename.endsWith('.json')) {
|
|
889
|
-
return createResponse({ error: 'Invalid file type. Only JSON files are allowed.' }, 400);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
switch (request.method) {
|
|
893
|
-
case 'GET': {
|
|
894
|
-
const file = await bucket.get(filename);
|
|
895
|
-
if (!file) {
|
|
896
|
-
return createResponse([], 200);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const atRestEnvelope = extractDataAtRestEnvelope(file);
|
|
900
|
-
if (atRestEnvelope) {
|
|
901
|
-
if (atRestEnvelope.algorithm !== DATA_AT_REST_ENCRYPTION_ALGORITHM) {
|
|
902
|
-
return createResponse({ error: 'Unsupported data-at-rest encryption algorithm' }, 500);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
if (atRestEnvelope.encryptionVersion !== DATA_AT_REST_ENCRYPTION_VERSION) {
|
|
906
|
-
return createResponse({ error: 'Unsupported data-at-rest encryption version' }, 500);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
try {
|
|
910
|
-
const encryptedData = await file.arrayBuffer();
|
|
911
|
-
const plaintext = await decryptJsonFromStorageWithRegistry(
|
|
912
|
-
encryptedData,
|
|
913
|
-
atRestEnvelope,
|
|
914
|
-
env
|
|
915
|
-
);
|
|
916
|
-
const decryptedPayload = JSON.parse(plaintext);
|
|
917
|
-
return createResponse(decryptedPayload);
|
|
918
|
-
} catch (error) {
|
|
919
|
-
console.error('Data-at-rest decryption failed:', error);
|
|
920
|
-
return createResponse({ error: 'Failed to decrypt stored data' }, 500);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const fileText = await file.text();
|
|
925
|
-
const data = JSON.parse(fileText);
|
|
926
|
-
return createResponse(data);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
case 'PUT': {
|
|
930
|
-
const newData = await request.json();
|
|
931
|
-
const serializedData = JSON.stringify(newData);
|
|
932
|
-
|
|
933
|
-
if (!isDataAtRestEncryptionEnabled(env)) {
|
|
934
|
-
await bucket.put(filename, serializedData);
|
|
935
|
-
return createResponse({ success: true });
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
939
|
-
return createResponse(
|
|
940
|
-
{ error: 'Data-at-rest encryption is enabled but not fully configured' },
|
|
941
|
-
500
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
try {
|
|
946
|
-
const encryptedPayload = await encryptJsonForStorage(
|
|
947
|
-
serializedData,
|
|
948
|
-
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
949
|
-
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
950
|
-
);
|
|
951
|
-
|
|
952
|
-
await bucket.put(filename, encryptedPayload.ciphertext, {
|
|
953
|
-
customMetadata: {
|
|
954
|
-
algorithm: encryptedPayload.envelope.algorithm,
|
|
955
|
-
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
956
|
-
keyId: encryptedPayload.envelope.keyId,
|
|
957
|
-
dataIv: encryptedPayload.envelope.dataIv,
|
|
958
|
-
wrappedKey: encryptedPayload.envelope.wrappedKey
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
} catch (error) {
|
|
962
|
-
console.error('Data-at-rest encryption failed:', error);
|
|
963
|
-
return createResponse({ error: 'Failed to encrypt data for storage' }, 500);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
return createResponse({ success: true });
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
case 'DELETE': {
|
|
970
|
-
const file = await bucket.get(filename);
|
|
971
|
-
if (!file) {
|
|
972
|
-
return createResponse({ error: 'File not found' }, 404);
|
|
973
|
-
}
|
|
974
|
-
await bucket.delete(filename);
|
|
975
|
-
return createResponse({ success: true });
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
default:
|
|
979
|
-
return createResponse({ error: 'Method not allowed' }, 405);
|
|
980
|
-
}
|
|
59
|
+
return await handleStorageRequest(request, env, pathname, createWorkerResponse);
|
|
981
60
|
} catch (error) {
|
|
982
61
|
console.error('Worker error:', error);
|
|
983
62
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
984
|
-
return
|
|
63
|
+
return createWorkerResponse({ error: errorMessage }, 500);
|
|
985
64
|
}
|
|
986
65
|
}
|
|
987
66
|
};
|