HUSHOSSign inCreate account

Last updated 15 September 2026

Security model

How HushOS authenticates you, derives your keys, encrypts your files, shares them, and handles reports, written so you can check each claim against the code.

Scope

This page describes what is implemented. The first half covers accounts: sign-in, password changes, account and recovery-key rotation, identity keys, the workspace key, recovery, and remembered browser unlock. The second half covers Drive: folder and file keys, uploads and downloads, sharing with people and by link, what happens when sharing stops, reports, and what the server can see. The byte-level protocols live in the @hushos/crypto package and are specified in docs/opaque-auth-design.md and docs/drive-design.md in the repository. Where this page and the code disagree, the code is right and this page has a bug.

Sign-in without sending a password

HushOS uses OPAQUE, a password-authenticated key exchange, through the Serenity implementation. During registration your browser and the server run a short protocol that leaves the server with a record it can use to verify future sign-ins. The password never leaves the browser, not even as a hash.

Protocol
OPAQUE (Ristretto), Serenity implementation
Hardening
Argon2id, 65,536 KiB memory, 3 iterations, parallelism 4
Server stores
The OPAQUE registration record
Client receives
An export key that only the client ever sees
Unknown email
Answered with a decoy record so lookups do not reveal accounts

OPAQUE produces two secrets. The session key is shared with the server; HushOS uses a separate random token for its application sessions. The export key is known only to the client. Only the export key is used to wrap the account key, and only after a further derivation step.

The account key

Your account key, also called the master key, is 32 random bytes generated in your browser. It is not derived from your password, so changing the password does not change the key. Two independent wrappings of the same key exist:

Password wrap
HKDF-SHA-256 over the OPAQUE export key, then XChaCha20-Poly1305
Recovery wrap
HKDF-SHA-256 over the recovery secret, then XChaCha20-Poly1305
Salt and nonce
Fresh 32-byte salt and 24-byte nonce on every wrap
Bound to
Your account ID, root revision, and credential or recovery revision

Every envelope carries associated data that names its purpose, version, and owner. An envelope moved to another account or another version fails to open.

Identity keys

At signup the browser also generates an X25519 key pair for encryption, an Ed25519 key pair for signatures, and an ML-KEM-768 key pair, the post-quantum key exchange NIST standardised as FIPS 203. The private halves are wrapped with keys derived from the account key and bound to their own public keys, so a swapped public key is detected when the envelope is opened. Only public keys and wrapped private material reach the server. The X25519 and ML-KEM keys together are what a share is sealed to, and what a report's keys are escrowed to. Both are described below.

The ML-KEM public key is signed by the identity's Ed25519 key, and the server refuses to store one without that signature. An account made before this key existed mints one on its next sign-in; its X25519 key, and so its fingerprint, do not change.

Encryption
X25519, libsodium
Post-quantum
ML-KEM-768 (FIPS 203), expanded from a 64-byte seed wrapped under the account key
Signatures
Ed25519, from a 32-byte seed wrapped under the account key
Binding
The Ed25519 key signs the ML-KEM key together with the X25519 key and your account ID

The workspace key

Every account starts with a personal workspace, and every workspace has its own 32-byte key, generated in your browser. It is random, not derived from your account key, so it can be granted to another person later without giving them anything of yours. What you hold is a grant: the workspace key wrapped under a key derived from your account key.

Workspace key
32 random bytes, created on your device when the workspace is
Grant wrap
HKDF-SHA-256 over the account key, then XChaCha20-Poly1305
Bound to
Your account ID, the workspace ID, your root revision, and the workspace key revision
Server stores
One grant per member per workspace, never the key

Folder and file keys hang off the workspace key, not off your account key. That is what keeps rotation cheap: replacing your account key rewraps one grant per workspace you belong to, and nothing beneath it moves.

Recovery

A 32-byte recovery secret is generated on your device and displayed as 24 English BIP39 words. It wraps the same account key, so recovering with the phrase preserves everything the key protects. A backup of the secret is stored encrypted by the account key, which is how the phrase can be shown again while your device is unlocked.

Resetting a password requires a verified email link and the phrase. The browser proves it holds the phrase by signing the reset with a key derived from it, re-registers with OPAQUE under the new password, and rewraps the unchanged account key. The server checks the signature and every binding, replaces the credentials in one transaction, and revokes all existing sessions. A reset issues a new recovery phrase, then signs you in with the new password.

Password changes and key rotation

