AutoPass

AutoPass security design

This document describes how AutoPass protects a vault, what each component can and cannot see, and where the design currently falls short. Parameters are taken from the code in this repository; file paths are given so you can check them.

AutoPass 1.0.0 has not been independently audited. Read "Known limitations" before trusting it with data you cannot afford to lose or leak.

Reporting a vulnerability

Email sales@endopowersports.com (Endo Powersports LLC, which operates AutoPass) with a description, the affected version or commit, and steps to reproduce. Please do not open a public issue for anything that could put users' vaults at risk.

Threat model

AutoPass is built so that the sync server can be fully compromised (database copied, code replaced, traffic read) without exposing vault contents, and without being able to trick a client into encrypting new data to keys the server knows.

In scope

Out of scope

What the server stores, and what it never sees

The server stores The server never receives
Email address (lowercased) and a random account ID The master password
KDF parameters: algorithm, iteration count, 16-byte salt The Secret Key
Secret Key version tag (not the key) The Account Unlock Key (AUK)
The encrypted keyset (private key + symmetric key, under the AUK) Keyset contents in plaintext
The keyset public key (SPKI); clients do not trust it, see below Any vault key in plaintext
SRP group name, salt and verifier v = g^x mod N The SRP exponent x
Per vault: owner, encrypted name, per-member role and wrapped vault key Vault names, item titles, URLs, usernames, passwords, notes, TOTP secrets, passkey private keys, tags
Per item: vault, encrypted overview, encrypted details, version, sync sequence number, trashed-at and updated-at timestamps
Per attachment: its random ID, vault, item, ciphertext, ciphertext size, SHA-256 of the ciphertext, upload time File names, types, contents and file keys
Sessions: SHA-256 hash of each bearer token, creation and last-use time, the device name the client gave, and the trusted device it came from Bearer tokens in plaintext
Vault invitations: vault, inviter, recipient account ID and email, role, the vault key and name sealed to the recipient, the inviter's proof The vault key or name inside an invitation
Share links: the encrypted item snapshot, a SHA-256 hash of the access token, expiry, view limit and count, creator, vault and item IDs Share-link decryption keys (they live only in the link's URL fragment)
Travel safety: an on/off flag per account and a safe-for-travel flag per vault
Account recovery (if set up): a second SRP salt and verifier derived from the recovery code, the keyset and Secret Key encrypted under a key derived from the code, and when it was set up The recovery code, or anything derived from it that could open the encrypted copy
Recovery sessions: SHA-256 hash of each short-lived recovery token, its expiry and which code it was opened with Recovery tokens in plaintext
Two-step verification (if on): the authenticator secret, AES-256-GCM encrypted under a key derived from the server secret, and the last time step used; the phone number (E.164) for text messages; an HMAC of each unused backup code; for each trusted device an HMAC of its token, its name, and when it was trusted, last used and expires Backup codes and trusted-device tokens in plaintext, and anything that decrypts the vault
A per-deployment secret for decoy responses (AUTOPASS_SECRET, or generated into the database)
Optional Redis (several API processes): rate-limit counters, failed-proof timestamps, and in-flight SRP handshakes, all expiring within 15 minutes. Key names are HMACs of the real keys; handshakes and other values are AES-256-GCM encrypted under a key derived from the server secret Emails or IP addresses in key names; handshake secrets readable without the server secret

What a server operator can learn is metadata: the email address, how many vaults and items an account has, ciphertext sizes, when items change, how many files are attached to each item and how big each one is (to within 29 bytes; files are not padded), and client IP addresses (used for rate limiting, in memory or, with Redis, only as HMACs in key names, and recorded in the reverse proxy's access log; the API's own request log records only time, method, path, status and duration). The schema is in apps/api/src/store.ts.

Key hierarchy

master password ─ NFKC ─ PBKDF2-HMAC-SHA256 (650,000 iter, 16-byte salt) ─► masterHash (32 B)
                                                                              │
Secret Key (128 bits, generated on the device) ──── HKDF-SHA256 salt ─────────┤
                                                                              │
              info "AutoPass/auk/<accountId>" ──► AUK  (AES-256-GCM, non-extractable)
              info "AutoPass/srp/<accountId>" ──► SRP x  ──► verifier v = g^x mod N

AUK ─AES-256-GCM, AAD accountId─► keyset { ECDH P-256 private key, 256-bit symKey }
symKey ─AES-256-GCM, AAD vaultId+accountId─► vault key (32 random bytes)
vault key ─AES-256-GCM, AAD vaultId+itemId+field─► item overview, item details
vault key ─AES-256-GCM, AAD vaultId─► vault name
file key (random, per file, kept in item details) ─AES-256-GCM, AAD vaultId+itemId+attachmentId─► file

recovery code (160 bits, optional) ── HKDF-SHA256, salt = 16 random bytes per code ──┐
              info "AutoPass/recovery-wrap/v1/<accountId>" ──► recovery wrapping key  │
              info "AutoPass/recovery-srp/v1/<accountId>"  ──► recovery SRP x ──► verifier
recovery wrapping key ─AES-256-GCM, AAD "autopass/recovery/v1|<accountId>"─► { keyset private key, symKey, Secret Key }

Two-secret key derivation (packages/core/src/kdf.ts)

Secret Key (packages/core/src/secretkey.ts)

128 bits from crypto.getRandomValues, rendered as a two-character version prefix plus 26 Crockford base32 characters in groups of 6-5-5-5-5. Parsing is strict: characters outside the alphabet or a wrong length are rejected rather than skipped. The key is shown once at account creation (with a downloadable Recovery Kit text file) and can be shown again in Settings after re-entering the master password. It is never sent to the server in a form the server can read: only account recovery, if set up, uploads it, encrypted under a key derived from the recovery code (see "Account recovery").

Keyset (packages/core/src/keyset.ts)

Vault keys

Item encryption (packages/core/src/aead.ts, vault.ts)

Attachments (packages/core/src/file.ts, apps/extension/src/engine/attachments.ts)

Encrypted export

Settings > Import & export > Export encrypted backup produces a JSON file whose payload is AES-256-GCM encrypted under a key derived from a separate backup password (minimum 8 characters) with PBKDF2-HMAC-SHA256 at 650,000 iterations and a fresh 16-byte salt. The Secret Key is not involved, so the backup password alone protects the file.

Sharing (packages/core/src/sharing.ts, apps/api/src/sharing.ts, apps/extension/src/engine/sharing.ts, apps/extension/src/engine/rotation.ts)

Sharing needs a sync server; an account without one shows "Set up sync to share".

Vault sharing and roles

Travel safety

Authentication: SRP-6a (packages/protocol/src/srp.ts)

The master password is never sent anywhere, not even hashed. Login is a password- authenticated key exchange:

Flow: POST /v1/auth/srp/start {email, A} returns {challengeId, B, accountId, kdf}; the client derives x, then POST /v1/auth/srp/finish {challengeId, M1} returns {M2, accessToken}.

Enumeration resistance and brute-force limits

Sessions and tokens

Local-only accounts and connecting a server later (apps/extension/src/engine/localfirst.ts)

The extension works without any server. Creating an account without one runs exactly the same client-side account creation (account ID, Secret Key, keyset, first vault) and registers nothing: the profile stores serverUrl: null, and the encrypted cache on the device is the vault. No outbox is kept and the engine makes no network requests for a local account (a unit test runs the whole lifecycle with a fetch that always throws). Changing the master password re-wraps the keyset locally under a new salt and AUK.

Connecting a server later (Settings > Sync across devices) first re-proves the master password on the device, then registers the existing account: the same server record a synced signup would send (account ID, email, KDF parameters, Secret Key version, encrypted keyset, public key), an SRP verifier derived from the current password, and the first vault. Every other vault is created with its own ID and wrapped key, and every item (including trashed items and passkeys) is uploaded with its own ID. Nothing is re-encrypted: ciphertext stays bound to its account, vault and item IDs, so the server learns exactly what it would have learned had the account synced from the start. Before each step the client asks the server what it already has, so a connect interrupted half way is simply run again. If registration says the email or account ID already exists, the client starts an SRP login but sends a proof only if the server reports this account's own ID and KDF salt; an email registered by someone else gets a clear error and never receives a guess. Turning sync off keeps the device's copy and signs out the session; the server copy stays for other devices.

Unlock with your device (packages/core/src/deviceunlock.ts, apps/extension/src/engine/biometric.ts, apps/extension/src/ui/deviceUnlock.ts)

Opt-in, per device: open the vault with Touch ID, Windows Hello or the device's screen lock instead of typing the master password. It never replaces the master password; it is a device-bound shortcut to the result of a password unlock.

How it works. The UI page creates a WebAuthn platform credential (authenticatorAttachment: "platform", userVerification: "required", residentKey: "discouraged", attestation none) with the PRF extension and a random 32-byte salt. PRF makes the authenticator return a 32-byte secret for that credential and salt, only after it has verified the user; the client also checks the UV flag in the authenticator data. That secret is never stored:

PRF output ─HKDF-SHA256, info "<ctx>|prf-key"─► AES-256-GCM key ─AAD "<ctx>|dk-device"─► device key DK (random 256-bit)
keyset symKey ─HKDF-SHA256, info "<ctx>|keyset-key"─► AES-256-GCM key ─AAD "<ctx>|dk-keyset"─► DK (second wrapping)
DK ─AES-256-GCM, AAD "<ctx>"─► bundle { privateKey, symKey, SRP x, KDF salt, sealedAt, expiresAt }
<ctx> = "autopass/device-unlock/v1|<accountId>|<credentialId>"

Unlocking runs navigator.credentials.get with allowCredentials = that one credential, user verification required, and the stored salt; the PRF output unwraps DK, DK opens the bundle, and the engine rebuilds the session exactly like a password unlock (the private key must match the account's public key, and only vault keys wrapped under the bundle's symKey are opened). The WebAuthn ceremony runs in the page because MV3 service workers have no navigator.credentials; the page passes only the credential ID and the PRF output to the worker (extension pages only, as for every RPC), which does all the cryptography.

Where it lives. One record, deviceUnlock, in device-local storage only: chrome.storage.local in the extension, localStorage in the web vault. It holds the credential ID, the PRF salt, the two wrapped copies of DK, the encrypted bundle, and plain copies of the expiry and KDF salt for the UI. It is never synced, exported or sent to the server, and the server has no notion of it. No new extension permission is used.

The bundle is password-equivalent on this device. It contains the SRP exponent x so that sync can log in again after a browser restart (when no session token is left). Whoever can make this device's authenticator answer (pass Touch ID / Windows Hello / the screen lock) and read this browser profile's storage can open the vault and log in to the server as the account, until the bundle expires or the password changes. It does not reveal the master password or the Secret Key, and the UI still requires the master password to show the Secret Key, change the master password, delete the account, turn off travel safety, connect a server, and turn device unlock on.

Decisions and limits.

Threat model. Device theft: the thief needs the device's user verification (a fingerprint, face or the device PIN/password) as well as the browser profile; without it the record is ciphertext under a key only the authenticator can produce, and the vault still needs the master password. Biometric bypass or a known device PIN: gives vault access on this device for up to 14 days; change the master password from another device (cuts server access at once and erases device unlock here at the next sync) or turn it off. Malware on the device that can drive the authenticator, or that runs while the vault is unlocked, was already out of scope; malware that only reads storage gets nothing it couldn't get before. Synced passkey providers: some platforms (e.g. iCloud Keychain) sync the credential; that alone doesn't open anything, because the bundle never leaves this device.

Browser extension isolation

The extension (apps/extension/src/background.ts, content.ts) is a Chrome MV3 extension whose service worker owns the vault engine.

Autofill origin policy (apps/extension/src/engine/urlmatch.ts)

A saved login is offered or filled only when all of these hold:

  1. The page is https: or http:. Nothing else (no file:, data:, extension pages).
  2. A login saved with an https: URL is never filled on an http: page.
  3. The host matches exactly, case-insensitively. A single leading www. and a trailing dot are ignored on both sides. There is no parent-domain or sibling- subdomain matching: a.github.io never matches b.github.io, and github.com.evil.io never matches github.com.
  4. The port must match exactly (an explicit default port is treated as the default).

A saved URL without a scheme is treated as https://.

Passkeys (apps/extension/src/engine/passkey.ts)

The extension includes a software WebAuthn authenticator:

Web vault

The API serves a standalone web vault at / so any device with a browser can use the same account. It runs the same engine in the page:

Vault health checks (apps/extension/src/engine/watchtower.ts, breach.ts, twofa.ts)

Weak, reused and http:// checks, and "two-factor available" (a bundled list of sites that support one-time codes, generated from the MIT-licensed 2fa.directory dataset by apps/extension/scripts/update-twofa.mjs), run entirely on the device.

Two checks contact Have I Been Pwned and are off by default (Settings > Vault health checks). They run in the UI only when Vault health is opened or "Check now" is pressed:

Server hardening (apps/api/src/server.ts)

Security review: fixed before 1.0

An internal review before release found the following; all are fixed in 1.0.0 and the design above describes the fixed behaviour.

  1. Ciphertext was not bound to its location. Item, vault-name and keyset ciphertexts now carry associated data (vault ID + item ID + field; vault ID; account ID), and item IDs are generated client-side. A server can no longer swap or move ciphertext between items, vaults or accounts undetected.
  2. A server could plant a vault whose key it knew. Own vault keys are now wrapped with a symmetric key that exists only inside the encrypted keyset, bound to vault ID + account ID. Public-key sealing (used only inside sharing invitations, which must be verified and accepted) binds both public keys into the KDF and the associated data.
  3. The client trusted the server's copy of its public key. The public key is now derived from the private key; a mismatching server copy is rejected.
  4. Decoy logins were distinguishable from real ones (ID format, timing, and a registration path that checked existence before validating). Decoys now look and cost the same, and registration validates first.
  5. Per-email lockouts let anyone lock a user out, and the client opened a new server session on every unlock. Lockouts are now keyed by email + IP with a looser per-email backstop, and the session token survives lock/unlock.
  6. Changing the master password did not require a fresh login. It now does.
  7. Inline UI could be clickjacked. The fill menu, save prompt and passkey prompt now hit-test, check opacity, ignore clicks for 500 ms after appearing, require trusted-event focus, and live on an always-mounted host.
  8. Secret Key parsing was lenient (invalid characters were skipped). It is now strict.
  9. Clients accepted any KDF iteration count from the server. They now require at least 650,000.
  10. Plain http:// server URLs were accepted. Only https:// is allowed now, except localhost for development.
  11. Autofill ignored the port when the saved URL had none. Ports must now match exactly.

Account recovery (packages/core/src/recovery.ts, apps/api/src/recovery.ts, apps/extension/src/engine/recovery.ts)

AutoPass still cannot reset a master password on its own: nobody, including whoever runs the server, holds anything that can decrypt a vault. What it offers is an optional, user-held recovery code that does the reset, without the server learning anything new. It is not organizer- or family-based recovery: no other person or administrator can reset an account.

The code and what it protects

Setting it up

Settings > Account recovery (also offered, skippable, right after the Secret Key during signup). The master password is re-entered and checked on the device. For a synced account the client then makes a fresh SRP login and the server accepts PUT /v1/account/recovery only on a session less than 10 minutes old, like a password change. The server stores the recovery SRP salt and verifier and the encrypted blob, replacing any previous code; replacing it also ends any recovery session opened with the old code. The code is shown once, with copy and a Recovery Kit download that also holds the Secret Key.

A local-only account keeps the salt, verifier and blob in the device profile instead, so "Forgot master password?" works on that device with no network. Connecting a sync server uploads them (with the connect's fresh session) and the device then drops its copy. After "Stop syncing this device" the device no longer holds a usable copy, and Settings says recovery isn't set up there until a new code is made.

Recovering

"Forgot master password?" on the lock screen or the sign-in screen asks for the email, the recovery code and a new master password.

  1. Recovery login (POST /v1/auth/recovery/start, /finish) is SRP-6a against the recovery verifier, built exactly like password login: decoys for unknown emails, the same per-IP rate-limit bucket, and failures count toward the same (email, IP) lockout as wrong passwords (both directions, tested). An account without recovery gets a decoy salt and verifier stable per email, and the account ID that password login already reports for that email, so the endpoint can't tell "no account", "no recovery set up" and "wrong code" apart (tested). The client checks the server's proof M2.
  2. A successful proof returns a recovery token, not a session: it lives in its own table, expires after 10 minutes, stops working if the code is replaced, and every other route treats it as unauthenticated (tested route by route). It can only GET /v1/recovery/account (the server record and the encrypted blob) and POST /v1/recovery/reset.
  3. The client opens the blob with the code, checks that the keyset's public key matches the account's, and re-keys exactly like a password change (new KDF salt, keyset re-wrapped under the new AUK, new SRP verifier). Nothing else is re-encrypted.
  4. The reset applies that change and, in the same transaction, consumes the code (recovery is off), and revokes every session, normal and recovery. Other devices must unlock with the new master password before they sync again.
  5. The device signs in (or, on a locked device, unlocks keeping its cache and queued changes) and immediately makes a new recovery code, shown once.

A local-only account does steps 3 to 5 on the device: the code opens the profile's blob, and the new password's keyset and a new code's blob replace the old ones in one write.

Without a recovery code

Two-step verification (apps/api/src/mfa.ts, apps/api/src/twilio.ts, apps/extension/src/engine/mfa.ts)

Optional, per account, for synced accounts (Settings > Two-step verification). After the master password, signing in asks for a code from an authenticator app (TOTP) or a text message, or one of 10 backup codes.

What it protects, and what it doesn't

Sign-in

  1. srp/finish (and the recovery login's recovery/finish) verifies the proof exactly as before: lockouts, rate limits and decoys are unchanged, and the server still proves itself with M2, which the client checks before anything else.
  2. If the account has two-step verification on and the request doesn't carry a valid trusted-device token for that account, the answer is {M2, mfaRequired, mfaToken, methods, phoneHint} and no session. mfaToken is 256 random bits, kept (as a SHA-256 key) in the server's guard for 5 minutes, single use, and bound to the account and the client's IP address; a token from another address is treated as unknown.
  3. POST /v1/auth/mfa/verify {mfaToken, method, code} returns the session (or, for a recovery login, the recovery token). 5 attempts per pending sign-in, after which it is gone; wrong codes also count toward a per-account limit of 10 per 15 minutes across all sign-ins (then 429). Failures look alike ("that code didn't work"; an unknown, expired, used or foreign token is "sign-in expired, start again").
  4. A session made this way counts as a fresh login (credential changes are allowed for 10 minutes, like after any login).

All of this short-lived state (pending sign-ins, attempt counters, text-message counters, unconfirmed setups) is kept through the guard, so it is shared by every API process when Redis is configured.

Every client flow handles it the same way (engine/mfa.ts): the login throws a typed error; the UI asks for the code; the client sends it, keeps the resulting session for at most 2 minutes in memory-only storage, for the same server, email and login secret, and runs the original operation again, which uses that session once instead of logging in again. This covers signing in on a new device, unlocking after the password changed elsewhere, connecting a server, account recovery (a recovery reset now returns a session for the device doing it, so one code covers the whole recovery), and the fresh login before a credential change. Background re-authentication never waits for a person: if the server asks for a code during sync, the device stops syncing with "Verify it's you to keep syncing", remembers that (memory only), and makes no further login attempts until the user clicks it and enters a code.

Methods

Turning a method on or off and making new backup codes need the master password (checked on the device) and a fresh login, like a password change. Turning off the last method turns two-step verification off and deletes the backup codes and every trusted device.

Trusted devices

"Trust this device" (ticked by default) makes the server issue a 256-bit device token, stored as an HMAC on the server and in the device's local storage (per server and account; chrome.storage.local in the extension, localStorage in the web vault). Every later login from that device sends it, and the server skips the code if it is valid for that account. It expires 30 days after it was last used (sliding). It is forgotten when the device signs out, when it's signed out or forgotten in Settings > Devices, by "Sign out everywhere else", and when two-step verification is turned off. Anyone who can read that device's storage gets the token, so trust only devices you alone use. The device that turns two-step verification on is trusted too, unless unticked.

Decoys and what's observable

A pending two-step sign-in is created only after a correct SRP proof, and a decoy's proof (unknown email) never verifies. So up to the point where a real account would ask for the code, an unknown email, a wrong password and a two-step account all look the same (srp/start is unchanged; srp/finish fails with the same 401), and whether an account has two-step verification, which methods, and the phone hint are visible only to someone who has its master password and Secret Key (tested). The code-step endpoints say nothing without a real pending sign-in. Timing differs between "correct password, no 2FA" and "correct password, 2FA" only after a correct proof.

What Twilio sees

The account's phone number, when a code is sent and checked, and the server's Twilio account. Not the email address, the account ID, or anything from the vault.

Known limitations

  1. No independent security audit yet. The cryptography uses standard Web Crypto primitives, but the protocol composition and the hand-written SRP implementation have only been reviewed internally and tested by the suite in this repository.
  2. Registration reveals whether an email is taken. POST /v1/account returns 409 for an existing email. Login resists enumeration; registration does not. Set AUTOPASS_REGISTRATION=closed once your accounts exist to remove this oracle.
  3. Email addresses are not verified. Nothing is ever emailed; the address is only a login name.
  4. No rollback protection per item. A malicious server cannot forge or move an item, but it can serve an older, genuine version of an item (or withhold recent changes).
  5. PBKDF2 is not memory-hard. GPUs and ASICs attack PBKDF2 more efficiently than Argon2id or scrypt. The Secret Key is what makes the server's data uncrackable; the KDF matters only if the Secret Key also leaks.
  6. The Secret Key is stored in plain form on trusted devices (chrome.storage.local in the extension, localStorage in the web vault) so a device can unlock with just the master password. Malware or a hostile extension that can read that storage gets the Secret Key, leaving only the master password protecting the cached ciphertext.
  7. The web vault trusts the server for its code. A compromised server could serve modified JavaScript that captures the master password the next time someone unlocks in the web vault. Use the extension where that matters.
  8. Clipboard clearing is blind. AutoPass clears the clipboard 90 seconds after a copy, even if you have since copied something else.
  9. Extension fingerprinting. The content-script chunk is listed in web_accessible_resources, so a website can detect that AutoPass is installed.
  10. Passkey RP-ID checks use a small built-in public-suffix list, not the full Public Suffix List. Uncommon shared-hosting suffixes are not covered.
  11. Attachment sizes and counts are visible to the server. File contents, names and types are not, but exact sizes (unpadded) and how many files each item has are.
  12. A conflict copy does not get its own copy of attachments. If an edit loses a sync conflict, the "(conflict copy)" item lists the same files, which stay bound to the original item; they can't be opened from the copy, and purging the original deletes them. Download them from the original item.
  13. Large uploads on slow connections can time out. The server allows 30 seconds per request; a 25 MB file needs roughly 7 Mbit/s upstream. A timed-out upload stays queued and retries on the next sync.
  14. Sharing limits. Rotation after a removal protects only what is written afterwards and doesn't hold up against a removed member colluding with the server, roles are enforced by the server rather than cryptographically, fingerprints must be compared by the people themselves (nothing pins a fingerprint once seen), and the share-link viewer's code comes from the server. See "Sharing" above.
  15. Shared limits depend on Redis, and the database is still single-host. Without REDIS_URL, rate limits, lockouts and pending sign-ins are per process: they reset when the server restarts, and N processes give an attacker N times the guesses. With Redis they are shared and survive API restarts, but the bundled Redis keeps nothing on disk, so restarting Redis resets them; and while Redis is unreachable or out of memory each process falls back to its own in-memory limits (logged, and shown as "redis":"down" on /healthz), so during an outage the limits are per process again. Someone with access to Redis can't read handshakes or see which emails or IPs are being limited, but can see how many keys there are and their timing, and can delete entries to reset limits; protect it with a password on a private network. All processes must share the same server secret. The SQLite database is still one file on one host, so every API process has to run where that file is.
  16. The sign-in proof format changed once, without versioning. Since the move to the RFC 5054 proof form, clients built before it can't sign in to an updated server (and the other way round) until both are updated. Nothing older was ever published.
  17. A local-only vault exists on one device. Until sync is turned on, losing or resetting the browser profile (or clearing site data for the web vault) loses the vault. Export encrypted backups, or connect a sync server.
  18. Reconnecting after a local password change. If sync is turned off, the master password is changed locally, and the same server is connected again, the server still holds the old verifier and the connect is refused; connect to a fresh server instead.
  19. Breached-site flags depend on dates the vault can't fully know. A login is flagged when its site's breach was added to the catalogue after the password was last changed (from password history, else the item's last-modified time, which also moves on unrelated edits and is the import time for imported items), so some older breaches are not flagged. Breaches are matched to the breached domain and its subdomains only.
  20. A recovery code is a second way into the account. It is as powerful as the master password and Secret Key together, and it is only as safe as where it is kept. Lockouts and rate limits slow online guessing, but the code's 160 bits are what make guessing hopeless. Recovery revokes sessions but cannot wipe other devices: until a device syncs, its offline copy still opens with the old master password (as after any password change).
  21. Recovery status is weakly observable. The recovery start endpoint's salt for an account changes when a code is set up, replaced or used, while a decoy's salt never changes. Someone polling it over time can tell that something changed for that email, not whether recovery is set up.
  22. Device unlock is password-equivalent on its device for up to 14 days (see "Unlock with your device"): someone who passes the device's user verification and has the browser profile can open the vault and log in to the server. A password change on another device revokes this only once the device syncs.
  23. Device unlock depends on the browser's WebAuthn PRF support. Browsers or authenticators without PRF (or without user verification) can't use it; the option says so instead.
  24. Two-step verification guards the server, not the data. It doesn't stop a device that already has the vault from opening it, and a signed-in, unlocked device can sign in again on its own if it is trusted. Text-message codes can be intercepted (SIM swap); prefer an authenticator app. Recovery with a recovery code also needs the second factor (or a trusted device, or a backup code), so keep backup codes with the Recovery Kit.
  25. Two-step secrets depend on the server secret. Authenticator secrets are encrypted, and backup codes and trusted-device tokens are HMACs, under the per-deployment secret; a copy of the database together with that secret reveals the authenticator secrets, and changing the secret makes every account's two-step verification unusable.
  26. An account whose only method is text messages can't verify on a server without Twilio configured (for example after the operator removes it), except with a backup code.