Security and encryption

Your portfolio is encrypted in your browser before it reaches our servers, so a full database leak would reveal nothing about your finances. It cannot protect you from a compromised server shipping modified code; that limit, and every other one, is listed below.

Primitives: XChaCha20-Poly1305 for encryption, Argon2id for password-based key derivation, both via libsodium.

Third-party audit: not yet. Planned once revenue can fund it.

How your keys fit together

Key hierarchy: your password and optional recovery code derive keys in your browser that unlock a random data key; the data key encrypts your portfolio; only ciphertext, a nonce, a salt and wrapped keys reach the server.In your browserOn our serversArgon2idArgon2idunwrapsor unwrapsfetched at sign-inencryptsXChaCha20-Poly1305Your passwordnever sentRecovery code24 words, opt-inKEKmemory onlyRecovery KEKmemory onlyData keywiped on sign-outYour portfolioreadableSalt and wrapped keysuseless without youCiphertext + noncebound to your account
Emerald outlines mark what you know or own. Everything below the dashed line is all our servers ever store.
  1. You type your password. Argon2id turns it and a salt into a key encryption key (KEK) inside your browser. The password itself never leaves the page.
  2. The KEK unwraps your data key, a random 256-bit key the server only ever stores in wrapped form. If you set up a recovery code, it unwraps a second wrapped copy.
  3. The data key encrypts your portfolio with XChaCha20-Poly1305, a fresh random nonce, and authenticated data bound to your account ID.
  4. Only ciphertext, the nonce, the salt and the wrapped keys reach our servers. The KEK and data key live in memory and are wiped on sign-out, tab close and idle timeout.

Sharing a portfolio

On the Family plan you can share a portfolio with one partner. Your personal portfolio is never shared: it stays under your own data key. Each portfolio you create besides it has its own random 256-bit key, stored only wrapped under the data key of each person in it.

  1. You create an invite. Your browser makes a random secret, wraps the portfolio key with it, and stores only the wrapped copy. The secret goes into the invite link after the # sign, the part of a link that browsers don't send to servers.
  2. You send the link to your partner yourself, by message or email. Quantive never sends it and never sees the secret.
  3. Your partner opens it while signed in with the email address you entered. Their browser unwraps the portfolio key with the secret and wraps it again under their own data key; the invite's copy is then deleted. An invite works once, within 7 days.
  4. If you remove your partner, or they leave, your browser encrypts the portfolio under a new key, so nothing saved afterwards can be read with the old one.

The full design, including why the key travels in a link rather than through public-key encryption, is in sections 5 to 9 of the encryption design document.

What we defend against

  • Database leaks. Our hosting provider (Supabase) only sees ciphertext. Anyone who steals a backup or the database itself sees ciphertext.
  • Hosting provider read access. Supabase staff cannot read your data, even with full database access.
  • Subpoenas of stored data. We can comply by handing over the encrypted blob, but the blob alone reveals nothing without your password.
  • Cross-user attacks. Your personal portfolio is cryptographically bound to your user ID, and a shared portfolio to its own ID. Even with full database write access, an attacker cannot move one user's data into another user's account, or one portfolio's into another, without it failing to decrypt.
  • A stolen or lost device, after sign-out. Your encryption key lives only in browser memory and is wiped on sign-out, tab close and idle timeout.
  • Automated attacks on the login. Sign-up and sign-in are rate-limited and gated by a CAPTCHA (Cloudflare Turnstile), which blunts bot sign-ups and credential-stuffing attempts.

What we do not protect against

These are real limits. We list them so you can weigh the protections above against them.

  • An actively malicious server. Every web app, including this one, downloads JavaScript from a server on every visit. A compromised server could ship modified code that exfiltrates your password as you type. Bitwarden, Proton Mail and Standard Notes share this limit, as does every web-based end-to-end encrypted service. Our first-party code ships as hashed, immutable assets, so replacing the bytes requires a deploy by us. Signed builds and a native client remain the stronger mitigation. Subresource integrity is not applied to our third-party origins because the vendors do not publish per-version content hashes, and a pinned hash would break on the next silent rotation.
  • A compromised device. Malware, keyloggers and malicious browser extensions run with your privileges. No application can defend its own user from this.
  • Metadata. We can see that you have an account, your email, when you saved data, and roughly how big your portfolio is. If you share a portfolio, we also see who shares it with whom and the email address an invite was sent to. We can't see what's in any of them.
  • The partner you share with. They can read, edit and export a shared portfolio, and keep copies of what they've seen. Removing them stops them reading later changes, not what they've already seen. Until it's used, an invite link is a key: send it privately. A leaked link alone doesn't let someone join, because it works only for the account it was addressed to.
  • A forgotten password without a recovery code. If you forget your password and skipped the recovery code, your encrypted data is permanently unrecoverable. Nobody else holds a key that can decrypt it, including us.
  • Coercion. If someone forces you to disclose your password, they disclose everything.
  • A supply-chain attack on our dependencies. We pin lockfiles and review updates, but we are not immune.

Forgotten passwords and recovery codes

Because we cannot read your data, we cannot reset it for you. If you forget your password, the only way back in is a 24-word recovery code, if you set one up.

Setting up a recovery code is opt-in, and we strongly recommend it. The code is a BIP-39 mnemonic with 256 bits of entropy. We display it once and never store it (only a wrapping derived from it). Save it somewhere offline: a printed copy, a safe or a password manager. You can set up or rotate it from Settings, under Security.

A note on password resetResetting your password through the email flow changes your account password, but it cannot rewrap your existing encrypted data: only your old password or your recovery code can do that. If you reset your password and have a recovery code, we'll ask for it on your next sign-in to restore access. If you reset your password and skipped the recovery code, your previously encrypted snapshots become permanently unrecoverable.

Verify it yourself

The design and the implementation are open to inspection.

Encryption design document
Threat model, primitive choices, key hierarchy, AAD framing, schema and migration plan.
Crypto source code (MIT)
Small, pure functions with no I/O. Tests cover round-trip, tamper detection, AAD binding and cross-user isolation.
Repository
The full source. File an issue if you find a problem.

Disclosure and contact

If you find a security issue, please disclose it responsibly and do not open a public issue. Email [email protected] or use a GitHub private advisory.

Accessibility

Quantive aims to follow WCAG 2.1 AA. As a micro-business under EU criteria we are not subject to formal conformance reporting, but we treat accessibility as a baseline quality bar. If you run into an issue, email [email protected].

Try the app with illustrative data, or start your own record.