Security

The lock, the numbers, and where it stops.

Everything here is in the repository already. This is the same account in one place, with the parameters read out of the constants the apps actually derive with — including the part that is uncomfortable to publish.

Your password does not make the key. It opens it.

When you create an account, your device generates a random 32-byte key and never sends it anywhere. Everything your library is encrypted with comes from that one key, and it is not derived from anything you chose or could be made to remember.

Your password’s job is narrower: it goes through Argon2id, on your device, to produce a second, password-derived key whose only purpose is to wrap — encrypt — a copy of the first one. That wrapped copy is what we store. Your recovery code wraps its own copy the same way, which is the whole reason a second door is possible at all: two locked boxes, one key inside each, and no key of ours anywhere.

From the unwrapped key, your device derives two more with HKDF — an Ed25519 keypair that signs you in, and the AES-256-GCM key that encrypts every file. Neither the password, the random key, nor anything derived from it is ever sent to us.

This is also why changing your password is fast and why it is not a remedy for a break-in: it re-wraps a 32-byte key and leaves your files untouched, so anything already decrypted by someone else stays decrypted. And it is why we cannot reset a password — there is no copy of the key on our side to re-wrap it with.

The parameters

These are a frozen cross-platform contract: the web app, the browser extension, and the mobile apps must derive identically or a door would not open, so they are pinned by test vectors in the repository rather than set per client. Every value below is rendered from the constant the apps use, not typed into this page.

Password KDF
Argon2id · 64 MiB · 3 passes · parallelism 1
KDF output
256-bit
Salt
SHA-256 of an app-wide constant and your username
Sub-keys
HKDF-SHA-256 · one to sign with, one to encrypt with
Files
AES-256-GCM · a fresh 96-bit IV for every file
Sign-in
Ed25519 signature · no password leaves the device
Generated password
7 words · 77 bits of entropy
Recovery code
256 bits from a CSPRNG

The salt is deterministic and public on purpose — it is derived from your username rather than stored, so two people who choose the same password still derive different keys, and there is no salt for us to hold. All of it, including the wire format of an encrypted file, is in github.com/stxapps/bracemark-monorepo.

What someone who took our server would have

Worth being specific about, because “end-to-end encrypted” is often said in a way that implies there is nothing else on the server. There is.

Every encrypted file, and its shape
One encrypted file per item you keep, each with a path, a size, and the time it last changed. That is a count of how many things you keep and a record of when you touch them — it is real, it is what a file-per-item design costs, and no amount of encryption removes it.
Your username and public key
The name you chose and the Ed25519 public key your sign-ins are checked against. The public key is a credential, not a secret; it verifies a signature and decrypts nothing.
Your sessions and your subscription row
A hashed token per signed-in device, and — if you pay — the payment provider’s identifier for your subscription. The hash cannot be turned back into a token, but neither row is encrypted with your key: we can see how many devices you are signed in on, when each was last used, and whether you pay. It is why we claim we cannot read your library, rather than that we hold nothing about you.
The wrapped keys themselves
One locked copy of your key per door — one the password opens, one the recovery code opens. They are ciphertext, and a copy of them is already handed out to anyone who asks; the section below is about what that means.

The privacy policy inventories every field we hold.

What someone who took our server would not have

A single address, title, tag, or note
All of it is inside the ciphertext, including the references between a link and its preview image. The server never held a key to any of it, so there is no key to take.
Anything to search
There is no index over your library at our end, because building one would need the plaintext to build it from. Search runs on your own devices.
A way to sign in as you
Signing in means signing a short, time-stamped message with a private key derived on your device. We hold the public half.

A legal order is a different route in, and it reaches our providers independently of us — that is answered in the FAQ.

The uncomfortable part: a weak password is attackable offline

Your device cannot unlock your key until it has the wrapped copy, and unlocking that key is what lets it prove who it is. So we hand the copy to anyone who names a username. There is no way around that in this design, and pretending otherwise would be worse than saying it.

What it means concretely: someone can fetch that box once and then guess at it on their own hardware, forever, with no request to us to rate-limit and no failed-login counter to trip. Argon2id is what makes each guess expensive — 64 MiB of memory and 3 passes per attempt, which is the difference between billions of guesses a second and a slow trickle. But a slow trickle still gets there against most passwords people invent.

Argon2id buys time; only entropy buys safety. This is the reason the app offers to generate your password and the reason we suggest taking it: 7 random words is about 77 bits, which is not a wall that takes a long time to climb — it is not a climbable wall. What decides that is where a password came from, not how it looks: one a person invented follows the patterns guessing software already models, so bringing your own is safe only if it was generated randomly too. The recovery code is generated with 256 bits for the same reason.

If our storage fails

The wrapped copies of your key are the most load-bearing rows we hold: lose them and a device that has never signed in has no way to open your library, even though the password and the recovery code are both still correct. Our storage is redundant, and redundancy is not a backup you control — the terms say so and this page is not going to say more than the terms do.

The answer that does not depend on us is the export, which is free on every plan and is a zip of one JSON line per item with your preview images beside it, decrypted and readable with nothing of ours installed. A device that has already synced also holds your whole library locally and opens without reaching us at all.

What this page does not prove

No third party has audited any of this, and we do not publish reproducible builds — so public source shows you what the code does, and not which build was served to your browser. We are not promising either one is coming. /about explains what each absence costs you.

One check does survive all that, and it needs nothing from us: your browser’s own network inspector shows every request this app makes, and your links leave as bytes nobody at our end can read. That is an observation about the build you are actually running, not a proof — a request log shows what was sent, not what a key is doing — and the account traffic beside it is readable exactly as the privacy policy says.

What holding no key does instead is remove the decisions we would otherwise be asking you to trust us with. We cannot read a link, hand one over, or reset a password, and none of that is a policy we could quietly revise.

Reporting

If you find something, tell us first

Write to security@bracemark.com rather than posting publicly. We aim to acknowledge within three working days and to tell you what we intend to do about it within ten. There is no bug bounty and we cannot offer payment; we will credit you when a report leads to a fix, unless you would rather we did not.

The scope, the reports we most want, and the handful of behaviours that are known and by design — this page’s offline-attack surface among them — are in our security policy, which /.well-known/security.txt also points at.