Public Access
Merge branch 'main' into the vaults screen, and let it rotate keys too
Main built vault key rotation while this branch was reshaping the screen that would drive it, so the two met in the same three files. Every other conflict was textual and resolved by taking both; these are the ones where a decision had to be made. **The view model.** Main taught TeamsViewModel three things and this branch had renamed and rewritten it into VaultsViewModel. All three are ported rather than dropped, because each is a behaviour rather than wording: adding somebody now wraps the vault to them on the spot instead of leaving SHARE KEY to be pressed, removing somebody rotates the vault and hands the new key to whoever is left, and a share reports how many generations were wrapped. The session calls they reach — ShareTeamVaultsAsync and RekeyTeamVaultsAsync — are scoped to a membership list rather than to one vault, and they are called that way here rather than narrowed: adding somebody is a change to the list, so every vault the list carries is one they can now fetch. This screen makes lists that carry one vault, so the sentences name one; where a list carries several, naming them all is the honest report, and the members section already says the list is shared. AddMemberAsync ran two lines over the length limit once the sharing was in it, so the calls behind it moved to AddOrInviteAsync and the three-way refusal to WhyNobodyCanBeAdded — the command reads as its guards now, which is what it was before the sharing arrived. **The tests.** Main's four new cases are ported to the vault-first API, including the one that matters most: the tampered key log is corrupted *before* the add, because the add is now a route to a wrap and a test that corrupted it afterwards would be asserting about the manual route only. SelectingAVault_ListsWhoHoldsAKey now expects two holders rather than one — main's fake records the creator's own self-grant, and a key-holder list that omitted it would show the one person who can certainly open a new vault as somebody who cannot. **The README.** The limits list is six rather than four or five: main's rotation entries and this branch's "a vault cannot be deleted" describe different things and both are true. "The rekey is flagged, never performed" is gone, since it is now performed, and M3 reads *Done* rather than *Done, except rekey*. One thing worth writing down that neither side had. An invitation claimed at sign-in still leaves the key owed, where an add does not: at the moment an invitation is issued there is no account and no published key to wrap to, and the claim happens on the invitee's machine, which holds nothing. Manual check 12.1 says so, because a reader who knows adding shares would otherwise read that step as stale. 1561 tests pass.
This commit is contained in:
@@ -186,10 +186,15 @@ signing in again brings it all back; the count in the confirmation is the one th
|
||||
else. Your session at the identity provider is *not* ended — DodoSSH has no way to end it — so on a machine
|
||||
that is not yours, sign out there too.
|
||||
|
||||
Two of M1's known gaps are visible immediately, so they are worth expecting rather than diagnosing: password
|
||||
authentication asks for the password every time, because nothing in the interface can create a vault
|
||||
credential yet (they do sync — there is just no editor for one); and unlock asks for the passphrase on every
|
||||
launch, because no device key is registered.
|
||||
**Neither a password nor a passphrase has to be typed twice**, and both ways out of that are opt-in. A
|
||||
password typed to connect is typed once: tick **Remember this password** under the box and it is saved to
|
||||
your keychain and bound to that host the moment the remote accepts it — or add one outright with **+
|
||||
PASSWORD** on the Vault screen. And unlock can be a Windows confirmation instead of the passphrase:
|
||||
Preferences → *This machine* → **REGISTER** keeps this machine's device key in the TPM, so a later launch
|
||||
offers **USE WINDOWS HELLO** on the unlock card. A machine with no TPM — and any desktop that is not
|
||||
Windows — is offered neither button and keeps asking for the passphrase, which Preferences says out loud
|
||||
rather than leaving you to notice. The passphrase never stops working either way: a declined confirmation
|
||||
leaves the box exactly where it was.
|
||||
|
||||
### Moving files
|
||||
|
||||
@@ -297,19 +302,31 @@ rather than only in the ID token or the userinfo response.
|
||||
they become owner and you become an admin, in a single transaction. Not two role changes — promoting first
|
||||
leaves it owned twice, demoting first leaves it owned by nobody, and there is nobody with the authority to
|
||||
finish a transfer that stopped in the middle. You are demoted rather than removed, so you keep your vault
|
||||
key grants; removing you would revoke them and flag the vault for rekey, and somebody handing a vault over
|
||||
is usually staying in it.
|
||||
key grants; removing you would revoke them and rotate the vault, and somebody handing a vault over is
|
||||
usually staying in it.
|
||||
|
||||
Five limits, stated rather than discovered:
|
||||
Six limits, stated rather than discovered:
|
||||
|
||||
- **A vault cannot be deleted.** Nothing in this product removes one, and the server refuses to archive the
|
||||
membership list behind a vault that still exists — a shared vault is readable *because* of membership, so
|
||||
archiving it would take the vault away from everybody holding a key, including you, quietly and all at
|
||||
once. The screen says so where you would otherwise go looking for the button.
|
||||
- **Removing a member is not retroactive.** It revokes their grants and flags the vault for rekey, and
|
||||
blocks future reads. Everything they already pulled is on their machine. Rotate the SSH credentials that
|
||||
matter — that is the actual remediation, and it is why there is no button labelled anything stronger.
|
||||
- **The rekey is flagged, never performed.** See the milestone note above.
|
||||
- **Removing a member is not retroactive.** It revokes their grants, rotates every vault behind that
|
||||
membership list your machine can open, and hands each new key to the members who are left — so nothing
|
||||
written from that point on is readable to them. Everything they already pulled is still on their machine.
|
||||
Rotate the SSH credentials that matter — that is the actual remediation, and it is why there is no button
|
||||
labelled anything stronger.
|
||||
- **A rotation moves the vault's contents too, and says so when it could not.** The key changes first, in
|
||||
one server transaction; then every item already stored is re-sealed under it, so the key somebody left
|
||||
with opens nothing that is still here. Existing items keep working throughout — everybody still in the
|
||||
vault holds the older keys as well as the new one, which is what stops a half-finished rotation making a
|
||||
vault unreadable, and what makes the pass safe to interrupt and run again. An item somebody else was
|
||||
editing at that moment is left for the next pass, and the message tells you which of the two you got.
|
||||
See [ADR 0010](docs/adr/0010-vault-key-rotation.md).
|
||||
- **Adding somebody shares the vaults you can open, including their history.** Membership is still one act
|
||||
and a key is still another — nothing changed about that — but the client now performs the second one for
|
||||
you, wrapping every generation it holds so the new member can read the vault back to its first item. A
|
||||
vault your machine holds no key to is skipped and says so; somebody who holds it has to share that one.
|
||||
- **Host key trust stays in your personal vault.** A pin approved for a shared vault's host is recorded and
|
||||
used from your own vault, not the shared one, so a colleague cannot pre-approve a fingerprint that your
|
||||
client will then trust silently for a host you defined. The cost is that each member approves a shared
|
||||
@@ -372,8 +389,20 @@ design's blue-tinted `#E3E7F4`, and the type scale is a point larger.
|
||||
|
||||
File transfer **is** here now, in the shape scoped storage allows: one remote pane and the queue, over
|
||||
either an SFTP host or a bucket. There is no local pane, because there is no browsable local filesystem to
|
||||
put in one — moving a file *in* from the phone needs the system document picker and is the next piece of
|
||||
work rather than a thing the screen pretends to do. Hosts and groups are made and corrected here now, from
|
||||
put in one. So the way in is **ADD FILES**, which is the system document picker: point at a document
|
||||
wherever it lives and it goes to the directory showing, rather than choosing on the left and pressing an
|
||||
arrow. What Android hands back is a `content://` URI with no path behind it and no promise of a seek, so
|
||||
the document is copied into the app's own cache and the copy is what the queue moves — which is what lets
|
||||
a stopped upload resume from where it stopped. The copy is deleted when the transfer finishes, kept while
|
||||
it is stopped so RESUME has something to read, and swept at the next launch.
|
||||
|
||||
**SAVE FILE is the way back out**, and it is the system's save picker for the same reason: there is nowhere
|
||||
this application could put a file that you would then be able to open. You choose where it goes before the
|
||||
transfer starts, the download runs into the cache, and the finished bytes are copied out to the document you
|
||||
chose. That order has one visible cost, and the screen says it rather than leaving it to be discovered: the
|
||||
picker creates the file when you dismiss it, so a download that then fails leaves an empty one there. The
|
||||
alternative is a picker that appears minutes later over whatever you moved on to — and often while the app
|
||||
is in the background, where Android will not show one at all. Hosts and groups are made and corrected here now, from
|
||||
a floating + on the Hosts screen, and both editors are cards in the list's own row rather than dialogs, so
|
||||
the form never covers the thing it is about. The keychain has no editor of its own: SSH keys and buckets are
|
||||
created on the desktop and sync down, and the phone will delete an item — behind the same counted
|
||||
@@ -518,10 +547,15 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
host key approved at that shell's prompt reaching the second machine as well. See
|
||||
[End-to-end verification](#end-to-end-verification).
|
||||
|
||||
Known gaps in the client, stated rather than implied by the interface: nothing in the interface can create
|
||||
a vault credential yet, so password authentication still asks for the password each time — SSH keys *are*
|
||||
editable, and binding one to a host is the way to connect without typing anything; and no device key is
|
||||
registered, so the passphrase is needed on every launch until the OS keystore is wired.
|
||||
The two gaps this milestone shipped with have both closed since. A vault credential can be created — from
|
||||
the Vault screen, or from the REMEMBER tick beside the connect password, which saves it and binds the host
|
||||
to it once the remote has accepted — so password authentication asks once rather than every time. And a
|
||||
device key is registered where the machine can hold one: Windows keeps it in the TPM under a CNG policy
|
||||
that makes the consent dialog a condition of *using* the key rather than a prompt this application draws,
|
||||
which is stronger than [ADR 0007](docs/adr/0007-device-key-protection.md) originally described and is why
|
||||
that ADR was corrected. What is left is the floor rather than a gap: a machine with no TPM, or a desktop
|
||||
that is not Windows, gets a store that reports itself unavailable and keeps asking for the passphrase —
|
||||
the honest answer rather than a degraded one.
|
||||
|
||||
Host key trust *is* in the vault, which is what makes trust-on-first-use worth having: a fingerprint
|
||||
approved on one machine is approved on all of them and survives a restart, and the server cannot drop a
|
||||
@@ -582,9 +616,9 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
directories, an interrupted **upload** starts again rather than resuming (an object cannot be written from
|
||||
the middle), and a rename is a copy and a delete rather than one atomic operation. Downloads do resume — a
|
||||
ranged GET is part of the protocol, which is the one place a bucket beats SFTP.
|
||||
- **M3 — shared vaults**, sharing, ACLs. *Done, except rekey.* Membership with roles, a public-key
|
||||
directory, the append-only key log served for clients to verify against, shared vaults, and vault key
|
||||
grants wrapped by a client and stored opaquely by the server. `VaultAccessService` now resolves team
|
||||
- **M3 — shared vaults**, sharing, ACLs. *Done.* Membership with roles, a public-key directory, the
|
||||
append-only key log served for clients to verify against, shared vaults, and vault key grants
|
||||
wrapped by a client and stored opaquely by the server. `VaultAccessService` now resolves team
|
||||
membership to permissions, so a viewer may pull and may not push; the desktop client reads and syncs
|
||||
every vault it holds a key for, and a real VAULTS screen replaces the placeholder. The screen is
|
||||
vault-shaped rather than team-shaped: naming a vault makes the membership list that carries it, so the
|
||||
@@ -595,20 +629,30 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
knowing before you rely on it; the reasoning is in
|
||||
[ADR 0009](docs/adr/0009-team-access-model.md).
|
||||
|
||||
**What is deliberately not here: the rekey itself.** Removing a member revokes their grants and flags
|
||||
every team vault `RekeyRequired`, and nothing acts on that flag. A rekey re-wraps every item's data key
|
||||
under a fresh vault key and can only be performed by a client that holds the current one; that is M5's
|
||||
key rotation. Until it lands the flag is what the interface reads to say a rotation is owed, which is
|
||||
more honest than a button that only appears to do it.
|
||||
**Membership changes now move the keys, not just the flag.** Adding somebody wraps every team vault the
|
||||
adding machine can open to them — every generation of each, so they can read the vault's history and not
|
||||
only what happens next. Removing somebody revokes their grants, advances each vault it can open to a
|
||||
fresh key generation in one server transaction, and wraps that key to the members who remain. What a
|
||||
rotation buys is exact: everything written from then on is unreadable to the person who left. The items
|
||||
already stored are then re-sealed under the new key as well, item by item and resumably — which is safe
|
||||
to do incrementally precisely because a vault at mixed generations stays readable. A change queued before
|
||||
the rotation is re-sealed as it is pushed, so nothing reaches the server under a superseded key at all.
|
||||
See [ADR 0010](docs/adr/0010-vault-key-rotation.md).
|
||||
|
||||
**Ownership transfer is here, and it is one write rather than two.** The member you name becomes owner
|
||||
and you become an admin, in a single transaction — because ownership is sole, so promoting first leaves
|
||||
the team owned twice and demoting first leaves it owned by nobody, and there is nobody left with the
|
||||
authority to finish a transfer that stopped in the middle. Nothing else is touched: you keep your vault
|
||||
key grants, because removing the outgoing owner would revoke them and flag every team vault for rekey,
|
||||
which is a much larger act than the one being asked for.
|
||||
- **M4 — hardening and ops**, packaging, self-hosting guide.
|
||||
- **M5 — multi-provider OIDC**, key rotation, per-item content keys.
|
||||
key grants, because removing the outgoing owner would revoke them and rotate every team vault, which is a
|
||||
much larger act than the one being asked for.
|
||||
- **M4 — hardening and ops**, packaging, self-hosting guide. *Decided ahead of the work, because the first
|
||||
release takes it irreversibly:* who signs the client and where it comes from.
|
||||
[ADR 0011](docs/adr/0011-android-distribution.md) puts the release key with the project rather than with
|
||||
a store, and rules out the arrangement a self-hosted product reaches for by default — the deployment
|
||||
serving the client binary, which hands it to the one party the whole trust model is about. An installed
|
||||
Android app can only ever be updated by a package signed with the same key, so this is the first
|
||||
release's decision to make and nobody else's afterwards.
|
||||
- **M5 — multi-provider OIDC**, identity key rotation, per-item content keys.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
Reference in New Issue
Block a user