Skip to content

Commit 8a77bef

Browse files
committed
#36: Localized dialog titles collide between OS versions in 8 languages, so detect OS version using caption icon automation ID instead
1 parent b03790d commit 8a77bef

7 files changed

Lines changed: 37 additions & 19 deletions

File tree

AuthenticatorChooser/AuthenticatorChooser.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
88
<ImplicitUsings>enable</ImplicitUsings>
99
<Nullable>enable</Nullable>
10-
<Version>0.4.0</Version>
10+
<Version>0.4.1</Version>
1111
<Authors>Ben Hutchison</Authors>
1212
<Copyright>© 2025 $(Authors)</Copyright>
1313
<Company>$(Authors)</Company>

AuthenticatorChooser/ChooserOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace AuthenticatorChooser;
44

55
public readonly record struct ChooserOptions(bool skipAllNonSecurityKeyOptions) {
66

7-
public Stopwatch overallStopwatch { get; init; } = null!;
7+
public Stopwatch overallStopwatch { get; } = new();
88

99
}

AuthenticatorChooser/PromptStrategy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace AuthenticatorChooser;
44

55
public interface PromptStrategy {
66

7+
bool canHandleTitle(string? actualTitle);
78
void submitChoice(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown);
89

910
}

AuthenticatorChooser/Windows11/Win1123H2Strategy.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ public class Win1123H2Strategy(ChooserOptions options): Win11Strategy(options) {
99

1010
private static readonly Condition NEXT_BUTTON_CONDITION = new PropertyCondition(AutomationElement.AutomationIdProperty, "OkButton");
1111

12+
public override bool canHandleTitle(string? actualTitle) => I18N.getStrings(I18N.Key.SIGN_IN_WITH_YOUR_PASSKEY)
13+
.Concat(options.skipAllNonSecurityKeyOptions ? I18N.getStrings(I18N.Key.MAKING_SURE_ITS_YOU) : [])
14+
.Any(expected => expected.Equals(actualTitle, StringComparison.CurrentCulture));
15+
1216
/**
1317
* If we're on the TPM dialog, and the user wants to absolutely always use security keys, then we just selected "Use another device" to see the list of all authenticator choices, so the dialog is closing because we selected something, so don't do anything else with the soon to be nonexistant dialog.
1418
* Otherwise, perform common checks like holding Shift and stopping if there are other options.

AuthenticatorChooser/Windows11/Win1125H2Strategy.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ public class Win1125H2Strategy(ChooserOptions options): Win11Strategy(options) {
1616
new PropertyCondition(AutomationElement.ClassNameProperty, "TextBlock"),
1717
new PropertyCondition(AutomationElement.HeadingLevelProperty, AutomationHeadingLevel.None));
1818

19+
public override bool canHandleTitle(string? actualTitle) => I18N.getStrings(I18N.Key.CHOOSE_A_PASSKEY)
20+
.Concat(options.skipAllNonSecurityKeyOptions ? I18N.getStrings(I18N.Key.SIGN_IN_WITH_A_PASSKEY) : [])
21+
.Any(expected => expected.Equals(actualTitle, StringComparison.CurrentCulture));
22+
1923
public override void submitChoice(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown) {
2024
if (I18N.getStrings(I18N.Key.CHOOSE_A_PASSKEY).Contains(actualTitle, StringComparer.CurrentCulture)) {
2125
if (findAuthenticatorChoices(outerScrollViewer) is not { } authenticatorChoices) return;

AuthenticatorChooser/Windows11/Win11Strategy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public abstract class Win11Strategy(ChooserOptions options): PromptStrategy {
1212

1313
protected ChooserOptions options { get; } = options;
1414

15+
public abstract bool canHandleTitle(string? actualTitle);
1516
public abstract void submitChoice(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown);
1617

1718
protected bool shouldSkipSubmission(AutomationElement desiredChoice, IEnumerable<AutomationElement> authenticatorChoices, bool isShiftDown) {

AuthenticatorChooser/Windows11/WindowsSecurityKeyChooser.cs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using ManagedWinapi.Windows;
22
using NLog;
3-
using System.Diagnostics;
43
using System.Runtime.InteropServices;
54
using System.Windows.Automation;
65
using System.Windows.Input;
@@ -18,11 +17,10 @@ public class WindowsSecurityKeyChooser(ChooserOptions options): AbstractSecurity
1817

1918
private static readonly Condition TITLE_CONDITION = new PropertyCondition(AutomationElement.ClassNameProperty, "TextBlock");
2019

21-
private ChooserOptions options = options;
20+
private PromptStrategy? strategy;
2221

2322
public override void chooseUsbSecurityKey(SystemWindow fidoPrompt) {
24-
Stopwatch overallStopwatch = Stopwatch.StartNew();
25-
options = options with { overallStopwatch = overallStopwatch };
23+
options.overallStopwatch.Restart();
2624
try {
2725
if (!isFidoPromptWindow(fidoPrompt)) {
2826
LOGGER.Trace("Window 0x{hwnd:x} is not a Windows Security window", fidoPrompt.HWnd);
@@ -38,29 +36,39 @@ public override void chooseUsbSecurityKey(SystemWindow fidoPrompt) {
3836
return;
3937
}
4038

41-
LOGGER.Trace("Found outerScrollViewer, looking for dialog title");
39+
LOGGER.Trace("Found outerScrollViewer, looking for dialog icon and title");
40+
41+
// #36: detect OS version using caption icon automation ID instead of title, which collides between versions in 19% of languages
42+
if (strategy == null) {
43+
// Icon appears with neither AutomationElement.FindFirst nor Inspect's UIA Content or Control Views, only Raw View
44+
string? captionIconAutomationId = TreeWalker.RawViewWalker.GetFirstChild(fidoEl).Current.AutomationId;
45+
strategy = captionIconAutomationId switch {
46+
"WindowLogo" => new Win1123H2Strategy(options),
47+
"WindowSecurityLogo" => new Win1125H2Strategy(options),
48+
_ => null
49+
};
50+
51+
if (strategy == null) {
52+
LOGGER.Error("Unsupported OS dialog version, Windows Security dialog box caption icon has unrecognized automation ID {id}", captionIconAutomationId);
53+
return;
54+
}
55+
}
4256

4357
// #21: title not rendered immediately
4458
if (outerScrollViewer.WaitForFirst(TreeScope.Children, TITLE_CONDITION, TimeSpan.FromSeconds(5), Startup.EXITING) is not { } titleLabel) {
45-
LOGGER.Debug("Window is not a passkey choice prompt because there is no TextBlock child of the ScrollViewer after retrying for {0:N3}", overallStopwatch.Elapsed.TotalSeconds);
59+
LOGGER.Debug("Window is not a passkey choice prompt because there is no TextBlock child of the ScrollViewer after retrying for {0:N3}", options.overallStopwatch.Elapsed.TotalSeconds);
4660
return;
4761
}
4862

49-
PromptStrategy strategy;
50-
string? actualTitle = titleLabel.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
51-
if (hasTitle(I18N.getStrings(I18N.Key.CHOOSE_A_PASSKEY).Concat(options.skipAllNonSecurityKeyOptions ? I18N.getStrings(I18N.Key.SIGN_IN_WITH_A_PASSKEY) : []))) {
52-
strategy = new Win1125H2Strategy(options);
53-
} else if (hasTitle(I18N.getStrings(I18N.Key.SIGN_IN_WITH_YOUR_PASSKEY).Concat(options.skipAllNonSecurityKeyOptions ? I18N.getStrings(I18N.Key.MAKING_SURE_ITS_YOU) : []))) {
54-
strategy = new Win1123H2Strategy(options);
55-
} else {
63+
string? actualTitle = titleLabel.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
64+
65+
if (!strategy.canHandleTitle(actualTitle)) {
5666
LOGGER.Debug("Window is not a passkey choice prompt because the first TextBlock child of the ScrollViewer has the name {actual}", actualTitle);
5767
return;
68+
} else {
69+
LOGGER.Trace("Window 0x{hwnd:x} is a Windows Security window", fidoPrompt.HWnd);
5870
}
5971

60-
bool hasTitle(IEnumerable<string> expectedTitles) => expectedTitles.Any(expected => expected.Equals(actualTitle, StringComparison.CurrentCulture));
61-
62-
LOGGER.Trace("Window 0x{hwnd:x} is a Windows Security window", fidoPrompt.HWnd);
63-
6472
bool isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
6573

6674
strategy.submitChoice(actualTitle!, fidoEl, outerScrollViewer, isShiftDown);

0 commit comments

Comments
 (0)