From bee6202949010c68ee532f85c896f1768554762d Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Tue, 4 Aug 2026 16:04:15 +0200 Subject: [PATCH 1/7] Let a host be moved to another vault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one thing the host editor's vault picker has always been unable to offer, and the comment beside it said so: an existing host's vault was not a field because the two vaults are encrypted under different keys. That is still true. What changed is that it is no longer a reason to have nothing. **A move is a copy and a tombstone, and it cannot be anything else.** A payload is sealed under its vault's key and its AAD binds the vault, the entity id and the item version, so no edit moves one and no server call could — the server holds ciphertext it cannot read. What crosses is the plaintext, in this process, between an unwrap under one key and a seal under another. VaultItemRepository gained MoveAsync for it, so the three decisions below live in one place with their reasons rather than being re-derived at each call site. The item takes a new id. Keeping it would put one entity id in two vaults, and the item table is keyed on the type and the id rather than on the vault — so the destination's row and the source's tombstone would be the same row, and the move would delete what it had just written. The write comes first and the tombstone second, which decides what an interruption leaves: a copy in both vaults, visible and deletable, rather than a tombstone with nothing on the other side. Both are queued rather than sent, so the window is a crash between two local writes; it is still worth being on the survivable side of. Two activity lines rather than one, because that is what the two vaults actually record. A single "moved" line would have to be written to one of them and would be missing from the other's history. **The group and the tags stay behind, and that is the half that makes this honest.** Both are items of the vault the host is leaving: the editor's group picker offers one vault's groups and the chips are drawn from one vault's tags. A host carrying either across would resolve it on the machine that moved it — groups and tags are resolved over every readable vault — and dangle for everybody else in the destination. The mover and their colleagues would be looking at two different hosts. Cleared and reported beats carried and invisible. The key or password binding is kept, and the difference is not inconsistency. Those genuinely resolve across vaults — one key on twenty hosts in three vaults is the arrangement they exist for — so clearing them would take a working host and make one that cannot connect. What the message does instead is name a binding that is now outside the destination, because that is precisely what the other members of it will not be able to resolve. **It is not in the editor**, on either head: the desktop puts it in the detail pane's ⋯ menu above the separator Delete sits below, and the phone beside EDIT. A picker inside the form would move a machine as a side effect of correcting a port, which is the bug the editor's own vault picker was fenced off to prevent in the first place. The panel takes the footer as the deletion question does, and says what will be left behind before the tap rather than after it — on a phone, where the status line afterwards is one line on a screen somebody has already navigated away from, that is the only place it reliably gets read. The phone hides the button where there is nowhere to go rather than offering one that answers with a refusal; the desktop keeps its menu entry either way, because a menu that grew and shrank would be a menu whose items move. One thing found while writing the test and deliberately not changed. The pass that follows every write on this screen reports what it moved and supersedes the confirmation — for a save and a delete as much as for a move — so the move's own sentence is what somebody sees offline. The test asserts it in that state and says why. Making confirmations survive their own sync pass is a question about the whole screen rather than about this. Four places said an item could never be moved, two of them sentences on screen in both heads. All four now say what is true, including the design gaps document, where the chevron beside the vault name stays undrawn for a different reason: a chevron on a subtitle implies an edit, and this is a re-seal, a new id and two references left behind. --- README.md | 14 +- docs/design-import-gaps.md | 2 +- .../Views/HostsScreen.axaml | 120 +++++--- src/DodoSSH.Client.App/Views/HostDrawer.axaml | 79 +++-- .../ViewModels/VaultViewModel.cs | 269 +++++++++++++++++- src/DodoSSH.Client.Sync/HostRepository.cs | 9 + .../VaultItemRepository.cs | 66 +++++ .../ScreenLayoutTests.cs | 30 ++ .../VaultSharingTests.cs | 115 ++++++++ 9 files changed, 634 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index bac43c1..1ea0717 100644 --- a/README.md +++ b/README.md @@ -342,10 +342,16 @@ Six limits, stated rather than discovered: into it that is not there. Items are filed into one vault at a time, and which one is asked at the moment the item is made. **A host's -editor has its own picker**, beside the name, because that is the decision that cannot be undone: the two -vaults are encrypted under different keys, so moving an item afterwards means deleting it and typing it -again — and the picker is therefore absent when you edit an existing host rather than present and refusing. -Keys, passwords and buckets take theirs from a standing "new items go to" picker on the Keychain screen. +editor has its own picker**, beside the name, because it is the decision on that form that decides who can +read the host. It is absent when you edit an existing one rather than present and refusing, and that is not +because the host is stuck: **a host can be moved to another vault** — "Move to another vault…" in the detail +pane's menu on the desktop, MOVE beside EDIT on the phone. It is a separate act because it is not a save. +The two vaults are encrypted under different keys, so a move is a re-seal into one and a tombstone in the +other; the host gets a new id, and **its group and its tags stay behind**, because both are items of the +vault it is leaving. A picker inside the form would do all of that as a side effect of correcting a port. +What a move cannot do is reach a machine that has already synced the host, which is the same limit +everything else about revocation has. Keys, passwords and buckets take theirs from a standing "new items go +to" picker on the Keychain screen and cannot be moved yet. Both default to your personal vault and neither moves on its own, because an item put in a shared vault is visible to everybody holding that vault's key. Choosing a vault in the host editor also decides which groups it can be filed under: a group is an item like any other and lives in exactly one vault. diff --git a/docs/design-import-gaps.md b/docs/design-import-gaps.md index 9cf65d9..830da52 100644 --- a/docs/design-import-gaps.md +++ b/docs/design-import-gaps.md @@ -157,7 +157,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference > | **Add Telnet**, and **Serial** in the toolbar | Omitted. `ISshConnection` is the only transport there is. This is also why the card subtitle's `ssh` is a constant today rather than a reading — it is stated in `HostRowViewModel.Summary`, which is the one place in this interface where a constant is printed on purpose. | > | **+ SSH ID, Certificate, FIDO2** | Omitted. `IDENTITIES` and `CERTIFICATES` have been on this document's list since the first import — neither is even a reserved `SyncEntityType` — and there is no security-key path anywhere in the SSH layer. One control offering three item types that do not exist. | > | The **Backspace / Default** row | Omitted. It is a terminal setting, and the client has no preferences store and no frame to carry one to the renderer — see the Preferences section. It would be a control whose value could not survive the window closing. | -> | The **chevron beside the vault name** | The name alone, on the pane about an existing host: an item cannot be moved between vaults, because the two are encrypted under different keys and moving one is a delete and a retype. The half of the question that *does* have an answer — where a new host goes — is asked in the host editor, as a picker beside the name; keys, passwords and buckets take theirs from the keychain screen's standing picker instead. | +> | The **chevron beside the vault name** | The name alone, and the move behind the pane's ⋯ menu instead. A host *can* now be moved between vaults, so the gap is no longer that there is nothing to offer — it is that a chevron on a subtitle implies an edit, and this is not one: the two vaults are encrypted under different keys, so it is a re-seal into one and a tombstone in the other, the host takes a new id, and its group and tags stay behind. A control that implied "just change this field" would be describing something else. Where a *new* host goes is still asked in the host editor, as a picker beside the name; keys, passwords and buckets take theirs from the keychain screen's standing picker and cannot be moved yet. | > | **Show more ⌄** | Not drawn as a disclosure. What it would hide — notes, the relay switch, forgetting the host key — is in the editor, one press away, and a second fold inside a pane that already scrolls is a second place for a field to be missing from. | > | **Port Forwarding** in the sidebar | Nothing, for the third time in this document. | > | The host grid's toolbar avatar, share and tag-filter controls | Omitted, as in v3 and for the same reasons. | diff --git a/src/DodoSSH.Client.Android/Views/HostsScreen.axaml b/src/DodoSSH.Client.Android/Views/HostsScreen.axaml index 821f6f2..fd589c3 100644 --- a/src/DodoSSH.Client.Android/Views/HostsScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/HostsScreen.axaml @@ -262,10 +262,12 @@ @@ -279,7 +281,7 @@ + Text="A host in a shared vault is readable by everybody holding that vault's key. It can be moved out later, with MOVE under the host — what it cannot do is become unreadable to somebody who has already synced it." /> @@ -432,45 +434,97 @@ BorderThickness="0,1,0,0" Padding="14,12"> - - - - - - - - - - + - - - + + + + - + + + + + + + + + + + + + +