Export Encryption - striae-org/striae GitHub Wiki
Overview
Striae uses package-level export encryption to protect exported forensic data while it is outside the live system.
Scope note:
- This guide describes package encryption for export/import workflows.
- It is separate from worker-side encryption-at-rest used for live user profile KV records, case/audit payloads, and file/image R2 storage. See Data-at-Rest Encryption for that topic.
Mandatory policy:
-
Case package ZIP exports must be encrypted.
-
Confirmation package ZIP exports must be encrypted.
-
Case archive packages must be encrypted.
-
Import flows for those package types are fail-closed and require
ENCRYPTION_MANIFEST.jsonplus successful worker-side decryption before payload processing continues. -
Encryption is distinct from signing.
-
Signing proves authenticity and tamper evidence for manifests, confirmation payloads, and audit exports.
-
Encryption protects exported payload confidentiality until a trusted Striae instance decrypts the package.
-
Public encryption material is distributed through app runtime config.
-
Private decryption material remains in the Data Worker.
For canonical signature payload rules and signature envelope details, see Manifest and Confirmation Signing.
Current Crypto Model
Algorithm
- Package encryption constant:
RSA-OAEP-AES-256-GCM - App-side implementation:
app/utils/forensics/export-encryption.ts - Worker-side decryption:
workers/data-worker/src/data-worker.ts
Package Structure
Encrypted packages carry:
- The normal exported filenames (
{case}_data.json,confirmation-data-*.json, image files, bundled audit files) - Ciphertext bytes written back to those files
ENCRYPTION_MANIFEST.jsondescribing how to decrypt them- The public signing key PEM used for signature verification workflows
- Any plaintext signature artifacts that remain intentionally readable, such as
FORENSIC_MANIFEST.json
ENCRYPTION_MANIFEST.json uses this shape:
{
"encryptionVersion": "1.0",
"algorithm": "RSA-OAEP-AES-256-GCM",
"keyId": "export-encryption-key-v1",
"wrappedKey": "base64url-wrapped-aes-key",
"iv": "base64url-iv",
"encryptedImages": [
{
"filename": "image-1.jpg",
"encryptedHash": "64-char sha256 hex"
}
]
}
encryptedHash is the SHA-256 hash of the encrypted bytes stored in the package, not the plaintext file.
Key Management
Public Encryption Keys (App Config)
The browser app resolves the current export encryption public key from runtime config using:
export_encryption_public_keyskeyed bykeyId(preferred)export_encryption_public_key+export_encryption_key_idfallback
Current helper:
getCurrentEncryptionPublicKeyDetails()inapp/utils/forensics/export-encryption.ts
Private Decryption Keys (Data Worker)
The Data Worker keeps private decryption material in secrets. Current deployments can provide either a registry or legacy single-key values:
EXPORT_ENCRYPTION_PRIVATE_KEYEXPORT_ENCRYPTION_KEY_IDEXPORT_ENCRYPTION_KEYS_JSONEXPORT_ENCRYPTION_ACTIVE_KEY_ID
Preferred registry format:
{
"activeKeyId": "kid_current",
"keys": {
"kid_current": "-----BEGIN PRIVATE KEY-----\\n...",
"kid_previous": "-----BEGIN PRIVATE KEY-----\\n..."
}
}
The worker rejects decryption when:
- encryption secrets are not configured
- no usable private key entries are available in the configured registry
- wrapped key, IV, or ciphertext fields are missing or invalid
Deployment Behavior
npm run deploy-config now mirrors signing-key generation for encryption keys:
- auto-generates
EXPORT_ENCRYPTION_PRIVATE_KEY - auto-generates
EXPORT_ENCRYPTION_PUBLIC_KEY - auto-generates
EXPORT_ENCRYPTION_KEY_ID - maintains
EXPORT_ENCRYPTION_KEYS_JSONin normalized nested format (activeKeyId+keys) - updates
EXPORT_ENCRYPTION_ACTIVE_KEY_IDto match the active private key entry - writes the public key and key ID into
app/config/config.json
npm run deploy-workers:secrets deploys legacy and registry-compatible export decryption secrets to the Data Worker.
Export-Time Workflows
Case Export ZIP
Primary implementation:
app/components/actions/case-export/download-handlers.ts
Flow:
- Build the plaintext case export payload and image set.
- Generate a signed forensic manifest.
- Resolve the active export encryption public key from runtime config.
- Generate one shared AES-256-GCM key for the package.
- Encrypt the data file and each exported image.
- Wrap the AES key with the configured RSA-OAEP public key.
- Write
ENCRYPTION_MANIFEST.jsoninto the ZIP. - Keep
FORENSIC_MANIFEST.jsonand the public signing key PEM in the package for later validation.
Current behavior notes:
- Case-package ZIP exports are encrypted and fail closed when no encryption public key is configured.
- The plaintext case data file and plaintext images are replaced by ciphertext in the final ZIP.
Confirmation Export ZIP
Primary implementation:
app/components/actions/confirm-export.ts
Flow:
- Build the plaintext confirmation export JSON.
- Compute and store the confirmation hash.
- Request a server-issued confirmation signature.
- Encrypt the signed confirmation JSON payload using the configured export encryption public key.
- Package the encrypted confirmation payload, public signing key PEM, and
ENCRYPTION_MANIFEST.json.
Current behavior notes:
- Confirmation exports do not include encrypted image entries.
- Confirmation encryption is mandatory. Export fails closed if no public encryption key is configured.
Case Archive Package
Primary implementation:
app/components/actions/case-manage/archive-package-builder.ts
The archive package builder is shared by two code paths:
- Initial archive action — invoked from
archiveCase()incase-manage/operations.tswhen the examiner archives a regular writable case. - Archived case re-export — invoked from
downloadCaseAsZip()incase-export/download-handlers.tswhen an examiner re-exports an already-archived case using theExport Archiveoption.
Both paths call buildArchivePackage() from archive-package-builder.ts, ensuring identical package content for all archive packages regardless of path.
Archive packaging encrypts:
- case data payload
- exported image files
- bundled audit trail JSON
- bundled audit signature JSON
Archive encryption is mandatory. Packaging fails if no encryption public key is configured.
The archive still includes plaintext validation metadata such as:
FORENSIC_MANIFEST.json- the public signing key PEM
ENCRYPTION_MANIFEST.json
Import and Decryption Workflows
Case Import Preview and Import
Primary implementation:
app/components/actions/case-import/zip-processing.tsapp/components/actions/case-import/orchestrator.ts
Behavior:
- Detect
ENCRYPTION_MANIFEST.jsonin the ZIP. - If present, preview reports the package as encrypted and defers detailed payload inspection.
- Import extracts ciphertext bytes from the case data file and any encrypted image or bundled audit files.
- App calls
decryptExportBatch(). - Data Worker decrypts the payload using
EXPORT_ENCRYPTION_PRIVATE_KEY. - App resumes the normal manifest-signature and integrity validation flow on the decrypted plaintext.
Confirmation Import
Primary implementation:
app/components/actions/case-import/confirmation-package.tsapp/components/actions/case-import/confirmation-import.ts
Behavior:
- Require
ENCRYPTION_MANIFEST.jsoninside the confirmation ZIP. - Treat the
confirmation-data-*.jsonfile as ciphertext. - Call
decryptExportBatch()with the encrypted confirmation payload. - Parse decrypted plaintext JSON.
- Run normal hash validation.
- Run normal signature validation.
This means confirmation imports fail closed when the package is not encrypted and also fail closed on hash or signature problems after decryption.
Unencrypted confirmation package imports are rejected.
Data Worker Decryption Endpoint
POST /api/forensic/decrypt-export
Primary implementation:
workers/data-worker/src/data-worker.ts
Request body:
{
"wrappedKey": "base64url-wrapped-aes-key",
"iv": "base64url-iv",
"encryptedData": "base64url-ciphertext",
"encryptedImages": [
{
"filename": "image-1.jpg",
"encryptedData": "base64url-ciphertext"
}
],
"keyId": "export-encryption-key-v1"
}
Response:
{
"success": true,
"plaintext": "decrypted json string",
"decryptedImages": [
{
"filename": "image-1.jpg",
"data": "base64-image-bytes"
}
]
}
Failure behavior:
500when decryption keys are not configured or no registry key can decrypt the payload500when wrapped-key unwrap or ciphertext decryption fails
Operational Notes
- Export encryption protects confidentiality; it does not replace signing validation.
- Import workflows still validate signatures and hashes after decryption.
- App runtime config may publicly expose encryption public keys and key IDs by design; only the private key is secret.
- There is no standalone public-key verification utility component in the current app UI. Validation and encryption behavior now live in shared workflow utilities and import/export actions.