1
- using ManagedWinapi . Windows ;
2
- using System . Windows . Automation ;
3
- using System . Windows . Input ;
4
- using ThrottleDebounce ;
5
-
6
- namespace AuthenticatorChooser ;
7
-
8
- public static class SecurityKeyChooser {
9
-
10
- private static readonly TimeSpan UI_RETRY_DELAY = TimeSpan . FromMilliseconds ( 8 ) ;
11
-
12
- public static void chooseUsbSecurityKey ( SystemWindow fidoPrompt ) {
13
- if ( isFidoPromptWindow ( fidoPrompt ) ) {
14
- AutomationElement fidoEl = fidoPrompt . toAutomationElement ( ) ;
15
- AutomationElement ? outerScrollViewer = fidoEl . FindFirst ( TreeScope . Children , new PropertyCondition ( AutomationElement . ClassNameProperty , "ScrollViewer" ) ) ;
16
- AutomationElement ? promptTitleEl = outerScrollViewer ? . FindFirst ( TreeScope . Children , new AndCondition (
17
- new PropertyCondition ( AutomationElement . ClassNameProperty , "TextBlock" ) ,
18
- singleSafePropertyCondition ( AutomationElement . NameProperty , I18N . getStrings ( I18N . Key . SIGN_IN_WITH_YOUR_PASSKEY ) ) ) ) ;
19
-
20
- if ( outerScrollViewer != null && promptTitleEl != null ) {
21
- List < AutomationElement > listItems = Retrier . Attempt ( _ =>
22
- outerScrollViewer . FindFirst ( TreeScope . Children , new PropertyCondition ( AutomationElement . AutomationIdProperty , "CredentialsList" ) ) . children ( ) . ToList ( ) ,
23
- maxAttempts : 25 , delay : _ => UI_RETRY_DELAY ) ;
24
-
25
- if ( listItems . FirstOrDefault ( listItem => nameContainsAny ( listItem , I18N . getStrings ( I18N . Key . SECURITY_KEY ) ) ) is { } securityKeyButton ) {
26
- ( ( SelectionItemPattern ) securityKeyButton . GetCurrentPattern ( SelectionItemPattern . Pattern ) ) . Select ( ) ;
27
-
28
- if ( ! Keyboard . IsKeyDown ( Key . LeftShift ) && ! Keyboard . IsKeyDown ( Key . RightShift )
29
- && listItems . All ( listItem => listItem == securityKeyButton || nameContainsAny ( listItem , I18N . getStrings ( I18N . Key . SMARTPHONE ) ) ) ) {
30
-
31
- AutomationElement nextButton = fidoEl . FindFirst ( TreeScope . Children , new PropertyCondition ( AutomationElement . AutomationIdProperty , "OkButton" ) ) ;
32
- ( ( InvokePattern ) nextButton . GetCurrentPattern ( InvokePattern . Pattern ) ) . Invoke ( ) ;
33
- } // Otherwise shift key was held down, or prompt contained extra options besides USB security key and pairing a new phone, such as an existing paired phone, PIN, or fingerprint
34
- } // Otherwise USB security key was not an option
35
- } // Otherwise could not find title, might be a UAC prompt
36
- } // Otherwise not a credential prompt, wrong window class name
37
- }
38
-
39
- // name/title are localized, so don't use those
40
- // #4: unfortunately, this class name is shared with the UAC prompt, detectable when desktop dimming is disabled
41
- public static bool isFidoPromptWindow ( SystemWindow window ) => window . ClassName == "Credential Dialog Xaml Host" ;
42
-
43
- private static bool nameContainsAny ( AutomationElement element , IEnumerable < string ? > suffices ) {
44
- string name = element . Current . Name ;
45
- return suffices . Any ( suffix => suffix != null && name . Contains ( suffix , StringComparison . CurrentCulture ) ) ;
46
- }
47
-
48
- private static Condition singleSafePropertyCondition ( AutomationProperty property , IEnumerable < string > allowedNames ) {
49
- Condition [ ] propertyConditions = allowedNames . Select < string , Condition > ( allowedName => new PropertyCondition ( property , allowedName ) ) . ToArray ( ) ;
50
- return propertyConditions . Length switch {
51
- 0 => Condition . FalseCondition ,
52
- 1 => propertyConditions [ 0 ] ,
53
- _ => new OrCondition ( propertyConditions )
54
- } ;
55
- }
56
-
1
+ using ManagedWinapi . Windows ;
2
+ using System . Windows . Automation ;
3
+ using System . Windows . Input ;
4
+ using ThrottleDebounce ;
5
+
6
+ namespace AuthenticatorChooser ;
7
+
8
+ public static class SecurityKeyChooser {
9
+
10
+ // #4: unfortunately, this class name is shared with the UAC prompt, detectable when desktop dimming is disabled
11
+ private const string WINDOW_CLASS_NAME = "Credential Dialog Xaml Host" ;
12
+
13
+ public static void chooseUsbSecurityKey ( SystemWindow fidoPrompt ) {
14
+ Console . WriteLine ( ) ;
15
+ if ( ! isFidoPromptWindow ( fidoPrompt ) ) {
16
+ Console . WriteLine ( $ "Window 0x{ fidoPrompt . HWnd : x} is not a Windows Security window") ;
17
+ return ;
18
+ }
19
+
20
+ AutomationElement fidoEl = fidoPrompt . toAutomationElement ( ) ;
21
+ AutomationElement ? outerScrollViewer = fidoEl . FindFirst ( TreeScope . Children , new PropertyCondition ( AutomationElement . ClassNameProperty , "ScrollViewer" ) ) ;
22
+ AutomationElement ? promptTitleEl = outerScrollViewer ? . FindFirst ( TreeScope . Children , new AndCondition (
23
+ new PropertyCondition ( AutomationElement . ClassNameProperty , "TextBlock" ) ,
24
+ singletonSafePropertyCondition ( AutomationElement . NameProperty , false , I18N . getStrings ( I18N . Key . SIGN_IN_WITH_YOUR_PASSKEY ) ) ) ) ;
25
+
26
+ if ( outerScrollViewer == null || promptTitleEl == null ) {
27
+ Console . WriteLine ( "Window is not a passkey choice prompt" ) ;
28
+ return ;
29
+ }
30
+
31
+ Condition idCondition = new PropertyCondition ( AutomationElement . AutomationIdProperty , "CredentialsList" ) ;
32
+ List < AutomationElement > authenticatorChoices = Retrier . Attempt ( _ =>
33
+ outerScrollViewer . FindFirst ( TreeScope . Children , idCondition ) . children ( ) . ToList ( ) ,
34
+ maxAttempts : 18 , // ~5 sec
35
+ delay : attempt => TimeSpan . FromMilliseconds ( Math . Min ( 500 , 1 << attempt ) ) ) ;
36
+
37
+ IEnumerable < string > securityKeyLabelPossibilities = I18N . getStrings ( I18N . Key . SECURITY_KEY ) ;
38
+ if ( authenticatorChoices . FirstOrDefault ( choice => nameContainsAny ( choice , securityKeyLabelPossibilities ) ) is not { } securityKeyChoice ) {
39
+ Console . WriteLine ( "USB security key is not a choice, skipping" ) ;
40
+ return ;
41
+ }
42
+
43
+ ( ( SelectionItemPattern ) securityKeyChoice . GetCurrentPattern ( SelectionItemPattern . Pattern ) ) . Select ( ) ;
44
+ Console . WriteLine ( "USB security key selected" ) ;
45
+
46
+ AutomationElement nextButton = fidoEl . FindFirst ( TreeScope . Children , new PropertyCondition ( AutomationElement . AutomationIdProperty , "OkButton" ) ) ;
47
+
48
+ if ( Keyboard . IsKeyDown ( Key . LeftShift ) || Keyboard . IsKeyDown ( Key . RightShift ) ) {
49
+ nextButton . SetFocus ( ) ;
50
+ Console . WriteLine ( "Shift is pressed, not submitting dialog box" ) ;
51
+ return ;
52
+ } else if ( ! authenticatorChoices . All ( choice => choice == securityKeyChoice || nameContainsAny ( choice , I18N . getStrings ( I18N . Key . SMARTPHONE ) ) ) ) {
53
+ nextButton . SetFocus ( ) ;
54
+ Console . WriteLine ( "Dialog box has a choice that is neither pairing a new phone nor USB security key (such as an existing phone, PIN, or biometrics), " +
55
+ "skipping because the user might want to choose it" ) ;
56
+ return ;
57
+ }
58
+
59
+ ( ( InvokePattern ) nextButton . GetCurrentPattern ( InvokePattern . Pattern ) ) . Invoke ( ) ;
60
+ Console . WriteLine ( "Next button pressed" ) ;
61
+ }
62
+
63
+ // name/title are localized, so don't use those
64
+ public static bool isFidoPromptWindow ( SystemWindow window ) => window . ClassName == WINDOW_CLASS_NAME ;
65
+
66
+ private static bool nameContainsAny ( AutomationElement element , IEnumerable < string ? > possibleSubstrings ) {
67
+ string name = element . Current . Name ;
68
+ return possibleSubstrings . Any ( possibleSubstring => possibleSubstring != null && name . Contains ( possibleSubstring , StringComparison . CurrentCulture ) ) ;
69
+ }
70
+
71
+ /// <summary>
72
+ /// <para>Create an <see cref="AndCondition"/> or <see cref="OrCondition"/> for a <paramref name="property"/> from a series of <paramref name="values"/>, which have fewer than 2 items in it.</para>
73
+ /// <para>This avoids a crash in the <see cref="AndCondition"/> and <see cref="OrCondition"/> constructors if the array has size 1.</para>
74
+ /// </summary>
75
+ /// <param name="property">The name of the UI property to match against, such as <see cref="AutomationElement.NameProperty"/> or <see cref="AutomationElement.AutomationIdProperty"/>.</param>
76
+ /// <param name="and"><c>true</c> to make a conjunction (AND), <c>false</c> to make a disjunction (OR)</param>
77
+ /// <param name="values">Zero or more property values to match against.</param>
78
+ /// <returns>A <see cref="Condition"/> that matches the values against the property, without throwing an <see cref="ArgumentException"/> if <paramref name="values"/> has length < 2.</returns>
79
+ private static Condition singletonSafePropertyCondition ( AutomationProperty property , bool and , IEnumerable < string > values ) {
80
+ Condition [ ] propertyConditions = values . Select < string , Condition > ( allowedValue => new PropertyCondition ( property , allowedValue ) ) . ToArray ( ) ;
81
+ return propertyConditions . Length switch {
82
+ 0 => and ? Condition . TrueCondition : Condition . FalseCondition ,
83
+ 1 => propertyConditions [ 0 ] ,
84
+ _ => and ? new AndCondition ( propertyConditions ) : new OrCondition ( propertyConditions )
85
+ } ;
86
+ }
87
+
57
88
}
0 commit comments