PopaDex Encryption: Technical Implementation
Source-grounded details of browser encryption, key handling, server processing, recovery and session limits.
This document describes the web application source reviewed on 12 September 2026. It is an implementation reference, not an independent audit or a verification of the deployed configuration. Read the user guide for a shorter explanation.
Encryption scope and activation
The application uses AES-256-GCM in browser encryption flows. The account form attaches its encryption controller when the user’s encryption setup is complete. It encrypts account name and current_balance; financial-record flows encrypt amount. New account creation submits separate ciphertexts for the account balance and its initial financial record because they use different authenticated contexts.
Account types, currencies, dates, identifiers and operational metadata remain available to the server. Other features, imports and provider integrations must be assessed separately; these field-level controls do not establish application-wide end-to-end confidentiality.
User#eligible_for_e2ee? controls rollout eligibility. User#e2ee_enabled? checks initialization and stored key parameters. Eligible users can be bootstrapped during onboarding or sign-in. Existing records can still require bulk conversion after initialization. Accounts without completed setup can store readable account names and balances.
Keys and algorithms
| Component | Implementation |
|---|---|
| Data encryption | AES-GCM with a 256-bit data-encryption key (DEK), random 96-bit IVs and authenticated ciphertext. |
| Password-derived wrapping key | Argon2id or PBKDF2-HMAC-SHA256, selected using stored parameters and the application’s compatibility policy. |
| DEK wrapping | AES-KW; the wrapped DEK and KDF parameters are stored server-side and can be cached in browser IndexedDB. |
| Recovery code generation | 32 random bytes, encoded as a human-readable Base32 code. |
| Recovery wrapping key | HKDF-SHA256 derives an AES-KW key from the recovery material and salt. |
| Native key access | A separate keychain bridge exists. Its presence does not establish identical behavior across web, iOS and Android. |
The server policy’s balanced profiles are Argon2id with three passes, 32,768 KiB memory and parallelism one, or PBKDF2 with 450,000 iterations. Other profiles and legacy parameters exist. These defaults must not be presented as the parameters of every stored key or as a certification of standards compliance.
Authenticated additional data (AAD) binds ciphertext to the context supplied by the caller. Account and financial-record contexts differ, and legacy formats remain supported. AES-GCM authentication detects changes that fail tag verification; random IVs alone do not prevent replay of an older valid ciphertext.
Password and browser storage handling
Password-derived key operations run in the browser, but account passwords also reach the server through ordinary authentication and password-change requests. This is not a password-authentication protocol that hides the submitted password from the server.
The current secure_password_storage helper stores password values as readable JSON in localStorage, with expiry metadata. Login and signup callers specify five minutes; the session password cache defaults to 30 minutes. The helper checks expiry and attempts cleanup, but storage expiry is application logic, not encryption or a guarantee of physical deletion at a deadline. Visibility and navigation handlers include exceptions that preserve recent passwords.
The active DEK is normally imported as a non-extractable Web Crypto key. Generation and rewrapping use temporary extractable keys. Non-extractability restricts key export; it does not prevent malicious code running in the application origin from reading displayed data, accessing cached passwords or invoking available decryption operations.
Recovery and password changes
The intended password-change operation unwraps the existing DEK with the old password-derived key and rewraps the same DEK with a new key. This does not require rewriting every financial record.
Recovery similarly aims to recover the existing DEK using recovery material, then wrap it for the new password. The password-reset controller can also complete a sign-in password reset without a recovered DEK, in which case existing encrypted values may become inaccessible.
The current recovery-code display sends the generated code to the server to render its dialog. Consequently, a claim that recovery codes never leave the device or are never available to the server is incorrect. Recovery material appearing in a request URL can also be exposed to request-handling infrastructure; this review does not establish what any deployed logs retain.
Local checks reproduced two defects in the reviewed password-based recovery path: setup passes already-zeroed key bytes into recovery wrapping, and password reset tries to wrap a non-extractable recovered key. These failures mean that the current recovery implementation cannot be relied on to preserve encrypted data. This check used synthetic data; it does not establish which deployed accounts are affected. See recovery keys and password reset.
Bank integrations and storage encryption
Bank-provider integrations retrieve and process data on the server. AggregatorEncryptionService encrypts selected provider fields and tokens using AES-256-GCM with server-managed keys, while configured metadata is left readable. The service includes decryption methods for application use.
FinancialRecord also uses Rails attribute encryption for amount. This server-managed layer is distinct from browser encryption and can wrap a browser-produced ciphertext. It does not make server-processed bank data inaccessible to the server.
Session controls
The server’s encryption-session timeout is 15 minutes. The browser crypto session and browser inactivity check use 30 minutes; the UI timeout monitor also has a 15-minute fallback. These paths are not a single, uniform inactivity timer.
A crypto-session lock clears the manager’s active DEK reference and broadcasts a lock event, preserving the wrapped key in IndexedDB. Logout attempts additional cache and key-store cleanup. Neither action guarantees erasure of every copy of plaintext, all local storage, browser history, downloads or operating-system memory. See session locking.
Security boundary
The implementation provides browser encryption for specific financial fields and separate server-managed encryption for some server-side data. It does not justify claims that every record is encrypted before the server sees it, that the server can never access recovery material, or that a compromised application server cannot obtain decryption access.
A database-only exposure and an active compromise of the application, its delivered JavaScript, authentication or recovery flows are different threats. This document does not claim protection against all of them, an independent penetration test, an audit certification, or a verified hosting jurisdiction.
Implementation references
The following paths identify the reviewed components in the application repository:
- Activation and scope:
app/models/user.rb,app/models/account.rb,app/models/financial_record.rb,app/views/accounts/_form.html.erb,app/controllers/accounts_controller.rb. - Browser encryption:
app/javascript/controllers/form_encryption_controller.js,app/javascript/crypto/encryption.js,app/javascript/crypto/aad.js,app/javascript/crypto/wrap.js. - Setup and password storage:
app/javascript/controllers/devise_password_sync_controller.js,app/javascript/crypto/secure_password_storage.js,app/services/kdf_security_policy.rb. - Recovery:
app/javascript/crypto/recovery.js,app/javascript/controllers/password_reset_with_e2ee_controller.js,app/controllers/api/crypto_params_controller.rb,app/controllers/passwords_controller.rb. - Sessions and integrations:
app/javascript/crypto/session_manager.js,app/javascript/controllers/session_timeout_controller.js,app/controllers/concerns/e2ee_session_management.rb,app/services/aggregator_encryption_service.rb.