There is no password reset
A "forgot password" link is a statement about who holds the key. An app that encrypts on the client cannot offer one - so recovery has to be designed at the moment the password is chosen, not at the moment it is lost.
Every account you have ever made came with a way back in. Type your address, open a mail, choose a new password, carry on. The link is so ordinary that its absence reads as a missing feature.
It is not a feature. It is a consequence. A reset link can only exist because somebody other than you can already read your data, and is willing to hand it back once you have proved who you are. Move the key to the user and the link stops being possible, in the same breath and for the same reason that the breach stops being possible.
What the link actually proves
In a server-backed app, the password authenticates. It does not encrypt. The rows sit in a database the vendor can read - encrypted at rest, usually, under a key the vendor also holds, which protects against a stolen disk and against nothing else. Resetting is therefore easy: prove your identity by another channel, and the server, which never lost access, gives it back.
Client-side encryption changes the job of the password. Now it derives the key that turns the bytes into data, and it does so on the device. Nobody else has that key. There is no second channel that can produce it, no support ticket that can, and no amount of proving your identity that helps, because identity was never what the key was checking.
selfstore states it as a limit rather than a footnote: password recovery does not exist, and losing the password loses the encrypted backup. That is a promise, not a gap. Any library making the opposite claim is telling you where the key is.
Design the recovery before the password screen
The mistake is treating recovery as an error path. By the time someone is locked out, every option has already been decided. The decisions all happen earlier, and there is one structural choice that makes the rest possible.
Encrypt the data under a random key, not under the password. The corpus gets a data key that no human ever sees. The password derives a second key whose only job is to seal that data key - a few hundred bytes. This is key wrapping, and it is what lets an app have more than one way in: each unlocker seals its own copy of the same data key.
The alternative, deriving the corpus key straight from the password, quietly fixes the number of unlockers at one and makes every change a full re-encrypt. Apps that get this wrong do not discover it while the data is small.
With wrapping in place, three unlockers are worth offering, and they fail in usefully different ways:
- A typed password. Portable, works on any device, survives a lost phone, and is the one people forget.
- A recovery code. Generated rather than chosen, so it carries real entropy, shown once, meant for paper or a password manager. It is the only unlocker that is immune to being forgotten, because nobody tries to remember it.
- A device credential. A platform passkey carrying the WebAuthn PRF extension yields a stable secret from the authenticator, so unlocking becomes a fingerprint. It is bound to one browser profile on one machine, which is the point and also the limit: it carries the password rather than replacing it, and the password field stays on the screen. An unlock that cannot fall back is a lockout waiting for the day the authenticator says no.
Granting a device its own independent key is a real trade, not a free convenience: it widens what a stolen, unlocked laptop gives away. Worth doing deliberately, in that light, rather than because the fingerprint prompt looked nice.
Rotation is forward-looking, and files remember
Changing the password re-seals the data key on the copy you are holding. It does nothing to the copies you are not holding.
This matters most for backups, because a backup is a snapshot with its own key
state baked in. A file written last March opens with last March’s password
forever, wherever it currently sits - a USB stick, a cloud drive, a mail
attachment. Rotating after a scare does not reach it. Some libraries let you
re-key a file in place; selfstore exposes changePassword(blob, { from, to })
for exactly that, and it is per file by nature, which is the honest shape for
the operation.
So the sentence a settings screen owes the user is longer than “password changed”. It is: new backups use the new password, files you already saved still open with the old one. If those old files matter, re-key them or delete them.
What the interface owes the user
- Say it before, in words, not in the error. “If you lose this password we cannot recover your data” belongs next to the field where the password is chosen. Discovering it at the failed unlock is discovering it too late.
- Get the recovery code out of the app while the stakes are low. Day one, before there is anything to lose, is the only moment a person will actually write it down.
- Make the second unlock happen early. A password typed once at signup and never again is a password that was never really learned. An app that requires it once more within the first week finds the problem while the data is still trivial.
- Skip password hints. They leak entropy to an attacker holding the file and reassure exactly the wrong person.
- Never transmit it. No mail, no telemetry, no crash report. A password that leaves the device has left the model.
The recovery that is not one
There is a design that looks like the best of both: the vendor escrows a copy of the key so support can help when things go wrong. It works. It is also, in precise terms, the server-side custody you left, with more moving parts.
One question settles which model an app is really running. If a court order arrived tomorrow, could the vendor produce readable data? If yes, the reset link is not a courtesy - it is a description of who holds the key, and any “zero-knowledge” wording on the marketing page is wrong.
An app that cannot reset your password is telling you something true. The cost is real, and it is paid by the small number of people who will lose data that nobody could have given back. The job is not to pretend that cost away. It is to make the loss require several unlikely things at once - a forgotten password, and a lost recovery code, and no enrolled device - instead of one forgotten string.