@striae-org/striae 4.3.3 → 5.0.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 +4 -0
- package/app/components/actions/case-export/download-handlers.ts +60 -4
- 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 +110 -10
- package/app/components/actions/confirm-export.ts +32 -3
- package/app/components/audit/user-audit.module.css +49 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
- 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/services/audit/audit-console-logger.ts +1 -1
- package/app/services/audit/audit-export-csv.ts +1 -1
- package/app/services/audit/audit-export-signing.ts +2 -2
- package/app/services/audit/audit-export.service.ts +1 -1
- package/app/services/audit/audit-worker-client.ts +1 -1
- package/app/services/audit/audit.service.ts +5 -75
- package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
- package/app/services/audit/index.ts +2 -2
- package/app/types/audit.ts +8 -7
- 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 +1 -1
- package/scripts/deploy-config.sh +97 -3
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/data-worker.example.ts +130 -0
- package/workers/data-worker/src/encryption-utils.ts +125 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- 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,4 +1,3 @@
|
|
|
1
|
-
import { type ConfirmationImportData } from '~/types';
|
|
2
1
|
import {
|
|
3
2
|
extractForensicManifestData,
|
|
4
3
|
type ManifestSignatureVerificationResult,
|
|
@@ -11,7 +10,6 @@ import {
|
|
|
11
10
|
type AuditExportSigningPayload,
|
|
12
11
|
verifyAuditExportSignature
|
|
13
12
|
} from './audit-export-signature';
|
|
14
|
-
import { verifyConfirmationSignature } from './confirmation-signature';
|
|
15
13
|
|
|
16
14
|
export interface ExportVerificationResult {
|
|
17
15
|
isValid: boolean;
|
|
@@ -19,9 +17,6 @@ export interface ExportVerificationResult {
|
|
|
19
17
|
exportType?: 'case-zip' | 'confirmation' | 'audit-json';
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
const CASE_EXPORT_FILE_REGEX = /_data\.(json|csv)$/i;
|
|
23
|
-
const CONFIRMATION_EXPORT_FILE_REGEX = /^confirmation-data-.*\.json$/i;
|
|
24
|
-
|
|
25
20
|
interface BundledAuditExportFile {
|
|
26
21
|
metadata?: {
|
|
27
22
|
exportTimestamp?: string;
|
|
@@ -45,12 +40,6 @@ interface BundledAuditExportFile {
|
|
|
45
40
|
auditEntries?: unknown;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
interface StandaloneAuditExportFile extends BundledAuditExportFile {
|
|
49
|
-
metadata?: BundledAuditExportFile['metadata'] & {
|
|
50
|
-
integrityNote?: string;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
43
|
export interface CasePackageIntegrityInput {
|
|
55
44
|
cleanedContent: string;
|
|
56
45
|
imageFiles: Record<string, Blob>;
|
|
@@ -100,146 +89,7 @@ function getSignatureFailureMessage(
|
|
|
100
89
|
return `The ${targetLabel} signature did not verify with the selected public key.`;
|
|
101
90
|
}
|
|
102
91
|
|
|
103
|
-
function
|
|
104
|
-
if (!candidate || typeof candidate !== 'object') {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const confirmationCandidate = candidate as Partial<ConfirmationImportData>;
|
|
109
|
-
return (
|
|
110
|
-
!!confirmationCandidate.metadata &&
|
|
111
|
-
typeof confirmationCandidate.metadata.hash === 'string' &&
|
|
112
|
-
!!confirmationCandidate.confirmations &&
|
|
113
|
-
typeof confirmationCandidate.confirmations === 'object'
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function isAuditExportCandidate(candidate: unknown): candidate is StandaloneAuditExportFile {
|
|
118
|
-
if (!candidate || typeof candidate !== 'object') {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const auditCandidate = candidate as StandaloneAuditExportFile;
|
|
123
|
-
const metadata = auditCandidate.metadata;
|
|
124
|
-
|
|
125
|
-
if (!metadata || typeof metadata !== 'object') {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
typeof metadata.exportTimestamp === 'string' &&
|
|
131
|
-
typeof metadata.exportType === 'string' &&
|
|
132
|
-
typeof metadata.scopeType === 'string' &&
|
|
133
|
-
typeof metadata.scopeIdentifier === 'string' &&
|
|
134
|
-
typeof metadata.hash === 'string' &&
|
|
135
|
-
!!metadata.signature &&
|
|
136
|
-
(auditCandidate.auditTrail !== undefined || auditCandidate.auditEntries !== undefined)
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function verifyAuditExportContent(
|
|
141
|
-
fileContent: string,
|
|
142
|
-
verificationPublicKeyPem: string
|
|
143
|
-
): Promise<ExportVerificationResult> {
|
|
144
|
-
try {
|
|
145
|
-
const parsedContent = JSON.parse(fileContent) as unknown;
|
|
146
|
-
|
|
147
|
-
if (!isAuditExportCandidate(parsedContent)) {
|
|
148
|
-
return createVerificationResult(
|
|
149
|
-
false,
|
|
150
|
-
'The JSON file is not a supported Striae audit export.',
|
|
151
|
-
'audit-json'
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const auditExport = parsedContent as StandaloneAuditExportFile;
|
|
156
|
-
const metadata = auditExport.metadata!;
|
|
157
|
-
|
|
158
|
-
const unsignedAuditExport = auditExport.auditTrail !== undefined
|
|
159
|
-
? {
|
|
160
|
-
metadata: {
|
|
161
|
-
exportTimestamp: metadata.exportTimestamp,
|
|
162
|
-
exportVersion: metadata.exportVersion,
|
|
163
|
-
totalEntries: metadata.totalEntries,
|
|
164
|
-
application: metadata.application,
|
|
165
|
-
exportType: metadata.exportType,
|
|
166
|
-
scopeType: metadata.scopeType,
|
|
167
|
-
scopeIdentifier: metadata.scopeIdentifier,
|
|
168
|
-
},
|
|
169
|
-
auditTrail: auditExport.auditTrail,
|
|
170
|
-
}
|
|
171
|
-
: {
|
|
172
|
-
metadata: {
|
|
173
|
-
exportTimestamp: metadata.exportTimestamp,
|
|
174
|
-
exportVersion: metadata.exportVersion,
|
|
175
|
-
totalEntries: metadata.totalEntries,
|
|
176
|
-
application: metadata.application,
|
|
177
|
-
exportType: metadata.exportType,
|
|
178
|
-
scopeType: metadata.scopeType,
|
|
179
|
-
scopeIdentifier: metadata.scopeIdentifier,
|
|
180
|
-
},
|
|
181
|
-
auditEntries: auditExport.auditEntries,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const recalculatedHash = await calculateSHA256Secure(JSON.stringify(unsignedAuditExport, null, 2));
|
|
185
|
-
const hashValid = recalculatedHash.toUpperCase() === metadata.hash!.toUpperCase();
|
|
186
|
-
|
|
187
|
-
const signaturePayload: Partial<AuditExportSigningPayload> = {
|
|
188
|
-
signatureVersion: metadata.signatureVersion,
|
|
189
|
-
exportFormat: 'json',
|
|
190
|
-
exportType: metadata.exportType,
|
|
191
|
-
scopeType: metadata.scopeType,
|
|
192
|
-
scopeIdentifier: metadata.scopeIdentifier,
|
|
193
|
-
generatedAt: metadata.exportTimestamp,
|
|
194
|
-
totalEntries: metadata.totalEntries,
|
|
195
|
-
hash: metadata.hash,
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const signatureResult = await verifyAuditExportSignature(
|
|
199
|
-
signaturePayload,
|
|
200
|
-
metadata.signature,
|
|
201
|
-
verificationPublicKeyPem
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
if (hashValid && signatureResult.isValid) {
|
|
205
|
-
return createVerificationResult(
|
|
206
|
-
true,
|
|
207
|
-
'The audit export passed signature and integrity verification.',
|
|
208
|
-
'audit-json'
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!hashValid && !signatureResult.isValid) {
|
|
213
|
-
return createVerificationResult(
|
|
214
|
-
false,
|
|
215
|
-
'The audit export failed signature and integrity verification.',
|
|
216
|
-
'audit-json'
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (!signatureResult.isValid) {
|
|
221
|
-
return createVerificationResult(
|
|
222
|
-
false,
|
|
223
|
-
getSignatureFailureMessage(signatureResult.error, 'audit export'),
|
|
224
|
-
'audit-json'
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return createVerificationResult(
|
|
229
|
-
false,
|
|
230
|
-
'The audit export failed integrity verification.',
|
|
231
|
-
'audit-json'
|
|
232
|
-
);
|
|
233
|
-
} catch {
|
|
234
|
-
return createVerificationResult(
|
|
235
|
-
false,
|
|
236
|
-
'The JSON file could not be read as a supported Striae audit export.',
|
|
237
|
-
'audit-json'
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export async function verifyBundledAuditExport(
|
|
92
|
+
async function verifyBundledAuditExport(
|
|
243
93
|
zip: {
|
|
244
94
|
file: (path: string) => { async: (type: 'text') => Promise<string> } | null;
|
|
245
95
|
},
|
|
@@ -412,264 +262,6 @@ export async function validateConfirmationHash(jsonContent: string, expectedHash
|
|
|
412
262
|
}
|
|
413
263
|
}
|
|
414
264
|
|
|
415
|
-
async function verifyCaseZipExport(
|
|
416
|
-
file: File,
|
|
417
|
-
verificationPublicKeyPem: string
|
|
418
|
-
): Promise<ExportVerificationResult> {
|
|
419
|
-
const JSZip = (await import('jszip')).default;
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
const zip = await JSZip.loadAsync(file);
|
|
423
|
-
const dataFiles = Object.keys(zip.files).filter((name) => CASE_EXPORT_FILE_REGEX.test(name));
|
|
424
|
-
|
|
425
|
-
if (dataFiles.length !== 1) {
|
|
426
|
-
return createVerificationResult(
|
|
427
|
-
false,
|
|
428
|
-
'The ZIP file must contain exactly one case export data file.',
|
|
429
|
-
'case-zip'
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const dataContent = await zip.file(dataFiles[0])?.async('text');
|
|
434
|
-
if (!dataContent) {
|
|
435
|
-
return createVerificationResult(false, 'The ZIP data file could not be read.', 'case-zip');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const manifestContent = await zip.file('FORENSIC_MANIFEST.json')?.async('text');
|
|
439
|
-
if (!manifestContent) {
|
|
440
|
-
return createVerificationResult(
|
|
441
|
-
false,
|
|
442
|
-
'The ZIP file does not contain FORENSIC_MANIFEST.json.',
|
|
443
|
-
'case-zip'
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const forensicManifest = JSON.parse(manifestContent) as SignedForensicManifest;
|
|
448
|
-
const manifestData = extractForensicManifestData(forensicManifest);
|
|
449
|
-
|
|
450
|
-
if (!manifestData) {
|
|
451
|
-
return createVerificationResult(false, 'The forensic manifest is malformed.', 'case-zip');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const cleanedContent = removeForensicWarning(dataContent);
|
|
455
|
-
const imageFiles: Record<string, Blob> = {};
|
|
456
|
-
|
|
457
|
-
await Promise.all(
|
|
458
|
-
Object.keys(zip.files).map(async (path) => {
|
|
459
|
-
if (!path.startsWith('images/') || path.endsWith('/')) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const zipEntry = zip.file(path);
|
|
464
|
-
if (!zipEntry) {
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
imageFiles[path.replace('images/', '')] = await zipEntry.async('blob');
|
|
469
|
-
})
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
const bundledAuditFiles = {
|
|
473
|
-
auditTrailContent: await zip.file('audit/case-audit-trail.json')?.async('text'),
|
|
474
|
-
auditSignatureContent: await zip.file('audit/case-audit-signature.json')?.async('text')
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
const casePackageResult = await verifyCasePackageIntegrity({
|
|
478
|
-
cleanedContent,
|
|
479
|
-
imageFiles,
|
|
480
|
-
forensicManifest,
|
|
481
|
-
verificationPublicKeyPem,
|
|
482
|
-
bundledAuditFiles
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
const signatureResult = casePackageResult.signatureResult;
|
|
486
|
-
const integrityResult = casePackageResult.integrityResult;
|
|
487
|
-
const bundledAuditVerification = casePackageResult.bundledAuditVerification;
|
|
488
|
-
|
|
489
|
-
if (bundledAuditVerification) {
|
|
490
|
-
return bundledAuditVerification;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (signatureResult.isValid && integrityResult.isValid) {
|
|
494
|
-
return createVerificationResult(
|
|
495
|
-
true,
|
|
496
|
-
'The export ZIP passed signature and integrity verification.',
|
|
497
|
-
'case-zip'
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (!signatureResult.isValid && !integrityResult.isValid) {
|
|
502
|
-
return createVerificationResult(
|
|
503
|
-
false,
|
|
504
|
-
'The export ZIP failed signature and integrity verification.',
|
|
505
|
-
'case-zip'
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (!signatureResult.isValid) {
|
|
510
|
-
return createVerificationResult(
|
|
511
|
-
false,
|
|
512
|
-
getSignatureFailureMessage(signatureResult.error, 'export ZIP'),
|
|
513
|
-
'case-zip'
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return createVerificationResult(false, 'The export ZIP failed integrity verification.', 'case-zip');
|
|
518
|
-
} catch {
|
|
519
|
-
return createVerificationResult(
|
|
520
|
-
false,
|
|
521
|
-
'The ZIP file could not be read as a supported Striae export.',
|
|
522
|
-
'case-zip'
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async function verifyConfirmationContent(
|
|
528
|
-
fileContent: string,
|
|
529
|
-
verificationPublicKeyPem: string
|
|
530
|
-
): Promise<ExportVerificationResult> {
|
|
531
|
-
try {
|
|
532
|
-
const parsedContent = JSON.parse(fileContent) as unknown;
|
|
533
|
-
|
|
534
|
-
if (!isConfirmationImportCandidate(parsedContent)) {
|
|
535
|
-
return createVerificationResult(
|
|
536
|
-
false,
|
|
537
|
-
'The JSON file is not a supported Striae confirmation export.',
|
|
538
|
-
'confirmation'
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const confirmationData = parsedContent as Partial<ConfirmationImportData>;
|
|
543
|
-
const hashValid = await validateConfirmationHash(fileContent, confirmationData.metadata!.hash);
|
|
544
|
-
const signatureResult = await verifyConfirmationSignature(confirmationData, verificationPublicKeyPem);
|
|
545
|
-
|
|
546
|
-
if (hashValid && signatureResult.isValid) {
|
|
547
|
-
return createVerificationResult(
|
|
548
|
-
true,
|
|
549
|
-
'The confirmation file passed signature and integrity verification.',
|
|
550
|
-
'confirmation'
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (!signatureResult.isValid && signatureResult.error === 'Confirmation content is malformed') {
|
|
555
|
-
return createVerificationResult(
|
|
556
|
-
false,
|
|
557
|
-
'The JSON file is not a supported Striae confirmation export.',
|
|
558
|
-
'confirmation'
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (!hashValid && !signatureResult.isValid) {
|
|
563
|
-
return createVerificationResult(
|
|
564
|
-
false,
|
|
565
|
-
'The confirmation file failed signature and integrity verification.',
|
|
566
|
-
'confirmation'
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (!signatureResult.isValid) {
|
|
571
|
-
return createVerificationResult(
|
|
572
|
-
false,
|
|
573
|
-
getSignatureFailureMessage(signatureResult.error, 'confirmation file'),
|
|
574
|
-
'confirmation'
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return createVerificationResult(
|
|
579
|
-
false,
|
|
580
|
-
'The confirmation file failed integrity verification.',
|
|
581
|
-
'confirmation'
|
|
582
|
-
);
|
|
583
|
-
} catch {
|
|
584
|
-
return createVerificationResult(
|
|
585
|
-
false,
|
|
586
|
-
'The confirmation content could not be read as a supported Striae confirmation export.',
|
|
587
|
-
'confirmation'
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
async function verifyConfirmationZipExport(
|
|
593
|
-
file: File,
|
|
594
|
-
verificationPublicKeyPem: string
|
|
595
|
-
): Promise<ExportVerificationResult> {
|
|
596
|
-
const JSZip = (await import('jszip')).default;
|
|
597
|
-
|
|
598
|
-
try {
|
|
599
|
-
const zip = await JSZip.loadAsync(file);
|
|
600
|
-
const confirmationFiles = Object.keys(zip.files).filter((name) => CONFIRMATION_EXPORT_FILE_REGEX.test(name));
|
|
601
|
-
|
|
602
|
-
if (confirmationFiles.length !== 1) {
|
|
603
|
-
return createVerificationResult(
|
|
604
|
-
false,
|
|
605
|
-
'The ZIP file is not a supported Striae confirmation export package.'
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const confirmationContent = await zip.file(confirmationFiles[0])?.async('text');
|
|
610
|
-
if (!confirmationContent) {
|
|
611
|
-
return createVerificationResult(
|
|
612
|
-
false,
|
|
613
|
-
'The confirmation JSON file inside the ZIP could not be read.',
|
|
614
|
-
'confirmation'
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return verifyConfirmationContent(confirmationContent, verificationPublicKeyPem);
|
|
619
|
-
} catch {
|
|
620
|
-
return createVerificationResult(
|
|
621
|
-
false,
|
|
622
|
-
'The ZIP file could not be read as a supported Striae export.'
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
export async function verifyExportFile(
|
|
628
|
-
file: File,
|
|
629
|
-
verificationPublicKeyPem: string
|
|
630
|
-
): Promise<ExportVerificationResult> {
|
|
631
|
-
const lowerName = file.name.toLowerCase();
|
|
632
|
-
|
|
633
|
-
if (lowerName.endsWith('.zip')) {
|
|
634
|
-
const confirmationZipResult = await verifyConfirmationZipExport(file, verificationPublicKeyPem);
|
|
635
|
-
if (confirmationZipResult.exportType === 'confirmation' || confirmationZipResult.isValid) {
|
|
636
|
-
return confirmationZipResult;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return verifyCaseZipExport(file, verificationPublicKeyPem);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (lowerName.endsWith('.json')) {
|
|
643
|
-
try {
|
|
644
|
-
const fileContent = await file.text();
|
|
645
|
-
const parsedContent = JSON.parse(fileContent) as unknown;
|
|
646
|
-
|
|
647
|
-
if (isConfirmationImportCandidate(parsedContent)) {
|
|
648
|
-
return verifyConfirmationContent(fileContent, verificationPublicKeyPem);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (isAuditExportCandidate(parsedContent)) {
|
|
652
|
-
return verifyAuditExportContent(fileContent, verificationPublicKeyPem);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return createVerificationResult(
|
|
656
|
-
false,
|
|
657
|
-
'Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.'
|
|
658
|
-
);
|
|
659
|
-
} catch {
|
|
660
|
-
return createVerificationResult(
|
|
661
|
-
false,
|
|
662
|
-
'The JSON file could not be read as a supported Striae confirmation or audit export.'
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
return createVerificationResult(
|
|
668
|
-
false,
|
|
669
|
-
'Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.'
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
265
|
export async function verifyCasePackageIntegrity(
|
|
674
266
|
input: CasePackageIntegrityInput
|
|
675
267
|
): Promise<CasePackageIntegrityResult> {
|
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
// Import validation messages
|
|
7
7
|
export const IMPORT_FILE_TYPE_NOT_ALLOWED =
|
|
8
|
-
'Only Striae case ZIP files
|
|
8
|
+
'Only Striae case ZIP files and encrypted confirmation ZIP packages exported from Striae are allowed.';
|
|
9
9
|
|
|
10
10
|
export const IMPORT_FILE_TYPE_NOT_SUPPORTED =
|
|
11
|
-
'The selected file is not a supported Striae case or confirmation
|
|
11
|
+
'The selected file is not a supported Striae case ZIP or encrypted confirmation package.';
|
|
12
12
|
|
|
13
13
|
// Import blocking messages
|
|
14
14
|
export const ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE =
|
|
15
15
|
'This archived case cannot be imported because the case already exists in your regular case list. Delete the regular case before importing this archive.';
|
|
16
16
|
|
|
17
|
+
export const ARCHIVED_SELF_IMPORT_NOTE =
|
|
18
|
+
'Archived export detected. Original exporter imports are only allowed after the case has been deleted from your regular case list.';
|
|
19
|
+
|
|
17
20
|
// Read-only case operations
|
|
18
21
|
export const CREATE_READ_ONLY_CASE_EXISTS_ERROR = (caseNumber: string): string =>
|
|
19
22
|
`Case "${caseNumber}" already exists as a read-only review case.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/scripts/deploy-config.sh
CHANGED
|
@@ -334,7 +334,7 @@ write_env_var() {
|
|
|
334
334
|
var_value=$(strip_carriage_returns "$var_value")
|
|
335
335
|
env_file_value="$var_value"
|
|
336
336
|
|
|
337
|
-
if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ]; then
|
|
337
|
+
if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ]; then
|
|
338
338
|
# Store as a quoted string so sourced .env preserves escaped newline markers (\n)
|
|
339
339
|
env_file_value=${env_file_value//\"/\\\"}
|
|
340
340
|
env_file_value="\"$env_file_value\""
|
|
@@ -508,6 +508,88 @@ configure_manifest_signing_credentials() {
|
|
|
508
508
|
echo ""
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
generate_export_encryption_key_pair() {
|
|
512
|
+
local private_key_file
|
|
513
|
+
local public_key_file
|
|
514
|
+
private_key_file=$(mktemp)
|
|
515
|
+
public_key_file=$(mktemp)
|
|
516
|
+
|
|
517
|
+
if ! node -e "const { generateKeyPairSync } = require('crypto'); const fs = require('fs'); const pair = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); fs.writeFileSync(process.argv[1], pair.privateKey, 'utf8'); fs.writeFileSync(process.argv[2], pair.publicKey, 'utf8');" "$private_key_file" "$public_key_file"; then
|
|
518
|
+
rm -f "$private_key_file" "$public_key_file"
|
|
519
|
+
return 1
|
|
520
|
+
fi
|
|
521
|
+
|
|
522
|
+
local private_key_pem
|
|
523
|
+
local public_key_pem
|
|
524
|
+
private_key_pem=$(cat "$private_key_file")
|
|
525
|
+
public_key_pem=$(cat "$public_key_file")
|
|
526
|
+
rm -f "$private_key_file" "$public_key_file"
|
|
527
|
+
|
|
528
|
+
private_key_pem="${private_key_pem//$'\r'/}"
|
|
529
|
+
public_key_pem="${public_key_pem//$'\r'/}"
|
|
530
|
+
|
|
531
|
+
EXPORT_ENCRYPTION_PRIVATE_KEY="${private_key_pem//$'\n'/\\n}"
|
|
532
|
+
EXPORT_ENCRYPTION_PUBLIC_KEY="${public_key_pem//$'\n'/\\n}"
|
|
533
|
+
|
|
534
|
+
export EXPORT_ENCRYPTION_PRIVATE_KEY
|
|
535
|
+
export EXPORT_ENCRYPTION_PUBLIC_KEY
|
|
536
|
+
|
|
537
|
+
write_env_var "EXPORT_ENCRYPTION_PRIVATE_KEY" "$EXPORT_ENCRYPTION_PRIVATE_KEY"
|
|
538
|
+
write_env_var "EXPORT_ENCRYPTION_PUBLIC_KEY" "$EXPORT_ENCRYPTION_PUBLIC_KEY"
|
|
539
|
+
|
|
540
|
+
return 0
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
configure_export_encryption_credentials() {
|
|
544
|
+
echo -e "${BLUE}🔐 EXPORT ENCRYPTION CONFIGURATION${NC}"
|
|
545
|
+
echo "================================="
|
|
546
|
+
|
|
547
|
+
local should_generate="false"
|
|
548
|
+
local regenerate_choice=""
|
|
549
|
+
|
|
550
|
+
if [ "$update_env" = "true" ]; then
|
|
551
|
+
should_generate="true"
|
|
552
|
+
elif [ -z "$EXPORT_ENCRYPTION_PRIVATE_KEY" ] || is_placeholder "$EXPORT_ENCRYPTION_PRIVATE_KEY" || [ -z "$EXPORT_ENCRYPTION_PUBLIC_KEY" ] || is_placeholder "$EXPORT_ENCRYPTION_PUBLIC_KEY"; then
|
|
553
|
+
should_generate="true"
|
|
554
|
+
else
|
|
555
|
+
echo -e "${GREEN}Current export encryption key pair: [HIDDEN]${NC}"
|
|
556
|
+
read -p "Generate new export encryption key pair? (press Enter to keep current, or type 'y' to regenerate): " regenerate_choice
|
|
557
|
+
regenerate_choice=$(strip_carriage_returns "$regenerate_choice")
|
|
558
|
+
if [ "$regenerate_choice" = "y" ] || [ "$regenerate_choice" = "Y" ]; then
|
|
559
|
+
should_generate="true"
|
|
560
|
+
fi
|
|
561
|
+
fi
|
|
562
|
+
|
|
563
|
+
if [ "$should_generate" = "true" ]; then
|
|
564
|
+
echo -e "${YELLOW}Generating export encryption RSA key pair...${NC}"
|
|
565
|
+
if generate_export_encryption_key_pair; then
|
|
566
|
+
echo -e "${GREEN}✅ Export encryption key pair generated${NC}"
|
|
567
|
+
else
|
|
568
|
+
echo -e "${RED}❌ Error: Failed to generate export encryption key pair${NC}"
|
|
569
|
+
exit 1
|
|
570
|
+
fi
|
|
571
|
+
else
|
|
572
|
+
echo -e "${GREEN}✅ Keeping current export encryption key pair${NC}"
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
if [ -z "$EXPORT_ENCRYPTION_KEY_ID" ] || is_placeholder "$EXPORT_ENCRYPTION_KEY_ID" || [ "$should_generate" = "true" ]; then
|
|
576
|
+
local generated_key_id
|
|
577
|
+
generated_key_id=$(generate_worker_subdomain_label)
|
|
578
|
+
if [ -z "$generated_key_id" ] || [ ${#generated_key_id} -ne 10 ]; then
|
|
579
|
+
echo -e "${RED}❌ Error: Failed to generate EXPORT_ENCRYPTION_KEY_ID${NC}"
|
|
580
|
+
exit 1
|
|
581
|
+
fi
|
|
582
|
+
EXPORT_ENCRYPTION_KEY_ID="$generated_key_id"
|
|
583
|
+
export EXPORT_ENCRYPTION_KEY_ID
|
|
584
|
+
write_env_var "EXPORT_ENCRYPTION_KEY_ID" "$EXPORT_ENCRYPTION_KEY_ID"
|
|
585
|
+
echo -e "${GREEN}✅ EXPORT_ENCRYPTION_KEY_ID generated: $EXPORT_ENCRYPTION_KEY_ID${NC}"
|
|
586
|
+
else
|
|
587
|
+
echo -e "${GREEN}✅ EXPORT_ENCRYPTION_KEY_ID: $EXPORT_ENCRYPTION_KEY_ID${NC}"
|
|
588
|
+
fi
|
|
589
|
+
|
|
590
|
+
echo ""
|
|
591
|
+
}
|
|
592
|
+
|
|
511
593
|
# Validate required variables
|
|
512
594
|
required_vars=(
|
|
513
595
|
# Core Cloudflare Configuration
|
|
@@ -564,6 +646,9 @@ required_vars=(
|
|
|
564
646
|
"MANIFEST_SIGNING_PRIVATE_KEY"
|
|
565
647
|
"MANIFEST_SIGNING_KEY_ID"
|
|
566
648
|
"MANIFEST_SIGNING_PUBLIC_KEY"
|
|
649
|
+
"EXPORT_ENCRYPTION_PRIVATE_KEY"
|
|
650
|
+
"EXPORT_ENCRYPTION_KEY_ID"
|
|
651
|
+
"EXPORT_ENCRYPTION_PUBLIC_KEY"
|
|
567
652
|
)
|
|
568
653
|
|
|
569
654
|
validate_required_vars() {
|
|
@@ -591,7 +676,7 @@ assert_contains_literal() {
|
|
|
591
676
|
local literal=$2
|
|
592
677
|
local description=$3
|
|
593
678
|
|
|
594
|
-
if ! grep -Fq "$literal" "$file_path"; then
|
|
679
|
+
if ! grep -Fq -- "$literal" "$file_path"; then
|
|
595
680
|
echo -e "${RED}❌ Error: ${description}${NC}"
|
|
596
681
|
echo -e "${YELLOW} Expected to find '$literal' in $file_path${NC}"
|
|
597
682
|
exit 1
|
|
@@ -743,6 +828,8 @@ validate_generated_configs() {
|
|
|
743
828
|
|
|
744
829
|
assert_contains_literal "app/config/config.json" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in app/config/config.json"
|
|
745
830
|
assert_contains_literal "app/config/config.json" "$ACCOUNT_HASH" "ACCOUNT_HASH missing in app/config/config.json"
|
|
831
|
+
assert_contains_literal "app/config/config.json" "$EXPORT_ENCRYPTION_KEY_ID" "EXPORT_ENCRYPTION_KEY_ID missing in app/config/config.json"
|
|
832
|
+
assert_contains_literal "app/config/config.json" "\"export_encryption_public_key\":" "export_encryption_public_key missing in app/config/config.json"
|
|
746
833
|
assert_contains_literal "app/routes/auth/login.tsx" "const APP_CANONICAL_ORIGIN = 'https://$PAGES_CUSTOM_DOMAIN';" "PAGES_CUSTOM_DOMAIN missing in app/routes/auth/login.tsx canonical origin"
|
|
747
834
|
|
|
748
835
|
assert_contains_literal "app/config/firebase.ts" "$API_KEY" "API_KEY missing in app/config/firebase.ts"
|
|
@@ -761,7 +848,7 @@ validate_generated_configs() {
|
|
|
761
848
|
assert_contains_literal "workers/user-worker/src/user-worker.ts" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in user-worker source"
|
|
762
849
|
|
|
763
850
|
local placeholder_pattern
|
|
764
|
-
placeholder_pattern="(\"(ACCOUNT_ID|PAGES_PROJECT_NAME|PAGES_CUSTOM_DOMAIN|KEYS_WORKER_NAME|USER_WORKER_NAME|DATA_WORKER_NAME|AUDIT_WORKER_NAME|IMAGES_WORKER_NAME|PDF_WORKER_NAME|KEYS_WORKER_DOMAIN|USER_WORKER_DOMAIN|DATA_WORKER_DOMAIN|AUDIT_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN|PDF_WORKER_DOMAIN|DATA_BUCKET_NAME|AUDIT_BUCKET_NAME|KV_STORE_ID|ACCOUNT_HASH|MANIFEST_SIGNING_KEY_ID|MANIFEST_SIGNING_PUBLIC_KEY|YOUR_FIREBASE_API_KEY|YOUR_FIREBASE_AUTH_DOMAIN|YOUR_FIREBASE_PROJECT_ID|YOUR_FIREBASE_STORAGE_BUCKET|YOUR_FIREBASE_MESSAGING_SENDER_ID|YOUR_FIREBASE_APP_ID|YOUR_FIREBASE_MEASUREMENT_ID)\"|'(PAGES_CUSTOM_DOMAIN|DATA_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN)')"
|
|
851
|
+
placeholder_pattern="(\"(ACCOUNT_ID|PAGES_PROJECT_NAME|PAGES_CUSTOM_DOMAIN|KEYS_WORKER_NAME|USER_WORKER_NAME|DATA_WORKER_NAME|AUDIT_WORKER_NAME|IMAGES_WORKER_NAME|PDF_WORKER_NAME|KEYS_WORKER_DOMAIN|USER_WORKER_DOMAIN|DATA_WORKER_DOMAIN|AUDIT_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN|PDF_WORKER_DOMAIN|DATA_BUCKET_NAME|AUDIT_BUCKET_NAME|KV_STORE_ID|ACCOUNT_HASH|MANIFEST_SIGNING_KEY_ID|MANIFEST_SIGNING_PUBLIC_KEY|EXPORT_ENCRYPTION_KEY_ID|EXPORT_ENCRYPTION_PUBLIC_KEY|YOUR_FIREBASE_API_KEY|YOUR_FIREBASE_AUTH_DOMAIN|YOUR_FIREBASE_PROJECT_ID|YOUR_FIREBASE_STORAGE_BUCKET|YOUR_FIREBASE_MESSAGING_SENDER_ID|YOUR_FIREBASE_APP_ID|YOUR_FIREBASE_MEASUREMENT_ID)\"|'(PAGES_CUSTOM_DOMAIN|DATA_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN)')"
|
|
765
852
|
|
|
766
853
|
local files_to_scan=(
|
|
767
854
|
"wrangler.toml"
|
|
@@ -1310,6 +1397,7 @@ prompt_for_secrets() {
|
|
|
1310
1397
|
prompt_for_var "HMAC_KEY" "Cloudflare Images HMAC signing key"
|
|
1311
1398
|
|
|
1312
1399
|
configure_manifest_signing_credentials
|
|
1400
|
+
configure_export_encryption_credentials
|
|
1313
1401
|
|
|
1314
1402
|
# Reload the updated .env file
|
|
1315
1403
|
source .env
|
|
@@ -1447,15 +1535,21 @@ update_wrangler_configs() {
|
|
|
1447
1535
|
echo -e "${YELLOW} Updating app/config/config.json...${NC}"
|
|
1448
1536
|
local escaped_manifest_signing_key_id
|
|
1449
1537
|
local escaped_manifest_signing_public_key
|
|
1538
|
+
local escaped_export_encryption_key_id
|
|
1539
|
+
local escaped_export_encryption_public_key
|
|
1450
1540
|
local escaped_account_hash
|
|
1451
1541
|
escaped_manifest_signing_key_id=$(escape_for_sed_replacement "$MANIFEST_SIGNING_KEY_ID")
|
|
1452
1542
|
escaped_manifest_signing_public_key=$(escape_for_sed_replacement "$MANIFEST_SIGNING_PUBLIC_KEY")
|
|
1543
|
+
escaped_export_encryption_key_id=$(escape_for_sed_replacement "$EXPORT_ENCRYPTION_KEY_ID")
|
|
1544
|
+
escaped_export_encryption_public_key=$(escape_for_sed_replacement "$EXPORT_ENCRYPTION_PUBLIC_KEY")
|
|
1453
1545
|
escaped_account_hash=$(escape_for_sed_replacement "$ACCOUNT_HASH")
|
|
1454
1546
|
|
|
1455
1547
|
sed -i "s|\"url\": \"[^\"]*\"|\"url\": \"https://$escaped_pages_custom_domain\"|g" app/config/config.json
|
|
1456
1548
|
sed -i "s|\"account_hash\": \"[^\"]*\"|\"account_hash\": \"$escaped_account_hash\"|g" app/config/config.json
|
|
1457
1549
|
sed -i "s|\"MANIFEST_SIGNING_KEY_ID\"|\"$escaped_manifest_signing_key_id\"|g" app/config/config.json
|
|
1458
1550
|
sed -i "s|\"MANIFEST_SIGNING_PUBLIC_KEY\"|\"$escaped_manifest_signing_public_key\"|g" app/config/config.json
|
|
1551
|
+
sed -i "s|\"EXPORT_ENCRYPTION_KEY_ID\"|\"$escaped_export_encryption_key_id\"|g" app/config/config.json
|
|
1552
|
+
sed -i "s|\"EXPORT_ENCRYPTION_PUBLIC_KEY\"|\"$escaped_export_encryption_public_key\"|g" app/config/config.json
|
|
1459
1553
|
echo -e "${GREEN} ✅ app config.json updated${NC}"
|
|
1460
1554
|
fi
|
|
1461
1555
|
|
|
@@ -187,7 +187,7 @@ fi
|
|
|
187
187
|
|
|
188
188
|
# Data Worker
|
|
189
189
|
if ! set_worker_secrets "Data Worker" "workers/data-worker" \
|
|
190
|
-
"R2_KEY_SECRET" "MANIFEST_SIGNING_PRIVATE_KEY" "MANIFEST_SIGNING_KEY_ID"; then
|
|
190
|
+
"R2_KEY_SECRET" "MANIFEST_SIGNING_PRIVATE_KEY" "MANIFEST_SIGNING_KEY_ID" "EXPORT_ENCRYPTION_PRIVATE_KEY" "EXPORT_ENCRYPTION_KEY_ID"; then
|
|
191
191
|
echo -e "${YELLOW}⚠️ Skipping Data Worker (not configured)${NC}"
|
|
192
192
|
fi
|
|
193
193
|
|