├── .editorconfig ├── .github ├── dependabot.yml ├── images │ ├── authenticator-prompt.png │ ├── demo.gif │ └── usb-prompt.png └── workflows │ └── dotnet.yml ├── .gitignore ├── AuthenticatorChooser.sln ├── AuthenticatorChooser ├── AbstractSecurityKeyChooser.cs ├── AuthenticatorChooser.csproj ├── ChooserOptions.cs ├── Extensions.cs ├── I18N.cs ├── Logging.cs ├── PromptStrategy.cs ├── Resources │ ├── LocalizedStrings.Designer.cs │ ├── LocalizedStrings.ar-SA.resx │ ├── LocalizedStrings.bg-BG.resx │ ├── LocalizedStrings.ca-ES.resx │ ├── LocalizedStrings.cs-CZ.resx │ ├── LocalizedStrings.da-DK.resx │ ├── LocalizedStrings.de-DE.resx │ ├── LocalizedStrings.el-GR.resx │ ├── LocalizedStrings.en-GB.resx │ ├── LocalizedStrings.es-ES.resx │ ├── LocalizedStrings.es-MX.resx │ ├── LocalizedStrings.et-EE.resx │ ├── LocalizedStrings.eu-ES.resx │ ├── LocalizedStrings.fi-FI.resx │ ├── LocalizedStrings.fr-CA.resx │ ├── LocalizedStrings.fr-FR.resx │ ├── LocalizedStrings.gl-ES.resx │ ├── LocalizedStrings.he-IL.resx │ ├── LocalizedStrings.hr-HR.resx │ ├── LocalizedStrings.hu-HU.resx │ ├── LocalizedStrings.id-ID.resx │ ├── LocalizedStrings.it-IT.resx │ ├── LocalizedStrings.ja-JP.resx │ ├── LocalizedStrings.ko-KR.resx │ ├── LocalizedStrings.lt-LT.resx │ ├── LocalizedStrings.lv-LV.resx │ ├── LocalizedStrings.nb-NO.resx │ ├── LocalizedStrings.nl-NL.resx │ ├── LocalizedStrings.pl-PL.resx │ ├── LocalizedStrings.pt-BR.resx │ ├── LocalizedStrings.pt-PT.resx │ ├── LocalizedStrings.resx │ ├── LocalizedStrings.ro-RO.resx │ ├── LocalizedStrings.ru-RU.resx │ ├── LocalizedStrings.sk-SK.resx │ ├── LocalizedStrings.sl-SI.resx │ ├── LocalizedStrings.sr-Latn-RS.resx │ ├── LocalizedStrings.sv-SE.resx │ ├── LocalizedStrings.th-TH.resx │ ├── LocalizedStrings.tr-TR.resx │ ├── LocalizedStrings.uk-UA.resx │ ├── LocalizedStrings.vi-VN.resx │ ├── LocalizedStrings.zh-CN.resx │ └── LocalizedStrings.zh-TW.resx ├── SecurityKeyChooser.cs ├── Startup.cs ├── WindowOpening │ ├── ShellHook.cs │ ├── ShellHook.resx │ └── WindowOpeningListener.cs ├── Windows11 │ ├── OsVersion.cs │ ├── Win1123H2Strategy.cs │ ├── Win1125H2Strategy.cs │ ├── Win11Strategy.cs │ └── WindowsSecurityKeyChooser.cs ├── YubiKey.ico ├── app.manifest └── packages.lock.json ├── License.txt └── Readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | 3 | # Exception Analyzers: Exception adjustments syntax error 4 | # default = error 5 | ; dotnet_diagnostic.Ex0001.severity = none 6 | 7 | # Exception Analyzers: Exception adjustments syntax error: Symbol does not exist or identifier is invalid 8 | # default = warning 9 | ; dotnet_diagnostic.Ex0002.severity = none 10 | 11 | # Exception Analyzers: Member may throw undocumented exception 12 | # default = warning 13 | dotnet_diagnostic.Ex0100.severity = none 14 | 15 | # Exception Analyzers: Member accessor may throw undocumented exception 16 | # default = warning 17 | dotnet_diagnostic.Ex0101.severity = none 18 | 19 | # Exception Analyzers: Implicit constructor may throw undocumented exception 20 | # default = warning 21 | dotnet_diagnostic.Ex0103.severity = none 22 | 23 | # Exception Analyzers: Member initializer may throw undocumented exception 24 | # default = warning 25 | dotnet_diagnostic.Ex0104.severity = none 26 | 27 | # Exception Analyzers: Delegate created from member may throw undocumented exception 28 | # default = silent 29 | ; dotnet_diagnostic.Ex0120.severity = none 30 | 31 | # Exception Analyzers: Delegate created from anonymous function may throw undocumented exception 32 | # default = silent 33 | ; dotnet_diagnostic.Ex0121.severity = none 34 | 35 | # Exception Analyzers: Member is documented as throwing exception not documented on member in base or interface type 36 | # default = warning 37 | dotnet_diagnostic.Ex0200.severity = none 38 | 39 | # Exception Analyzers: Member accessor is documented as throwing exception not documented on member in base or interface type 40 | # default = warning 41 | dotnet_diagnostic.Ex0201.severity = none -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/AuthenticatorChooser" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/images/authenticator-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldaviva/AuthenticatorChooser/7cb4056d8d94ea298d62798ed6956eccef8dc18d/.github/images/authenticator-prompt.png -------------------------------------------------------------------------------- /.github/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldaviva/AuthenticatorChooser/7cb4056d8d94ea298d62798ed6956eccef8dc18d/.github/images/demo.gif -------------------------------------------------------------------------------- /.github/images/usb-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldaviva/AuthenticatorChooser/7cb4056d8d94ea298d62798ed6956eccef8dc18d/.github/images/usb-prompt.png -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | env: 11 | ProjectName: AuthenticatorChooser 12 | 13 | runs-on: windows-latest 14 | 15 | strategy: 16 | matrix: 17 | include: 18 | - targetPlatform: win-x64 19 | - targetPlatform: win-arm64 20 | 21 | steps: 22 | - name: Clone 23 | uses: actions/checkout@v4 24 | 25 | - name: Restore 26 | run: dotnet restore --locked-mode --verbosity normal 27 | 28 | - name: Build 29 | run: dotnet build ${{ env.ProjectName }} --no-restore --runtime ${{ matrix.targetPlatform }} --configuration Release --no-self-contained 30 | 31 | - name: Publish 32 | run: dotnet publish ${{ env.ProjectName }} --no-build --runtime ${{ matrix.targetPlatform }} --configuration Release --no-self-contained -p:PublishSingleFile=true 33 | 34 | - name: Upload build artifacts 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: ${{ env.ProjectName }}-${{ matrix.targetPlatform }} 38 | path: ${{ env.ProjectName }}/bin/Release/net8.0-windows/${{ matrix.targetPlatform }}/publish/*.exe 39 | if-no-files-found: error 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #Ignore thumbnails created by Windows 3 | Thumbs.db 4 | #Ignore files built by Visual Studio 5 | *.obj 6 | *.exe 7 | *.pdb 8 | *.user 9 | *.aps 10 | *.pch 11 | *.vspscc 12 | *_i.c 13 | *_p.c 14 | *.ncb 15 | *.suo 16 | *.tlb 17 | *.tlh 18 | *.bak 19 | *.cache 20 | *.ilk 21 | *.log 22 | [Bb]in 23 | [Dd]ebug*/ 24 | *.lib 25 | *.sbr 26 | obj/ 27 | [Rr]elease*/ 28 | _ReSharper*/ 29 | [Tt]est[Rr]esult* 30 | .vs/ 31 | #Nuget packages folder 32 | packages/ 33 | -------------------------------------------------------------------------------- /AuthenticatorChooser.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34701.34 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthenticatorChooser", "AuthenticatorChooser\AuthenticatorChooser.csproj", "{24618EB8-29AF-47BF-A5D2-5A8C8E724991}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {C11C7C56-6448-422F-A1C6-62859829E0CE} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /AuthenticatorChooser/AbstractSecurityKeyChooser.cs: -------------------------------------------------------------------------------- 1 | using ManagedWinapi.Windows; 2 | 3 | namespace AuthenticatorChooser; 4 | 5 | public abstract class AbstractSecurityKeyChooser: SecurityKeyChooser { 6 | 7 | public abstract void chooseUsbSecurityKey(T fidoPrompt); 8 | 9 | public abstract bool isFidoPromptWindow(SystemWindow window); 10 | 11 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/AuthenticatorChooser.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WinExe 6 | net8.0-windows 7 | win-x64;win-arm64 8 | enable 9 | enable 10 | 0.5.0 11 | Ben Hutchison 12 | © 2025 $(Authors) 13 | $(Authors) 14 | major 15 | true 16 | app.manifest 17 | YubiKey.ico 18 | en 19 | true 20 | embedded 21 | latest 22 | false 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | true 51 | 52 | 53 | -------------------------------------------------------------------------------- /AuthenticatorChooser/ChooserOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace AuthenticatorChooser; 4 | 5 | public readonly record struct ChooserOptions(bool skipAllNonSecurityKeyOptions, int? autoSubmitPinLength) { 6 | 7 | public Stopwatch overallStopwatch { get; } = new(); 8 | 9 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Automation; 2 | 3 | namespace AuthenticatorChooser; 4 | 5 | public static class Extensions { 6 | 7 | public static bool nameContainsAny(this AutomationElement element, IEnumerable possibleSubstrings) { 8 | string name = element.Current.Name; 9 | // #2: in addition to a prefix, there is sometimes also a suffix after the substring 10 | return possibleSubstrings.Any(possibleSubstring => name.Contains(possibleSubstring, StringComparison.CurrentCulture)); 11 | } 12 | 13 | /// 14 | /// Create an or for a from a series of , which have fewer than 2 items in it. 15 | /// This avoids a crash in the and constructors if the array has size 1. 16 | /// 17 | /// The name of the UI property to match against, such as or . 18 | /// true to make a conjunction (AND), false to make a disjunction (OR) 19 | /// Zero or more property values to match against. 20 | /// A that matches the values against the property, without throwing an if has length < 2. 21 | public static Condition singletonSafeCondition(this AutomationProperty property, bool and, IEnumerable values) { 22 | Condition[] propertyConditions = values.Select(allowedValue => new PropertyCondition(property, allowedValue)).ToArray(); 23 | return propertyConditions.Length switch { 24 | 0 when and => Condition.TrueCondition, 25 | 0 => Condition.FalseCondition, 26 | 1 => propertyConditions[0], 27 | _ when and => new AndCondition(propertyConditions), 28 | _ => new OrCondition(propertyConditions) 29 | }; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Logging.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using NLog.Config; 3 | using NLog.Layouts; 4 | using NLog.Targets; 5 | 6 | namespace AuthenticatorChooser; 7 | 8 | internal static class Logging { 9 | 10 | private static readonly SimpleLayout MESSAGE_FORMAT = new( 11 | " ${level:format=FirstCharacter:lowercase=true} | ${date:format=yyyy-MM-dd HH\\:mm\\:ss.fff} | ${logger:shortName=true:padding=-25} | ${message:withException=true:exceptionSeparator=\n}"); 12 | 13 | private static readonly LogLevel LOG_LEVEL = LogLevel.Debug; 14 | 15 | public static void initialize(bool enableFileAppender, string? logFilename) { 16 | logFilename = logFilename != null ? Environment.ExpandEnvironmentVariables(logFilename) : Path.Combine(Path.GetTempPath(), Path.ChangeExtension(nameof(AuthenticatorChooser), ".log")); 17 | 18 | LoggingConfiguration logConfig = new(); 19 | 20 | if (enableFileAppender) { 21 | logConfig.AddRule(LOG_LEVEL, LogLevel.Fatal, new FileTarget("fileAppender") { 22 | Layout = MESSAGE_FORMAT, 23 | FileName = logFilename 24 | }); 25 | } 26 | 27 | logConfig.AddRule(LOG_LEVEL, LogLevel.Fatal, new ConsoleTarget("consoleAppender") { 28 | Layout = MESSAGE_FORMAT, 29 | DetectConsoleAvailable = true 30 | }); 31 | 32 | LogManager.Configuration = logConfig; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/PromptStrategy.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Automation; 2 | 3 | namespace AuthenticatorChooser; 4 | 5 | public interface PromptStrategy { 6 | 7 | bool canHandleTitle(string? actualTitle); 8 | Task handleWindow(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown); 9 | 10 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace AuthenticatorChooser.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class LocalizedStrings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal LocalizedStrings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AuthenticatorChooser.Resources.LocalizedStrings", typeof(LocalizedStrings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Choose a different passkey. 65 | /// 66 | internal static string chooseADifferentPasskey { 67 | get { 68 | return ResourceManager.GetString("chooseADifferentPasskey", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Choose a passkey. 74 | /// 75 | internal static string chooseAPasskey { 76 | get { 77 | return ResourceManager.GetString("chooseAPasskey", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Making sure it’s you. 83 | /// 84 | internal static string makingSureItsYou { 85 | get { 86 | return ResourceManager.GetString("makingSureItsYou", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Making sure it's you. 92 | /// 93 | internal static string makingSureItsYou2 { 94 | get { 95 | return ResourceManager.GetString("makingSureItsYou2", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Security key. 101 | /// 102 | internal static string securityKey { 103 | get { 104 | return ResourceManager.GetString("securityKey", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Sign in with a passkey. 110 | /// 111 | internal static string signInWithAPasskey { 112 | get { 113 | return ResourceManager.GetString("signInWithAPasskey", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Sign in with your passkey. 119 | /// 120 | internal static string signInWithYourPasskey { 121 | get { 122 | return ResourceManager.GetString("signInWithYourPasskey", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to iPhone, iPad, or Android device. 128 | /// 129 | internal static string smartphone { 130 | get { 131 | return ResourceManager.GetString("smartphone", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to Use another device. 137 | /// 138 | internal static string useAnotherDevice { 139 | get { 140 | return ResourceManager.GetString("useAnotherDevice", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to This Windows device. 146 | /// 147 | internal static string windows { 148 | get { 149 | return ResourceManager.GetString("windows", resourceCulture); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.ar-SA.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | مفتاح الأمان 122 | 123 | 124 | جهاز iPhone أو iPad أو Android 125 | 126 | 127 | جهاز Windows هذا 128 | 129 | 130 | تسجيل الدخول باستخدام رمز المرور الخاص بك 131 | 132 | 133 | استخدام جهاز آخر 134 | 135 | 136 | ‏‏التأكد من شخصيتك 137 | 138 | 139 | اختيار رمز مرور 140 | 141 | 142 | تسجيل الدخول باستخدام مفتاح مرور 143 | 144 | 145 | اختيار رمز مرور مختلف 146 | 147 | 148 | ‏‏التأكد من شخصيتك 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.bg-BG.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Защитен ключ 122 | 123 | 124 | iPhone, iPad или устройство с Android 125 | 126 | 127 | Това устройство с Windows 128 | 129 | 130 | Влезте с вашия ключ за достъп 131 | 132 | 133 | Използване на друго устройство 134 | 135 | 136 | Проверка на самоличността ви 137 | 138 | 139 | Избор на ключ за достъп 140 | 141 | 142 | Влезте с ключа за достъп 143 | 144 | 145 | Изберете друг ключ за достъп 146 | 147 | 148 | Проверка на самоличността ви 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.cs-CZ.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Klíč zabezpečení 122 | 123 | 124 | iPhone, iPad nebo zařízení s Androidem 125 | 126 | 127 | Toto zařízení s Windows 128 | 129 | 130 | Přihlaste se pomocí svého klíče 131 | 132 | 133 | Použít jiné zařízení 134 | 135 | 136 | Ověřujeme, jestli jste to vy. 137 | 138 | 139 | Volba klíče 140 | 141 | 142 | Přihlášení pomocí klíče 143 | 144 | 145 | Vybrat jiný klíč 146 | 147 | 148 | Ujišťujeme se, že jste to vy 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.da-DK.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Sikkerhedsnøgle 122 | 123 | 124 | iPhone-, iPad- eller Android-enhed 125 | 126 | 127 | Denne Windows-enhed 128 | 129 | 130 | Log på med din adgangsnøgle på 131 | 132 | 133 | Brug en anden enhed 134 | 135 | 136 | Vi vil lige sikre os, at det er dig 137 | 138 | 139 | Vælg en adgangsnøgle 140 | 141 | 142 | Log på med en adgangsnøgle 143 | 144 | 145 | Vælg en anden adgangsnøgle 146 | 147 | 148 | Vi sikrer os, at det er dig 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.en-GB.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Security key 122 | 123 | 124 | iPhone, iPad, or Android device 125 | 126 | 127 | This Windows device 128 | 129 | 130 | Sign in with your passkey 131 | 132 | 133 | Use another device 134 | 135 | 136 | Making sure it’s you 137 | 138 | 139 | Choose a passkey 140 | 141 | 142 | Sign in with a passkey 143 | 144 | 145 | Choose a different passkey 146 | 147 | 148 | Making sure it's you 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.et-EE.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Turbevõti 122 | 123 | 124 | iPhone, iPad või Androidi seade 125 | 126 | 127 | See Windowsi seade 128 | 129 | 130 | Logige sisse oma parooliga 131 | 132 | 133 | Kasuta muud seadet 134 | 135 | 136 | Teie isiku tuvastamine 137 | 138 | 139 | Vali pääsuvõti 140 | 141 | 142 | Logige sisse pääsuvõtmega 143 | 144 | 145 | Vali muu pääsuvõti 146 | 147 | 148 | Kontrollime, kas see olete teie 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.he-IL.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | מפתח אבטחה 122 | 123 | 124 | מכשיר iPhone‏, iPad או Android 125 | 126 | 127 | מכשיר Windows זה 128 | 129 | 130 | היכנס באמצעות המפתח 131 | 132 | 133 | השתמש במכשיר נוסף 134 | 135 | 136 | ‏‏מוודא שזה אתה 137 | 138 | 139 | בחר מפתח 140 | 141 | 142 | כניסה באמצעות מפתח 143 | 144 | 145 | בחר מפתח אחר 146 | 147 | 148 | אנחנו מוודאים שזה אתה 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.id-ID.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Kunci keamanan 122 | 123 | 124 | Perangkat iPhone, iPad, atau Android 125 | 126 | 127 | Perangkat Windows ini 128 | 129 | 130 | Masuk dengan kunci sandi Anda 131 | 132 | 133 | Gunakan perangkat lain 134 | 135 | 136 | Memastikan bahwa ini Anda 137 | 138 | 139 | Pilih kunci sandi 140 | 141 | 142 | Masuk dengan kunci sandi 143 | 144 | 145 | Pilih kunci sandi lain 146 | 147 | 148 | Memastikan bahwa ini adalah Anda 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.it-IT.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Chiave di sicurezza 122 | 123 | 124 | iPhone, iPad o dispositivo Android 125 | 126 | 127 | Questo dispositivo Windows 128 | 129 | 130 | Accedi con la passkey 131 | 132 | 133 | Usa un altro dispositivo 134 | 135 | 136 | Verifica dell'identità in corso 137 | 138 | 139 | Scegli una passkey 140 | 141 | 142 | Accedi con una passkey 143 | 144 | 145 | Scegli una passkey diversa 146 | 147 | 148 | Verifica della tua identità 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.ja-JP.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | セキュリティ キー 122 | 123 | 124 | iPhone、iPad、または Android デバイス 125 | 126 | 127 | この Windows デバイス 128 | 129 | 130 | パスキーを使用してサインインします 131 | 132 | 133 | 別のデバイスを使用する 134 | 135 | 136 | ご本人確認をします 137 | 138 | 139 | パスキーの選択 140 | 141 | 142 | パスキーを使用してサインインします 143 | 144 | 145 | 別のパスキーを選択する 146 | 147 | 148 | 本人確認をしています 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.ko-KR.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 보안 키 122 | 123 | 124 | iPhone, iPad 또는 Android 디바이스 125 | 126 | 127 | 이 Windows 장치 128 | 129 | 130 | 암호 키로 로그인 131 | 132 | 133 | 다른 장치 사용 134 | 135 | 136 | 사용자 본인인지 확인 137 | 138 | 139 | 암호 키 선택 140 | 141 | 142 | 암호 키로 로그인 143 | 144 | 145 | 다른 암호 키 선택 146 | 147 | 148 | 본인 확인 중 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.nb-NO.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Sikkerhetsnøkkel 122 | 123 | 124 | iPhone, iPad eller Android-enhet 125 | 126 | 127 | Denne Windows-enheten 128 | 129 | 130 | Logg på med tilgangsnøkkelen 131 | 132 | 133 | Bruk en annen enhet 134 | 135 | 136 | Kontrollerer at det er deg 137 | 138 | 139 | Velg en tilgangsnøkkel 140 | 141 | 142 | Logg på med en adgangsnøkkel 143 | 144 | 145 | Velg en annen tilgangsnøkkel 146 | 147 | 148 | Vi kontrollerer at det er deg 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.pt-BR.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Chave de Segurança 122 | 123 | 124 | iPhone, iPad ou dispositivo Android 125 | 126 | 127 | Este dispositivo Windows 128 | 129 | 130 | Entre com sua chave de acesso 131 | 132 | 133 | Usar outro dispositivo 134 | 135 | 136 | Verificando se é você mesmo 137 | 138 | 139 | Escolher uma chave de acesso 140 | 141 | 142 | Entrar com uma chave de acesso 143 | 144 | 145 | Escolha uma senha diferente 146 | 147 | 148 | Verificando se é você mesmo 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Security key 122 | 123 | 124 | iPhone, iPad, or Android device 125 | 126 | 127 | This Windows device 128 | 129 | 130 | Sign in with your passkey 131 | 132 | 133 | Use another device 134 | 135 | 136 | Making sure it’s you 137 | 138 | 139 | Choose a passkey 140 | 141 | 142 | Sign in with a passkey 143 | 144 | 145 | Choose a different passkey 146 | 147 | 148 | Making sure it's you 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.ru-RU.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Ключ безопасности 122 | 123 | 124 | Устройство iPhone, iPad или Android 125 | 126 | 127 | Это устройство с Windows 128 | 129 | 130 | Войдите, используя ключ доступа 131 | 132 | 133 | Использовать другое устройство 134 | 135 | 136 | Подтверждение вашей личности 137 | 138 | 139 | Выберите ключ доступа 140 | 141 | 142 | Войдите, используя ключ доступа 143 | 144 | 145 | Выберите другой код доступа 146 | 147 | 148 | Подтверждение вашей личности 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.sl-SI.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Varnostni ključ 122 | 123 | 124 | Naprava iPhone, iPad ali Android 125 | 126 | 127 | Ta naprava s sistemom Windows 128 | 129 | 130 | Vpišite se s ključem 131 | 132 | 133 | Dodaj še eno napravo 134 | 135 | 136 | Preverjamo, ali ste to res vi 137 | 138 | 139 | Izberite ključ 140 | 141 | 142 | Vpišite se s ključem 143 | 144 | 145 | Izberite drug ključ 146 | 147 | 148 | Preverjanje vaše identitete 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.sv-SE.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Säkerhetsnyckel 122 | 123 | 124 | iPhone-, iPad- eller Android-enhet 125 | 126 | 127 | Den här Windows-enheten 128 | 129 | 130 | Logga in med din nyckel 131 | 132 | 133 | Använd en annan enhet 134 | 135 | 136 | Kontrollerar att du är du 137 | 138 | 139 | Välj en nyckel 140 | 141 | 142 | Logga in med en nyckel 143 | 144 | 145 | Välj en annan nyckel 146 | 147 | 148 | Kontrollerar att du är du 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.th-TH.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | คีย์ความปลอดภัย 122 | 123 | 124 | iPhone, iPad หรืออุปกรณ์ Android 125 | 126 | 127 | อุปกรณ์ Windows นี้ 128 | 129 | 130 | ลงชื่อเข้าใช้ด้วยหมายเลขรหัสผ่านของคุณ 131 | 132 | 133 | ใช้อุปกรณ์อื่น 134 | 135 | 136 | โปรดตรวจสอบว่าเป็นคุณ 137 | 138 | 139 | เลือกหมายเลขรหัสผ่าน 140 | 141 | 142 | ลงชื่อเข้าใช้ด้วยหมายเลขรหัสผ่าน 143 | 144 | 145 | เลือกหมายเลขรหัสผ่านอื่น 146 | 147 | 148 | กำลังตรวจสอบให้แน่ใจว่าเป็นคุณ 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.uk-UA.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Ключ безпеки 122 | 123 | 124 | iPhone, iPad або пристрій з Android 125 | 126 | 127 | Цей пристрій Windows 128 | 129 | 130 | Увійдіть за допомогою ключа доступу 131 | 132 | 133 | Використовувати інший пристрій 134 | 135 | 136 | Перевірка вашої особи 137 | 138 | 139 | Виберіть ключ доступу 140 | 141 | 142 | Увійдіть за допомогою ключа доступу 143 | 144 | 145 | Виберіть інший ключ доступу 146 | 147 | 148 | Перевірка вашої особи 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.vi-VN.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Khóa bảo mật 122 | 123 | 124 | iPhone, iPad hoặc thiết bị Android 125 | 126 | 127 | Thiết bị Windows này 128 | 129 | 130 | Đăng nhập bằng phím mật khẩu của bạn 131 | 132 | 133 | Sử dụng thiết bị khác 134 | 135 | 136 | Đảm bảo đó chính là bạn 137 | 138 | 139 | Chọn mã khóa 140 | 141 | 142 | Đăng nhập bằng mã khóa 143 | 144 | 145 | Chọn một mã khóa khác 146 | 147 | 148 | Hãy đảm bảo đây chính là bạn 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.zh-CN.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 安全密钥 122 | 123 | 124 | iPhone、iPad 或 Android 设备 125 | 126 | 127 | 此 Windows 设备 128 | 129 | 130 | 使用密钥登录 131 | 132 | 133 | 使用其他设备 134 | 135 | 136 | 请确认是你本人 137 | 138 | 139 | 选择通行密钥 140 | 141 | 142 | 使用密钥登录 143 | 144 | 145 | 选择其他通行密钥 146 | 147 | 148 | 确保这是你本人 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/Resources/LocalizedStrings.zh-TW.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 安全性金鑰 122 | 123 | 124 | iPhone、iPad 或 Android 裝置 125 | 126 | 127 | 此 Windows 裝置 128 | 129 | 130 | 使用您的金鑰登入 131 | 132 | 133 | 使用其他裝置 134 | 135 | 136 | 請確認是您 137 | 138 | 139 | 選擇金鑰 140 | 141 | 142 | 使用密鑰登入 143 | 144 | 145 | 選擇其他金鑰 146 | 147 | 148 | 驗證您的身分 149 | 150 | -------------------------------------------------------------------------------- /AuthenticatorChooser/SecurityKeyChooser.cs: -------------------------------------------------------------------------------- 1 | namespace AuthenticatorChooser; 2 | 3 | public interface SecurityKeyChooser { 4 | 5 | void chooseUsbSecurityKey(WINDOW fidoPrompt); 6 | 7 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/WindowOpening/ShellHook.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Windows.Forms; 3 | 4 | namespace AuthenticatorChooser.WindowOpening; 5 | 6 | public interface ShellHook: IDisposable { 7 | 8 | event EventHandler? shellEvent; 9 | 10 | } 11 | 12 | public partial class ShellHookImpl: Form, ShellHook { 13 | 14 | private const string USER32 = "user32.dll"; 15 | 16 | public event EventHandler? shellEvent; 17 | 18 | private readonly uint subscriptionId; 19 | 20 | public ShellHookImpl() { 21 | subscriptionId = registerWindowMessage("SHELLHOOK"); 22 | registerShellHookWindow(Handle); 23 | } 24 | 25 | protected override void WndProc(ref Message message) { 26 | if (message.Msg == subscriptionId) { 27 | shellEvent?.Invoke(this, new ShellEventArgs(shellEvent: (ShellEventArgs.ShellEvent) message.WParam.ToInt32(), windowHandle: message.LParam)); 28 | } 29 | 30 | base.WndProc(ref message); 31 | } 32 | 33 | protected override void Dispose(bool disposing) { 34 | deregisterShellHookWindow(Handle); 35 | base.Dispose(disposing); 36 | } 37 | 38 | [LibraryImport(USER32, EntryPoint = "RegisterWindowMessageW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] 39 | private static partial uint registerWindowMessage(string lpString); 40 | 41 | [LibraryImport(USER32, EntryPoint = "DeregisterShellHookWindow")] 42 | [return: MarshalAs(UnmanagedType.Bool)] 43 | private static partial bool deregisterShellHookWindow(IntPtr hWnd); 44 | 45 | [LibraryImport(USER32, EntryPoint = "RegisterShellHookWindow")] 46 | [return: MarshalAs(UnmanagedType.Bool)] 47 | private static partial bool registerShellHookWindow(IntPtr hWnd); 48 | 49 | } 50 | 51 | public class ShellEventArgs(ShellEventArgs.ShellEvent shellEvent, IntPtr windowHandle): EventArgs { 52 | 53 | public readonly ShellEvent shellEvent = shellEvent; 54 | public readonly IntPtr windowHandle = windowHandle; 55 | 56 | public enum ShellEvent { 57 | 58 | HSHELL_WINDOWCREATED = 1, 59 | HSHELL_WINDOWDESTROYED = 2, 60 | HSHELL_ACTIVATESHELLWINDOW = 3, 61 | HSHELL_WINDOWACTIVATED = 4, 62 | HSHELL_GETMINRECT = 5, 63 | HSHELL_REDRAW = 6, 64 | HSHELL_TASKMAN = 7, 65 | HSHELL_LANGUAGE = 8, 66 | HSHELL_ACCESSIBILITYSTATE = 11, 67 | HSHELL_APPCOMMAND = 12 68 | 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/WindowOpening/ShellHook.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /AuthenticatorChooser/WindowOpening/WindowOpeningListener.cs: -------------------------------------------------------------------------------- 1 | using ManagedWinapi.Windows; 2 | 3 | namespace AuthenticatorChooser.WindowOpening; 4 | 5 | public interface WindowOpeningListener: IDisposable { 6 | 7 | event EventHandler? windowOpened; 8 | 9 | } 10 | 11 | public class WindowOpeningListenerImpl: WindowOpeningListener { 12 | 13 | public event EventHandler? windowOpened; 14 | 15 | private readonly ShellHook shellHook = new ShellHookImpl(); 16 | 17 | public WindowOpeningListenerImpl() { 18 | shellHook.shellEvent += onWindowOpened; 19 | } 20 | 21 | private void onWindowOpened(object? sender, ShellEventArgs args) { 22 | if (args.shellEvent == ShellEventArgs.ShellEvent.HSHELL_WINDOWCREATED) { 23 | windowOpened?.Invoke(this, new SystemWindow(args.windowHandle)); 24 | } 25 | } 26 | 27 | public void Dispose() { 28 | shellHook.shellEvent -= onWindowOpened; 29 | shellHook.Dispose(); 30 | GC.SuppressFinalize(this); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Windows11/OsVersion.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using System.Management; 3 | 4 | namespace AuthenticatorChooser.Windows11; 5 | 6 | /// Microsoft Windows 11 Pro 7 | /// 24H2 8 | /// 10.0.26100.3775 (major version is 10 on Windows 11) 9 | /// AMD64 10 | internal readonly record struct OsVersion(string name, string marketingVersion, Version version, string architecture) { 11 | 12 | private const string NT_CURRENTVERSION_KEY = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion"; 13 | 14 | public static OsVersion getCurrent() { 15 | using ManagementObjectSearcher wmiSearch = new(new SelectQuery("Win32_OperatingSystem", null, ["Caption", "Version"])); 16 | using ManagementObjectCollection wmiResults = wmiSearch.Get(); 17 | using ManagementObject wmiResult = wmiResults.Cast().First(); 18 | 19 | return new OsVersion( 20 | name: (string) wmiResult["Caption"], 21 | marketingVersion: Registry.GetValue(NT_CURRENTVERSION_KEY, "DisplayVersion", null) as string ?? string.Empty, 22 | version: Version.Parse($"{wmiResult["Version"]}.{Registry.GetValue(NT_CURRENTVERSION_KEY, "UBR", 0) as int? ?? 0}"), 23 | architecture: Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE") ?? string.Empty); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Windows11/Win1123H2Strategy.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System.Windows.Automation; 3 | 4 | namespace AuthenticatorChooser.Windows11; 5 | 6 | public class Win1123H2Strategy(ChooserOptions options): Win11Strategy(options) { 7 | 8 | private static readonly Logger LOGGER = LogManager.GetLogger(typeof(Win1123H2Strategy).FullName!); 9 | 10 | public override bool canHandleTitle(string? actualTitle) => I18N.getStrings(I18N.Key.SIGN_IN_WITH_YOUR_PASSKEY) 11 | .Concat(options.skipAllNonSecurityKeyOptions || options.autoSubmitPinLength >= MIN_PIN_LENGTH ? I18N.getStrings(I18N.Key.MAKING_SURE_ITS_YOU) : []) 12 | .Any(expected => expected.Equals(actualTitle, StringComparison.CurrentCulture)); 13 | 14 | /** 15 | * 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 nonexistent dialog. 16 | * Otherwise, perform common checks like holding Shift and stopping if there are other options. 17 | * Finally, click Next. 18 | */ 19 | public override async Task handleWindow(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown) { 20 | CancellationTokenSource stopFinding = new(); 21 | 22 | /* 23 | * If the TPM contains a passkey for this RP, Windows will ask for your fingerprint/PIN/face, and you have to select "Use another device" and click Next to see all the authenticator choices. 24 | * #5, #11: power series backoff, max=500 ms per attempt, ~1 minute total 25 | */ 26 | Task?> authenticatorChoicesTask = findAuthenticatorChoices(outerScrollViewer, stopFinding.Token); 27 | 28 | Task pinFieldTask = options.autoSubmitPinLength >= MIN_PIN_LENGTH && I18N.getStrings(I18N.Key.MAKING_SURE_ITS_YOU).Contains(actualTitle, StringComparer.CurrentCulture) 29 | ? findPinField(outerScrollViewer, stopFinding.Token) : new TaskCompletionSource().Task; 30 | 31 | await Task.WhenAny(authenticatorChoicesTask, pinFieldTask); 32 | await stopFinding.CancelAsync(); 33 | 34 | if (pinFieldTask.IsCompletedSuccessfully) { 35 | if (pinFieldTask.Result is { } pinField) { 36 | LOGGER.Debug("Found PIN field"); 37 | autosubmitPin(fidoEl, outerScrollViewer, pinField); 38 | } 39 | return; 40 | } 41 | if (authenticatorChoicesTask is not { IsCompletedSuccessfully: true, Result: { } authenticatorChoices }) { 42 | LOGGER.Warn("Could not find authenticator choices after retrying for 1 minute. Giving up and not automatically selecting Security Key."); 43 | return; 44 | } 45 | 46 | bool isLocalWindowsHelloTpmPrompt = false; 47 | AutomationElement? desiredChoice = getSecurityKeyChoice(authenticatorChoices); 48 | if (desiredChoice == null && options.skipAllNonSecurityKeyOptions) { 49 | desiredChoice = authenticatorChoices.FirstOrDefault(choice => choice.nameContainsAny(I18N.getStrings(I18N.Key.USE_ANOTHER_DEVICE))); // #15 50 | isLocalWindowsHelloTpmPrompt = desiredChoice != null; 51 | } 52 | 53 | if (desiredChoice == null) { 54 | LOGGER.Debug("Desired choice not found, skipping"); 55 | return; 56 | } 57 | 58 | /* 59 | * Select the desired choice in preparation to click Next later. 60 | * However, if we're both on the TPM credential screen (PIN/fingerprint/face) AND the user is holding Shift to not submit the dialog, then don't select it, because that would submit the "Use another device" choice to move from the TPM credential dialog to the authenticator choice list dialog. 61 | */ 62 | if (!(isLocalWindowsHelloTpmPrompt && isShiftDown)) { 63 | ((SelectionItemPattern) desiredChoice.GetCurrentPattern(SelectionItemPattern.Pattern)).Select(); 64 | } 65 | 66 | if (isLocalWindowsHelloTpmPrompt) { 67 | // do nothing because the prompt either has already closed or will remain open due to Shift being held down 68 | } else if (fidoEl.FindFirst(TreeScope.Children, NEXT_BUTTON_CONDITION) is not { } nextButton) { 69 | LOGGER.Error("Could not find Next button in Windows Security dialog box, skipping this dialog box instance"); 70 | } else if (!shouldSkipSubmission(desiredChoice, authenticatorChoices, isShiftDown)) { 71 | ((InvokePattern) nextButton.GetCurrentPattern(InvokePattern.Pattern)).Invoke(); 72 | LOGGER.Info("Next button pressed {0:N3} sec after dialog appeared", options.overallStopwatch.Elapsed.TotalSeconds); 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Windows11/Win1125H2Strategy.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System.Windows.Automation; 3 | using Unfucked; 4 | 5 | namespace AuthenticatorChooser.Windows11; 6 | 7 | public class Win1125H2Strategy(ChooserOptions options): Win11Strategy(options) { 8 | 9 | private static readonly Logger LOGGER = LogManager.GetLogger(typeof(Win1125H2Strategy).FullName!); 10 | 11 | private static readonly Condition LINK_CONDITION = new AndCondition( 12 | new PropertyCondition(AutomationElement.ClassNameProperty, "Hyperlink"), 13 | AutomationElement.NameProperty.singletonSafeCondition(false, I18N.getStrings(I18N.Key.CHOOSE_A_DIFFERENT_PASSKEY))); 14 | 15 | private static readonly Condition AUTHENTICATOR_NAME_CONDITION = new AndCondition( 16 | new PropertyCondition(AutomationElement.ClassNameProperty, "TextBlock"), 17 | new PropertyCondition(AutomationElement.HeadingLevelProperty, AutomationHeadingLevel.None)); 18 | 19 | public override bool canHandleTitle(string? actualTitle) => I18N.getStrings(I18N.Key.CHOOSE_A_PASSKEY) 20 | .Concat(options.skipAllNonSecurityKeyOptions || options.autoSubmitPinLength >= MIN_PIN_LENGTH ? I18N.getStrings(I18N.Key.SIGN_IN_WITH_A_PASSKEY) : []) 21 | .Any(expected => expected.Equals(actualTitle, StringComparison.CurrentCulture)); 22 | 23 | public override async Task handleWindow(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown) { 24 | if (I18N.getStrings(I18N.Key.CHOOSE_A_PASSKEY).Contains(actualTitle, StringComparer.CurrentCulture)) { 25 | if (await findAuthenticatorChoices(outerScrollViewer) is not { } authenticatorChoices) return; 26 | 27 | if (getSecurityKeyChoice(authenticatorChoices) is not { } desiredChoice) { 28 | LOGGER.Debug("Desired choice not found, skipping"); 29 | return; 30 | } 31 | 32 | if (!shouldSkipSubmission(desiredChoice, authenticatorChoices, isShiftDown)) { 33 | ((SelectionItemPattern) desiredChoice.GetCurrentPattern(SelectionItemPattern.Pattern)).Select(); 34 | LOGGER.Info("Choice selected {0:N3} sec after dialog appeared", options.overallStopwatch.Elapsed.TotalSeconds); 35 | } 36 | } else { 37 | /* 38 | * The choice to use a non-TPM passkey was moved from the list to a separate link in 25H2. 39 | * Here, the user wants to skip all non-security-key options, and this prompt is one of the authenticator challenges like entering a TPM PIN or plugging in a security key 40 | */ 41 | if (await outerScrollViewer.WaitForFirstAsync(TreeScope.Children, AUTHENTICATOR_NAME_CONDITION) is not { } authenticatorNameEl) { 42 | LOGGER.Debug("Could not find name of the current authenticator while trying to skip a non-security-key option, ignoring dialog"); 43 | return; 44 | } 45 | 46 | if (I18N.getStrings(I18N.Key.SECURITY_KEY).Contains(authenticatorNameEl.Current.Name, StringComparer.CurrentCulture)) { 47 | if (options.autoSubmitPinLength >= MIN_PIN_LENGTH) { 48 | autosubmitPin(fidoEl, outerScrollViewer); 49 | } else { 50 | LOGGER.Debug("The current authenticator is already a security key, so there is nothing to do on this dialog"); 51 | } 52 | return; 53 | } 54 | 55 | if (outerScrollViewer.FindFirst(TreeScope.Children, LINK_CONDITION) is not { } chooseADifferentPasskeyLink) { 56 | LOGGER.Warn("Could not find 'Choose a different passkey' link in dialog"); 57 | return; 58 | } 59 | 60 | ((InvokePattern) chooseADifferentPasskeyLink.GetCurrentPattern(InvokePattern.Pattern)).Invoke(); 61 | LOGGER.Info("Requested list of all authenticators {0:N3} sec after dialog appeared", options.overallStopwatch.Elapsed.TotalSeconds); 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Windows11/Win11Strategy.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System.Windows.Automation; 3 | using Unfucked; 4 | 5 | namespace AuthenticatorChooser.Windows11; 6 | 7 | public abstract class Win11Strategy(ChooserOptions options): PromptStrategy { 8 | 9 | protected const int MIN_PIN_LENGTH = 4; // https://support.yubico.com/hc/en-us/articles/4402836718866-Understanding-YubiKey-PINs#h_01HPHYDEAT97H0AJ4SZ48MWHW4 10 | 11 | private static readonly Logger LOGGER = LogManager.GetLogger(typeof(Win11Strategy).FullName!); 12 | 13 | private static readonly Condition CHOICES_LIST_CONDITION = new PropertyCondition(AutomationElement.ClassNameProperty, "ListView"); 14 | protected static readonly Condition NEXT_BUTTON_CONDITION = new PropertyCondition(AutomationElement.AutomationIdProperty, "OkButton"); 15 | 16 | protected ChooserOptions options { get; } = options; 17 | 18 | public abstract bool canHandleTitle(string? actualTitle); 19 | public abstract Task handleWindow(string actualTitle, AutomationElement fidoEl, AutomationElement outerScrollViewer, bool isShiftDown); 20 | 21 | protected bool shouldSkipSubmission(AutomationElement desiredChoice, IEnumerable authenticatorChoices, bool isShiftDown) { 22 | if (isShiftDown) { 23 | LOGGER.Info("Shift is pressed, not submitting dialog box"); 24 | return true; 25 | } else if (!options.skipAllNonSecurityKeyOptions && !authenticatorChoices.All(choice => choice == desiredChoice || choice.nameContainsAny(I18N.getStrings(I18N.Key.SMARTPHONE)))) { 26 | LOGGER.Info( 27 | "Dialog box has a choice that is neither pairing a new phone nor USB security key (such as an existing phone, PIN, or biometrics), skipping because you might want to choose it. You may override this behavior with --skip-all-non-security-key-options."); 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | } 33 | 34 | protected static async Task?> findAuthenticatorChoices(AutomationElement outerScrollViewer, CancellationToken ct = default) { 35 | using CancellationTokenSource stopFinding = CancellationTokenSource.CreateLinkedTokenSource(Startup.EXITING, ct); 36 | IReadOnlyList? authenticatorChoices = 37 | await outerScrollViewer.WaitForFirstAsync(TreeScope.Children, CHOICES_LIST_CONDITION, el => Task.FromResult(el.Children().ToList()), TimeSpan.FromSeconds(30), stopFinding.Token); 38 | if (authenticatorChoices == null) { 39 | LOGGER.Warn("Could not find authenticator choices after retrying for 1 minute. Giving up and not automatically selecting Security Key."); 40 | } 41 | return authenticatorChoices; 42 | } 43 | 44 | protected static AutomationElement? getSecurityKeyChoice(IEnumerable authenticatorChoices) { 45 | return authenticatorChoices.FirstOrDefault(choice => choice.nameContainsAny(I18N.getStrings(I18N.Key.SECURITY_KEY))); 46 | } 47 | 48 | protected static async Task findPinField(AutomationElement outerScrollViewer, CancellationToken ct) => 49 | await outerScrollViewer.WaitForFirstAsync(TreeScope.Descendants, new PropertyCondition(AutomationElement.IsPasswordProperty, true), 50 | TimeSpan.FromMinutes(3), ct); 51 | 52 | protected void autosubmitPin(AutomationElement fidoEl, AutomationElement outerScrollViewer, AutomationElement? pinField = null) { 53 | CancellationTokenSource windowClosed = new(); 54 | Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, fidoEl, TreeScope.Element, cleanUp); 55 | 56 | Task.Run(async () => { 57 | LOGGER.Debug("Waiting for security key PIN prompt to appear"); 58 | pinField ??= await findPinField(outerScrollViewer, windowClosed.Token); 59 | 60 | if (pinField != null) { 61 | Automation.AddAutomationPropertyChangedEventHandler(pinField, TreeScope.Descendants, onPinTyped, ValuePattern.ValueProperty); 62 | 63 | // skipping this current value read seems to also prevent any events from being fired for some reason 64 | onPinTyped(this, new AutomationPropertyChangedEventArgs(ValuePattern.ValueProperty, null, ((ValuePattern) pinField.GetCurrentPattern(ValuePattern.Pattern)).Current.Value)); 65 | LOGGER.Debug("Found security key PIN prompt, waiting for the user to type {0:N0} characters before submitting it", options.autoSubmitPinLength); 66 | } else { 67 | LOGGER.Debug("No security key PIN prompt found"); 68 | } 69 | }, windowClosed.Token); 70 | 71 | void onPinTyped(object sender, AutomationPropertyChangedEventArgs e) { 72 | try { 73 | int typedPinLength = ((string) e.NewValue).Length; 74 | if (typedPinLength == options.autoSubmitPinLength) { 75 | LOGGER.Info("Submitting security key PIN prompt because the user typed {0:N0} characters", typedPinLength); 76 | cleanUp(); 77 | AutomationElement okButton = fidoEl.FindFirst(TreeScope.Children, NEXT_BUTTON_CONDITION); 78 | ((InvokePattern) okButton.GetCurrentPattern(InvokePattern.Pattern)).Invoke(); 79 | } 80 | } catch (Exception exception) when (exception is not OutOfMemoryException) { 81 | LOGGER.Error(e); 82 | } 83 | } 84 | 85 | void cleanUp(object? sender = null, AutomationEventArgs? e = null) { 86 | Automation.RemoveAutomationPropertyChangedEventHandler(fidoEl, onPinTyped); 87 | Automation.RemoveAutomationEventHandler(WindowPattern.WindowClosedEvent, fidoEl, cleanUp); 88 | windowClosed.Cancel(); 89 | windowClosed.Dispose(); 90 | if (sender != null) { 91 | LOGGER.Debug("Security key PIN window closed"); 92 | } 93 | } 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/Windows11/WindowsSecurityKeyChooser.cs: -------------------------------------------------------------------------------- 1 | using ManagedWinapi.Windows; 2 | using NLog; 3 | using System.Runtime.InteropServices; 4 | using System.Windows.Automation; 5 | using System.Windows.Input; 6 | using Unfucked; 7 | 8 | namespace AuthenticatorChooser.Windows11; 9 | 10 | public class WindowsSecurityKeyChooser(ChooserOptions options): AbstractSecurityKeyChooser { 11 | 12 | // #4: unfortunately, this class name is shared with the UAC prompt, detectable when desktop dimming is disabled 13 | private const string WINDOW_CLASS_NAME = "Credential Dialog Xaml Host"; 14 | private const string ALT_TAB_CLASS_NAME = "XamlExplorerHostIslandWindow"; 15 | 16 | private static readonly Logger LOGGER = LogManager.GetLogger(typeof(WindowsSecurityKeyChooser).FullName!); 17 | 18 | private static readonly Condition TITLE_CONDITION = new PropertyCondition(AutomationElement.ClassNameProperty, "TextBlock"); 19 | 20 | private PromptStrategy? strategy; 21 | 22 | public override void chooseUsbSecurityKey(SystemWindow fidoPrompt) { 23 | options.overallStopwatch.Restart(); 24 | try { 25 | if (!isFidoPromptWindow(fidoPrompt)) { 26 | LOGGER.Trace("Window 0x{hwnd:x} is not a Windows Security window", fidoPrompt.HWnd); 27 | return; 28 | } else if (SystemWindow.ForegroundWindow.ClassName == ALT_TAB_CLASS_NAME) { // #8 29 | LOGGER.Debug("Alt+Tab is being held, not interacting with Windows Security window"); 30 | return; 31 | } 32 | 33 | AutomationElement? fidoEl = fidoPrompt.ToAutomationElement(); 34 | if (fidoEl?.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "ScrollViewer")) is not { } outerScrollViewer) { 35 | LOGGER.Debug("Window is not a passkey choice prompt because it does not have a ScrollViewer child"); 36 | return; 37 | } 38 | 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 | } 56 | 57 | // #21: title not rendered immediately 58 | if (outerScrollViewer.WaitForFirst(TreeScope.Children, TITLE_CONDITION, TimeSpan.FromSeconds(5), Startup.EXITING) is not { } titleLabel) { 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); 60 | return; 61 | } 62 | 63 | string? actualTitle = titleLabel.GetCurrentPropertyValue(AutomationElement.NameProperty) as string; 64 | 65 | if (!strategy.canHandleTitle(actualTitle)) { 66 | LOGGER.Debug("Window is not a passkey choice prompt because the first TextBlock child of the ScrollViewer has the name {actual}", actualTitle); 67 | return; 68 | } else { 69 | LOGGER.Trace("Window 0x{hwnd:x} is a Windows Security window", fidoPrompt.HWnd); 70 | } 71 | 72 | bool isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); 73 | 74 | strategy.handleWindow(actualTitle!, fidoEl, outerScrollViewer, isShiftDown); 75 | 76 | } catch (ElementNotAvailableException e) { 77 | LOGGER.Error(e, "Element in Windows Security dialog box disappeared before this program could interact with it, skipping this dialog box instance"); 78 | } catch (COMException e) { 79 | LOGGER.Error(e, "UI Automation error while selecting security key, skipping this dialog box instance"); 80 | } catch (Exception e) when (e is not OutOfMemoryException) { 81 | LOGGER.Error(e, "Uncaught exception while handling Windows Security dialog box, skipping it"); 82 | } 83 | } 84 | 85 | // Window name and title are localized, so don't match against those 86 | public override bool isFidoPromptWindow(SystemWindow window) => window.ClassName == WINDOW_CLASS_NAME; 87 | 88 | } -------------------------------------------------------------------------------- /AuthenticatorChooser/YubiKey.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldaviva/AuthenticatorChooser/7cb4056d8d94ea298d62798ed6956eccef8dc18d/AuthenticatorChooser/YubiKey.ico -------------------------------------------------------------------------------- /AuthenticatorChooser/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 54 | 62 | 63 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /AuthenticatorChooser/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net8.0-windows7.0": { 5 | "McMaster.Extensions.CommandLineUtils": { 6 | "type": "Direct", 7 | "requested": "[4.1.1, )", 8 | "resolved": "4.1.1", 9 | "contentHash": "zxgDY+G5yVq2q8sVB3Z275Qkxed1jC95nwAfnlSyoG4l5Nicvd4+ke1jXusEZEfyuErlAgXCKS937c13FmZWBg==", 10 | "dependencies": { 11 | "System.ComponentModel.Annotations": "5.0.0" 12 | } 13 | }, 14 | "mwinapi": { 15 | "type": "Direct", 16 | "requested": "[0.3.0.5, )", 17 | "resolved": "0.3.0.5", 18 | "contentHash": "jb1ox29TMcxe0klNF5yui+ehXHUdEZOuykYaoGI9Okt0s7K5bNWxOzqsng0VVVvvFnW9wNfS4hWYOsE9Xup0GQ==", 19 | "dependencies": { 20 | "System.Management": "6.0.0" 21 | } 22 | }, 23 | "NLog": { 24 | "type": "Direct", 25 | "requested": "[6.0.5, )", 26 | "resolved": "6.0.5", 27 | "contentHash": "Gq/L18qnZhIfrfqXtUBKdDMtT8cZB+974C99H+w6DxdlIcT4wYCCGjRgnUij3spUH1HWScF6JRaCyv11CHZJ6g==" 28 | }, 29 | "System.Management": { 30 | "type": "Direct", 31 | "requested": "[9.0.10, )", 32 | "resolved": "9.0.10", 33 | "contentHash": "kJY2C6MjKSqfRkEnc8gn4Jth81Anrgxxpu0MffjEadfpp0Ll/gdGpYnDhRWZd+iFttkfZC0uCjFmCrZARRqq4w==", 34 | "dependencies": { 35 | "System.CodeDom": "9.0.10" 36 | } 37 | }, 38 | "ThrottleDebounce": { 39 | "type": "Direct", 40 | "requested": "[3.0.0-beta5, )", 41 | "resolved": "3.0.0-beta5", 42 | "contentHash": "hvmR5CBGzzRAO+jJBkZuPUfXeChZRR9X2vfVLYEo7CffFV2oz4yuX9sj80GqH6oxj5KYXodJ+oGaYOtIE+H5tw==" 43 | }, 44 | "Unfucked.Windows": { 45 | "type": "Direct", 46 | "requested": "[0.0.1-beta.5, )", 47 | "resolved": "0.0.1-beta.5", 48 | "contentHash": "ywiO7EUXzacJvicTEOKqD4skEGJawVdYAJvzmQvOkM55BdK/o4APoqI49171PmA4TJhzcExbNmN+gr1kfIVigw==", 49 | "dependencies": { 50 | "ThrottleDebounce": "3.0.0-beta5", 51 | "Unfucked": "0.0.1-beta.9", 52 | "mwinapi": "0.3.0.5" 53 | } 54 | }, 55 | "Workshell.PE.Resources": { 56 | "type": "Direct", 57 | "requested": "[4.0.0.147, )", 58 | "resolved": "4.0.0.147", 59 | "contentHash": "ZX8AusOHS5MeLrfLar2kHE6QqCyF/XRkkuPczfhoIGo0geI9DHqwyu303rw+Q+gaoBcdi7GkwB+v6QRUvIzdgA==", 60 | "dependencies": { 61 | "System.Drawing.Common": "7.0.0", 62 | "Workshell.PE": "4.0.0.147" 63 | } 64 | }, 65 | "Microsoft.Win32.SystemEvents": { 66 | "type": "Transitive", 67 | "resolved": "7.0.0", 68 | "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" 69 | }, 70 | "System.CodeDom": { 71 | "type": "Transitive", 72 | "resolved": "9.0.10", 73 | "contentHash": "00dAIR9Zx+F+AaipjaQmudX3VVpzYvT0bKVD3WcJq6om6pKNrldnp5bSR0VV6IlwDBa1HObGD+sTFaT/I9bBng==" 74 | }, 75 | "System.ComponentModel.Annotations": { 76 | "type": "Transitive", 77 | "resolved": "5.0.0", 78 | "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" 79 | }, 80 | "System.Drawing.Common": { 81 | "type": "Transitive", 82 | "resolved": "7.0.0", 83 | "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", 84 | "dependencies": { 85 | "Microsoft.Win32.SystemEvents": "7.0.0" 86 | } 87 | }, 88 | "Unfucked": { 89 | "type": "Transitive", 90 | "resolved": "0.0.1-beta.9", 91 | "contentHash": "rwjRGKTGiwK5ZASsGFi3iKcJNMvALBlJ0HZzt7We0d8mfL0TgXpfXE+Icg97jvtJdHmH1pc446tb27LULFJKTQ==" 92 | }, 93 | "Workshell.PE": { 94 | "type": "Transitive", 95 | "resolved": "4.0.0.147", 96 | "contentHash": "iDvqFHgx9j4jlfRQEMk6VM5kf3AKVim7ZVveap96hxYh5ppRTY83f4ByNdvTHS9Jw3t6h2KPK8ZtRW7rinna1g==" 97 | } 98 | }, 99 | "net8.0-windows7.0/win-arm64": { 100 | "System.Management": { 101 | "type": "Direct", 102 | "requested": "[9.0.10, )", 103 | "resolved": "9.0.10", 104 | "contentHash": "kJY2C6MjKSqfRkEnc8gn4Jth81Anrgxxpu0MffjEadfpp0Ll/gdGpYnDhRWZd+iFttkfZC0uCjFmCrZARRqq4w==", 105 | "dependencies": { 106 | "System.CodeDom": "9.0.10" 107 | } 108 | }, 109 | "Microsoft.Win32.SystemEvents": { 110 | "type": "Transitive", 111 | "resolved": "7.0.0", 112 | "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" 113 | }, 114 | "System.Drawing.Common": { 115 | "type": "Transitive", 116 | "resolved": "7.0.0", 117 | "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", 118 | "dependencies": { 119 | "Microsoft.Win32.SystemEvents": "7.0.0" 120 | } 121 | } 122 | }, 123 | "net8.0-windows7.0/win-x64": { 124 | "System.Management": { 125 | "type": "Direct", 126 | "requested": "[9.0.10, )", 127 | "resolved": "9.0.10", 128 | "contentHash": "kJY2C6MjKSqfRkEnc8gn4Jth81Anrgxxpu0MffjEadfpp0Ll/gdGpYnDhRWZd+iFttkfZC0uCjFmCrZARRqq4w==", 129 | "dependencies": { 130 | "System.CodeDom": "9.0.10" 131 | } 132 | }, 133 | "Microsoft.Win32.SystemEvents": { 134 | "type": "Transitive", 135 | "resolved": "7.0.0", 136 | "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" 137 | }, 138 | "System.Drawing.Common": { 139 | "type": "Transitive", 140 | "resolved": "7.0.0", 141 | "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", 142 | "dependencies": { 143 | "Microsoft.Win32.SystemEvents": "7.0.0" 144 | } 145 | } 146 | } 147 | } 148 | } --------------------------------------------------------------------------------