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.json plus 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.json describing 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_keys keyed by keyId (preferred)
  • export_encryption_public_key + export_encryption_key_id fallback

Current helper:

  • getCurrentEncryptionPublicKeyDetails() in app/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_KEY
  • EXPORT_ENCRYPTION_KEY_ID
  • EXPORT_ENCRYPTION_KEYS_JSON
  • EXPORT_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_JSON in normalized nested format (activeKeyId + keys)
  • updates EXPORT_ENCRYPTION_ACTIVE_KEY_ID to 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:

  1. Build the plaintext case export payload and image set.
  2. Generate a signed forensic manifest.
  3. Resolve the active export encryption public key from runtime config.
  4. Generate one shared AES-256-GCM key for the package.
  5. Encrypt the data file and each exported image.
  6. Wrap the AES key with the configured RSA-OAEP public key.
  7. Write ENCRYPTION_MANIFEST.json into the ZIP.
  8. Keep FORENSIC_MANIFEST.json and 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:

  1. Build the plaintext confirmation export JSON.
  2. Compute and store the confirmation hash.
  3. Request a server-issued confirmation signature.
  4. Encrypt the signed confirmation JSON payload using the configured export encryption public key.
  5. 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() in case-manage/operations.ts when the examiner archives a regular writable case.
  • Archived case re-export — invoked from downloadCaseAsZip() in case-export/download-handlers.ts when an examiner re-exports an already-archived case using the Export Archive option.

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.ts
  • app/components/actions/case-import/orchestrator.ts

Behavior:

  1. Detect ENCRYPTION_MANIFEST.json in the ZIP.
  2. If present, preview reports the package as encrypted and defers detailed payload inspection.
  3. Import extracts ciphertext bytes from the case data file and any encrypted image or bundled audit files.
  4. App calls decryptExportBatch().
  5. Data Worker decrypts the payload using EXPORT_ENCRYPTION_PRIVATE_KEY.
  6. 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.ts
  • app/components/actions/case-import/confirmation-import.ts

Behavior:

  1. Require ENCRYPTION_MANIFEST.json inside the confirmation ZIP.
  2. Treat the confirmation-data-*.json file as ciphertext.
  3. Call decryptExportBatch() with the encrypted confirmation payload.
  4. Parse decrypted plaintext JSON.
  5. Run normal hash validation.
  6. 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:

  • 500 when decryption keys are not configured or no registry key can decrypt the payload
  • 500 when 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.

Related Documentation