Audit follow-ups: keyboard, wording, performance, transfer, sync, retry and relay fixes #22

Merged
jaap-jan merged 21 commits from claude/audit-followups into main 2026-10-02 12:59:56 +00:00
6 changed files with 161 additions and 10 deletions
Showing only changes of commit a6d3924e0d - Show all commits

No files matched your search

@@ -136,7 +136,7 @@
<Border Classes="settingscard">
<StackPanel Margin="24,20" Spacing="10">
<TextBlock Classes="settingsrowcaption"
Text="Unlocking with Windows Hello moved to Security — it registers this machine rather than a preference of this screen's, and Security is where the rest of this machine's trust facts live." />
Text="Unlocking with this machine's device key moved to Security — it registers this machine rather than a preference of this screen's, and Security is where the rest of this machine's trust facts live." />
<TextBlock Classes="settingsrowcaption"
Text="Terminal font, cursor and scrollback — the renderer hard-codes those three. Text size above is the one that is not." />
<TextBlock Classes="settingsrowcaption"
@@ -70,15 +70,15 @@
<Border Classes="settingsrow last">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0" MaxWidth="640">
<TextBlock Classes="settingsrowtitle" Text="Windows Hello on this machine" />
<TextBlock Classes="settingsrowtitle" Text="{Binding DeviceUnlock.Name}" />
<TextBlock Classes="settingsrowcaption"
Text="Registers this machine so a later launch can open the keychain with a Windows confirmation instead of your passphrase. Your passphrase keeps working."
Text="{Binding DeviceUnlock.Offer}"
IsVisible="{Binding CanRegisterDevice}" />
<TextBlock Classes="settingsrowcaption"
Text="Registered · the TPM holds a device key that can open the keychain with a Windows confirmation."
Text="{Binding DeviceUnlock.Registered}"
IsVisible="{Binding CanForgetDevice}" />
<TextBlock Classes="settingsrowcaption"
Text="This machine has nowhere to keep a device key, so the keychain will keep asking for your passphrase. That needs a TPM and a Windows keystore willing to release the key."
Text="{Binding DeviceUnlock.Unavailable}"
IsVisible="{Binding HasNoDeviceKeyOption}" />
</StackPanel>
<Button Grid.Column="1" Classes="accent" Height="38" Content="REGISTER"
@@ -44,15 +44,15 @@
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
key. Absent rather than disabled, because a greyed-out "Use Touch ID" 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"
<Button Classes="ghost" Content="{Binding DeviceUnlock.Button}"
Command="{Binding UnlockWithDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanUnlockWithDevice}"
ToolTip.Tip="Opens the keychain with this machine's device key. Windows will ask you to confirm." />
ToolTip.Tip="{Binding DeviceUnlock.Tip}" />
</StackPanel>
<TextBlock Classes="hint" Text="{Binding StatusMessage}" TextWrapping="Wrap" />
@@ -0,0 +1,85 @@
namespace DodoSSH.Client.Shell.ViewModels;
/// <summary>
/// What unlocking with this machine's device key is called here, and what it asks the person for.
/// </summary>
/// <remarks>
/// <para>
/// The words used to be Windows's everywhere: "USE WINDOWS HELLO" on a Mac, "Waiting for Windows…" over a
/// Linux PIN prompt, and a settings row that told a MacBook it needed a TPM. Each desktop has its own device
/// key store with its own gesture — see <c>WindowsDeviceKeyStore</c>, <c>MacDeviceKeyStore</c> and
/// <c>LinuxDeviceKeyStore</c> — and a button has to name the one the person is about to be shown, or it reads
/// as a feature for some other machine.
/// </para>
/// <para>
/// One record rather than a property per sentence on the shell, so that the five places these words appear
/// cannot disagree about which platform they are on.
/// </para>
/// </remarks>
/// <param name="Name">What the feature is called, as a settings row's title.</param>
/// <param name="Button">The unlock card's button, in the capitals every button here uses.</param>
/// <param name="Wait">The status line while the platform's prompt is up.</param>
/// <param name="Gesture">What opens the keychain instead of the passphrase, to finish a sentence.</param>
/// <param name="Keeper">What holds the key, to finish "Registered · …".</param>
/// <param name="Needs">What a machine with no device key store lacks, as a sentence of its own.</param>
internal sealed record DeviceUnlockWording(
string Name,
string Button,
string Wait,
string Gesture,
string Keeper,
string Needs)
{
internal static DeviceUnlockWording Windows { get; } = new(
"Windows Hello on this machine",
"USE WINDOWS HELLO",
"Waiting for Windows…",
"a Windows confirmation",
"the TPM holds a device key",
"That needs a TPM and a Windows keystore willing to release the key.");
internal static DeviceUnlockWording Mac { get; } = new(
"Touch ID on this Mac",
"USE TOUCH ID",
"Waiting for Touch ID…",
"Touch ID or your login password",
"the Secure Enclave holds a device key",
"That needs a Mac with a Secure Enclave and a login password set.");
internal static DeviceUnlockWording Linux { get; } = new(
"A device PIN on this machine",
"USE DEVICE PIN",
"Waiting for your PIN…",
"a PIN the TPM checks",
"the TPM holds a device key",
"That needs a TPM 2.0 this account is allowed to use.");
internal static DeviceUnlockWording Android { get; } = new(
"Fingerprint unlock on this phone",
"USE FINGERPRINT",
"Waiting for your fingerprint…",
"your fingerprint",
"the phone's keystore holds a device key",
"That needs a screen lock and a keystore willing to release the key.");
/// <summary>The wording for the platform this process is running on.</summary>
internal static DeviceUnlockWording ForThisPlatform { get; } =
OperatingSystem.IsAndroid() ? Android
: OperatingSystem.IsMacOS() ? Mac
: OperatingSystem.IsLinux() ? Linux
: Windows;
/// <summary>The settings caption for a machine that can register.</summary>
internal string Offer =>
$"Registers this machine so a later launch can open the keychain with {Gesture} instead of your passphrase. Your passphrase keeps working.";
/// <summary>The settings caption for a machine that has.</summary>
internal string Registered => $"Registered · {Keeper} that can open the keychain with {Gesture}.";
/// <summary>The settings caption for a machine that cannot.</summary>
internal string Unavailable =>
$"This machine has nowhere to keep a device key, so the keychain will keep asking for your passphrase. {Needs}";
/// <summary>The unlock button's tooltip.</summary>
internal string Tip => $"Opens the keychain with this machine's device key, after {Gesture}.";
}
@@ -2819,8 +2819,10 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
/// Naming the wrong operating system here is not cosmetic — it is the sentence a user reads while
/// deciding whether the prompt in front of them is the one this application asked for.
/// </remarks>
private static string GestureWait =>
OperatingSystem.IsAndroid() ? "Waiting for your fingerprint…" : "Waiting for Windows…";
private static string GestureWait => DeviceUnlockWording.ForThisPlatform.Wait;
/// <summary>What unlocking with the device key is called on this platform; the views read it whole.</summary>
internal DeviceUnlockWording DeviceUnlock { get; } = DeviceUnlockWording.ForThisPlatform;
/// <summary>Opens the vault with this machine's device key instead of the passphrase.</summary>
/// <remarks>
@@ -0,0 +1,64 @@
using DodoSSH.Client.Shell.ViewModels;
namespace DodoSSH.Client.App.Tests;
/// <summary>
/// That unlocking with the device key is described in the words of the platform it runs on.
/// </summary>
/// <remarks>
/// The unlock card offered "USE WINDOWS HELLO" on a Mac and the settings page told a Linux machine it needed a
/// Windows keystore. Each platform's wording is checked for every other platform's names, because that is the
/// mistake: a sentence borrowed from the one platform the feature was written on first.
/// </remarks>
public sealed class DeviceUnlockWordingTests
{
public static TheoryData<string> Platforms => ["Windows", "Mac", "Linux", "Android"];
[Theory]
[MemberData(nameof(Platforms))]
public void EachPlatform_NamesOnlyItsOwnGesture(string platform)
{
var wording = For(platform);
var foreign = new Dictionary<string, string[]>(StringComparer.Ordinal)
{
["Windows"] = ["Touch ID", "Secure Enclave", "fingerprint", "PIN"],
["Mac"] = ["Windows", "TPM", "fingerprint", "PIN"],
["Linux"] = ["Windows", "Touch ID", "Secure Enclave", "fingerprint"],
["Android"] = ["Windows", "TPM", "Touch ID", "Secure Enclave"],
}[platform];
string[] said =
[wording.Name, wording.Button, wording.Wait, wording.Offer, wording.Registered, wording.Unavailable, wording.Tip];
foreach (var sentence in said)
{
sentence.ShouldNotBeNullOrWhiteSpace();
foreach (var name in foreign)
{
sentence.ShouldNotContain(name, Case.Sensitive, $"{platform}: \"{sentence}\"");
}
}
wording.Button.ShouldBe(wording.Button.ToUpperInvariant(), "every button on these screens is in capitals");
}
[Fact]
public void TheWordingInUse_IsThisMachines()
{
var expected = OperatingSystem.IsMacOS() ? DeviceUnlockWording.Mac
: OperatingSystem.IsLinux() ? DeviceUnlockWording.Linux
: DeviceUnlockWording.Windows;
DeviceUnlockWording.ForThisPlatform.ShouldBeSameAs(expected);
}
private static DeviceUnlockWording For(string platform) => platform switch
{
"Windows" => DeviceUnlockWording.Windows,
"Mac" => DeviceUnlockWording.Mac,
"Linux" => DeviceUnlockWording.Linux,
_ => DeviceUnlockWording.Android,
};
}