diff --git a/docs/android-port.md b/docs/android-port.md index 027e28b..f7f12fc 100644 --- a/docs/android-port.md +++ b/docs/android-port.md @@ -198,6 +198,13 @@ Used as the device name on connection and activity log entries, and when registe returns something like `localhost`, which would make every log entry from a phone indistinguishable. Needs a real device name from the head. +**Done** — `PhoneEnvironment.DeviceName` reads what the user typed into Android's own Settings, falling back +to the marketing model, and `MainWindowViewModel` takes it as an optional constructor argument that the +desktop head does not pass. It reaches all three places the machine name was: enrollment, device +registration, and every connection log entry. The registration is the one that had to be fixed before the +device key could be offered on a phone at all — the account's device list is what a lost handset is revoked +from, and a list of identical `localhost` rows is a revocation nobody dares press. + ### 8. The Windows-only bits of the desktop head Listed for completeness; none of these is ported, they are simply absent from an Android head. @@ -452,6 +459,13 @@ go at 360dp: fallback, and a prompt that came back after being dismissed would be a modal you cannot get out of to type into it. It watches three properties rather than one because startup sets the state to `Locked` before it has asked the keystore whether there is a key to offer, and does both inside the busy wrapper. + + **Enrolling one is on PREFERENCES**, and until it was, none of the above could ever happen on a phone: + the store, the gate and the lock screen's button all shipped, and nothing in this head could create the + key they are about — so `CanUnlockWithDevice` was false on every launch of every phone. Registering needs + an unlocked keychain and a reachable server (the vault has to be open to seal the bundle, and the wrap has + to reach the account or a lost phone could never be revoked), which is why the offer is on a screen behind + SETTINGS rather than beside the button it turns on. 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, diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 59dac04..16183d3 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -1166,3 +1166,71 @@ unchanged and there is nowhere to change it. Nothing claims to know *when* it wa **Failure means:** a rename that moved the slug could take one an archived team is still holding, and that archived team could then never be brought back. An "edited" timestamp anywhere on the screen is invented data — `team` has no updated-at column, so there is nothing behind it. + +--- + +## Phase 13 — Unlocking the phone with a fingerprint + +Every check here needs a real Android device or emulator with a screen lock and a fingerprint enrolled on +the phone itself, and none has a headless equivalent: the whole feature is a keystore key the platform will +not release without a gesture, and there is no gesture in a test process. What *is* covered automatically is +the shell's half — `ShellFlowTests` registers, relaunches, unlocks and withdraws against a fake keystore, so +what is left here is the platform half plus the one thing only a person can see, which is which dialogue +comes up. + +### 13.1 The offer is on PREFERENCES, and only when there is something to offer + +Unlock the keychain, go to SETTINGS → Preferences on a phone with a screen lock. + +**Pass:** REGISTER THIS PHONE is there under THIS PHONE. On a phone with **no** screen lock at all, neither +button is drawn and the paragraph saying this phone has nowhere to keep a device key is. + +**Failure means:** if the button is drawn on a phone with no screen lock, `AndroidDeviceKeyStore` +`IsAvailableAsync` is no longer asking the keyguard — and registering there would put a wrap on the account +that nothing can ever open, on a phone whose key no gesture can release. + +### 13.2 Registering asks for the fingerprint, and says which phone it registered + +Press REGISTER THIS PHONE while signed in. + +**Pass:** the system's own biometric prompt appears, titled "Register this phone". Confirm it, and the status +line names **this phone** — the name from Android's Settings, or the model — rather than `localhost`. The +button is replaced by STOP UNLOCKING HERE. Cancel the prompt instead and nothing changes but the message. + +**Failure means:** a status line reading `localhost` means the head is no longer passing +`PhoneEnvironment.DeviceName`, and the account's device list is about to fill with rows nobody can tell +apart. No prompt at all means the cipher is not being bound to it — see `BiometricGate`, where binding is +the entire point. + +### 13.3 The lock screen then opens without the passphrase + +Lock the keychain, close the app, and launch it again. + +**Pass:** the prompt is raised on arrival, and confirming it opens the keychain with nothing typed. UNLOCK +WITH FINGERPRINT is on the screen behind it. Lock from inside the running app instead and **no** prompt is +raised — that rule is deliberate; see `PhoneShell.TryOfferDeviceUnlock`. + +**Failure means:** a button that is absent after a successful registration is the wrap not reaching the local +cache. A prompt raised after an in-app lock trains the reflex of authenticating at a prompt nobody asked for. + +### 13.4 Enrolling a new fingerprint on the phone destroys the key · **the security property** + +With DodoSSH registered, add another fingerprint in Android's own Settings. Then launch DodoSSH. + +**Pass:** no fingerprint button, and the passphrase opens the vault as it always did. Registering again from +PREFERENCES restores it. + +**Failure means:** `setInvalidatedByBiometricEnrollment` has been dropped, and anybody who can add their own +fingerprint to an unlocked phone has inherited the vault. This is the check that says the phone's fast path +is not a downgrade of the passphrase. + +### 13.5 Withdrawing stops this phone, and clears the account + +Press STOP UNLOCKING HERE. + +**Pass:** it goes back to offering REGISTER, a relaunch asks for the passphrase, and the device is gone from +the account — check from the desktop head, or by registering the same phone again and seeing one device +rather than two. There is no confirmation prompt, deliberately. + +**Failure means:** a phone that still unlocks itself after this is the local half not happening, which is the +half that matters when the handset is the thing that was lost. diff --git a/src/DodoSSH.Client.Android/App.axaml.cs b/src/DodoSSH.Client.Android/App.axaml.cs index f0f54fa..25ca427 100644 --- a/src/DodoSSH.Client.Android/App.axaml.cs +++ b/src/DodoSSH.Client.Android/App.axaml.cs @@ -135,7 +135,13 @@ public sealed partial class DodoSshApp : Avalonia.Application // makes a launch after the first one arrive online rather than merely enrolled. resume: async (url, refreshToken, cancellationToken) => await ServerConnection .ResumeAsync(url, refreshToken, TimeProvider.System, cancellationToken) - .ConfigureAwait(false)); + .ConfigureAwait(false), + + // Difference 5: what this phone is called. The shell's default is Environment.MachineName, which + // answers localhost here — so without this the account's device list would show one localhost + // per phone, on the very screen a lost device is revoked from, and every log entry a phone wrote + // would name the same machine. See PhoneEnvironment.DeviceName. + deviceName: PhoneEnvironment.DeviceName); // Started rather than awaited: framework initialisation must not block on a schema migration. The // view model shows its own progress and handles its own failures. diff --git a/src/DodoSSH.Client.Android/Views/MoreScreen.axaml b/src/DodoSSH.Client.Android/Views/MoreScreen.axaml index 48b1461..db4fe4c 100644 --- a/src/DodoSSH.Client.Android/Views/MoreScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/MoreScreen.axaml @@ -157,7 +157,8 @@ Width="22" VerticalAlignment="Center" /> - + diff --git a/src/DodoSSH.Client.Android/Views/PhoneShell.axaml b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml index 825e1cb..c5d896b 100644 --- a/src/DodoSSH.Client.Android/Views/PhoneShell.axaml +++ b/src/DodoSSH.Client.Android/Views/PhoneShell.axaml @@ -174,10 +174,12 @@ @@ -185,9 +187,7 @@ CommandParameter="{x:Static vm:ShellScreen.More}" /> - + diff --git a/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml b/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml new file mode 100644 index 0000000..74c6b00 --- /dev/null +++ b/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + +