-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8374f77
Showing
64 changed files
with
2,176 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
[*] | ||
|
||
# Exception Analyzers: Exception adjustments syntax error | ||
# default = error | ||
; dotnet_diagnostic.Ex0001.severity = none | ||
|
||
# Exception Analyzers: Exception adjustments syntax error: Symbol does not exist or identifier is invalid | ||
# default = warning | ||
; dotnet_diagnostic.Ex0002.severity = none | ||
|
||
# Exception Analyzers: Member may throw undocumented exception | ||
# default = warning | ||
dotnet_diagnostic.Ex0100.severity = none | ||
|
||
# Exception Analyzers: Member accessor may throw undocumented exception | ||
# default = warning | ||
dotnet_diagnostic.Ex0101.severity = none | ||
|
||
# Exception Analyzers: Implicit constructor may throw undocumented exception | ||
# default = warning | ||
dotnet_diagnostic.Ex0103.severity = none | ||
|
||
# Exception Analyzers: Member initializer may throw undocumented exception | ||
# default = warning | ||
dotnet_diagnostic.Ex0104.severity = none | ||
|
||
# Exception Analyzers: Delegate created from member may throw undocumented exception | ||
# default = silent | ||
; dotnet_diagnostic.Ex0120.severity = none | ||
|
||
# Exception Analyzers: Delegate created from anonymous function may throw undocumented exception | ||
# default = silent | ||
; dotnet_diagnostic.Ex0121.severity = none | ||
|
||
# Exception Analyzers: Member is documented as throwing exception not documented on member in base or interface type | ||
# default = warning | ||
dotnet_diagnostic.Ex0200.severity = none | ||
|
||
# Exception Analyzers: Member accessor is documented as throwing exception not documented on member in base or interface type | ||
# default = warning | ||
dotnet_diagnostic.Ex0201.severity = none |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: .NET | ||
|
||
on: | ||
push: | ||
branches: [ master ] | ||
workflow_dispatch: | ||
|
||
jobs: | ||
build: | ||
env: | ||
ProjectName: AuthenticatorChooser | ||
|
||
runs-on: windows-latest | ||
|
||
steps: | ||
- name: Clone | ||
uses: actions/checkout@v4 | ||
|
||
- name: Restore | ||
run: dotnet restore --locked-mode --verbosity normal | ||
|
||
- name: Build | ||
run: dotnet build ${{ env.ProjectName }} --no-restore --configuration Release --no-self-contained --verbosity normal | ||
|
||
- name: Publish | ||
run: dotnet publish ${{ env.ProjectName }} --no-build --configuration Release -p:PublishSingleFile=true --self-contained false | ||
|
||
- name: Upload build artifacts | ||
uses: actions/upload-artifact@v4 | ||
with: | ||
name: ${{ env.ProjectName }}.exe | ||
path: ${{ env.ProjectName }}/bin/Release/net8.0-windows/win-x64/publish/*.exe | ||
if-no-files-found: error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
|
||
#Ignore thumbnails created by Windows | ||
Thumbs.db | ||
#Ignore files built by Visual Studio | ||
*.obj | ||
*.exe | ||
*.pdb | ||
*.user | ||
*.aps | ||
*.pch | ||
*.vspscc | ||
*_i.c | ||
*_p.c | ||
*.ncb | ||
*.suo | ||
*.tlb | ||
*.tlh | ||
*.bak | ||
*.cache | ||
*.ilk | ||
*.log | ||
[Bb]in | ||
[Dd]ebug*/ | ||
*.lib | ||
*.sbr | ||
obj/ | ||
[Rr]elease*/ | ||
_ReSharper*/ | ||
[Tt]est[Rr]esult* | ||
.vs/ | ||
#Nuget packages folder | ||
packages/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio Version 17 | ||
VisualStudioVersion = 17.9.34701.34 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthenticatorChooser", "AuthenticatorChooser\AuthenticatorChooser.csproj", "{24618EB8-29AF-47BF-A5D2-5A8C8E724991}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{24618EB8-29AF-47BF-A5D2-5A8C8E724991}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {C11C7C56-6448-422F-A1C6-62859829E0CE} | ||
EndGlobalSection | ||
EndGlobal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>WinExe</OutputType> | ||
<TargetFramework>net8.0-windows</TargetFramework> | ||
<RuntimeIdentifier>win-x64</RuntimeIdentifier> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<ApplicationManifest>app.manifest</ApplicationManifest> | ||
<Version>0.0.0</Version> | ||
<Authors>Ben Hutchison</Authors> | ||
<Copyright>© 2024 $(Authors)</Copyright> | ||
<Company>$(Authors)</Company> | ||
<RollForward>major</RollForward> | ||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> | ||
<ApplicationManifest>app.manifest</ApplicationManifest> | ||
<ApplicationIcon>YubiKey.ico</ApplicationIcon> | ||
<NeutralLanguage>en</NeutralLanguage> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Content Include="YubiKey.ico" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="mwinapi" Version="0.3.0.5" /> | ||
<PackageReference Include="throttledebounce" Version="2.0.0" /> | ||
<PackageReference Include="Workshell.PE.Resources" Version="3.0.0.130" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<FrameworkReference Include="Microsoft.WindowsDesktop.App" /> <!-- UseWindowsForms is insufficient to refer to UIAutomationClient --> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Compile Update="Resources\Strings.Designer.cs"> | ||
<DesignTime>True</DesignTime> | ||
<AutoGen>True</AutoGen> | ||
<DependentUpon>Strings.resx</DependentUpon> | ||
</Compile> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<EmbeddedResource Update="Resources\Strings.resx"> | ||
<Generator>ResXFileCodeGenerator</Generator> | ||
<LastGenOutput>Strings.Designer.cs</LastGenOutput> | ||
</EmbeddedResource> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using ManagedWinapi.Windows; | ||
using System.Windows.Automation; | ||
|
||
namespace AuthenticatorChooser; | ||
|
||
public static class Extensions { | ||
|
||
public static IntPtr toHwnd(this AutomationElement element) { | ||
return new IntPtr(element.Current.NativeWindowHandle); | ||
} | ||
|
||
public static SystemWindow toSystemWindow(this AutomationElement element) { | ||
return new SystemWindow(element.toHwnd()); | ||
} | ||
|
||
public static AutomationElement toAutomationElement(this SystemWindow window) { | ||
return AutomationElement.FromHandle(window.HWnd); | ||
} | ||
|
||
public static IEnumerable<AutomationElement> children(this AutomationElement parent) { | ||
return parent.FindAll(TreeScope.Children, Condition.TrueCondition).Cast<AutomationElement>(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using AuthenticatorChooser.Resources; | ||
using System.Globalization; | ||
using Workshell.PE; | ||
using Workshell.PE.Resources; | ||
using Workshell.PE.Resources.Strings; | ||
|
||
namespace AuthenticatorChooser; | ||
|
||
public static class I18N { | ||
|
||
private const string FIDOCREDPROV_MUI_FILENAME = "fidocredprov.dll.mui"; | ||
|
||
public enum Key { | ||
|
||
SECURITY_KEY, | ||
SMARTPHONE, | ||
WINDOWS | ||
|
||
} | ||
|
||
private static readonly IReadOnlyDictionary<Key, string?> RUNTIME_OS_FILE_STRINGS; | ||
|
||
static I18N() { | ||
StringTableResource.Register(); | ||
|
||
string fidocredprovMuiFilePath = Path.Combine(Environment.GetEnvironmentVariable("SystemRoot") ?? "C:\\Windows", "System32", CultureInfo.CurrentUICulture.Name, FIDOCREDPROV_MUI_FILENAME); | ||
|
||
IList<string?> peFileStrings = getPeFileStrings(fidocredprovMuiFilePath, [ | ||
(15, 230), | ||
(15, 231), | ||
(15, 232) | ||
]); | ||
|
||
var strings = new Dictionary<Key, string?> { | ||
[Key.SECURITY_KEY] = peFileStrings[0], | ||
[Key.SMARTPHONE] = peFileStrings[1], | ||
[Key.WINDOWS] = peFileStrings[2] | ||
}; | ||
RUNTIME_OS_FILE_STRINGS = strings.AsReadOnly(); | ||
} | ||
|
||
public static string getStringCompileTime(Key key) => key switch { | ||
Key.SECURITY_KEY => Strings.securityKey, | ||
Key.SMARTPHONE => Strings.smartphone, | ||
Key.WINDOWS => Strings.windows, | ||
_ => throw new ArgumentOutOfRangeException(nameof(key), key, null) | ||
}; | ||
|
||
public static string? getStringRuntime(Key key) => RUNTIME_OS_FILE_STRINGS[key]; | ||
|
||
public static IEnumerable<string> getStrings(Key key) { | ||
yield return getStringCompileTime(key); | ||
|
||
if (getStringRuntime(key) is { } runtimeString) { | ||
yield return runtimeString; | ||
} | ||
} | ||
|
||
public static string? getPeFileString(string peFile, int stringTableId, int stringTableEntryId) { | ||
return getPeFileStrings(peFile, [(stringTableId, stringTableEntryId)])[0]; | ||
} | ||
|
||
public static IList<string?> getPeFileStrings(string peFile, IList<(int stringTableId, int stringTableEntryId)> queries) { | ||
|
||
using PortableExecutableImage file = PortableExecutableImage.FromFile(peFile); | ||
|
||
IDictionary<int, StringTable?> stringTableCache = new Dictionary<int, StringTable?>(); | ||
ResourceType? stringTables = ResourceCollection.Get(file).FirstOrDefault(type => type.Id == ResourceType.String); | ||
IList<string?> results = new List<string?>(queries.Count); | ||
|
||
foreach ((int stringTableId, int stringTableEntryId) in queries) { | ||
if (!stringTableCache.TryGetValue(stringTableId, out StringTable? stringTable)) { | ||
stringTable = (stringTables?.FirstOrDefault(resource => resource.Id == stringTableId) as StringTableResource)?.GetTable(); | ||
|
||
stringTableCache[stringTableId] = stringTable; | ||
} | ||
|
||
results.Add(stringTable?.FirstOrDefault(entry => entry.Id == stringTableEntryId)?.Value); | ||
} | ||
|
||
return results; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
using AuthenticatorChooser.WindowOpening; | ||
using ManagedWinapi.Windows; | ||
using System.Diagnostics; | ||
using System.Windows.Automation; | ||
using System.Windows.Forms; | ||
using System.Windows.Input; | ||
using ThrottleDebounce; | ||
|
||
namespace AuthenticatorChooser; | ||
|
||
internal static class Program { | ||
|
||
private static readonly TimeSpan UI_RETRY_DELAY = TimeSpan.FromMilliseconds(8); | ||
|
||
[STAThread] | ||
public static void Main() { | ||
Application.SetCompatibleTextRenderingDefault(false); | ||
Application.EnableVisualStyles(); | ||
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); | ||
|
||
using WindowOpeningListener windowOpeningListener = new WindowOpeningListenerImpl(); | ||
windowOpeningListener.windowOpened += (_, window) => chooseUsbSecurityKey(window); | ||
|
||
foreach (SystemWindow fidoPromptWindow in SystemWindow.FilterToplevelWindows(isFidoPromptWindow)) { | ||
chooseUsbSecurityKey(fidoPromptWindow); | ||
} | ||
|
||
Console.WriteLine(); | ||
Application.Run(); | ||
} | ||
|
||
private static void chooseUsbSecurityKey(SystemWindow fidoPrompt) { | ||
Stopwatch stopwatch = Stopwatch.StartNew(); | ||
if (!isFidoPromptWindow(fidoPrompt)) { | ||
Console.WriteLine($"Window 0x{fidoPrompt.HWnd:x} is not a Windows Security window"); | ||
return; | ||
} | ||
|
||
Console.WriteLine($"Found FIDO prompt window (HWND=0x{fidoPrompt.HWnd:x}) after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
AutomationElement fidoEl = fidoPrompt.toAutomationElement(); | ||
Console.WriteLine($"Converted window to AutomationElement after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
|
||
AutomationElement outerScrollViewer = fidoEl.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.ClassNameProperty, "ScrollViewer")); | ||
if (outerScrollViewer.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Sign in with your passkey")) == null) { // not localized by Windows | ||
Console.WriteLine("Window is not a passkey reading prompt"); | ||
return; | ||
} | ||
|
||
Console.WriteLine($"Window is the passkey prompt after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
|
||
List<AutomationElement> listItems = Retrier.Attempt(_ => | ||
outerScrollViewer.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "CredentialsList")).children().ToList(), // ClassName=ListView | ||
maxAttempts: 25, delay: _ => UI_RETRY_DELAY, beforeRetry: () => Console.WriteLine("No list found, retrying")); | ||
Console.WriteLine($"Found list of authenticator choices after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
|
||
if (listItems.FirstOrDefault(listItem => nameEndsWithAny(listItem, I18N.getStrings(I18N.Key.SECURITY_KEY))) is not { } securityKeyButton) { | ||
Console.WriteLine("USB security key is not a choice, skipping"); | ||
return; | ||
} | ||
|
||
Console.WriteLine($"Prompted for credential type after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
((SelectionItemPattern) securityKeyButton.GetCurrentPattern(SelectionItemPattern.Pattern)).Select(); | ||
Console.WriteLine($"USB key selected after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
|
||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) { | ||
Console.WriteLine("Shift is pressed, not submitting dialog box"); | ||
return; | ||
} else if (!listItems.All(listItem => listItem == securityKeyButton || nameEndsWithAny(listItem, I18N.getStrings(I18N.Key.SMARTPHONE)))) { | ||
Console.WriteLine("Dialog box has a choice that isn't smartphone or USB security key (such as PIN or biometrics), skipping because the user might want to choose it"); | ||
return; | ||
} | ||
|
||
AutomationElement nextButton = fidoEl.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "OkButton")); | ||
((InvokePattern) nextButton.GetCurrentPattern(InvokePattern.Pattern)).Invoke(); | ||
stopwatch.Stop(); | ||
Console.WriteLine($"Next button clicked after {stopwatch.ElapsedMilliseconds:N0} ms"); | ||
} | ||
|
||
private static bool nameEndsWithAny(AutomationElement element, IEnumerable<string?> suffices) { | ||
string name = element.Current.Name; | ||
return suffices.Any(suffix => suffix != null && name.EndsWith(suffix, StringComparison.CurrentCulture)); | ||
} | ||
|
||
// name/title are localized, so don't use those | ||
private static bool isFidoPromptWindow(SystemWindow window) => window.ClassName == "Credential Dialog Xaml Host"; | ||
|
||
} |
Oops, something went wrong.