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:
@@ -260,10 +260,16 @@ vault is visible to everybody in that team and moving it back means deleting and
|
||||
|
||||
### The Android head
|
||||
|
||||
`src/DodoSSH.Client.Android` is a phone-first head that shares every view model with the desktop one. It is
|
||||
**partly built**: what exists is the unlock screen, the platform integration under it, and a shell that
|
||||
names each state it has not built yet rather than showing an empty screen. What it is for, what was decided
|
||||
and what is left are in [`docs/android-port.md`](docs/android-port.md).
|
||||
`src/DodoSSH.Client.Android` is a phone-first head that shares every view model with the desktop one — the
|
||||
keychain and a terminal, which is the scope [`docs/android-port.md`](docs/android-port.md) decided on and
|
||||
the reasoning behind it. Sign in, unlock, browse hosts, open a shell, and read the keychain; the two
|
||||
host-key decisions and the counted delete confirmations are there too, and none of them were softened to
|
||||
fit 360dp.
|
||||
|
||||
What it does **not** have is file transfer — deliberately, since scoped storage means there is no local
|
||||
pane to put beside the remote one — and the four list screens the desktop grew last (pins, snippets, logs,
|
||||
teams), whose view models are already shared and which are additive rather than structural. Importing an
|
||||
`~/.ssh/config` has no meaning on a phone at all.
|
||||
|
||||
It is deliberately **not** in `DodoSSH.slnx`. Putting it there would make the `android` workload and a full
|
||||
Android SDK a prerequisite of `dotnet build DodoSSH.slnx` for everybody; it has its own CI job instead, which
|
||||
@@ -284,13 +290,18 @@ API 36 is not a preference. `Avalonia.Controls.WebView` ships only a `net10.0-an
|
||||
anything lower cannot resolve it and the head loses its terminal. The floor is API 28, which is where
|
||||
`BiometricPrompt` and StrongBox-backed keys exist without an AndroidX shim.
|
||||
|
||||
Three things about it are worth expecting rather than discovering.
|
||||
Four things about it are worth expecting rather than discovering.
|
||||
|
||||
**Signing in is not built yet, and that is the one real gap.** The desktop client receives the authorization
|
||||
response on a loopback `TcpListener`; reusing that on a phone would be a security regression rather than a
|
||||
shortcut, because on a shared device any other app can bind a loopback port — the attack RFC 8252 §8.3 names.
|
||||
It needs an app link instead. A phone enrolled from the desktop client unlocks here perfectly well, because
|
||||
**unlocking needs no network at all**, so this blocks first-run rather than the product.
|
||||
**Signing in does not use the desktop's loopback redirect, and must not.** On a shared device any other
|
||||
application can bind a loopback port and race for the authorization code — the attack RFC 8252 §8.3 names.
|
||||
The phone registers a redirect with the system instead and is handed the response as an intent. Everything
|
||||
above that — PKCE, the state check, discovery, the token exchange, the key binding — is the same code the
|
||||
desktop runs, because the only thing that varies is where the response arrives.
|
||||
|
||||
The redirect is a private-use scheme (`dev.dodotech.dodossh:`) rather than an Android App Link, and the
|
||||
limit is worth knowing: another app can declare the same scheme, and Android will offer a chooser rather
|
||||
than refuse. PKCE is what makes an intercepted code useless. An App Link closes it properly and costs an
|
||||
`assetlinks.json` on your own server's domain.
|
||||
|
||||
**A fingerprint releases the device key, and re-enrolling a fingerprint destroys it.** The key is generated
|
||||
with `setInvalidatedByBiometricEnrollment`, which is what stops somebody who can add their own fingerprint to
|
||||
@@ -301,9 +312,15 @@ passphrase again and re-registering — which the unlock screen treats as ordina
|
||||
client's promise that locking the vault does not close your shells is only true here behind a foreground
|
||||
service. The notification is the price of that promise; it goes when the last shell does.
|
||||
|
||||
**The recovery code screen blocks screenshots.** `FLAG_SECURE` is raised for that one state and lowered
|
||||
again afterwards, so the screen's own claim is true and a shell is still screenshotable. It stops the
|
||||
accident worth stopping — the only copy of an unrecoverable code landing in a cloud photo library, or in the
|
||||
recent-apps thumbnail — and stops nothing determined, since a second phone photographs a screen perfectly
|
||||
well.
|
||||
|
||||
**Nothing has been run on a device.** It compiles, links, packages, and carries the right native libraries
|
||||
for arm64 — that is verified. Everything about its runtime behaviour is not, and `docs/android-port.md` says
|
||||
which claims those are.
|
||||
for arm64 — that is verified, and CI verifies it on every change. Everything about its runtime behaviour is
|
||||
not, and `docs/android-port.md` says which claims those are.
|
||||
|
||||
### End-to-end verification
|
||||
|
||||
|
||||
Reference in New Issue
Block a user