Skip to content

Financial Instrument Reveal API

Overview

Financial instrument data (IBAN, Canadian local account numbers) is stored encrypted in the Payment Gateway database. By default, only obfuscated values are exposed (e.g. FR76 **** **** **** **** *890 123). The Reveal API provides a secure mechanism to retrieve the full decrypted data when there is a legitimate reason.

Design Decisions

End-to-end encryption with JWE

Data is never transmitted in cleartext between the Payment Gateway and the consumer. Instead, the API uses JWE (JSON Web Encryption), which is a hybrid encryption standard:

  1. A random symmetric key is generated per JWE token
  2. The payload is encrypted with AES-256-GCM using that symmetric key (fast, authenticated)
  3. The symmetric key itself is encrypted with the consumer's RSA-OAEP-256 public key

The consumer decrypts the symmetric key with their RSA private key, then uses it to decrypt the payload. This ensures that only the holder of the ephemeral private key can access the data, even if the transport layer is compromised. AES-256-GCM is handled transparently by the JWE library -- consumers only deal with RSA key pairs.

Ephemeral key pairs with cache-based registration

Rather than reusing long-lived keys, each reveal operation uses a fresh RSA key pair. The public key is registered in the AlanCache (Redis-backed) with a 60-second TTL and consumed (deleted) on first use, enforcing single-use keys.

This provides:

  • Key isolation: each reveal gets its own encryption context
  • Single-use enforcement: a key cannot be replayed
  • Automatic expiry: stale keys are garbage-collected by Redis TTL

Mandatory auditability

Every reveal operation requires a reason (free-form string explaining why the reveal is needed) and an actor (formatted string identifying who is requesting). Both are logged at key registration and at the reveal step, providing full traceability.

Actor format conventions:

  • Backend: component:<component_name> (e.g. component:banking_documents)
  • Frontend: <role>:<user_id> (e.g. member:abc123, admin:def456)

Separate methods per financial instrument type

Each financial instrument type has its own reveal method and details dataclass, following the same convention as the creation actions (create_iban_account_financial_instrument, create_ca_local_account_financial_instrument).

FI Type Reveal Method Details Dataclass
IBAN Account reveal_iban_account_details IBANAccountDetails
CA Local Account reveal_ca_local_account_details CALocalAccountDetails

Architecture

Two-step reveal flow (JWE)

This is the low-level flow used by both backend and frontend consumers. The consumer manages the RSA key pair and the JWE decryption.

sequenceDiagram
    participant Consumer
    participant PG as Payment Gateway
    participant Cache as AlanCache (Redis)
    participant DB as Database

    Consumer->>Consumer: Generate ephemeral RSA key pair
    Consumer->>PG: register_reveal_key(public_key, reason, actor)
    PG->>Cache: Store public key (TTL 60s)
    PG-->>Consumer: reveal_key_id

    Consumer->>PG: reveal_*_details(fi_id, reveal_key_id)
    PG->>Cache: Fetch & delete key (single-use)
    PG->>DB: Decrypt FI data
    PG->>PG: Build details dataclass
    PG->>PG: Encrypt as JWE with consumer's public key
    PG-->>Consumer: JWE token

    Consumer->>Consumer: Decrypt JWE with private key
    Consumer->>Consumer: Parse details from JSON payload
Hold "Alt" / "Option" to enable pan & zoom

One-shot convenience helpers (backend only)

For backend consumers, convenience helpers wrap the entire lifecycle into a single atomic call. The ephemeral key pair is generated and discarded internally -- the private key never leaves the function.

sequenceDiagram
    participant Caller as Backend Component
    participant Helper as reveal_*_details()
    participant PG as RevealQueries
    participant Cache as AlanCache (Redis)
    participant DB as Database

    Caller->>Helper: reveal_iban_account_details(queries, session, fi_id, reason, actor)
    Helper->>Helper: Generate ephemeral RSA key pair
    Helper->>PG: register_reveal_key(public_key, reason, actor)
    PG->>Cache: Store public key (TTL 60s)
    PG-->>Helper: reveal_key_id
    Helper->>PG: reveal_iban_account_details(fi_id, reveal_key_id)
    PG->>Cache: Fetch & delete key
    PG->>DB: Decrypt FI data
    PG->>PG: Encrypt as JWE
    PG-->>Helper: JWE token
    Helper->>Helper: Decrypt JWE with private key
    Helper-->>Caller: IBANAccountDetails
