Files
DodoSSH/src/DodoSSH.Client.App/Views/UnlockCard.axaml
T
jaap-jan fe9d7fc289 Give DodoSSH a phone, and a shared shell for both heads to drive
The Android head from docs/android-port.md, taken as far as its step 6.

Step 3, the spike, is answered and its throwaway screen is gone: libsodium.so and
libe_sqlite3.so are both in the arm64 APK, so NSec resolves its native half on Android
despite shipping no Android build, and the local cache opens. Two findings the audit
could not have had: Avalonia.Controls.WebView only ships net10.0-android36.0, which
settles the open "which Android versions" question at targetSdk 36; and Android has
blocked cleartext HTTP since API 28, so the terminal renderer needs a network security
config scoped to 127.0.0.1 or the WebView loads nothing.

DodoSSH.Client.Shell is new and is why the phone can exist: the view models, the terminal
renderer files and the palette moved there so both heads drive one state machine and draw
from one set of tokens. The desktop head is otherwise untouched and its 144 tests still
pass.

The platform pieces behind interfaces that already existed: the profile directory from
filesDir, a device key wrapped by a StrongBox-backed key that a fingerprint releases, and
a foreground service so a shell outliving a vault lock stays true on a platform that
stops backgrounded processes.

Sign-in is deliberately absent rather than approximated. It needs an app link, because
reusing the desktop loopback listener is the attack RFC 8252 section 8.3 names.
2026-07-31 20:58:48 +02:00

99 lines
5.6 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
x:Class="DodoSSH.Client.App.Views.UnlockCard"
x:DataType="vm:MainWindowViewModel">
<!--
The unlock screen's contents.
Extracted from MainWindow.axaml so that it can be laid out and looked at: MainWindow cannot be shown in
the headless session at all — WebView2's adapter refuses its thread, which LayoutHarnessTests pins — so
markup that stays inside that file is markup no test can measure. This card is the one on the screen
with the least room to spare and the most conditional content: a disclosure about shells left running,
and a way out for a forgotten passphrase, both of which appear underneath a form that already fills most
of the height the window guarantees.
A bare StackPanel rather than a card, because the card is the frame MainWindow puts around it.
-->
<StackPanel Spacing="12">
<TextBlock Classes="heading" Text="Unlock your vault" />
<TextBlock Text="{Binding AccountName}" Foreground="{StaticResource Info}" />
<!--
Named because locking has to put the keyboard here explicitly, and reached from the window through
PassphraseBox. The terminal's native child window keeps Win32 focus when it is collapsed, so without
that this box would show a caret and silently swallow the passphrase — see NativeKeyboardFocus.
Enter unlocks. A KeyBinding on the box rather than a handler on the window, because this is a property
of the control the passphrase is typed into and not of the shell's state: the keyboard is put here on
every lock, so the one gesture everybody makes after typing a password reaches the command this box
exists for. A single-line TextBox does not handle Enter itself, so nothing is being fought over.
-->
<TextBox x:Name="UnlockPassphrase" Text="{Binding Passphrase}"
PlaceholderText="vault passphrase" PasswordChar="•">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding UnlockCommand}" />
</TextBox.KeyBindings>
</TextBox>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="accent" Content="UNLOCK" Command="{Binding UnlockCommand}"
IsEnabled="{Binding !IsBusy}" />
<!--
Shown only when this machine has both a registered wrap and a keystore still willing to release the
key. Absent rather than disabled, because a greyed-out "Use Windows Hello" on a machine that never
had it invites the reading that something is broken — and the passphrase box beside it is not a
fallback, it is the ordinary way in.
-->
<Button Classes="ghost" Content="USE WINDOWS HELLO"
Command="{Binding UnlockWithDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanUnlockWithDevice}"
ToolTip.Tip="Opens the vault with this machine's device key. Windows will ask you to confirm." />
</StackPanel>
<TextBlock Classes="hint" Text="{Binding StatusMessage}" TextWrapping="Wrap" />
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
Text="This works with no network: the salt and the wrapped key are already on this machine." />
<!--
Stated here because the lock screen is what hides it. The terminal's WebView is collapsed while
locked, so a shell left running is invisible as well as unstopped — and a screen saying "Unlock your
vault" over a machine that still holds authenticated SSH channels is exactly the kind of half-truth
this project writes down instead of implying. Visible only when there is something to disclose, so an
ordinary launch stays quiet.
-->
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
BorderThickness="1" CornerRadius="4" Padding="10,8"
IsVisible="{Binding HasLiveSessions, FallbackValue=False}">
<StackPanel Spacing="4">
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Info}"
FontWeight="SemiBold" TextWrapping="Wrap" />
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
Text="Locking closes the vault, not your terminals: a job you started keeps running, and its output is waiting behind this screen. It also means this machine still holds an open, authenticated channel to those hosts — locked describes the vault, not the connections. Quit DodoSSH to end them." />
</StackPanel>
</Border>
<!--
The way out of a forgotten passphrase, and the only one there is. Nothing can recover a passphrase —
there is no server-side reset by design — so the honest offer is to reset this machine and sign in
again, which costs whatever this machine had not yet pushed and nothing else. Stated here rather than
left to be discovered, because somebody stuck on this screen has no other route and quitting the
application does not help.
-->
<Border Height="1" Background="{StaticResource BorderSubtle}" />
<StackPanel Spacing="6">
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
Text="Forgotten your passphrase? Nothing can recover it — not even whoever runs the server. What you can do is reset this machine and sign in again; the vault is on the server and comes back." />
<Button Classes="ghost" Content="RESET THIS MACHINE"
Command="{Binding SignOutCommand}" HorizontalAlignment="Left" />
</StackPanel>
</StackPanel>
</UserControl>