Encryption Architecture¶
Broker credentials are the most sensitive data in the platform. They grant direct access to trading accounts with real money. These credentials must be encrypted at rest, in transit, and during processing. The private key never leaves hardware.
4pass implements a 3-level encryption key hierarchy with per-user isolation, ensuring that a breach of one user's data cannot cascade to any other user.
Encryption Key Hierarchy¶
flowchart TB
subgraph HSM["AWS KMS HSM (FIPS 140-2 Level 3)"]
MASTER["Master Key<br/>(RSA-4096 or AES-256)<br/>Never exportable"]
end
subgraph Server["Application Server"]
ACCOUNT_KEY["Per-Account Encryption Key<br/>32-byte AES-256 key<br/>(unique per trading account)"]
DECRYPT["Decrypted Credentials<br/>(in-memory only, never persisted)"]
end
subgraph DB["PostgreSQL"]
ENC_ACCOUNT_KEY["Encrypted Account Key<br/>(wrapped by master key)"]
ENC_CREDS["Encrypted Credentials<br/>AES-256-GCM ciphertext<br/>+ 12-byte nonce + GCM tag"]
end
MASTER -->|"Envelope decrypt<br/>(KMS API call)"| ACCOUNT_KEY
ENC_ACCOUNT_KEY -->|"Stored encrypted"| ACCOUNT_KEY
ACCOUNT_KEY -->|"AES-256-GCM decrypt"| DECRYPT
ENC_CREDS -->|"Read from DB"| DECRYPT
Three levels of protection:
| Level | Key | Protected By | Purpose |
|---|---|---|---|
| Level 1 | KMS Master Key | AWS HSM hardware (non-exportable) | Encrypts/decrypts per-account keys |
| Level 2 | Per-Account Key | Encrypted by master key, stored in DB | Encrypts/decrypts actual credentials |
| Level 3 | Credential Ciphertext | Encrypted by per-account key | The broker API keys and secrets at rest |
Why 3 Levels?
If the database is compromised, the attacker gets Level 3 (ciphertext) and Level 2 (encrypted account keys) but cannot decrypt anything without Level 1 (the KMS master key in the HSM). If application memory is dumped, the attacker gets one user's decrypted credentials but not others — each account has a unique key.
Frontend-to-Backend Encryption¶
When a user submits broker credentials through the dashboard, a hybrid RSA-4096 + AES-256-GCM encryption scheme protects the data end-to-end — even if TLS is compromised by a MITM proxy or CDN misconfiguration.
sequenceDiagram
participant Browser as Browser (Web Crypto API)
participant API as FastAPI
participant KMS as AWS KMS HSM
participant DB as PostgreSQL
Browser->>API: 1. GET /auth/encryption-key
API->>KMS: GetPublicKey(key_id)
KMS-->>API: RSA-4096 public key (PEM)
API-->>Browser: Public key (cached)
Note over Browser: 2. Generate random AES-256 key<br/>crypto.subtle.generateKey("AES-GCM", 256)
Note over Browser: 3. Encrypt credentials with AES-GCM<br/>Random 12-byte IV per operation
Note over Browser: 4. Encrypt AES key with RSA-OAEP-256<br/>crypto.subtle.encrypt("RSA-OAEP", publicKey, aesKey)
Browser->>API: 5. POST /trading/accounts<br/>{encrypted_key, encrypted_data, iv, tag}
API->>KMS: 6. Decrypt(encrypted_key)<br/>Private key stays in HSM
KMS-->>API: Decrypted AES-256 key
API->>API: 7. AES-GCM decrypt credentials<br/>using decrypted AES key + IV + tag
API->>API: 8. Re-encrypt with per-account<br/>storage key (AES-256-GCM)
API->>DB: 9. Store encrypted credentials<br/>(nonce + ciphertext + GCM tag)
Note over API: AES key and plaintext credentials<br/>exist only in memory, then garbage collected
Critical security properties of this flow:
- Forward secrecy — each credential submission generates a fresh random AES-256 key. Compromising one request's key doesn't decrypt any other request.
- HSM-backed decryption — the RSA-4096 private key never leaves the KMS HSM. Step 6 is a KMS API call; the plaintext AES key is returned to the application, but the private key remains in hardware.
- Web Crypto API — the browser uses the native
crypto.subtleAPI (not a JavaScript library), which runs in a secure context and is resistant to side-channel attacks. - No plaintext in transit — even though HTTPS encrypts the connection, the credential payload is independently encrypted. A TLS-intercepting proxy sees only ciphertext.
Storage Encryption (Per-User AES-256-GCM)¶
Once credentials reach the server, they are re-encrypted with a per-account key for storage in PostgreSQL.
| Property | Value |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption) |
| Key size | 32 bytes (256 bits) per trading account |
| Nonce (IV) | 12 bytes (96 bits), random per encryption operation |
| Authentication | GCM tag provides integrity + confidentiality |
| Key isolation | Each TradingAccount has its own unique encryption key |
| Key storage | Account keys encrypted with master key, stored in encrypted_key DB column |
| Encryption version | Version 2 (current); Version 1 (Fernet/AES-128-CBC) for legacy data |
Database column format:
┌──────────┬────────────────────────┬──────────┐
│ 12-byte │ Variable-length │ 16-byte │
│ Nonce │ Ciphertext │ GCM Tag │
└──────────┴────────────────────────┴──────────┘
Base64-encoded, stored as TEXT
Nonce Uniqueness
A 12-byte random nonce is generated for every encryption operation using os.urandom(12). Reusing a nonce with the same key in AES-GCM completely breaks the security of the scheme. The random generation ensures this never happens (collision probability: 2^-48 per pair).
Master Key Management¶
The master key that protects per-account keys has two modes, selected automatically based on the USE_AWS_KMS environment variable:
| Property | Value |
|---|---|
| Key type | KMS-managed AES-256 symmetric key |
| Envelope encryption | KMS encrypts per-account keys; application uses plaintext data keys |
| HSM backing | FIPS 140-2 Level 3 hardware security module |
| Audit | All key operations logged in AWS CloudTrail |
| Rotation | KMS automatic key rotation (annual) |
| Key ID | AWS_KMS_KEY_ID environment variable |
| Region | AWS_REGION / AWS_DEFAULT_REGION (default: ap-southeast-1) |
| Property | Value |
|---|---|
| Key type | Fernet symmetric key derived from ENCRYPTION_KEY env var |
| Derivation | PBKDF2-HMAC-SHA256, 100,000 iterations |
| Salt | Configurable via KEY_DERIVATION_SALT |
| Purpose | Local development and testing only |
Automatic Mode Selection
The application automatically selects KMS or local mode based on USE_AWS_KMS=true|false. No code changes required when moving between development and production — only environment variables change.
Encryption Versioning¶
4pass supports two encryption versions with automatic forward migration:
| Version | Algorithm | Key Size | Status | Introduced |
|---|---|---|---|---|
| v1 | Fernet (AES-128-CBC + HMAC-SHA256) | 128-bit | Legacy | Initial release |
| v2 | AES-256-GCM | 256-bit | Current (default) | Security upgrade |
Automatic upgrade on read:
flowchart TB
READ["Read encrypted credential"] --> CHECK{"Version?"}
CHECK -->|v1| DECRYPT_V1["Decrypt with Fernet"]
CHECK -->|v2| DECRYPT_V2["Decrypt with AES-256-GCM"]
DECRYPT_V1 --> REENCRYPT["Re-encrypt as v2"]
REENCRYPT --> SAVE["Save upgraded ciphertext"]
DECRYPT_V2 --> USE["Return plaintext"]
SAVE --> USE
- Existing v1 data is decrypted and transparently re-encrypted as v2 on the next read
- New data always uses v2 (AES-256-GCM)
- Backward compatibility maintained — both versions can be decrypted indefinitely
- The version byte is prepended to the ciphertext, enabling future algorithm upgrades
Security Properties Summary¶
| Property | Mechanism | Guarantee |
|---|---|---|
| Forward secrecy | Unique AES-256 key per frontend request | Compromising one submission doesn't decrypt others |
| Per-user isolation | Unique 32-byte key per trading account | Compromising one account key affects only that account |
| HSM-backed | RSA-4096 private key in KMS FIPS 140-2 L3 hardware | Private key never exists in software memory |
| Authenticated encryption | AES-256-GCM with 128-bit authentication tag | Tampering detected and rejected before decryption |
| No plaintext at rest | Credentials encrypted before DB write | Database dump reveals only ciphertext |
| No plaintext in transit | RSA+AES hybrid encryption over HTTPS | Double encryption layer — TLS failure doesn't expose credentials |
| Audit trail | KMS operations logged in CloudTrail | All decryption events are traceable and alertable |
| Key rotation | KMS automatic annual rotation + version migration | Forward security without downtime |
What an Attacker Needs
To decrypt a single user's broker credentials, an attacker must simultaneously possess:
- Database access (to get the encrypted credential and encrypted account key)
- KMS Decrypt permission (to unwrap the account key with the master key)
- The specific account key (to decrypt the credential with AES-256-GCM)
Compromising any one of these alone reveals nothing.