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.

— The onbalance Team

Core Principles

  1. 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. 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. 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. 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. 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

Algorithms and parameters used in onbalance.
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.

Ready to put this
into action?
Download the app
today & start your
financial journey

onbalance app on iPhone