Keep session passwords in an end-to-end encrypted vault - #111
Merged
Merged
Conversation
A session's password lived only in the browser that chose or was told it, so clearing that browser, opening the session elsewhere, or closing the terminal that printed it could lose the session for good. A frame that failed to decrypt also deleted the only stored copy. Each account now has a session vault: a P-256 key pair whose private half the service stores only encrypted under a random vault key, itself wrapped by a 160-bit recovery key shown once and never sent. Session passwords are sealed to the account key (ECDH, HKDF-SHA256, AES-256-GCM bound to the session and recipient), so any browser that unlocks the vault opens them and the service opens none of them. - Web: required one-time setup with a confirmed recovery key; unlock in a new browser; the unlocked key is a non-extractable CryptoKey in IndexedDB; sign-out locks it; reset needs a fresh sign-in and carries running sessions across. - CLI: seals each encrypted session's password to the vault at registration, pinning the key the browser hands over at `shell login` (or on first use for machines linked earlier) and refusing a key that changed. - Sharing seals to colleagues' vault keys and asks before sealing to a key that changed since it was last used. - Migration from before the vault is idempotent: cached passwords and shares sealed to old browser keys move into the vault once, never overwriting a copy that may be right with one that is only a guess. - A password is no longer deleted on a decryption failure; only an unproven guess is, and every saved source is tried before asking. - Browser-started sessions use 128-bit passwords and are refused on machines that cannot receive one.
added 2 commits
September 11, 2026 14:08
Assigning a session from the owner's browser now seals its password to the people added, so whoever is responsible opens it without being told the password. An assignee made elsewhere is sealed to automatically only if this browser already trusts their key; otherwise the session page offers it to the owner in one click, so the service's assignee list alone never decides who can read a session. From review: - A password that opened a session is sealed into the vault only when the vault does not already hold exactly it, and a changed share no longer rebuilds an open terminal. Together these ended a reconnect every poll. - A typed password is written only once it has opened the session, and a guess can never replace a password known to work. - A vault reset carries finished sessions across too. - Everyone holding a copy can be shared with again, for copies sealed to an old browser key or a vault since reset. - A proven password is tried before a vault share, which anyone holding the public key could have made. On phones, the consent and vault buttons no longer collapse to the height of their text, and the recovery key field shows the whole key. A gitleaks allowlist covers the two fixed test vectors, whose keys exist only for those tests.
Sealing a session's password to any assignee whose key this browser had used before let an owner or admin, who can assign anyone to any session including themselves, read every session of someone who had once shared one with them, with no action from that person. The assignee list now never seals a password on its own. The owner assigning from their own browser records the people as chosen and seals to them; a seal that fails is retried from that record. Anyone assigned by someone else is offered to the owner on the session page, in one click.
This was referenced Sep 11, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A session's password lived only in the browser that chose or was told it. Clearing that browser, opening the session on another device, or closing the terminal that printed it could lose the session for good, and a frame that failed to decrypt deleted the only stored copy.
What
Each account gets a session vault: a P-256 key pair whose private half the service stores only encrypted under a random vault key, which is itself wrapped by a 160-bit recovery key shown once and never sent. Session passwords are sealed to the account key (ephemeral ECDH, HKDF-SHA256, AES-256-GCM bound to the session id and recipient), so any browser that unlocks the vault opens them and the service opens none of them.
Service (
app/server)007_account_keys.sql(additive,CREATE TABLE IF NOT EXISTS).GET/POST /api/vault: create-only; reset needs a sign-in within 10 minutes and the current version, applied in one conditional statement.GET /api/account/keyfor the CLI.POST /api/sessionsaccepts the CLI'sowner_share. It is shape-checked and never fails a registration.accountKey. Non-owners may store a share only for themselves. Owners seesharedWith.Web (
app/src)CryptoKeyin IndexedDB. Sign-out locks it.CLI (
internal/account,cmd/shell)shell loginloopback callback. Machines linked earlier pin the key on first use.Accounts and sessions from before the vault
Migration is idempotent and runs in each browser until done:
Security review
A focused review of this diff found nothing exploitable above the 0.8 confidence bar. One hardening point it raised is applied here: a verified cached password is tried before a vault share, since a share is sealed with an ephemeral key and cannot be authenticated.
.github/SECURITY.mddescribes the vault's scope and its trust-on-first-use points.Verification
go vet, gofmt, andgo test ./internal/... ./cmd/...pass.Deploy order
Migrate first (007 is additive, so the current Worker is unaffected), then deploy the Worker. The CLI half ships with the next CLI release; until then new sessions from older CLIs are saved to the vault the first time they open in a browser.
Since review
Assignees can open what they are assigned.
Fixed from the code review:
Phone: consent and vault buttons no longer collapse, and the recovery key field shows the whole key.
Secrets:
.envor browser state file is in any commit.