Encryption and passwords
Your vault is sealed with AES-GCM and a password stretched with Argon2id. The unlock key is never extractable from the browser.
Ledgerly holds nothing of yours, so the only thing worth trusting is the code. Here is the latest full audit, a daily report on whether the protective code moved, and the parts of the wallet you can read for yourself.
No file that handles your keys, your recovery phrase or your transactions changed today. The wallet behaves exactly as it did in the last published audit.
8 of 17 checks passed as written, 6 were fixed during the audit, 3 were reviewed and accepted on purpose, and 0 remain open. One thing stays outstanding on purpose: an independent third-party penetration test. Everything here is our own review, and we say so rather than dressing it up.
Your vault is sealed with AES-GCM and a password stretched with Argon2id. The unlock key is never extractable from the browser.
Unlocked signing keys exist only inside an isolated worker. The page you look at never receives them.
A browser sweep after revealing a phrase found no copy in storage, cookies, the page, memory or any network request.
A strict content policy, HSTS, no referrers and a minimal permissions policy, all pinned by automated tests.
Locking one tab locks the others, and signing out clears the session everywhere before it navigates.
Every database table has row-level security, writes are scoped to your own profile, and the public counters cannot be forged.
| Check | Area | Result | Evidence |
|---|---|---|---|
| F1 | Table privileges | FIXED | Every table granted the full privilege set to signed-in people, and two granted it to anonymous visitors. That set includes emptying a table, which no access rule filters. Privileges are now the minimum each table needs. |
| F2 | Database web requests | FIXED | The database's built-in web request helpers were executable by anyone holding a database role, which is the classic path to making a server fetch somewhere it should not. Execution is now backend only. The daily scheduled job is unaffected. |
| F3 | Token feed ingest | FIXED | Values from the third-party token index went into the database unchecked: unbounded text, any URL scheme in the logo field, and any row shape. Every field is now shape-checked, length-bounded, and a logo must be a plain https URL. The feed response is capped and time limited. |
| F4 | Directory search | FIXED | The search box was cleaned by removing four characters. It is now an allowlist: letters, digits, space, dash and underscore, capped at 40 characters, so nothing left can carry meaning in the filter it is placed into. |
| F5 | Number handling | FIXED | The fuzzer crashed the bounds helper with an exotic value. It now returns the floor instead of throwing. |
| F6 | Scheduled job token table | FIXED | The table holding the job secret had no written access rule, only an absence of privileges. It now carries an explicit deny rule for visitors and signed-in people. |
| A1 | Sign-in surface | PASS | No anonymous sign-in, no auto-confirm. Password reset and sign-up redirects are same-origin and land on public pages. No credential or key is committed. |
| A2 | Route gates | PASS | The wallet is gated behind the unlock screen and the profile gate; no hidden admin or debug route exists. |
| A3 | Server endpoints | PASS | Both scheduled endpoints demand a secret compared in constant time and refuse an empty or wrong one. The publishable key is not accepted as a secret. |
| A4 | Access rules | PASS | Every table has row level security, an explicit privilege grant and at least one rule. Writes are scoped to the signer. Anonymous writing is limited to a zero-value metrics event that can never be read back. |
| A5 | Roles | PASS | No role is stored on a profile row. |
| A6 | Definer functions | PASS | All pin their search path and are executable only by the backend. |
| A7 | Headers | PASS | Content security policy, no-sniff, no-referrer, HSTS with subdomains, camera-only permissions, and framing restricted to our own origins. Hostile X-Forwarded-*, Origin, Referer and User-Agent values are neither reflected nor trusted. |
| A8 | Dependencies | PASS | No high or critical vulnerability in the lockfile. |
| R1 | Published reports readable by anyone | ACCEPTED | The daily code report table is public by design: it is the content of the public Security Center and the same text is in the public repository. It holds file names and hashes, never keys or user data. |
| R2 | Web request extension installed in the shared schema | ACCEPTED | Placed there by the platform and needed by the daily scheduled job. Execution has been revoked from everyone but the backend, which removes the risk the placement carries. |
| R3 | Inline scripts allowed by the policy | ACCEPTED | The server-rendered page ships an inline hydration payload, so inline scripts cannot be forbidden without breaking the app. Everything else is closed: no plugins, no external forms, no third-party frames. |
Full report: docs/security/2026-08-31-full-security-audit.md
The code that holds your keys is published: key generation, encryption, the vault format, transaction building, signing, the isolated worker and the extension shell. Every publish ships a checksum list and is tagged to a released version, so you can compare what you installed against what we published. Our servers, analytics and deployment stay private, and they never receive a key, a recovery phrase or a password.