onbalance security whitepaper
Introduction
Hello!
When building onbalance, our goal was to guarantee the confidentiality of users' financial data without sacrificing usability, multi-device access, or collaboration within an organization.
Using approaches proven in password managers and crypto wallets, we designed a system where confidentiality is an inherent property of the architecture, rather than a promise that depends on flawless security operations.
We're glad you're interested in how onbalance's confidentiality guarantees work! In this document, we've described the security architecture and cryptographic model of the mobile application and server. This document uses technical language and may require familiarity with specialized terminology. However, for specialists familiar with the subject, it should provide comprehensive answers and, we hope, help build trust in our application.
The system combines proven modern algorithms (Argon2id, AES-256-GCM, ECDH/ECDSA P-256) and a strict distributed key model that minimizes trust in the server. Our goal is to provide end-to-end encryption of user data, autonomous operation without constant server connection, and the ability to securely recover access without third-party involvement.
We hope this material will be useful or, at the very least, interesting to you.
Core Principles
-
1
End-to-End Encryption
The organization data (accounts, transactions, projects, counterparties, and categories) is encrypted locally using a symmetric key (Organization Key). The key never leaves the device in plaintext. -
2
Zero-Knowledge Server
The server performs only transport and synchronization roles. It stores:
- user public keys,
- salts and challenge values for authentication,
- encrypted data and key containers.
The master password, private keys, and recovery phrases are never transmitted to the server.
-
3
Offline-First
All data operations are performed locally. The server is only used to transmit encrypted changes (diffs) between user devices. Without a connection, the application remains fully functional. -
4
Controlled Synchronization
Users can synchronize data between their own devices. In the future, we plan to enable collaboration with other users through encrypted Organization Key exchange. -
5
Key Isolation
Each key has a strictly limited scope of use. Compromise of one key pair does not reveal others.
Where is Organization Data Stored?
All financial data is grouped into organizations — independent containers with their own encryption key (Organization Key). The Organization Key is randomly generated when creating an organization and never leaves the device in plaintext.
Data is stored:
- On the server as modifications encrypted with the Organization Key.
- In local application databases, encrypted at the file level with the Organization Key.
Types of Keys and Passwords
Master Password
The main user password — what the user enters in the application when logging in. The foundation for authorization and end-to-end encryption.
Type: string with minimum complexity requirements.
Used to generate Master Key and Auth Keys.
Created and entered by the user during registration or when changing it.
Stored only by the user (memorized or in a password manager). Not transmitted to the server.
Master Key
Type: symmetric AES-256 key.
Used to encrypt Account Private Key.
Generated on the device deterministically from Master Password using Argon2id (salt — user id).
Not stored. Generated in memory only when the user enters the master password: during registration, login, or master password change.
Not transmitted to the server.
Auth Keys (Auth Public Key + Auth Private Key)
Authentication key.
Type: asymmetric Elliptic Curve prime256v1 key. Public/private pair.
Used for passwordless authentication: the client signs a server-provided challenge with this key, and the server verifies it using the public part.
Generated on the device deterministically from Master Password using Argon2id (random salt), passed through HMAC.
Auth Public Key is sent to the server and stored in the server database. The salt used during generation is also sent to the server and stored.
Auth Private Key is not transmitted to the server and not stored.
Account Keys (Account Public Key + Account Private Key)
Account key.
Type: asymmetric Elliptic Curve prime256v1 key. Public/private pair.
Used to encrypt Organization Key.
Each user with access to an organization has their own copy of the Organization Key, encrypted using their Account Keys. The symmetric key derived from Account Keys is also used to encrypt local user data about organizations (app.sqlite).
Generated on the device randomly during user registration.
For authorized users, the Account Public Key / Account Private Key pair is stored in Secure Storage along with the session token. It is deleted along with the session upon logout. Secure Storage uses Keychain on iOS and AES+KeyStore on Android.
Account Public Key is sent to the server and stored on the server. Account Private Key is encrypted in two forms: with Master Key and with Recovery Key. Both encrypted copies are sent to the server and stored in encrypted form.
Organization Key
Organization key.
Type: symmetric AES-256 key.
Used for end-to-end encryption of organization data. Also used to encrypt local organization data on the device (sync.sqlite).
Generated on the device randomly when creating an organization.
Not transmitted to the server in plaintext.
Stored in centralized storage on the server in encrypted form for each user with access to the organization: each user has a collection of organization keys encrypted with their Account Keys.
On the device, stored in the app.sqlite database (separate database for each user on the device) in records about the user's organizations. The database is encrypted at the file level (not records) with a symmetric key derived from Account Keys.
For each user's organization, a separate database is created locally — sync.sqlite. It stores all data for the organization, including all organization entities and their modifications for the synchronization mechanism. The database is encrypted at the file level (not at the record level) using the Organization Key.
Recovery Phrase
Secret phrase issued to the user during registration that can be used to recover access.
Type: 12-word string based on BIP39 mnemonic.
Used to generate Recovery Key and Recovery Auth Keys.
Generated randomly. Issued to the user for safekeeping.
Stored by the user, outside the application. Not transmitted to the server.
Recovery Key
Type: symmetric AES-256 key.
Used for backup encryption of Account Private Key for access recovery.
Generated on the device deterministically from Recovery Phrase.
Not stored. Not transmitted to the server.
Recovery Auth Keys (Recovery Public Key + Recovery Private Key)
Type: asymmetric Elliptic Curve prime256v1 key. Public/private pair.
Used for authentication during access recovery: the client signs a server-provided challenge with this key, and the server verifies it using the public part.
Generated on the device deterministically from Recovery Phrase.
Recovery Public Key is sent to the server and stored.
Recovery Private Key is not transmitted to the server and not stored.
Algorithms and Parameters
| Component | Algorithm | Parameters |
|---|---|---|
| KDF | Argon2id | Memory: 64MB, Iter: 3, Par: 2 |
| Symmetric encryption | AES-256-GCM | IV: 12 bytes |
| Asymmetric encryption | ECDH (P-256) | AES-GCM for data |
| Signatures | ECDSA (P-256) | SHA-256 |
Scenarios
Registration
User enters their name and email.
Application sends to the server: email.
Server sends verificationCode to email.
Server returns: verificationKey.
User enters the received code.
Application sends to the server: email, verificationKey,
verificationCode.
Server returns: verificationToken.
User creates and enters master password.
Application randomly generates salt authSalt.
Application generates Auth Keys from master password and authSalt.
Application randomly generates Account Keys.
Application randomly generates userId (UUID).
Application generates Master Key from master password and userId as
salt.
Application encrypts Account Private Key with Master Key, and gets
protectedAccountPrivateKey.
Application randomly generates deviceId (UUID).
Application generates random recovery phrase (Recovery Phrase).
Application generates Recovery Key and Recovery Auth Keys from Recovery Phrase.
Application encrypts Account Private Key with Recovery Key, and gets
recoveryProtectedAccountPrivateKey.
Application sends to the server: email, name,
userId, deviceId, verificationToken,
authSalt, authPublicKey, accountPublicKey,
protectedAccountPrivateKey, recoveryAuthPublicKey,
recoveryProtectedAccountPrivateKey.
Server saves authSalt, authPublicKey,
accountPublicKey, protectedAccountPrivateKey,
recoveryAuthPublicKey, recoveryProtectedAccountPrivateKey
for the user.
Server creates session.
Server returns: userId, accessToken,
expiresAt.
Application saves session (accessToken, Account Keys, etc.) in local
Secure Storage.
User is registered and authorized.
Recovery phrase is created, displayed to the user with prompt to save it.
Login
User enters email.
Application sends to the server: email.
Server sends verificationCode to email.
Server returns: verificationKey.
User enters the received code.
Application sends to the server: email, verificationKey,
verificationCode.
Server generates random challenge, saves it, and returns:
verificationToken, authSalt, challenge,
challengeSessionToken.
User enters master password.
Application generates Auth Keys from master password and authSalt.
Application signs challenge with Auth Keys, and gets
signature.
Application generates random deviceId (UUID).
Application sends to the server: email, verificationToken,
signature, challengeSessionToken, deviceId.
Server verifies signature using original challenge and the
user's Auth Public Key. If signature is invalid, returns error.
Server creates session.
Server returns: userId, accountPublicKey,
protectedAccountPrivateKey, accessToken,
expiresAt.
Application generates Master Key from master password (still in memory since login
is ongoing) and userId as salt.
Application decrypts protectedAccountPrivateKey with Master Key, and
gets Account Private Key.
Application saves session (accessToken, Account Keys, etc.) in local
Secure Storage.
User is authorized.
Master Password Change
Scenario is launched from user profile, meaning they must already be authorized.
User enters current master password and new master password.
Application sends request to the server to change master password.
Server returns: authSalt, challenge,
challengeSessionToken.
Application generates Auth Keys from current master password and
authSalt.
Application signs challenge with Auth Keys, and gets
signature.
Application randomly generates new salt newAuthSalt.
Application generates new Auth Keys from new master password and
newAuthSalt.
Application retrieves current Account Keys from Secure Storage. They don't change when password changes.
Application generates new Master Key from new master password and
userId as salt.
Application encrypts Account Private Key with new Master Key, and gets
newProtectedAccountPrivateKey.
Application sends to the server: signature,
challengeSessionToken, newAuthSalt,
newAuthPublicKey, newAccountPublicKey,
newProtectedAccountPrivateKey.
Server verifies signature using original challenge and the
user's previous Auth Public Key. If signature is invalid, returns
error.
Server saves newAuthSalt, newAuthPublicKey,
newAccountPublicKey, newProtectedAccountPrivateKey for the
user.
Master password is changed.
Other sessions are logged out.
Creating New Recovery Phrase
Scenario is launched from the profile at the user's initiative and requires entering the current master password. User is authorized.
Application sends request to the server to change recovery phrase.
Server returns: authSalt, challenge,
challengeSessionToken.
Application generates Auth Keys from master password and authSalt.
Application signs challenge with Auth Keys, and gets
signature.
Application generates random recovery phrase (Recovery Phrase).
Application generates Recovery Key and Recovery Auth Keys from Recovery Phrase.
Application encrypts Account Private Key with Recovery Key, and gets
recoveryProtectedAccountPrivateKey.
Application sends to the server: signature,
challengeSessionToken, recoveryAuthPublicKey,
recoveryProtectedAccountPrivateKey.
Server verifies signature using original challenge and the
user's Auth Public Key. If signature is invalid, returns error.
Server saves recoveryAuthPublicKey and
recoveryProtectedAccountPrivateKey for the user.
Recovery phrase is changed, displayed to the user with prompt to save it.
Access Recovery
Scenario is launched from master password entry screen (Recover Master Password button), with email already verified in the previous step.
From the user's perspective, recovery consists of two stages.
Step 1: Recovery Phrase Verification
User enters recovery phrase (Recovery Phrase).
Application generates Recovery Auth Keys from Recovery Phrase.
Application signs challenge received at code verification stage with
Recovery Auth Keys, and gets signature.
Application sends to the server: email,
verificationToken (received at code verification stage),
signature, challengeSessionToken (received at code
verification stage).
Server verifies signature using original challenge and the
user's Recovery Auth Public Key. If signature is invalid, returns
error.
Server returns: userId, recoveryToken,
recoveryProtectedAccountPrivateKey.
Application generates Recovery Key from Recovery Phrase.
Application decrypts recoveryProtectedAccountPrivateKey with Recovery
Key, and gets Account Private Key.
Stage result — obtained userId, recoveryToken (for
entering stage 2), and Account Keys.
Step 2: Setting New Master Password
User enters new master password they want to set.
Application randomly generates new salt newAuthSalt.
Application generates new Auth Keys from new master password and
newAuthSalt.
Application generates new Master Key from new master password and
userId as salt.
Application encrypts Account Private Key with new Master Key, and gets
newProtectedAccountPrivateKey.
Application randomly generates deviceId (UUID).
Application sends to the server: userId, recoveryToken,
deviceId, newAuthSalt, newAuthPublicKey,
newAccountPublicKey, newProtectedAccountPrivateKey.
Server saves newAuthSalt, newAuthPublicKey,
newAccountPublicKey, newProtectedAccountPrivateKey for the
user.
Server creates session.
Server returns: accessToken, expiresAt.
Application saves session (accessToken, Account Keys, etc.) in local
Secure Storage.
Master password is changed to a new one, user is authorized.
Other sessions are logged out.
Security and Trust
- All keys are created on the device.
- The server never knows the master password, private keys, or symmetric keys.
- All organization data, including change history, is encrypted end-to-end.
- Even if the server is compromised, an attacker cannot decrypt organization data without the master password.
Conclusion
The application architecture is built on the principle of minimal cryptographic trust assumptions. All encryption is performed locally, and the server serves only as a transport layer. This approach ensures a high level of security, autonomy, and user data confidentiality, making the system resilient even to complete compromise of the server infrastructure.