Security
Every password manager says it takes security seriously. This page gives you the specifics instead: the exact algorithms, the exact parameters, and a complete account of what we hold. Judge KalmPass on what it does rather than on what it claims.
Your vault is encrypted in your browser before anything is sent. The key comes from your master password, which never leaves your device. We store ciphertext, and we store it encrypted a second time under a key kept outside the database. There is no mechanism by which KalmPass can read your items, and no support process that can restore them for you.
What protects your data
1. Turning a password into a key
Your master password is stretched with PBKDF2-HMAC-SHA256 at 1,000,000 iterations, salted with your email address. OWASP's current floor for PBKDF2-SHA256 is 600,000; we sit comfortably above it. Salting with the address means two people who choose the same password still derive different keys, and rules out precomputed tables.
The result is split by HKDF-SHA256 into two independent branches: an encryption key that stays in the browser tab, and an auth key that is sent to the server to prove who you are. Because HKDF outputs for different labels are independent, holding the auth key reveals nothing about the encryption key. A fully compromised server still cannot decrypt a single item.
2. Encrypting the vault
Items are encrypted with AES-256-GCM under a random vault key, with a fresh 96-bit IV drawn from the CSPRNG for every single encryption. Item names, usernames, URLs and notes are all inside the encrypted blob, so the database cannot be mined for which sites you hold credentials for. Plaintext is padded to a 256-byte boundary first, so the stored length does not disclose how long a note or a password is.
The vault key itself is random and stable. Your master password wraps it rather than encrypting items directly, which is why changing your password takes a moment instead of re-encrypting your whole vault, and why your Recovery Key keeps working afterwards.
3. A second layer on our side
Everything the client sends is encrypted again by our server under a key derived from a Cloudflare Worker secret that is never stored in the database. The ciphertext is bound to its row with AES-GCM additional authenticated data, so a blob cannot be lifted out of one record and replayed into another.
This layer is not what keeps us out. The client layer already does that. It is there so that a leaked database backup, a stolen storage credential, or a subpoena served on the storage layer alone yields nothing readable at all.
4. Even the email address
Account addresses are stored as a keyed HMAC blind index for lookup, plus an encrypted copy. The database holds no readable identifier, so it cannot be mined for a customer list or matched against any other list.
Signing in
| Password verifier |
The server stores HMAC(pepper, PBKDF2(auth_key, salt, 100k)). The pepper
lives in a Worker secret, not the database, so a stolen database cannot be cracked
offline at all.
|
|---|---|
| Sessions | 256-bit random tokens in an HttpOnly, Secure, SameSite=Strict cookie. Only the HMAC of the token is stored, so the session table cannot be used to mint a login. Sessions expire after 30 minutes idle and 12 hours absolute, whichever comes first. |
| Two-factor | Optional TOTP (RFC 6238) on the login itself, verified in constant time across the accepted clock-drift window. Ten single-use backup codes are issued, stored only as peppered hashes. |
| Rate limiting | Failures are counted by email and by IP separately, with exponential lockout, so a flood from one address cannot lock a different account out. Counters are keyed by blind index. |
| Enumeration | Login spends identical work whether or not the address exists, and always returns the same error. Account reset requests always answer the same way. |
| CSRF | SameSite=Strict on the session cookie, plus a strict Origin check on every state-changing request. |
| The page itself |
A Content-Security-Policy with no inline script and no external origins at all, plus
HSTS, X-Frame-Options: DENY, and no-store on everything. The
app loads nothing from a CDN, because a CDN is a place someone else can change your
crypto.
|
Passkeys
A passkey can open your vault instead of your master password, without weakening any of the above. It works through the WebAuthn PRF extension: an authenticator will produce a deterministic secret for a given site, and only after you have proved yourself to the device with a fingerprint, a face or a PIN.
That secret is treated exactly as the Recovery Key is. Your browser splits it into an encryption branch, which wraps your vault key, and an auth branch, whose hash is all we store. So registering a passkey adds another wrapped copy of a key we still cannot open, and it is phishing resistant for free, because the credential is bound to this site by the platform and a lookalike cannot coax the same secret out of your device.
One honesty note. We do not verify a WebAuthn signature: the passkey is used for the secret it derives, not as a signing credential. That means the server cannot tell a genuine authenticator from somebody who has obtained that secret another way. It is the same position as with the Recovery Key, and acceptable for the same reason: the secret never leaves your device in a form anyone else can use.
The extension and the phone apps
The browser extension decrypts inside the extension, with the same code as this site. Out of the box it can reach kalmpass.net and nothing else, so it runs nothing on the pages you visit until you press Fill. Before filling, it checks the page is the site the login was saved for and that the connection is encrypted, and asks you first if either is not. If you turn on save offers, a small script watches login forms so it can offer to save what you type. A page can use that script to suggest a login for itself and for nothing else: it cannot ask the extension for a password or a list of your logins.
The iPhone and Android apps are this web app, packaged, so the encryption is identical. Unlocking with Face ID, Touch ID or a fingerprint works like a passkey. The app makes a random key, registers it with us the way a passkey is registered, and gives it to your phone's secure hardware to keep. The phone releases it only after a biometric check, every time, and destroys it if a new face or fingerprint is added to the phone.
Filling passwords into other apps uses a copy of your vault on the phone, encrypted exactly as we store it, and opened only after that same check. On iPhone, the name of each site and its username are given to iOS so it can suggest them above the keyboard. Passwords are not.
Recovery, honestly
Zero-knowledge encryption and "email me a password reset" cannot both be true. If a server can restore your vault from an email link, then that server can read your vault, and so can anyone who breaks into it.
KalmPass resolves this the way an emergency kit does. At signup you are issued a Recovery Key: 125 bits of randomness, printed in Crockford base32 so it cannot be misread. Your vault key is wrapped under it as well as under your master password. Forget the password, use the key, set a new one. Nothing is lost, and we still never held anything that could open your vault.
Using the Recovery Key issues a fresh one, because the old one has now been typed into a browser. You can also replace it at any time from Settings.
There is nothing anyone can do. We can clear the account so the address can be used again, but the data is unrecoverable by us, by you, by anyone. This is the price of the guarantee, and we would rather state it plainly than bury it.
What we can see
| Item contents | No. Encrypted in your browser under a key we do not have. |
|---|---|
| Item names, URLs, usernames | No. They are inside the same encrypted blob. |
| How many items you have | Yes. Row counts are visible. Sizes are padded, so lengths are not. |
| Your email address | Only by decrypting it with the Worker secret, which we do to send you mail. It is not readable from the database alone. |
| When you sign in, and from what kind of device | Yes. It is in your own activity log, encrypted at rest. |
| Your master password or Recovery Key | Never. Neither is transmitted, in any form, ever. |
Outbound requests
In normal use KalmPass makes no request to anywhere but its own origin. The single exception is the exposed password check, which you have to press a button to run: your browser SHA-1s a password locally and sends only the first five hex characters to Have I Been Pwned, which returns every hash sharing that prefix. The match is found on your device. We proxy the request so HIBP does not see your IP either.
What this does not protect against
Being straight about the limits is part of the job:
- A compromised device. Malware on your machine, or a browser extension with page access, can read your vault while it is unlocked. Nothing server-side can prevent that.
- A weak master password. A million PBKDF2 rounds multiplies an attacker's cost; it does not rescue a password that was guessable to begin with. Use a passphrase.
- A malicious server operator. We cannot decrypt what you have already sent, but in principle we could serve you altered JavaScript in future. That risk is inherent to every web-delivered password manager. It is why KalmPass ships no third-party code whatsoever: no CDN, no analytics, no framework, and no dependency that could be compromised to change what runs in your browser. It is also why the source is published under the AGPL, so the page you are served can be read against the code it came from.
- Losing both keys. Covered above. Keep a backup.
Reporting a problem
If you find a vulnerability, please email security@kalmpass.net before disclosing it publicly. We will confirm receipt, keep you updated, and credit you if you would like that. We will not threaten anyone who reports a problem in good faith.
Read it yourself
Everything described on this page is in the public repository, under the AGPL. The client is served unminified, so you can also just open your browser’s view-source and compare the two. None of this asks you to take our word for anything.
Last reviewed 15 September 2026.