Account settings offers three actions. Each requires your current password through a fresh OPAQUE exchange. The password stays on your device, and the server receives only protocol messages and encrypted key bundles.

Change password
Rewraps the same master key; preserves the recovery phrase and identity keys
Rotate recovery key
Creates new 24-word recovery credentials; preserves the password, master key, and identity keys
Rotate master key
Generates a new master key and recovery phrase; rewraps the existing identity keys and every workspace grant, and preserves the password

The server replaces the affected records in one transaction and rejects stale concurrent changes. Each action revokes existing sessions and pending recovery attempts. Your browser then signs in again and saves a fresh device-unlock bundle. If that sign-in fails, the sign-in page confirms the change and explains which password to use. After either rotation, save and confirm the new recovery phrase before continuing into the app.

Master-key rotation rewraps every workspace grant you hold in the same transaction, so the workspace keys, and everything that will hang off them, stay unchanged. Rotating a master key cannot erase old ciphertext, or revoke private identity keys someone already extracted from an older key bundle.

Remembered browser unlock

The plaintext account key lives only inside a Web Worker. To let a browser reopen your account without the password, the worker wraps the key with a non-exportable AES-256-GCM device key held in IndexedDB. What is persisted is a versioned encrypted bundle and a lock revision; the key itself never enters application state. Locking or signing out clears remembered access across tabs in that browser. Password changes, password recovery, and either key rotation revoke sessions on other browsers too. Those browsers lose access when they next check the session; revocation cannot erase keys already held offline.

Sessions and transport

Verification links
32 random bytes, sent in a URL fragment, stored only as SHA-256, valid 30 minutes
Login attempts
Random handles, stored hashed, consumed after 5 minutes or one finish
Sessions
7 days idle, 30 days at most, 32-byte token, only its SHA-256 stored
Cookies
HttpOnly, SameSite=Lax, Secure with the __Host- prefix under HTTPS
Mutations
Exact Origin check, JSON only, 16 KiB body limit, per-address rate limits
Responses
private, no-store and no-referrer

Folder and file keys

Every folder and file in Drive is a node with its own random 32-byte key, generated in your browser and wrapped under its parent's key; the root folder's key is wrapped under the workspace key. A file's node key encrypts the file's metadata (its name, type, size and modification time) and wraps a separate content key for each version of the file. Nothing about a file is stored in the clear: the server holds a tree of ids and envelopes.

Node key
32 random bytes per folder or file, wrapped under the parent node key
Metadata
Name, type, size and modified time, encrypted under the node key
Content key
32 random bytes per file version, wrapped under the node key
Wrap
XChaCha20-Poly1305 with a fresh 24-byte nonce
Bound to
The workspace, the node, its parent and the parent key epoch, or the version and object

Every envelope carries associated data naming where it belongs, so an envelope moved to another folder, another version or another workspace fails to open. A key epoch counts each time a node's key changes; it is what lets a rotation (below) tell old envelopes from new ones.

Uploads and downloads

A file is encrypted in your browser's crypto worker before any of it is sent, in independent 8 MiB chunks, each with its own authentication tag. The encrypted chunks go straight from your browser to the object store using short-lived signed URLs, so the application server never handles file bytes. Downloads come back the same way and are decrypted on your device; previews read only the ranges they need.

Cipher
XChaCha20-Poly1305, one 16-byte tag per 8 MiB chunk
Chunk nonces
Derived from a 16-byte per-object nonce and the chunk index
Transfer
Browser to object store directly; URLs valid 15 minutes for shared content, an hour otherwise
Resume
A journal on your device records each chunk’s digest before it is sent
Thumbnails
Rendered on the uploader’s device and sealed into the file’s own encrypted object, under a key derived from the content key; the server cannot tell which files have one
Size
The server records the size the store confirms for the encrypted object; the file’s own size is sealed in its envelope

Independent chunks are what make ranged previews and resumed uploads plain arithmetic. A chunk is never re-encrypted under the same key and nonce with different bytes: a resumed upload proves the file is the same one, chunk digest by chunk digest, or starts over with fresh material.

Sharing with people

Sharing a folder or file with another HushOS account seals that node's key to their identity keys, on your device. The server stores the sealed box and never holds the key. The recipient opens it with their private identity keys, which only their devices hold, and mounts the shared item as a root of its own under "Shared with me".

