How Hush works
What happens to a password when you save it, what the server's database really holds, and how the apps keep it safe on your devices.
October 2026 · web, Android and the hush CLIIn one sentence: everything is locked on your device before it leaves it, with a key made from your master password, and the master password never leaves the device. The server keeps locked boxes it has no key for.
- 01The pieces
- 02From master password to keys
- 03What happens when you…
- 04What the server database holds
- 05If the database leaked
- 06The Android app
- 07The web app
- 08The server
- 09Backups and break-glass
- 10Honest limits
All cryptography is libsodium; nothing is hand-rolled. Hush has not had an external audit.
The pieces
The apps on your devices do all the encryption. The server only stores and returns what they send.
| Piece | What it does | Holds keys? |
|---|---|---|
| Web app | A static site. All crypto runs in a Web Worker, apart from the page. | Yes, in memory while unlocked |
| Android app | React Native with native libsodium. Same algorithms and byte format as the web, so both open the same vault. | Yes, in memory while unlocked |
hush CLI | Terminal client: read secrets, run commands with them, open backups offline. | Only while a command runs |
| API | Sign-up, sign-in, sessions, rate limits, storing and returning encrypted items. | Never |
| Database | MongoDB for accounts and items, Redis for sessions and rate limits. | Never |
From master password to keys
Hush uses three keys, and each has one job. Only one of them, the auth key, is ever sent to the server, and it can't decrypt anything.
Changing the master password only re-wraps the one vault key. Items don't have to be re-encrypted, and every device keeps working with the same items.
Signing in needs something sent to the server. The auth key comes from the same password, but it is a different subkey, so knowing it gives the server nothing it can decrypt with.
The optional recovery key
A recovery key is a second way to the same vault key, for a forgotten master password. It is 32 random bytes, shown once as 56 letters and digits in groups of 4 (the last few are a checksum, so a typo is caught before anything is sent). From it the device derives two subkeys: a recovery auth key, which the server hashes like the auth key, and a recovery wrap key, which never leaves the device and wraps a second copy of the vault key. There is no Argon2id here: 256 random bits can't be guessed. Using it sets a new master password and a new recovery key. Keep it on paper or offline, never inside Hush: it is as powerful as the master password.
What an encrypted item is
The item (name, username, password, notes, sections, icon…) becomes JSON, is padded up to a multiple of 64 bytes so its exact length doesn't show, and is encrypted. The result is a small envelope:
{ "v": 1, // format version
"n": "Hq0mN8xZ…24 random bytes…", // nonce
"c": "9fKe1Lw3…ciphertext + 16-byte tag…" } // the locked item
The encryption also binds in the item's id (pm:item:v1:<id>). If anyone changes even one byte, or moves the ciphertext to a different item, decrypting fails. Every failure gives the same error, so it reveals nothing about why.
What happens when you…
- The device picks a random salt and runs Argon2id on your master password.
- It derives the auth key and the wrap key, makes a random vault key, and wraps it.
- It sends email, KDF settings + salt, auth key, wrapped vault key. Nothing else.
- The server hashes the auth key again (Argon2id) and stores the hash, never the auth key itself.
- The device asks for the account's KDF settings and salt. Unknown emails get believable fake ones, so this step doesn't reveal who has an account.
- Argon2id → auth key + wrap key. Only the auth key is sent.
- If it matches, the server returns a session token and the wrapped vault key. With two-step sign-in on, it first asks for a code from your authenticator app (or a one-time recovery code), unless the device was remembered in the last 30 days.
- The device unwraps the vault key and wipes the wrap key from memory.
- The device generates the item's id (random UUID).
- It encrypts the item with the vault key, bound to that id.
- It sends the id + envelope, and the revision number it is replacing.
- If another device saved in between, the server answers "changed elsewhere" instead of overwriting.
- The device proves the current password (its auth key) again.
- It wraps the same vault key with the new password's wrap key, and checks that the result unwraps.
- The server swaps the KDF settings, auth hash and wrapped key, then signs out every session everywhere.
- Email + recovery key. Only the recovery auth key is sent.
- With two-step sign-in on, a code too. Then the server returns the vault key wrapped by the recovery key.
- The device unwraps it, wraps it for a new master password, and makes a new recovery key, which you write down first.
- The server swaps in both and signs out every session. Items are untouched.
- Master password again; the server makes a secret, shown as a QR code drawn on the device.
- Your authenticator app's first code turns it on, and you get 10 recovery codes, each good once.
- The server keeps the secret sealed with a server key that isn't in the database or backups, and only hashes of the recovery codes.
Locking a device wipes the vault key from memory. Unlocking again needs the master password (or, if you turned it on, the fingerprint, see section 06), because no device stores the vault key.
What the server database holds
MongoDB has two collections. Here is what an account and an item look like in the database (shortened values):
users: one document per account
{ "_id": ObjectId("6714…"),
"email": "you@example.com",
"kdf": { "alg": "argon2id13", "opsLimit": 3, "memLimit": 67108864,
"salt": "q3VtR9…==" }, // public by design: devices need it
"authHash": "$argon2id$v=19$m=19456,t=2,p=1$…$…", // hash of the auth key, not the key
"wrappedVaultKey": { "v": 1, "n": "…", "c": "…48 bytes…" }, // locked by the wrap key
"recovery": { "authHash": "$argon2id$…", // only with a recovery key
"wrappedVaultKey": { … } }, // locked by the recovery wrap key
"twoFactor": { "secret": { "n": "…", "c": "…" }, // only with 2FA: sealed with a server key
"lastStep": 59321…, "recoveryCodes": [ "…keyed hashes…" ] },
"createdAt": …, "updatedAt": … }
items: one document per item
{ "userId": ObjectId("6714…"),
"itemId": "3f1c9a2e-…-4…", // random, made by the device
"envelope": { "v": 1, "n": "…", "c": "…" }, // the whole item, encrypted
"revision": 3, // +1 per save
"deletedAt": null, // a date while in the trash (30 days)
"createdAt": …, "updatedAt": … }
Redis holds only short-lived things: sessions, stored as a hash of the token pointing to your user id (ending after 2 h unused, 24 h at most), two-step challenges (5 min), "remembered device" tokens (hashed, 30 days) and rate-limit counters (15 min). If Redis leaked, the stored hashes couldn't be used as tokens.
| The server can see | The server can't see |
|---|---|
|
|
If the database leaked
Assume the worst: someone copies the entire database, or a backup file. What can they do?
No, not without the vault key. That needs the wrap key, which needs your master password.
No. The database holds a hash of the auth key, and the server wants the key itself. With two-step sign-in, they would also need your authenticator.
Yes, offline, and slowly. Every guess costs a full Argon2id run (64 MiB, 3 passes). This is why the master password matters most.
How long trying every possibility would take, assuming a well-equipped attacker managing 1,000 guesses a second against these settings (a rough figure; Argon2id's memory cost is what keeps GPUs slow):
| Master password | Time to try them all |
|---|---|
| A common password, or one from a previous leak | Minutes: guessed from a list, not by trying every possibility |
| 8 random lowercase letters (~38 bits) | About 7 years, or less with luck |
| 4 random words from a 7,776-word list (~52 bits) | About 100,000 years |
| 6 random words, or 12+ random characters (~78+ bits) | Far longer than the age of the universe |
Two-step sign-in doesn't change this table: it guards the server, and a copied database is attacked without asking the server anything. The recovery key's copy of the vault key can't be guessed (256 random bits).
What a malicious server could still do
- Withhold or delete items. Backups cover this.
- Serve an older copy of an item. Revisions aren't bound into the ciphertext yet (a known limit).
- Offer weaker key settings to a device that has never signed in before. Devices that have signed in remember the strongest settings they've seen and refuse anything weaker.
It can't read items, change them unnoticed, swap one item's ciphertext into another, or get your master password.
The Android app
Hush for Android runs the same crypto code as the web app on native libsodium. It produces byte-for-byte the same output, so the phone and the laptop open the same vault, and a test checks that they still match.
What lives where on the phone
| What | Where | How it's protected |
|---|---|---|
| Vault key | Memory only | Wiped when the app locks. An app restart always lands on the lock screen. |
| Session token | Secure storage | Encrypted by the Android Keystore (hardware-backed on most phones). Ends on the server after 2 h unused, or 24 h at most. |
| Offline copy | A file in the app's private folder | The same ciphertext the server has (KDF settings, wrapped vault key, envelopes), sealed again under a random key kept in secure storage. The file alone, copied off the phone, is useless. |
| Fingerprint unlock (opt-in) | Keystore entry tied to your fingerprint | Holds the wrap key, and the auth key so a fingerprint can sign in again when the session has ended. Opens only after a strong (Class 3) fingerprint check, with no PIN fallback. Android destroys it when a new fingerprint is enrolled. The master password is required again after 7 days, after a phone restart, and after a password change. |
| KDF floor | Secure storage | The strongest key settings seen for your email; weaker ones from the server are refused. |
How it guards the screen and the clipboard
Locks after time in the background (default 30 s; immediately / 30 s / 1 min / 5 min) and after 5 minutes without a touch. Locking also removes every vault screen from the back history.
While unlocked, screenshots, screen recording and the recent-apps preview show a blank screen (on by default).
Copied secrets are marked sensitive, so Android 13+ hides them in the paste preview and keyboards don't store them. They are cleared after 30 s, even when Hush is in the background.
No signal? The phone unlocks from its offline copy, read-only, if it synced within the last 3 days. It goes live again by itself when the server is back.
Autofill
Opt-in. When a sign-in form appears, Android offers "Hush · fill for <site>". Tapping it opens a protected Hush screen: unlock with fingerprint or master password, pick a login, filled. If the screen unlocked just for this, the key is wiped again on close. Every Hush field, on the phone and on the web, is kept away from other password managers, so nothing typed into Hush is offered for saving or filled by them.
Permissions
| The app asks for | Why |
|---|---|
| Internet, network state | Talk to the server. Network state comes with the QR scanner library. |
| Fingerprint / biometric | Fingerprint unlock, if you turn it on. |
| Camera | Scanning a website's 2FA QR code. Asked the first time you scan; decoding happens on the phone. |
| Screen-capture detection, vibrate | Blocking screenshots; haptic feedback. |
Storage, media, microphone and overlay permissions are removed from the app. Android's automatic cloud backup is off for Hush, so its files are not copied to Google. Every release is signed with Hush's own release key, and Android refuses an update signed with any other key.
The web app
The vault key and wrap key live only inside a Web Worker. The page sends it items to encrypt or decrypt and never holds a key itself. Locking wipes the keys and shuts the worker down.
The session token is kept for the tab only. A reload lands on the lock screen, because keys live only in memory. Auto-lock after 5 minutes idle (adjustable).
The page can run only its own scripts (no inline handlers, no eval, Trusted Types on), can only talk to its own server, and can't be framed by another site. This limits the damage a cross-site scripting bug could do.
Fonts and brand icons are bundled. Hush never fetches favicons, because that would tell a third party which sites you have accounts on. Copied secrets are cleared after 30 s.
Imports (Chrome, Bitwarden, 1Password CSV/JSON) are read in the browser and encrypted before upload; the plain file never reaches the server. Encrypted exports use their own passphrase with heavier Argon2id (256 MiB), because a file can be attacked offline forever.
The server
| Layer | Protection |
|---|---|
| HTTPS only | Every connection is encrypted, with HSTS. |
| Sign-in limits | 60 sign-in requests / 15 min per IP; 10 attempts / 15 min per email; 10 password changes / 15 min per account. |
| Sessions | Random tokens; the server stores only their hash. Sign-out ends one session, and a password change ends all of them. |
| Strict input checks | Every request is checked against a schema, and unknown fields are rejected. Item ids must be random UUIDs and belong to one account. Bodies are capped at 256 KB. |
| No body logging | Request bodies are never logged. |
Today the server runs on a private network that only its developer's devices can reach. It moves to dedicated hosting before anyone else can sign up.
Backups and break-glass
The database is dumped every night, and each archive is encrypted with age to a backup key whose private half never touches the server. Copies are pulled to a separate machine and kept for 90 days.
Backups contain exactly what the database contains: ciphertext, the wrapped vault key and the auth hash. Nothing in them is readable without your master password, and without the backup key a stolen archive can't even be guessed at offline.
If the server is ever gone, hush recover opens a backup with the backup key and the master password: no server, no Docker, no network. It can show items or write an encrypted export to import into a new Hush.
Honest limits
| Limit today | What it means |
|---|---|
| No reset | A forgotten master password means the vault is gone, unless you made a recovery key. Nobody, including the server, can reset it. |
| Two-step sign-in guards the server, not the encryption | Authenticator codes stop someone with only your password from signing in. A copied database can still be attacked offline. Passkeys are planned. |
| A stolen session token | Gives access to the ciphertext until it's 2 h unused (24 h at most), and can move items to the trash. It can't decrypt anything, and deleting for good asks for the master password. |
| Older copies of items | A malicious server could serve an older version of an item. Fixing it needs a format change. |
| Offline copy after a password change elsewhere | The phone's offline copy still opens with the old password until it syncs or turns 3 days old. |
| Memory | Key bytes are wiped, but JavaScript strings (e.g. a revealed password) can't be wiped from memory. |
| Fingerprint unlock | A deliberate trade-off: the wrap key (and the auth key) are kept on the phone, behind a fingerprint-only Keystore entry. |
| Not audited | Built carefully on libsodium and reviewed in-house, but no outside security audit yet. |
1. A long, unique master passphrase. 2. Keeping your devices locked and up to date. 3. A recovery key kept offline. 4. Two-step sign-in.