Public Access
Write down what the phone found, and stop it rotting
docs/android-port.md was an audit of work not started; it now says what is built. Three of its statements needed correcting rather than extending, and they are marked where they sit: the Android version question is settled and was never as open as it looked, because Avalonia.Controls.WebView ships only a net10.0-android36.0 assembly and nothing lower can resolve it; cleartext to loopback has to be permitted explicitly, which the audit missed entirely; and the spike produced a structural change it did not anticipate, in DodoSSH.Client.Shell. A CI job of its own, because the head is deliberately not in DodoSSH.slnx and a project outside the solution is a project nobody notices breaking. It packages as well as builds: a native library with no Android ABI and an assembly that will not dex are both invisible to a compile, and both are exactly what this head is exposed to. The README says plainly that signing in is not built, that a fingerprint re-enrolment destroys the device key, that a notification appears while a shell is open, and that none of it has run on a device.
This commit is contained in:
@@ -20,7 +20,7 @@ zero-knowledge.
|
||||
| Component | Choice |
|
||||
| --- | --- |
|
||||
| Backend | ASP.NET Core on .NET 10, PostgreSQL + EF Core |
|
||||
| Client | Avalonia (C#) for Windows/Linux/macOS; terminal pane is a WebView running xterm.js |
|
||||
| Client | Avalonia (C#): a desktop head for Windows/Linux/macOS and a phone-first Android head, sharing one set of view models; terminal pane is a WebView running xterm.js |
|
||||
| Auth | OIDC, provider-agnostic (Entra ID, Keycloak, Auth0, Authentik) |
|
||||
| Vault | End-to-end encrypted; X25519 + Ed25519 + XChaCha20-Poly1305, Argon2id unlock |
|
||||
| Connections | Client-direct SSH by default, with an optional raw-TCP server relay |
|
||||
@@ -70,18 +70,26 @@ src/
|
||||
DodoSSH.Client.Transfer the transfer queue, part files and resume, and the local file listing
|
||||
DodoSSH.Client.ObjectStore S3-compatible buckets, behind the same interface as SFTP
|
||||
DodoSSH.Client.Import reading ~/.ssh/config, with no I/O of its own
|
||||
DodoSSH.Client.App Avalonia; the only project that knows about a UI toolkit
|
||||
DodoSSH.Client.Shell the view models both heads drive, the renderer's files, the palette
|
||||
DodoSSH.Client.App the desktop head: its views, and its Windows integration
|
||||
DodoSSH.Client.Android the phone head: its views, and its Android integration
|
||||
tests/ one test project per source project
|
||||
docs/adr/ architecture decision records
|
||||
docs/design-import-gaps.md what the client's design asked for and this build has not got
|
||||
docs/platform-flags.md what differs off Windows, and the gotchas that have cost time
|
||||
docs/manual-checks.md what no test can reach, and what to look for when checking by hand
|
||||
docs/android-port.md what an Android client would take, and what has been decided about it
|
||||
docs/android-port.md the Android head: what was decided, what is built, what is left
|
||||
```
|
||||
|
||||
Everything under `src/DodoSSH.Client.*` except `App` is deliberately free of Avalonia. That is the
|
||||
seam that lets the SSH layer, the terminal's flow control and the OIDC flow be tested without a UI
|
||||
toolkit or a browser engine — which is most of why they are testable at all.
|
||||
Everything under `src/DodoSSH.Client.*` except the two heads and `Shell` is deliberately free of Avalonia.
|
||||
That is the seam that lets the SSH layer, the terminal's flow control and the OIDC flow be tested without a
|
||||
UI toolkit or a browser engine — which is most of why they are testable at all.
|
||||
|
||||
`Shell` is the narrow exception and it earns it: what it takes from Avalonia is `Dispatcher`, the asset
|
||||
loader and a resource dictionary, none of which imply a window, and what it holds is the shell's state
|
||||
machine — which two heads have to agree on exactly rather than approximately. The rule's purpose was never
|
||||
Avalonia-avoidance for its own sake; it was that the layers with the hard logic stay testable, and none of
|
||||
them are here.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -250,6 +258,53 @@ Items are filed into one vault at a time. When more than one vault is writable,
|
||||
show a picker; it defaults to your personal vault and never moves on its own, because an item put in a team
|
||||
vault is visible to everybody in that team and moving it back means deleting and retyping.
|
||||
|
||||
### 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).
|
||||
|
||||
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
|
||||
builds *and packages* it, because the two failures it is most exposed to — a native library with no Android
|
||||
ABI, and an assembly that resolves but will not dex — are both invisible to a plain compile.
|
||||
|
||||
Building it needs the workload and **API 36 specifically**:
|
||||
|
||||
```bash
|
||||
dotnet workload install android
|
||||
```
|
||||
|
||||
```bash
|
||||
dotnet build src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj
|
||||
```
|
||||
|
||||
API 36 is not a preference. `Avalonia.Controls.WebView` ships only a `net10.0-android36.0` assembly, so
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
**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
|
||||
an unlocked phone from inheriting the vault. The cost is that adding a finger legitimately means typing the
|
||||
passphrase again and re-registering — which the unlock screen treats as ordinary, because it is.
|
||||
|
||||
**A notification appears while a shell is open.** Android stops backgrounded processes, and the desktop
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
### End-to-end verification
|
||||
|
||||
One suite runs against a real server rather than a stub. It needs a Docker daemon and nothing else, so it
|
||||
|
||||
Reference in New Issue
Block a user