Skip to main content
All Docs
FeaturesagentOS Block ManagerUpdated April 11, 2026

Financial Data Encryption

Financial Data Encryption

The platform encrypts sensitive financial data both at rest (in the database) and in transit (over the network). This page describes how the encryption works, what data is protected, and how to configure it.

Encryption at Rest

All financial data stored in the database is encrypted using AES-256-GCM (256-bit key, 96-bit IV, 128-bit authentication tag). This algorithm is NIST-approved and exceeds the FIPS 140-2 requirement.

How It Works

Encryption is applied at the application layer, not the database layer. Individual columns are encrypted before being written to the database and decrypted after being read. The rest of your application works with plaintext values — encryption and decryption are transparent.

Encrypted values are stored with a version prefix:

enc:v1:<base64-encoded ciphertext>

The enc:v1: prefix allows the system to detect whether a stored value is encrypted, and provides a migration path if the encryption scheme is ever updated.

What Is Encrypted

EntityFieldReason
Management Account SourcesconfigBank connection details, account references

Additional tables (ledger transactions, bank feed data, service charge demands, reserve fund balances) are registered for encryption as those features are introduced.

Backward Compatibility

If a value in the database does not carry the enc:v1: prefix, it is treated as plaintext and returned as-is. This means:

  • Records written before this release continue to work without any migration step.
  • You can enable encryption incrementally — new writes are encrypted, existing plaintext records remain readable.
  • To encrypt existing records, re-save them through the normal update flow.

Encryption in Transit

All connections to the platform are protected by HTTPS. A Strict-Transport-Security (HSTS) header is returned with every response:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
DirectiveValueMeaning
max-age63072000 (2 years)Browsers enforce HTTPS for this duration
includeSubDomainsApplies to all subdomains
preloadEligible for browser HSTS preload lists

This prevents protocol downgrade attacks and ensures all financial data in transit is protected by TLS 1.2 or higher.

Configuration

ENCRYPTION_KEY

Encryption at rest requires a 256-bit key supplied as a 64-character hex string via the ENCRYPTION_KEY environment variable.

Generate a key:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Example output:

a3f1c2e4b5d6789012345678abcdef01a3f1c2e4b5d6789012345678abcdef01

Important: Store this key securely (e.g. in your secrets manager). If the key is lost or changed without a migration, previously encrypted records cannot be decrypted.

If ENCRYPTION_KEY is not set:

  • Financial data is stored in plaintext
  • A warning is logged on every write: [financial-encryption] ENCRYPTION_KEY not configured
  • If encrypted records exist and the key is missing, the field value is returned as [encrypted]

This behaviour allows local development without a key, but ENCRYPTION_KEY must be set in all production and staging environments.

Admin: Encryption Health Check

Managing agents can verify that encryption is active and passing self-tests via the encryptionHealth tRPC endpoint.

Response fields:

FieldDescription
configuredtrue if ENCRYPTION_KEY is set
algorithmActive cipher — AES-256-GCM
keyLengthKey size in bits — 256
selfTestPassedtrue if an internal encrypt/decrypt round-trip succeeded

All calls to encryptionHealth are audit-logged for compliance purposes.

Developer Reference

Field-Level Functions

import {
  encryptFinancialField,
  decryptFinancialField,
  encryptAmount,
  decryptAmount,
  encryptRecord,
  decryptRecord,
  isEncrypted,
  checkEncryptionHealth,
} from "@/lib/financial-encryption";

encryptFinancialField(value)

Encrypts a string field. Returns null if input is null/undefined. Prefixes the result with enc:v1:.

const encrypted = encryptFinancialField("GB29NWBK60161331926819");
// → "enc:v1:..."

decryptFinancialField(value)

Decrypts an encrypted string field. Returns plaintext as-is if not prefixed (backward compat).

const plain = decryptFinancialField("enc:v1:...");
// → "GB29NWBK60161331926819"

encryptAmount(value) / decryptAmount(value)

Encrypt and decrypt numeric values (amounts, balances). decryptAmount returns a number | null.

const enc = encryptAmount(1500.00);   // → "enc:v1:..."
const num = decryptAmount(enc);       // → 1500

encryptRecord(record, fields) / decryptRecord(record, fields, numericFields?)

Batch encrypt or decrypt named fields on a record object. The original record is not mutated.

const toStore = encryptRecord(
  { amount: "1500.00", reference: "SC-2024-001", description: "Service charge" },
  ["amount", "reference"]
);
// → { amount: "enc:v1:...", reference: "enc:v1:...", description: "Service charge" }

const fromDb = decryptRecord(toStore, ["amount", "reference"]);
// → { amount: "1500.00", reference: "SC-2024-001", description: "Service charge" }

isEncrypted(value)

Returns true if the value carries the enc:v1: prefix.

isEncrypted("enc:v1:..."); // → true
isEncrypted("plaintext");  // → false

checkEncryptionHealth()

Returns the current encryption configuration status.

const health = checkEncryptionHealth();
// → { configured: true, algorithm: "AES-256-GCM", keyLength: 256, selfTestPassed: true }

Compliance

This implementation satisfies the following requirements:

  • Financial Data Encryption — At Rest and In Transit (pinned spec)
  • FCA-Aligned Client Money Handling — financial data protected in storage
  • Multi-Tenant Architecture — single encryption key per deployment ensures tenant isolation
  • GDPR / UK Data Residency — encryption at rest for all stored PII and financial identifiers