Skip to content

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

  1. Overview
  2. Cryptographic Primitives
  3. Encryption Flags & Enforcement
  4. Key Lifecycle
  5. Route Classification
  6. Request Encryption Flow
  7. 6.1 Pre-Auth Routes (RSA path)
  8. 6.2 Authenticated Routes (AES path)
  9. Response Encryption Flow
  10. Client Implementations
  11. 8.1 Web UI (React/Redux)
  12. 8.2 Mobile App (Flutter/Dart)
  13. Backend Implementation (Python/FastAPI)
  14. WebSocket Encryption
  15. At-Rest Field Encryption (DB)
  16. Security Properties
  17. Error Handling
  18. 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)

# backend/.env
DB_FIELD_ENCRYPTION=1

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)

# backend/.env
ENFORCE_TRANSPORT_ENCRYPTION=0   # set to 1 to reject unencrypted requests

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/.envdotenv.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)

  1. Serialize request payload (form/query or JSON).
  2. Encrypt with backend RSA public key (OAEP-SHA256).
  3. Send under data with x-encrypted: true.
  4. Backend decrypts in RequestMiddleware and replaces request body with plaintext.

6.2 Authenticated Routes (AES path)

  1. Encrypt JSON payload with session AES key using AES-256-GCM.
  2. Send { data: { iv, ciphertext, tag } } with x-encrypted: true.
  3. 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_keyencrypted_key, ciphertext, nonce, tag
  • Post-auth: encrypt_with_aes_keyiv, 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.ts
  • ui/src/utils/key_manager.ts
  • ui/src/utils/workers/decryption.worker.ts
  • ui/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.ts decrypts 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 response
  • TransportEncryptionEnforcementMiddleware — optional strict transport gate
  • SecurityHeadersMiddleware — 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 failed
  • ENCRYPTION_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