The seal is hybrid: an X25519 agreement between your identity key and theirs, and an ML-KEM-768 encapsulation to their post-quantum key, combined into one key that seals the node key. Opening it needs both private halves, so a copy of the database taken today stays shut even if a quantum computer later breaks X25519. A share to someone whose account has not yet minted its post-quantum key is sealed under X25519 alone and re-sealed hybrid by your device the next time you open Drive after they have one.

Seal
X25519 agreement plus ML-KEM-768 encapsulation, HKDF-SHA-256 over both secrets, then XChaCha20-Poly1305 over the node key
Envelope
1160 bytes: nonce, the 1088-byte ML-KEM ciphertext, and the sealed node key
Whose key
A contact key you pinned, with a fingerprint you can compare in person
Roles
Viewer, or editor; an editor's uploads land in your storage
Authorization
Checked on every request by walking up the tree to the nearest live share
Server holds
The sealed box, the role, and who shared with whom

Because the server serves public keys, a dishonest server could substitute one of its own. That is why contacts are pinned: the first time you use someone's key it is recorded in your encrypted settings, both of you can compare a fingerprint, and a later change is shown as a warning that you must accept, never applied silently. The fingerprint covers the X25519 key; the post-quantum key is vouched for by the contact's signing key, checked on your device before it is used, and its digest joins the pin. A different post-quantum key served later is refused like any changed key, and one that goes missing is refused as a downgrade. Every path that seals to a contact goes through the pin, including the re-keying that follows a revocation, so nothing is ever sealed to a key you did not vouch for.

A link is a share without a recipient. Your browser makes a 32-byte secret and puts it in the URL after the #, which browsers never send to servers. The node key is sealed under a key derived from that secret and, if you set one, a password stretched with Argon2id on the visitor's device. The server holds the sealed box and a hash of the link's path; it can open nothing, and it cannot tell whether a password was set.

Secret
32 random bytes in the URL fragment, never sent to the server
Password
Argon2id (64 MiB, 3 iterations) with a per-link salt, mixed into the key
Expiry
Optional, at most a year; revocation is immediate
Visitor
Browses, previews and downloads in their own browser, with no account
Keeping a copy
A signed-in visitor re-encrypts each file under their own keys

When sharing stops

Stopping a share or a link does two things. The server refuses that person on the next request, on every route. Then your browser rotates: the shared item and everything beneath it get fresh keys under a new epoch, every remaining share and link is re-sealed under the new keys, to the pinned keys of each remaining person, and the old keys are gone. Whoever was cut off holds nothing that still opens anything. Content is not re-encrypted; the guarantee is about keys and authorization, and download links already issued expire within 15 minutes.

Reports

Anyone who can see something through a link or a share can report it. Reporting is the one path by which an operator can see content, and it is built so that only a report opens it: when you report an item, your browser seals that item's key to the identity key of each current operator of the service. The report records what was reported and holds the bytes past the owner's deletion so the report can be acted on. An operator opens it under their own identity, and can dismiss it, remove the content, hold it, suspend the uploader, or file it with the relevant authority.

Who can report
Anyone with access through a share or a link
What is sealed
The reported item's key, to each operator's X25519 and ML-KEM-768 keys together, on the reporter's device
What is kept
A snapshot of the reported tree and a hold on its stored bytes
Evidence
Optionally copied to a separate write-once bucket the application cannot delete from
Not kept
Uploader network addresses; the service does not log them

Nothing you do not share can be reported, because nobody but you can see it.

What the server can see

Never
File names, contents, thumbnails, folder names, or any key
Later, with a quantum computer
Still nothing shared between accounts: those envelopes need a lattice key it never held. Reports are sealed under X25519 alone.
Always
Your email, the shape of your tree (ids and parents), the size of each encrypted object, times, and how much you store
Sharing
Which accounts you share with, and that a link exists; not what is behind it
Reports
The content of an item, once someone with access reports it

Pages and headers

Every page carries a Content Security Policy: scripts run only from the app's own origin, under a per-request nonce, with WebAssembly allowed and eval not. The page may connect only to itself and to the object store. Every response carries X-Content-Type-Options, X-Frame-Options: DENY, a referrer policy, a cross-origin opener policy, a permissions policy that switches off camera, microphone, location and payment, and, over HTTPS, HSTS for a year.

What comes next

The rest of the suite is built on the same keys and the same rules: content encrypted on your device under keys wrapped beneath your workspace, sealed to identities when shared, documented here in the same detail when it ships.