Security & Encryption
Last updated: July 18, 2026
MyFreedom is built around one promise: we don’t keep your financial data in any form we can read. Your holdings, their values, currencies, and notes are encrypted on your own device before they’re ever saved, with a key that only you hold. The person who runs the servers — that’s the founder, and there is no “admin override” — sees only scrambled blobs.
This page explains that in two passes: first in plain English, then in full technical detail for people who want to check the claim rather than take our word for it.
Part 1 · In plain English
Your numbers are locked before they leave your device. When you add or import a holding, your browser scrambles every sensitive field — the name, the amount, the currency, any notes — using a key that lives only on your device. Only the scrambled version is sent to us. What sits in our database is unreadable gibberish.
The key is yours, and only yours. It’s unlocked by your passkey — the Face ID, Touch ID, or security key you already use to sign in — which never leaves your device. As a backup, your browser generates a one-time recovery code and shows it to you once; we never receive it. If you lose both your passkey and your recovery code, not even we can get your data back. That isn’t a limitation we forgot to fix — it’s the whole point.
So what does “we can’t read it” actually rule out? A curious or careless employee, a stolen database backup, a breached server, or a legal demand served on us — none of them can turn your stored data back into readable numbers, because the key that would unlock it was never ours to hand over.
What this does not cover (the honest part)
A security page that only lists strengths isn’t trustworthy. Here’s what this design deliberately does not hide:
- Generic category names stay readable. Labels like “Stocks” or “Cash” are shared, built-in categories — not your data — so they aren’t encrypted. Any custom category you name yourself is encrypted.
- We can see shape, not substance. We can tell how many entries an account has and the dates you recorded them — but not the names, amounts, or currencies.
- You’re trusting the code we serve. Because the encryption happens in your browser, a dishonest operator could in principle ship bad code. This design binds our hands against quietly reading the database at rest; it can’t protect you from a targeted attack you’d have to detect. We keep the mechanism public so it stays checkable.
- Sharing means sharing. If you invite a partner to a portfolio, you grant their device a copy of that account’s key, so they can read it — by design. Remove them and rotate the key and a cutoff is enforced cryptographically, not just by a setting.
- We don’t email you your net worth. A server that could email your total would be a server that can read your total — so that feature is turned off.
Part 2 · For the technically curious
The client-side crypto is standard Web Crypto (no home-rolled ciphers). Here is the whole scheme.
Record encryption
- Every sensitive record is sealed with
AES-256-GCMunder a fresh 96-bit random IV, stored asv1.<iv>.<ciphertext>(base64). GCM gives confidentiality and integrity, so tampering is detected on decrypt. - A holding folds name, currency, symbol, notes, tags into one blob; a valuation folds value, currency, quantity, unit price; a portfolio or custom category holds its name. One opaque column per row, nothing else.
Key hierarchy
- Account Data Key (ADK) — a per-account
AES-256-GCMkey that encrypts every record in that account. - Identity keypair — each user has an
ECDH P-256keypair. The ADK is wrapped to your public key (ephemeralECDH→HKDF-SHA256→AES-GCM), so sharing an account is just wrapping the same ADK to a partner’s public key — no plaintext key ever moves. - Personal KEK — your private key is itself wrapped by a key-encryption key derived with
HKDF-SHA256from your passkey’sWebAuthn PRFoutput. The PRF secret lives in your authenticator and is never transmitted; PRF support is required (there is no weaker fallback path). - Recovery — a 160-bit code (Crockford base32) generated in the browser, shown once, and never sent to the server. It independently unwraps the same private key. A server-generated code would let us decrypt, so we don’t generate it.
What the server stores
Only opaque material it cannot interpret: your public key (SPKI), your wrapped private key, the wrapped-ADK shares, and per-record ciphertext. All aggregation, FX conversion, sorting, and net-worth math runs in your browser after decryption — the server never does value arithmetic.
What is deliberately not encrypted
as_of_dateon a valuation — it leaks timing, not amounts, and enables on-or-before lookups without decryption.- Structural foreign keys and row ids — needed to relate records.
- System category labels (account-less, no ADK) — shared, generic, not user data.
No plaintext columns exist
During the rollout the database briefly kept the old plaintext columns alongside the ciphertext (a dual-read window). Those columns — holding.name, currency, notes, valuation.value_amount, and the rest — have been physically dropped from the schema (migration 0008). There is no longer a column anywhere that could hold a readable value.
Import is client-side too
A CSV import is parsed and encrypted entirely in your browser; only ciphertext is sent, as one atomic transaction. There is no server-side import path that could see your values — it was removed.
Honest limitations
- Browser-delivered E2E can’t defend against a malicious operator serving compromised JavaScript. This bounds passive access to data at rest, not an active targeted attack.
- Access roles (viewer vs. editor) are enforced by the server, not by cryptography; a true cutoff on removal comes from key rotation.
- There’s no password KDF (PBKDF2/argon2) because the root secret is high-entropy PRF output or a random recovery code, not a human password.
Underneath
On top of the encryption, every tenant is isolated by Postgres row-level security (enabled and forced, deny-by-default), the web server runs as a non-superuser role that cannot bypass it, and OAuth tokens are stripped after sign-in. Questions? Write to support@myfreedom.money.