Yesterday's post on shell.online said that when the owner's browser sees a session it holds the password for, it seals that password once per organization member who has published a key. That sentence was accurate, and it was describing a bug. The pull request merged to main yesterday, titled "Fix what production found", closes what it describes as thirteen reports from using the app. One of them was that sentence.
The report was about assignees. The bug was not.
The report read "passwords can be bypassed on Mobile by changing assignee". Changing the assignee was not the bypass. The shareAnyPending routine in the workspace page ran on every poll, took every member with a published browser key, and sealed the session password to each of them as soon as the owner's browser saw a session it held the password for. Assigning a session to a colleague looked like it granted them something. They had held the password all along. Assignment only changed who could type.
Nothing about the envelope was wrong. The construction is still ECDH P-256 into HKDF-SHA256 into AES-GCM, done with WebCrypto in the browser, and the accounts service still stores one opaque blob per recipient and can open none of them. But the recipient list is the whole of the access rule, and the recipient list was "the team".
Three changes, all in the repo
The New Session form now has a fieldset headed "Who can open it", one checkbox per reachable teammate, and it starts empty. The comment above it in the shell.online source puts it plainly: "Starting from nobody makes the answer something chosen rather than something discovered later." The password is still generated in the browser rather than typed. Ticking a name replaces inventing a secret and passing it around by hand, which was the other half of the same report. A teammate who has never opened the app in a browser has no published key, so the form lists them separately and says there is nowhere to send them a password.
The rule moved out of the component. sealTargets in app/src/lib/session-share.ts takes the roster, the current user, the chosen list and the set already sealed, and returns who still needs it. Its test file states the property that matters in a comment: "not choosing somebody is not sharing with them." Chosen but holding no key is skipped. Chosen but yourself is skipped. Chosen but not in the team is ignored. The server-side checks were already in place. The keys route refuses any recipient who is not an organization member, and only the session owner may put shares at all.
The audience is stored next to the password in localStorage, scoped to the signed-in account. The service is not asked who holds a copy, because it is not trusted with the password and should not be the record of who can read it either. The same applies to a session started from a terminal on a linked machine: a password typed into the gate is now kept, and its audience starts empty.
The button that was left out
The session page has a "Who can open it" section and it only adds people. There is no remove. The comment on the SessionAudience component explains the choice: removing somebody "would not reach into their browser and take back the copy they hold, so a control that offered it would be describing something that did not happen." A share link plus its password is a bearer capability, as the terms say. The honest form of revocation is to stop the session and start another one with a different list. A button that appears to revoke is worse than no button.
Multiple assignees, merged this morning
A separate change landed today. A session can now be assigned to several people at once. canEdit in session-view.ts grants typing to the owner and to anyone in the assigneeUids array, and it still reads the old single assigneeUid field for clients that predate the change. Migration 006 adds the array column to PostgreSQL, backfills it from the single column, and keeps the single column populated with the first assignee, so the previous Worker remains safe to roll back to. The terms were updated in the same PR: a session "has an owner and may have multiple assignees, and only they can edit it".
Put the two changes side by side and the model is now separable. Assignment decides who may type, and the session owner or a team owner or admin can change it. The audience decides who can open the session at all, and only the person holding the password can extend it. Before this week those two lists were tangled, and the tangle is what made the report look like it was about assignees.
The bug nobody reported
The same PR fixed something no report mentioned. The clipboard component derived every session's password on every poll, because its effect depended on the session object the poll replaces rather than on the sealed bytes inside it. The workspace polls every four seconds. That is one ECDH derivation per session every four seconds, on a phone, and the PR names it as the likeliest cause of the report that "tabs are a bit slow on mobile". The effect now keys on the session id, the sender public key and the ciphertext.
The default Pilot starts from
The Pilot Protocol README lists two lines under security that describe the posture shell.online has now arrived at for session passwords: nodes are private by default, and reachability comes from a signed mutual trust handshake. Address an agent on Pilot without that handshake and the daemon answers "hostname not found or no mutual trust". A peer you did not name has no path to you. Between the first release carrying the web app and yesterday's fix, a browser terminal on shell.online had the opposite default, and the fix was to make the list of who can reach a session a list somebody actually wrote.
Top comments (0)