Hold "Alt" / "Option" to enable pan & zoom

Frontend flow (controllers)

Frontend consumers use the two-step flow via REST endpoints. The frontend generates the RSA key pair in the browser (Web Crypto API), registers the public key, calls reveal, and decrypts the JWE client-side. The private key never leaves the browser.

sequenceDiagram
    participant Browser
    participant API as Backend Controller
    participant PG as RevealQueries
    participant Cache as AlanCache (Redis)
    participant DB as Database

    Browser->>Browser: Generate RSA key pair (Web Crypto API)
    Browser->>API: POST /register-reveal-key {public_key, reason}
    API->>PG: register_reveal_key(public_key, reason, actor)
    PG->>Cache: Store public key (TTL 60s)
    PG-->>API: reveal_key_id
    API-->>Browser: {reveal_key_id}

    Browser->>API: POST /reveal-fi-details {fi_id, reveal_key_id}
    API->>PG: reveal_*_details(fi_id, reveal_key_id)
    PG->>Cache: Fetch & delete key
    PG->>DB: Decrypt FI data
    PG->>PG: Encrypt as JWE
    PG-->>API: JWE token
    API-->>Browser: {jwe_token}

    Browser->>Browser: Decrypt JWE with private key
    Browser->>Browser: Display financial instrument details
Hold "Alt" / "Option" to enable pan & zoom

Note

The frontend flow controllers are not yet implemented. This diagram describes the target architecture.

Use Cases

A component generating legal documents needs the full IBAN to include in contracts or payment confirmations.

from components.payment_gateway.public.parties import (
    FinancialInstrumentRevealQueries,
    backend_reveal_actor,
    reveal_iban_account_details,
)

reveal_queries = FinancialInstrumentRevealQueries.create()
details = reveal_iban_account_details(
    reveal_queries,
    session,
    id=financial_instrument_id,
    reason="contract_generation",
    actor=backend_reveal_actor("banking_documents"),
)

# details.iban => "FR7612345678901234567890123"
# details.display_value => "FR76 1234 5678 9012 3456 7890 123"
# details.bank_country_code => "FR"
# details.bic => "BNPAFRPP"

Backend: Canadian account for regulatory reporting

An external component needs full Canadian banking details for regulatory filings or compliance exports.

from components.payment_gateway.public.parties import (
    FinancialInstrumentRevealQueries,
    backend_reveal_actor,
    reveal_ca_local_account_details,
)

reveal_queries = FinancialInstrumentRevealQueries.create()
details = reveal_ca_local_account_details(
    reveal_queries,
    session,
    id=financial_instrument_id,
    reason="regulatory_reporting",
    actor=backend_reveal_actor("compliance"),
)

# details.institution_number => "003"
# details.transit_number => "12345"
# details.account_number => "9876543210"
# details.display_value => "003-12345-9876543210"

Frontend: member dashboard (target architecture)

A member clicks "Show full IBAN" on their dashboard. The frontend generates a key pair in the browser, registers the public key, calls reveal, and decrypts client-side.

// 1. Generate RSA key pair in browser
const keyPair = await crypto.subtle.generateKey(
  { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
  true,
  ["encrypt", "decrypt"],
);
const publicKeyPem = await exportPublicKeyAsPem(keyPair.publicKey);

// 2. Register key with Payment Gateway
const { revealKeyId } = await api.post("/register-reveal-key", {
  publicKey: publicKeyPem,
  reason: "member_dashboard_view",
});

// 3. Call reveal endpoint
const { jweToken } = await api.post("/reveal-fi-details", {
  financialInstrumentId,
  revealKeyId,
});

// 4. Decrypt JWE client-side
const details = await decryptJwe(keyPair.privateKey, jweToken);
// details.displayValue => "FR76 1234 5678 9012 3456 7890 123"

Note

The frontend controllers and React hooks are not yet implemented.

Security Properties

Property Mechanism
Data never in cleartext on the wire JWE encryption with consumer's public key
Key isolation per operation Ephemeral RSA key pairs, one per reveal
Single-use enforcement Cache delete-on-read
Automatic key expiry 60s Redis TTL
Auditability Mandatory reason + actor logged at every step
Backend key confidentiality Private key stays inside one-shot helper function
Frontend key confidentiality Private key stays in browser memory (Web Crypto API)

Public API Reference

Queries

Convenience Helpers

Entities

Exceptions