Accounts and the vault¶
What we store can't be used to test password guesses¶
When you log in, your browser and our website have a short back-and-forth (a method called OPAQUE) that only works out if you typed the right password. Your browser also scrambles the password slowly on purpose (about a second), so every guess is expensive.
your browser our website
your password + slow scramble
message 1 ----------------------> answers using what it
<---------------------- stored when you signed up
message 2 ----------------------> "right" or "wrong",
nothing more
a PASSWORD KEY only the right
password can produce: it unlocks
your vault and never leaves you
- We learn only "right" or "wrong". Nothing we store, and nothing we see while you log in, lets us (or anyone who copied our database) try guesses on their own computers. The only way to guess is to try logging in, which we limit. The one exception is someone who takes over our servers completely, and so gets both the database and our secret keys: they could guess offline, but each guess is slow on purpose.
Your vault: a locked box we store for you¶
Your vault holds what makes roamd work anywhere: your browser key (what your computers recognise), the identities of your computers (so impostors are spotted) and your own settings: Voice and update choices, when the page locks itself, your names, groups and order for computers, your saved commands, per-session notification choices, the project labels you put on sessions, which guests may send files, and which terminals each of your browsers had open (machine and session names, nothing of what was on screen). After you replace your browser key, it also holds the old one until every computer has switched. If you choose to, it also holds the SSH login (address, username, password or key) of a server you set up from the web app: only when you tick the box during setup, and you can forget it again from the machine's ⋯ → Details. Anyone who unlocks your vault can use that login, so it's off unless you pick it.
+-------------------------- your vault --------------------------+
| browser key computer identities your settings |
+------------------------------+---------------------------------+
|
locked with the VAULT KEY (random)
|
+-----------------------+-----------------------+
| | |
vault key locked vault key locked vault key locked
with your with your with each PASSKEY
PASSWORD KEY RECOVERY KEY you add (optional)
(normal use) (if you forget (Face ID, fingerprint,
the password) security key)
We store all of this, but locked. Without your password, your recovery key or one of your passkeys, it's unreadable, to us as well. Session notification choices also save a delivery rule that the server can read, so it can respect muted sessions while your apps are closed. That rule reveals which opaque session identifier is muted, without revealing its name or contents.
Settings may be saved while an earlier copy is still on its way to the browser. roamd keeps the newer settings and fetches again quietly. Ordinary overlapping saves do not show the older-settings warning. A genuinely older or incomplete copy is still refused.
fetch starts -> newer settings saved -> delayed copy -> fetch again
genuinely older or incomplete copy -> stop and ask
A few things are not in the vault: they stay in each browser on that device only. These are the text size, when the on-screen key row shows, the hands-free keyboard shortcut and the email last used to log in. The vault is for what should follow you to every browser.
Logging in on a new browser¶
Your vault holds the key to all your computers, so your password alone isn't enough on a browser your account hasn't used before. We also email you a 6-digit code: someone who has guessed or stolen your password still can't get in without your inbox.
new browser our website
----------- -----------
you type email + password
OPAQUE back-and-forth <-------> right password?
new browser? email a code,
hold back the vault
type the code from the email --------> check the code
<-------- your locked vault key
unlock it with the password key
download + unlock your vault <-------- locked vault items
- After that, this browser is remembered, and you only need your password there.
- The email says someone logged in with your master password. If it wasn't you, change your password: they know it. Once a new browser is let in, we also email you a short notice with its name and rough place.
- The browser you signed up on, and browsers already logged in when this check arrived, count as remembered.
- Changing your password, or using your recovery key, forgets every other browser, so each one needs a code again.
- Too many wrong codes in a day pauses codes until the next day, even the right one, and we email you. Codes are only asked for after the right password (or passkey), so this usually means someone else knows it: change it.
- The same goes for a passkey (below): on a browser your account hasn't used before, it replaces your password but not the second step.
Choosing the second step¶
The emailed code is the second step. If someone took over your email as well as learning your password, the code wouldn't stop them, so you can choose something stronger on the Account page, under Second step for new browsers:
| Choice | A new browser needs | Emailed codes |
|---|---|---|
| Emailed code (default) | password (or passkey), then the code we email you | yes |
| Authenticator app | password (or passkey), then the code your app shows | no longer log you in |
| Passkey | password, then one of your passkeys | no longer log you in |
Account page: choose -> your master password
-> your CURRENT second step (code or passkey)
-> (app) scan the QR code, type one code
-> we email you that it changed
- Authenticator apps: any standard one works, including most password managers. The QR code is made in your browser; each code works once.
- Passkey: a new browser needs your password and your passkey, so neither a stolen phone nor a stolen password is enough on its own.
- Browsers you've already used don't ask for the second step.
- Lost your phone? Your recovery key, together with a code we email you, gets you back in; your second step then goes back to emailed codes, and we tell you so.
Passkeys: Face ID, fingerprint or a security key¶
A passkey is a key kept by your phone, your laptop or a small USB security key, and unlocked with your face, your fingerprint or a PIN. Add one on the Account page (you'll be asked for your master password). From then on, Log in with passkey on the login screen takes the place of your master password.
your browser our website
------------ -----------
click "Log in with passkey" ----------> a random challenge
Face ID / fingerprint / PIN:
the passkey signs the challenge
and makes its own secret
send the signature only ----------> check it with the
passkey's public half
new browser? email a code,
hold back the vault
type the code from the email ----------> check it
<---------- your vault key, locked
with the passkey's secret
unlock it with the secret
(which never left your device)
- No password to type. The passkey makes a secret that only it can make, and your browser uses that secret to lock another copy of your vault key. We store the locked copy but never see the secret.
- A new browser still needs your second step, just as with your password (by default the emailed code). So someone with your phone, or your Apple or Google account, still can't get in without your inbox. After that, the browser is remembered and the passkey alone is enough there. (If you chose a passkey as your second step, a new browser needs your password first, since the passkey is the second step.)
- The passkey has to be able to make that secret. Nearly all phones, laptops and newer security keys can. If yours can't, roamd says so when you try to add it.
- Passkeys can't be phished. A passkey only works on roamd's real website, so a fake login page gets nothing. There's no code for anyone to trick you into reading out.
- We email you whenever a passkey is added or removed. If it wasn't you, change your master password (and remove any passkey you don't recognise).
- Changing your master password doesn't remove passkeys. Using your recovery key does: it removes every passkey, because whoever holds the recovery key is the owner and a thief may have added one. Add yours again afterwards. Removing one on the Account page (which also asks for your master password) stops it working at once. You can't remove your last passkey while a passkey is your second step: choose another second step first.
- A passkey is as safe as the device that holds it, or the Apple, Google or password-manager account it syncs through.
Confirming your email¶
When you create an account, we email you a 6-digit code. The account only exists once you type it in, so nobody can set up an account in your name, or hold on to your address.
YOUR BROWSER OUR WEBSITE YOUR INBOX
create account ----------> make a random code ----> "Your roamd
(keep only a scrambled code: 123456"
copy of it)
type 123456 -------------> scrambled copies match?
yes -> account created
no -> try again (only a
few tries per code)
- The code only works for a short time. You can ask for a new one; that cancels the old one.
- We don't reveal who has an account. If the address already has one, the screen looks exactly the same, and the email tells its owner someone tried, instead of giving a code.
- The email is sent through an email delivery service. It sees your email address and the code. It also sees the text of the other emails we send (for example "a new browser logged in", with the browser's name and a rough place, or "your computer went offline", with its name). Those emails never contain a password or a code. We don't keep copies of the emails.
If you forget your password¶
Your recovery key unlocks the second copy of the vault key, and you choose a new password. We also email you a code first, so someone who finds your written-down recovery key still needs your inbox. Everything is still there. Lose both the password and the recovery key and the vault is gone for good. We have no master key.
Other protections¶
- Guessing is limited: a handful of tries per minute for each email from each computer, and a wider limit on the email across every computer together, so spreading guesses around doesn't help. Because a stranger's tries are counted on their own computer first, they can't use up the tries you have from yours.
- Nobody can flood an inbox: an address only gets a few emails it didn't ask for (a sign-up code, a sharing invitation, an organisation invitation) in an hour and in a day. Emails about your own account's security aren't held back by it.
- Made-up failed logins can't hide real history: failed logins have a small share of the Activity page and of an organisation's audit log to themselves, so no number of them can push out anything else.
- Nobody can find out who has an account: asking about an email that isn't registered gets an answer that looks just like a real one.
- Logging in replaces the session this browser already had, so an old session cookie can't be kept alive beside the new one.
- Changing your password logs out your other browsers, and they'll need an emailed code (or a passkey) to log in again.
- You're emailed at once whenever your password is changed or reset, a passkey is added or removed, or your second step changes, with the way to undo it. A new browser being let in gets a notice too. A thief who gets in can't change things quietly.
- A new recovery key waits before it works. You can make one (Account → Recovery key), but it only takes over after a wait, we email you at once, and your old key keeps working through the wait and for a while after. Using your old key cancels it. So someone who gets into your account can't lock you out by replacing the key.
- You can see and sign out your browsers (Account → My browsers): each one's name, place and last use. Signing one out also makes it need an emailed code again. We keep a browser's name and place, never its address.
- Tabs share one unlock. A new tab of the same browser opens already unlocked, until you lock, log out, or about a month passes. If you'd rather every tab asked for your master password, turn off Account → Tabs → Stay unlocked in every tab of this browser. It is a little less secure when on: anyone who can use this browser while you're logged in could open your vault. This is a choice for this device only.
- The page locks when you close the tab (with sharing off), or when you lock or log out. If you choose, it also locks after some time without use (Account → Locking; off unless you turn it on, and it follows you to every browser). Locking only closes the vault in that tab: you stay signed in, your computers keep running, and unlocking needs your master password (or a passkey). An organisation can require locking after at most a set time, and then "never" isn't offered.
Per-session notification preferences are encrypted vault items. Their writes also store an opaque device/session routing rule and muted flag in the same transaction. This delivery metadata is readable by the hub so enforcement works with no browser open. Session names and terminal contents are not included in the rule.
Password logins: OPAQUE¶
Logins use OPAQUE (RFC 9807, an asymmetric password-authenticated key exchange): Ristretto255 for the oblivious PRF and the key exchange (3DH), SHA-512, and Argon2id (memory-hard; OWASP-recommended settings or stronger) as the slow step inside it.
sign-up: browser registers the password against the new account's id;
the hub keeps the OPAQUE record (no salt, no proof hash)
login: credential request --> hub answers with the record
(for an unknown address, a made-up
one shaped the same)
browser finishes --> hub checks the finalization:
right or wrong
browser alone gets the export key
--HKDF-SHA256--> password key (locks the vault key)
- No offline guessing for the hub. Neither the stored record nor anything seen during a login lets the hub test password guesses; guessing takes live logins, which are rate-limited per visitor and per email. The hub's own OPAQUE secrets are kept outside the database in production, so a stolen database alone doesn't allow offline guessing either. Someone with both the database and those secrets can guess offline, at one Argon2id run per guess, as with any password system.
- Unknown addresses get a response shaped exactly like a real one (OPAQUE's built-in fake records), and no password finishes it.
- Sensitive changes (new password, adding or removing a passkey) re-run the OPAQUE login in the signed-in session first; the result is good for one change, briefly.
- The browser refuses Argon2 settings weaker than the ones roamd creates accounts with.
Vault structure¶
vault key: 256-bit random, generated in the browser
|
+-- sealed under the password key -> stored on the hub
+-- sealed under the recovery key -> stored on the hub
+-- sealed under each PRF passkey -> stored on the hub (optional)
+-- seals each item with XChaCha20-Poly1305
(bound to the item's name and revision)
- Items: the browser's SSH key (Ed25519), the pinned identities of your machines, Voice settings (per-machine Voice on/off, keywords, threshold, whether the Dictate note was seen; a Smart choice saved by an older version is ignored and dropped on the next save), update and idle-lock choices, machine names, groups and order, saved commands, which guest shares may send files, one record of open terminals per browser (machine and session names only), an SSH login per machine set up over SSH with the save box ticked (address, username, password or private key and passphrase, host fingerprint, time saved; forgetting seals it empty) and, during a key replacement, retired keys with the machines still to switch (see Usage).
- Per device, not in the vault (local browser storage): terminal text size, on-screen key row mode, the hands-free keyboard shortcut, the email last used to log in, an identifier for this browser's open-terminals record, and the newest revision seen of each item (used for the rollback check below).
- Binding each item to its name and revision means the hub can't swap items or pass off an old version as new.
- Rollback and deletion: each browser remembers the newest revision it has seen of every item (revision numbers only), across reloads, and refuses an older one or a full load that leaves an item out. roamd never deletes items, so an unsealed "deleted" from the hub is ignored. Limit: a browser that has never seen the vault can't tell a consistent older copy from the latest; the worst that does is drop a pinned identity, and a machine with no pin is refused, not trusted.
- Choosing to go on. The hub is what is being checked, so nothing it says can clear the refusal; only the person can. The message says the likely harmless causes first (a restored backup or reset), then offers Use the server's copy, confirmed in a second step that says what may be lost. Confirming forgets this browser's record for that vault only (the record is keyed by a hash of the vault key, so a new or recreated vault never trips it); an open vault reloads, a refused login asks for another sign-in. While the page is open the warning is shown once, not every poll.
- Sync: browsers fetch changes since their last sync. Revision marks only move forward. A response is checked against marks known before its request. If a concurrent save or fetch advanced them while it was in flight, the browser retries without applying older values or showing a rollback warning. Late write acknowledgements cannot replace newer loaded values, and responses from a reset or locked vault are ignored. Writes must name the next revision; on a conflict the browser re-applies its change on the latest version.
request starts -> marks advance -> old response -> retry, keep new values
response below marks known at request start -> refuse older copy
Recovery key¶
160 random bits plus a checksum (to catch typos), shown as 9 groups of 4 characters. Like the password, it yields a separate key and proof; the hub keeps only a hash of the proof.
recovery key -> proof checked by hub -> code emailed to the account
code -> hub returns vault key sealed under the recovery key
browser: unlock vault key, register a new password (OPAQUE), re-seal
hub: replace password material, end every session, second step back to email
The emailed code is needed whatever second step the account uses: the recovery key is for when the app or passkey is what was lost, so it resets the second step to email (and the owner is told). It also removes every passkey, so one a thief added can't survive taking the account back.
Account protections¶
| Protection | Behaviour |
|---|---|
| Guessing limits | Per visitor, per email from one visitor, and (looser, per hour) per email from everyone. A stranger fills their own counters first, so the owner's allowance from their usual address stays; spreading guesses across many addresses is still capped |
| Unasked email | Per recipient address, per hour and per day, for sign-up codes and invitations. Account-security emails and alerts are outside it, so a flood can't make them wait |
| Failed-login records | Kept in their own small pool in Activity and in an organisation's audit log, so forged ones can't push out other entries |
| Emailed-code guessing | Few tries per code; wrong codes also count per account across the day, so fresh codes don't buy fresh guesses. Past that limit no code works until the next day, and the owner is emailed |
| Account privacy | Unknown emails get responses shaped like real ones, at login and at sign-up (where the owner of a taken address is emailed instead of a code), so you can't probe who has an account |
| New-browser check | Right password from a browser unknown to the account: the hub withholds the locked vault key and emails a code; the code finishes that login |
| Passkeys | A verified passkey assertion stands in for the password; a browser unknown to the account still needs the emailed code. Adding or removing one needs the current password and emails the owner |
| Password change | Requires the current password; ends other sessions and forgets other browsers; emails the owner at once |
| Recovery | Ends all sessions and forgets every browser; the recovering browser is remembered; emails the owner at once |
| New recovery key | Needs the current password; waits before it works; emails the owner at once; the old key keeps working through the wait and for a while after; recovering cancels a waiting key |
| Browser list | Each session keeps the browser's plain name and place, when it started and when it was last used (never an address). The owner can sign another browser out: its session ends and its remembered-browser token is forgotten, so the password alone no longer works there |
| Email confirmation | Every account proves its address at sign-up (see below) |
Vault backup: the export is the vault's items as the hub holds them, each still sealed with the vault key and bound to its name and revision, so the file is as private as the vault and the hub is asked for nothing it wasn't already holding. Nothing is uploaded. An import opens every item with the current vault first (so a changed file, or one from another account, restores nothing), then saves each as a new revision. The browser key, retired keys and machine identity pins are never restored from a file: an old copy could undo a key replacement or a machine's check. The file is a copy of the vault, not of the account: opening it still needs the password or the recovery key.
New recovery keys: the browser makes the key and locks the vault key with it, then sends the hub a proof of it and the locked copy, with the current password re-checked just now (one check, one change). The hub holds it as waiting: it can't be used to recover, the owner is emailed straight away, and it takes over only after a delay. At that point the key in use is retired, but still accepted for a further period; then it is dropped. One key can wait at a time. Recovering with any key that works cancels a waiting one, and the owner can cancel it from the account page. Against an attacker this gives, in turn: a stolen session alone can't make a key (no password check); the password alone gets nowhere, because new browsers need the emailed code; the password and the inbox can start a key but the owner is emailed and their own key still works and cancels it; only all of the password, the inbox and the recovery key is the owner.
Change alerts: the changes that could lock an owner out or let someone in (password changed or reset, passkey added or removed, second step changed) each email the account address straight away, saying what changed, the browser and place it came from, and how to undo it with the recovery key. They are sent in the background, never contain a password or a code, and their contents are never logged.
Sessions: a random 256-bit token in an HttpOnly; Secure; SameSite cookie; the hub
stores only its hash, and ends it on its own schedule too, whatever the cookie says.
Staying unlocked across a reload: the tab keeps the vault key sealed (AES-GCM) under a per-tab WebCrypto key that is non-extractable: page script can use it but never read it out, so a copy of the tab's storage is useless elsewhere. The sealed copy lives in the tab's own storage, so it goes when the tab closes; the per-tab key is deleted on logout or lock (a leftover from a tab closed without locking is cleared after a while). Idle lock only applies if the owner set one (it is a vault setting, so it follows the account, and an organisation can cap it); the key is wiped from memory on lock. Locking ends the unlocked state, not the hub session.
Staying unlocked in every tab (on by default; a per-device choice in local storage, kept as 0 when turned off):
the sealed copy goes in local storage and the WebCrypto key is stored under one shared id
instead of the tab's, so new tabs of the same browser profile unlock from it. Lock, logout
and the 30 day key clean-up delete it for all tabs. The cost: the sealed vault key now
outlives a closed tab, so whoever can run script in this origin, or use this profile while
logged in, can open the vault. The settings card says so beside the checkbox. With it off, the
sealed copy and the key stay per tab, as described above.
Email confirmation¶
sign-up --> hub: address free?
yes -> hold the new account in memory, with a random
6-digit code (uniform, OS randomness) as a hash
no -> hold nothing usable; email the owner a notice
answer "code sent" either way, shaped the same
--> SMTP over TLS --> user's inbox
code --> hub: hash matches, unexpired, tries left?
yes -> account created (address confirmed), session
no -> one try used up
- Codes are short-lived with a small per-code try budget; sending and checking are also rate-limited per address or account. A pending sign-up lives only in the hub's memory; a leaked database holds only hashes of live codes.
- A sign-up for a taken address changes nothing; no code can finish it.
- Remembered-browser tokens are always issued by the hub (a token already in the browser is kept only if it is already remembered for that account), and both they and sessions expire on the hub as well as in the cookie.
- Addresses with characters mail software treats specially (angle brackets, quotes, commas and similar) are refused, so the address on the account is the one emails go to.
- The hub logs neither email contents nor recipients; a failed send is logged by kind only. The email provider sees the recipient address and the code, under its own retention policy.
New-browser check¶
password right --> browser known to this account?
yes -> session + locked vault key
no -> email a code (same code store as confirmation),
answer "code sent", no session, no vault key
code --> right -> session + locked vault key,
remember this browser
- Recognition: a random 256-bit token in an
HttpOnly; Secure; SameSitecookie; the hub stores its hash per account. A browser with a live session for the account also counts as known (browsers signed in before the check existed). - The code carries the same lifetime, try budget and rate limits as confirmation codes, and is only accepted for a login whose password was just proved. A right code also confirms the address.
- The email tells the owner their master password was used, so a leaked password is noticed. When the browser is then let in, the owner is also emailed a notice (browser name and place) in the background.
- Password change forgets every other browser; recovery forgets all of them and remembers the recovering one (the recovery key is at least as strong a proof).
- When the hub sends no email (local development), every browser counts as known.
- Limit: with the default (email) second step, this protects the vault from a leaked password, not from an attacker who also controls the inbox. Accounts can choose an authenticator app or a passkey instead (below). Someone who unlocked the vault earlier may have copied the browser key: Replace browser key retires it on every machine.
Second step choices¶
| Step | After the first factor (password, or passkey on its own) | Emailed login codes |
|---|---|---|
| a code emailed to the account | on | |
| app | TOTP (RFC 6238, the standard authenticator-app settings, with a little clock drift allowed), each code once | off |
| passkey | after the password, a WebAuthn assertion from one of the account's passkeys; a passkey alone on a new browser is refused | off |
- App keys (160-bit) are made on the hub, shown once as a QR code drawn in the browser (plus the base32 key), and confirmed with one code before they take effect. The hub keeps them sealed (XChaCha20-Poly1305) under a key derived from its OPAQUE secrets, which live outside the database, bound to the account.
- Wrong app codes share the daily per-account cap with emailed codes.
- Changing needs the password re-checked in the session, then the current second step (emailed code, app code or passkey assertion); the owner is emailed the result. Choosing a passkey needs at least one passkey on the account.
- Recovery needs the recovery key and an emailed code (whatever the second step), and resets the second step to email.
Passkeys (WebAuthn)¶
add (signed in, password re-checked just now):
hub: registration options (user verification required,
no attestation)
browser: discoverable credential; asks its PRF extension to
evaluate a fixed roamd input
PRF output --HKDF-SHA256--> passkey key
vault key sealed under the passkey key,
bound to the credential ID
hub: verifies; stores the public key, signature counter,
sealed copy, name, dates; emails the owner
log in:
hub: single-use challenge, no credential list
browser: assertion (+ PRF output, which stays in the tab)
hub: verifies signature, origin, relying party, user
verification, counter, and that the credential
belongs to the named account
browser unknown to the account? email a code,
hold the verified login until the code comes back
session + remember this browser; returns the copy
browser: PRF output -> passkey key -> vault key
- The PRF output never leaves the browser. The page drops all extension results before sending an answer to the hub, and the browser tests fail if one is ever sent.
- New browsers: a passkey replaces the password, never the emailed code. The code step uses the same code store, lifetime, try budget and rate limits as the password path, and the email names the passkey. A verified passkey login waits server-side (memory only) for its code; a passkey removed meanwhile can't finish.
- PRF is required. Without it the passkey couldn't unlock the vault, and since it never replaces the code it would add nothing; the page refuses to add one, and the hub refuses a registration without a sealed copy. The passkey must be a discoverable one, so login needs no email typed first.
- The sealed copy is bound to its credential ID, so the hub can't hand one passkey's copy to another.
- The login options list no credentials, so they reveal nothing about which accounts or passkeys exist. Verification uses a maintained WebAuthn library; challenges are short-lived and single-use, which also stops replays from synced passkeys that report no counter. A counter that goes backwards (a cloned authenticator) is refused.
- Adding needs the master password re-checked just now as well as a session, and the owner is emailed. Removing needs a session and the master password re-checked just now, and the owner is emailed. A password change keeps passkeys (the vault key doesn't change); the password-changed message says to check the list. Recovery removes all of them, in the same step that resets the second step to email, and the reset email says so. The last passkey can't be removed while a passkey is the second step.
- Limit: on a known browser, a passkey alone opens the vault, so it is as strong as the device, or the account it syncs through. Elsewhere the inbox is still needed.
Web hardening¶
Every response carries a strict Content-Security-Policy: no inline or third-party script,
connections only to this site, no framing, plus nosniff and no-referrer. The browser test
suite fails on any policy violation.