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:
- A random symmetric key is generated per JWE token
- The payload is encrypted with AES-256-GCM using that symmetric key (fast, authenticated)
- 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
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
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
Note
The frontend flow controllers are not yet implemented. This diagram describes the target architecture.
Use Cases¶
Backend: legal document generation¶
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¶
- FinancialInstrumentRevealQueries -- core two-step reveal flow
- register_reveal_key
(public_key_pem, reason, actor)->reveal_key_id - reveal_iban_account_details
(session, id, reveal_key_id)-> JWE token - reveal_ca_local_account_details
(session, id, reveal_key_id)-> JWE token
Convenience Helpers¶
- reveal_iban_account_details
(queries, session, id, reason, actor)-> IBANAccountDetails - reveal_ca_local_account_details
(queries, session, id, reason, actor)-> CALocalAccountDetails - generate_reveal_key_pair
()->(private_pem, public_pem) - decrypt_reveal_jwe
(private_pem, jwe_token)->dict - backend_reveal_actor
(component_name)->str - frontend_reveal_actor
(role, user_id)->str
Entities¶
- IBANAccountDetails -- iban, bank_country_code, bic, display_value
- CALocalAccountDetails -- institution_number, transit_number, account_number, display_value
Exceptions¶
- RevealKeyNotFoundException -- key expired or already consumed
- FinancialInstrumentNotFoundException -- FI ID does not exist
- FinancialInstrumentTypeNotSupportedException -- FI type mismatch