Public Access
Give the phone both pickers, and settle who signs the APK
The files screen could browse a remote and delete on it, and that was all: there is no browsable local filesystem on Android for a second pane to show, so the gesture the desktop is built around — choose on the left, press the arrow — has nothing to stand on. What replaces it is the platform's own two pickers. ADD FILES is ACTION_OPEN_DOCUMENT, so a document is pointed at wherever it lives and goes to the directory showing; SAVE FILE is ACTION_CREATE_DOCUMENT for the selected row. Both stage through the application's cache, and that copy is a requirement rather than a shortcut. android-port.md predicted a picked document would be a third IRemoteFileStore beside SFTP and S3; it cannot be. FileTransferQueue seeks, because an upload resumes from the byte the last attempt reached, and a content:// URI has no path behind it, no length worth trusting, no promised seek and no grant that survives the document being edited underneath it. Copying first costs one class in the head and nothing at all in the shared layers, where the alternative was every resume rule rewritten around a stream that cannot rewind. The copy is deleted when the transfer completes, kept while it is stopped so RESUME still has something to read, and swept at the next launch — which is the one moment emptying that directory is provably safe, since nothing has queued anything yet. Coming out had a decision going in did not: when to ask where it goes. The save picker is raised before the transfer, so the download runs into the same staging directory and hands its bytes to a callback the head supplied, held against the transfer id so a RETRY still lands where the person pointed. Asking afterwards would put the picker minutes from the button that caused it and, on a phone, usually while the application is backgrounded and Android will not show one at all. The cost is that the picker creates its file when it is dismissed, so a download that then fails leaves an empty one there; that is said on the screen, in the README and in the manual checks rather than left to be discovered. A delivery that fails keeps the staged bytes for the sweep instead of throwing away the one copy of something just fetched over somebody's network. The foreground service counts transfers now, which is the half of it that matters most here: a shell survives backgrounding because somebody is looking at it, and an upload has to survive precisely when nobody is. Queued counts as active, so putting five files in and locking the phone moves five files. The seam was built for this and wired to () => 0 because nothing could fill the queue. Alongside it, ADR 0010 answers the second question android-port.md left open, and it had to be answered before the first release rather than at upload time: a new Play app must use App Bundles and therefore Play App Signing, and an installed app can only be updated by a package signed with the same key, so the first release picks an identity for good. The project holds the key, offline and never in CI — the workflow's package step now says so where somebody would break it — and a DodoSSH deployment never serves the client, because a download link on your own server hands the binary that holds the plaintext to the party the whole threat model is about. The README's M1 gap note was stale in both halves and is replaced by what is actually true: credentials have an editor and a REMEMBER tick, and the device key registers into the TPM under a CNG policy that makes the consent dialog a condition of using it. What is left is the floor rather than a gap — no TPM, or no Windows, means the passphrase on every launch.
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
|
||||
|
||||
@@ -360,8 +365,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
|
||||
@@ -506,10 +523,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
|
||||
@@ -594,7 +616,13 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
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.
|
||||
- **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 0010](docs/adr/0010-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**, key rotation, per-item content keys.
|
||||
|
||||
## Licence
|
||||
|
||||
Reference in New Issue
Block a user