End-to-End Encryption (E2E) — Technical Reference¶
This document describes the complete end-to-end encryption system that protects every HTTP request and response between clients (Web UI, Mobile App) and the backend API.
Table of Contents¶
- Overview
- Cryptographic Primitives
- Encryption Flags & Enforcement
- Key Lifecycle
- Route Classification
- Request Encryption Flow
- 6.1 Pre-Auth Routes (RSA path)
- 6.2 Authenticated Routes (AES path)
- Response Encryption Flow
- Client Implementations
- 8.1 Web UI (React/Redux)
- 8.2 Mobile App (Flutter/Dart)
- Backend Implementation (Python/FastAPI)
- WebSocket Encryption
- At-Rest Field Encryption (DB)
- Security Properties
- Error Handling
- Sequence Diagrams
1. Overview¶
The system uses a hybrid RSA + AES-GCM encryption scheme:
- RSA-2048 / OAEP-SHA256 is used only to securely exchange AES keys. It is never used to encrypt bulk payloads.
- AES-256-GCM encrypts every request body and response body after the initial key exchange.
There are two distinct transport encryption paths depending on whether the user is authenticated:
| Path | Trigger | Key used to encrypt request | Key used to encrypt response |
|---|---|---|---|
| Pre-Auth (RSA) | Routes in PRE_AUTH_ENCRYPTED_ROUTES |
Backend RSA public key (chunked OAEP) | Client's ephemeral RSA public key → AES |
| Post-Auth (AES) | Any request with x-encrypted: true and a valid session AES key |
Session AES key | Session AES key |
All clients signal intent to encrypt by setting the HTTP header x-encrypted: true. Routes that do not need encryption (e.g. /api/v1/auth/public_key, /api/v1/core/translations/fetch) are excluded explicitly.
A third, separate encryption layer — at-rest DB field encryption — is controlled independently via DB_FIELD_ENCRYPTION and is described in §11.
2. Cryptographic Primitives¶
| Primitive | Parameters | Purpose |
|---|---|---|
| RSA-OAEP | 2048-bit key, SHA-256 hash + MGF1 | AES key transport on pre-auth routes; request body encryption for login forms |
| AES-GCM | 256-bit key, 96-bit (12-byte) random nonce | Bulk payload encryption for both requests and responses |
| GCM authentication tag | 128 bits | Authenticated encryption — detects tampering before decryption |
| AES-ECB + PKCS7 | 128-bit key | DB at-rest field encryption via EncryptedType (see §11) |
The server holds an RSA key pair loaded from PEM files at startup (PRIVATE_KEY_PATH, PUBLIC_KEY_PATH). Clients retrieve the public key once from /api/v1/auth/public_key and store it locally.
3. Encryption Flags & Enforcement¶
The system has three independent flags, each with a distinct scope:
3.1 DB_FIELD_ENCRYPTION (backend — at-rest)¶
Set in backend/app/core/config.py. Controls whether EncryptedType model columns (KYC data, session tokens, credentials) are encrypted when written to the database and decrypted on read. Backward-compatible alias ENCRYPTION is also exported for legacy imports.
3.2 ENFORCE_TRANSPORT_ENCRYPTION (backend — transport enforcement)¶
When 1, the TransportEncryptionEnforcementMiddleware rejects any POST/PUT/PATCH request that does not carry x-encrypted: true, unless the path is in ENCRYPTION_EXEMPT_PATHS.
3.3 Client-side flags (Web / Mobile — rollout toggles only)¶
| Client | Flag | Source |
|---|---|---|
| Web UI | VITE_REACT_APP_ENCRYPTION=1 |
ui/.env / window.ENV.ENCRYPTION |
| Mobile | ENCRYPTION=true |
mobile/.env → dotenv.env['ENCRYPTION'] |
These control whether the client sends x-encrypted: true. They are rollout toggles, not enforcement.
4. Key Lifecycle¶
Phase 1 – Key Bootstrap (once per session)
──────────────────────────────────────────
Client Server
│ │
│── GET /api/v1/auth/public_key ───▶│
│◀─ { public_key: "-----BEGIN..." }─│
│ │
│ [Client generates RSA key pair] │
│ [Client stores private key locally (IndexedDB / SecureStorage)]
│ │
Phase 2 – Login (pre-auth RSA path)
────────────────────────────────────
│ POST /api/v1/auth/login │
│ body: RSA-encrypted form data │
│ (includes client's public_key) │
│──────────────────────────────────▶│
│ │ [Server decrypts body]
│ │ [Server generates AES-256 session key]
│ │ [Server stores AES key in UserSession.aes_key]
│ │ [Server encrypts response with client public_key]
│ │ [Returns: encrypted_key + ciphertext + nonce + tag]
│◀──────────────────────────────────│
│ [Client decrypts encrypted_key │
│ using its RSA private key to │
│ recover the AES session key] │
│ [Client stores AES key] │
Phase 3 – Authenticated requests (AES path)
────────────────────────────────────────────
│ POST /api/v1/some/endpoint │
│ x-encrypted: true │
│ body: { data: { iv, ciphertext, tag } }
│──────────────────────────────────▶│
│ │ [Server decrypts body with session AES key]
│ │ [Server encrypts response with session AES key]
│◀──────────────────────────────────│
│ { iv, ciphertext, tag } │
│ [Client decrypts with AES key] │
AES key encoding¶
| Layer | Format |
|---|---|
Backend (UserSession.aes_key) |
Hex string — decoded with bytes.fromhex() |
Mobile (SecureStorageAuthentication) |
Base64 string — decoded with base64.decode() |
Web (KeyManager) |
Raw base64 — decoded in browser runtime |
5. Route Classification¶
Pre-Auth Encrypted Routes (PRE_AUTH_ENCRYPTED_ROUTES)¶
/api/v1/auth/login
/api/v1/auth/validate
/api/v1/auth/2fa/request
/api/v1/auth/2fa/verify
/api/v1/auth/2fa/validate
/api/v1/auth/agent/validate
/api/v1/auth/password
/api/v1/transaction/transfer/receipt
/api/v1/2fa/totp/setup
/api/v1/2fa/totp/verify
/api/v1/2fa/totp/verify-login
/api/v1/2fa/totp/verify-backup-code
/api/v1/auth/security-questions/predefined
/api/v1/auth/security-questions/verify
/api/v1/auth/security-questions/setup
Encryption-Exempt Paths (ENCRYPTION_EXEMPT_PATHS)¶
/api/v1/auth/public_key
/api/v1/core/translations/fetch
/api/docs
/api/redoc
/api/openapi.json
/api
/api/health
/health
/metrics
Binary Response Paths¶
document/download customer/download file/download
attachment statement/export simulator/download
report/download qr-code/image
6. Request Encryption Flow¶
6.1 Pre-Auth Routes (RSA path)¶
- Serialize request payload (form/query or JSON).
- Encrypt with backend RSA public key (OAEP-SHA256).
- Send under
datawithx-encrypted: true. - Backend decrypts in
RequestMiddlewareand replaces request body with plaintext.
6.2 Authenticated Routes (AES path)¶
- Encrypt JSON payload with session AES key using AES-256-GCM.
- Send
{ data: { iv, ciphertext, tag } }withx-encrypted: true. - Backend decrypts in
RequestMiddleware.decrypt_with_aes_key.
7. Response Encryption Flow¶
For encrypted requests, server encrypts JSON responses before returning:
- Pre-auth:
encrypt_with_frontend_public_key→encrypted_key,ciphertext,nonce,tag - Post-auth:
encrypt_with_aes_key→iv,ciphertext,tag
Login failures on encrypted login flow are also encrypted.
8. Client Implementations¶
8.1 Web UI (React/Redux)¶
Primary files:
ui/src/utils/axios.tsui/src/utils/key_manager.tsui/src/utils/workers/decryption.worker.tsui/src/routes.tsx
Startup initialization (routes.tsx)¶
Bounded retry startup:
maxAttempts = ENCRYPTION ? 2 : 1
for attempt:
await keyManager.initializeAppSecurity()
validate private key + public key + backend key
break if valid else retry / fail on last attempt
KeyManager behavior (current)¶
- Generates RSA keypair using Web Crypto API.
- Public key is used for outbound request encryption.
- Private key is held as CryptoKey for decryption operations.
- Stores key material in IndexedDB with AES-wrapped values where applicable.
- Supports v1 → v2 storage compatibility handling.
Axios / worker decrypt path¶
- Axios detects request-key responses via
encrypted_key. - KeyManager decrypts AES key and stores it.
decryption.worker.tsdecrypts payload (nonce/ciphertext/tag) and returns JSON.
Redux middleware¶
Handles encrypted-session failures (DECRYPTION_FAILED, auth states, retries).
8.2 Mobile App (Flutter/Dart)¶
Primary file:
mobile/lib/openapi/lib/encryption_http_client.dart
Behavior:
- request encryption in
_interceptRequest/_handleJsonRequest/_handleFormRequest - response decrypt in
send() - isolate usage for heavy crypto operations
9. Backend Implementation (Python/FastAPI)¶
Primary file:
backend/app/core/middleware.py
Key pieces:
RequestMiddleware— decrypt request / encrypt responseTransportEncryptionEnforcementMiddleware— optional strict transport gateSecurityHeadersMiddleware— CORS/security headers
10. WebSocket Encryption¶
Mobile helper methods use AES-GCM with session key:
encryptWebSocketMessage(...)decryptWebSocketMessage(...)
11. At-Rest Field Encryption (DB)¶
Primary file:
backend/app/models/common.py(EncryptedType)
EncryptedType transparently encrypts/decrypts DB columns when DB_FIELD_ENCRYPTION is enabled.
12. Security Properties¶
| Property | Mechanism |
|---|---|
| Confidentiality | RSA-OAEP + AES-256-GCM |
| Integrity | GCM auth tag verification |
| Authenticity | TLS + JWT/session key linkage |
| Key isolation | Server private key never leaves backend |
| Auditability | Trace IDs + audit trail logging |
USSD traffic is explicitly excluded from transport encryption.
13. Error Handling¶
DECRYPTION_FAILED(400): payload/key decryption failedENCRYPTION_REQUIRED(422): enforcement blocked unencrypted mutating request
14. Sequence Diagrams¶
Login flow (pre-auth)¶
sequenceDiagram
participant C as Client
participant E as EnforcementMiddleware
participant R as RequestMiddleware
participant H as Handler
C->>E: GET /api/v1/auth/public_key
E->>R: exempt path
R->>H: pass through
H-->>C: public key
C->>E: POST /api/v1/auth/login (x-encrypted)
E->>R: pass through
R->>H: decrypted plaintext
H-->>R: login response
R-->>C: encrypted_key + ciphertext + nonce + tag
Authenticated request flow¶
sequenceDiagram
participant C as Client
participant E as EnforcementMiddleware
participant R as RequestMiddleware
participant H as Handler
C->>E: POST /api/v1/endpoint (x-encrypted)
E->>R: pass through
R->>H: decrypted plaintext
H-->>R: JSON response
R-->>C: iv + ciphertext + tag