Public Access
Give the phone the rest of its screens, and a way in
All seven screens of the design, plus the two it does not draw because it starts at an enrolled phone: naming a server, and choosing a passphrase. The five states docs/android-port.md worried about losing at 360dp are all here and none of them softened. The changed-key refusal is a full-screen panel rather than a bottom sheet, because a sheet is swipe-to-dismiss by convention and that screen must have no way forward. The recovery code raises FLAG_SECURE for its own state and lowers it afterwards, so the sentence about screenshots is true rather than decorative. The delete confirmations keep their counts and replace the row in place. Signing in works, and the seam it needed is worth more than the implementation: IAuthorizationCallback now sits between OidcClient and the loopback listener, so the two heads differ in where the response arrives and in nothing else. PKCE, the state check, discovery, the token exchange and the key binding stay one implementation — a second OIDC client would be a second place for a security bug to live. The phone registers a private-use scheme with the system rather than binding a loopback port, which on a shared device any other app can do first. The accessory key row needed TerminalWorkspace.SendInputAsync: ordinary typing goes from the renderer straight down the socket, and there was no way in for the keys a software keyboard does not have. Ctrl latches, because one thumb cannot chord, and the latch is drawn — a modifier that is on and does not look on is how somebody sends ^L to a database prompt believing they typed an l. 597 client tests green, including two new ones for the input path and one for the terminal surface command. Nothing has run on a device.
This commit is contained in:
+48
-19
@@ -1,7 +1,9 @@
|
||||
# An Android client: what it would take
|
||||
|
||||
**Status: started. Steps 3, 4 and 6 are done; the interface is next.** `src/DodoSSH.Client.Android` is a
|
||||
real head that builds and packages. The four decisions that shape the work are recorded in
|
||||
**Status: the decided scope is built. Steps 3–8 are done.** `src/DodoSSH.Client.Android` is a phone-first
|
||||
head that builds and packages: the keychain and a terminal, which is exactly the scope
|
||||
[decided](#scope-the-keychain-and-a-terminal), plus the sign-in and enrollment states needed to reach them
|
||||
from a phone that has never been used before. The four decisions that shape the work are recorded in
|
||||
[Decisions](#decisions-taken); everything below them is the audit they were taken against, and it has held
|
||||
up — with three corrections and one addition, all marked **✅ settled** or **⚠️ corrected** where they
|
||||
belong.
|
||||
@@ -437,30 +439,57 @@ go at 360dp:
|
||||
`setInvalidatedByBiometricEnrollment` is on, which is what stops somebody who can add a fingerprint to an
|
||||
unlocked phone from inheriting the vault — the cost being that re-enrolment permanently destroys the key,
|
||||
which the load path treats as ordinary rather than exceptional.
|
||||
5. **Android sign-in**: Custom Tabs plus an app link, behind the existing seams. Not the loopback listener —
|
||||
on a shared device any app can bind a loopback port, which is the attack RFC 8252 §8.3 names.
|
||||
5. ~~**Android sign-in.**~~ **Done**, and the seam it needed turned out to be worth more than the
|
||||
implementation. `IAuthorizationCallback` now sits between `OidcClient` and the loopback listener, so the
|
||||
two heads differ in *where the response arrives* and in nothing else — PKCE, the state check, discovery,
|
||||
the token exchange and the key binding are one implementation, which is the point. A second OIDC client
|
||||
would have been a second place for a security bug to live.
|
||||
|
||||
**Not started, and it is now the gap that matters most.** The head composes a sign-in delegate that
|
||||
throws, and `NeedsServer` draws a screen saying so in the user's words. A phone enrolled from the desktop
|
||||
client unlocks here perfectly well, because unlocking needs no network — so this blocks first-run, not
|
||||
the product. It is the next piece of work.
|
||||
The system browser by `ACTION_VIEW` rather than a Custom Tab: a Custom Tab is the nicer surface and
|
||||
shares the same cookie jar, but it needs `androidx.browser` and this needs nothing. Worth revisiting when
|
||||
something else pulls AndroidX in.
|
||||
|
||||
**A private-use scheme, not an App Link**, and the difference is worth writing down rather than
|
||||
discovering. The redirect is `dev.dodotech.dodossh:/callback` — the reversed package name, which RFC 8252
|
||||
§7.1 recommends. Another application *can* also declare that scheme, and Android will show a chooser
|
||||
rather than refuse; what stops that being a compromise is PKCE, since the code is useless without a
|
||||
verifier that never leaves the process. An App Link would close it properly, at the cost of an
|
||||
`assetlinks.json` on the DodoSSH server's own domain — a server change, and the natural upgrade.
|
||||
6. ~~**The foreground service.**~~ **Done** — `SessionForegroundService`, with `SessionKeepAlive` starting and
|
||||
stopping it from a count rather than a lifecycle. `TerminalWorkspace.LiveSessionCount` is the source of
|
||||
truth deliberately: it already knows that a session whose shell exited is not live, which a counter
|
||||
incremented on open would not, and a phone showing "1 shell connected" over nothing would be exactly the
|
||||
dishonesty the unlock screen's count exists to prevent.
|
||||
7. **The interface**, phone-first. The actual project, and the one that dominates the estimate. **Started:**
|
||||
the shell switches on `ShellState` and the unlock screen is built from the design — including both of its
|
||||
◆ disclosures, the shell count and the paragraph saying *locked* describes the keychain and not this
|
||||
phone's access to the hosts. Every other state draws a `PendingScreen` naming what is missing, in the
|
||||
desktop head's `NotBuiltScreen` tradition and for the same reason.
|
||||
8. **The terminal**, last — the highest-value screen, and the one whose remaining unknowns are cheapest to
|
||||
resolve once the shell around it exists. Plus an accessory key row: a software keyboard has no Ctrl, Esc,
|
||||
Tab or arrows, and every Android SSH client ships one for exactly this reason.
|
||||
7. ~~**The interface**, phone-first.~~ **Done for the decided scope** — all seven screens of the design,
|
||||
plus the two states the design does not draw because it starts at an enrolled phone (naming a server, and
|
||||
choosing a passphrase).
|
||||
|
||||
Steps 1–4 and 6 are done. Step 5 is ordinary work behind seams that already exist, and is what first-run
|
||||
waits on. Step 7 still dominates everything else put together — the unlock screen is one of nine
|
||||
destinations and the least structural of them — and step 8 is small only because step 7 comes first.
|
||||
All five of the ◆ states this document worried about are built and none of them softened:
|
||||
|
||||
- the changed-key refusal has no continue button, and is a full-screen panel rather than a bottom sheet
|
||||
precisely because a sheet is swipe-to-dismiss by convention;
|
||||
- the recovery code cannot be skipped, and `FLAG_SECURE` is raised for that state alone so the screen's
|
||||
claim about screenshots is true rather than decorative;
|
||||
- the delete confirmations keep their counts and replace the row in place;
|
||||
- the unknown-key prompt shows the fingerprint in full, wrapping rather than clipping;
|
||||
- the conflict log is a banner on the host list rather than a screen nobody opens.
|
||||
|
||||
The nav rail's eight destinations became four. Pins, snippets, logs, import and teams are not built here:
|
||||
import has no meaning under scoped storage, and the other four are list screens whose view models already
|
||||
exist — they are additive rather than structural.
|
||||
8. ~~**The terminal.**~~ **Done** — one `NativeWebView` for every session, as on the desktop and for the
|
||||
same reason, with the tab strip scrolling horizontally rather than wrapping so that opening a tab never
|
||||
reflows a terminal while output is arriving.
|
||||
|
||||
The accessory key row needed one addition to the shared layer: `TerminalWorkspace.SendInputAsync`, since
|
||||
ordinary typing goes from the renderer straight down the socket and there was no way in for input that
|
||||
has no key to produce it. Ctrl latches rather than being held — one thumb cannot chord — and the latch is
|
||||
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
||||
believing they typed an `l`.
|
||||
|
||||
All eight are done for the decided scope. What is left is not on this list: the four destinations the
|
||||
phone does not draw, file transfer (deliberately deferred, and see the decision above for the shape it
|
||||
takes), the App Link upgrade, and — the one that is not optional — running any of it on a device.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user