Skip to content

Commit

Permalink
#7: Allow only single instance
Browse files Browse the repository at this point in the history
  • Loading branch information
Aldaviva committed Sep 19, 2024
1 parent 1e6c305 commit bcd10b1
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 83 deletions.
2 changes: 1 addition & 1 deletion AuthenticatorChooser/AuthenticatorChooser.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.1.1</Version>
<Version>0.2.0</Version>
<Authors>Ben Hutchison</Authors>
<Copyright>© 2024 $(Authors)</Copyright>
<Company>$(Authors)</Company>
Expand Down
70 changes: 35 additions & 35 deletions AuthenticatorChooser/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
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>();
}

/// <summary>Remove null values.</summary>
/// <returns>Input enumerable with null values removed.</returns>
public static IEnumerable<T> Compact<T>(this IEnumerable<T?> source) where T: class {
return source.Where(item => item != null)!;
}

/// <summary>Remove null values.</summary>
/// <returns>Input enumerable with null values removed.</returns>
public static IEnumerable<T> Compact<T>(this IEnumerable<T?> source) where T: struct {
return (IEnumerable<T>) source.Where(item => item != null);
}

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>();
}

/// <summary>Remove null values.</summary>
/// <returns>Input enumerable with null values removed.</returns>
public static IEnumerable<T> Compact<T>(this IEnumerable<T?> source) where T: class {
return source.Where(item => item != null)!;
}

/// <summary>Remove null values.</summary>
/// <returns>Input enumerable with null values removed.</returns>
public static IEnumerable<T> Compact<T>(this IEnumerable<T?> source) where T: struct {
return (IEnumerable<T>) source.Where(item => item != null);
}

}
83 changes: 57 additions & 26 deletions AuthenticatorChooser/Program.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
using AuthenticatorChooser.WindowOpening;
using ManagedWinapi.Windows;
using System.Windows.Forms;

namespace AuthenticatorChooser;

internal static class Program {

[STAThread]
public static void Main() {
Application.SetCompatibleTextRenderingDefault(false);
Application.EnableVisualStyles();
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);

using WindowOpeningListener windowOpeningListener = new WindowOpeningListenerImpl();
windowOpeningListener.windowOpened += (_, window) => SecurityKeyChooser.chooseUsbSecurityKey(window);

foreach (SystemWindow fidoPromptWindow in SystemWindow.FilterToplevelWindows(SecurityKeyChooser.isFidoPromptWindow)) {
SecurityKeyChooser.chooseUsbSecurityKey(fidoPromptWindow);
}

_ = I18N.getStrings(I18N.Key.SMARTPHONE); // ensure localization is loaded eagerly

Application.Run();
}

using AuthenticatorChooser.WindowOpening;
using ManagedWinapi.Windows;
using Microsoft.Win32;
using System.Security.Principal;
using System.Windows.Forms;

namespace AuthenticatorChooser;

internal static class Program {

private const string PROGRAM_NAME = nameof(AuthenticatorChooser);

private static readonly IEqualityComparer<string> CASE_INSENSITIVE_COMPARER = StringComparer.InvariantCultureIgnoreCase;

[STAThread]
public static int Main(string[] args) {
Application.SetCompatibleTextRenderingDefault(false);
Application.EnableVisualStyles();
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);

if (args.Intersect(["--help", "/help", "-h", "/h", "-?", "/?"], CASE_INSENSITIVE_COMPARER).Any()) {
string processFilename = Path.GetFileName(Environment.ProcessPath)!;
MessageBox.Show(
$"""
{processFilename}
Runs this program in the background normally, waiting for FIDO credentials dialog boxes to open and choosing the Security Key option each time.
{processFilename} --autostart-on-logon
Registers this program to start automatically every time the current user logs on to Windows.
{processFilename} --help
Shows usage.
For more information, see https://github.com/Aldaviva/{PROGRAM_NAME}
(press Ctrl+C to copy this message)
""", $"{PROGRAM_NAME} usage", MessageBoxButtons.OK, MessageBoxIcon.Information);
} else if (args.Contains("--autostart-on-logon", CASE_INSENSITIVE_COMPARER)) {
Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run", PROGRAM_NAME, Environment.ProcessPath!);
} else {
using Mutex singleInstanceLock = new(true, $@"Local\{PROGRAM_NAME}_{WindowsIdentity.GetCurrent().User?.Value}", out bool isOnlyInstance);
if (!isOnlyInstance) return 1;

using WindowOpeningListener windowOpeningListener = new WindowOpeningListenerImpl();
windowOpeningListener.windowOpened += (_, window) => SecurityKeyChooser.chooseUsbSecurityKey(window);

foreach (SystemWindow fidoPromptWindow in SystemWindow.FilterToplevelWindows(SecurityKeyChooser.isFidoPromptWindow)) {
SecurityKeyChooser.chooseUsbSecurityKey(fidoPromptWindow);
}

_ = I18N.getStrings(I18N.Key.SMARTPHONE); // ensure localization is loaded eagerly

Application.Run();
}

return 0;
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0-windows\win-x64\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0-windows\win-x64\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>
23 changes: 17 additions & 6 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

*Program that runs in the background to automatically skip the Windows "Sign in with your passkey" phone prompt and go straight to the USB security key option.*

<!-- MarkdownTOC autolink="true" bracket="round" autoanchor="false" levels="1,2,3" -->
<!-- MarkdownTOC autolink="true" bracket="round" autoanchor="false" levels="1,2" -->

- [Problem](#problem)
- [Solution](#solution)
- [Requirements](#requirements)
- [Installation](#installation)
- [Demo](#demo)

<!-- /MarkdownTOC -->

Expand All @@ -34,26 +35,33 @@ This is a background program that runs headlessly in your Windows user session.

<p align="center"><img src=".github/images/demo.gif" alt="demo" width="464" /></p>

This program does not interfere with local TPM passkey prompts (like requesting your Windows Hello PIN or biometrics). It also does not automatically submit FIDO prompts that contain additional options besides a USB security key and pairing a new Bluetooth smartphone, such as the cases when you already have a paired phone, or you previously declined a Windows Hello factor like a PIN but want to try a PIN again from the authenticator choice dialog. You can [edit the registry if you want to unpair an existing phone](https://github.com/Aldaviva/AuthenticatorChooser/wiki/Unpairing-Bluetooth-smartphone).
Internally, this program uses [Microsoft UI Automation](https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-uiautomationoverview) to read and interact with the dialog box.

### Overriding the automatic next behavior
This program does not interfere with local TPM passkey prompts (like requesting your Windows Hello PIN or biometrics). It also does not automatically submit FIDO prompts that contain additional options besides a USB security key and pairing a new Bluetooth smartphone, such as the cases when you already have a paired phone, or you previously declined a Windows Hello factor like a PIN but want to try a PIN again from the authenticator choice dialog. [You can edit the registry if you want to unpair an existing phone](https://github.com/Aldaviva/AuthenticatorChooser/wiki/Unpairing-Bluetooth-smartphone).

If this program skips the authenticator choice dialog when you don't want it to, for example, if you want to use a smartphone Bluetooth passkey only once or infrequently, you can hold <kbd>Shift</kbd> when the dialog appears to temporarily suppress this program from automatically submitting the security key choice once.

Internally, this program uses [Microsoft UI Automation](https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-uiautomationoverview) to read and interact with the dialog box.
Even if this program doesn't click the Next button (because an extra choice was present, or you were holding <kbd>Shift</kbd>), it will still highlight the Security Key option and focus the Next button for you, so you can just press <kbd>Enter</kbd> or <kbd>Space</kbd> to choose the Security Key anyway.

## Requirements

- Windows 11 23H2 or later, or Windows 11 22H2 with Moment 4 (KB5031455 or KB5030310)
- It can also run on earlier versions, such as Windows 11 21H2 and Windows 10, although it won't do anything there because those versions can't pair and exchange passkeys with phones in the first place.
- It can also run on earlier versions, such as Windows 11 21H2 and Windows 10, although it won't do anything there because those versions can't pair and exchange passkeys with phones in the first place, so there's nothing to fix.
- [.NET Desktop Runtime 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) or later
- This program is compatible with x64 and ARM64 CPU architectures
- This program is compatible with x64 and ARM64 CPU architectures.

## Installation

1. [Download the latest release ZIP archive for your CPU architecture.](https://github.com/Aldaviva/AuthenticatorChooser/releases/latest)
1. Extract the `AuthenticatorChooser.exe` file from the ZIP archive to a directory of your choice, like `C:\Program Files\AuthenticatorChooser\`.
1. Run the program by double-clicking `AuthenticatorChooser.exe`.
- Nothing will appear because it's a background program with no UI, but you can tell it's running by searching for `AuthenticatorChooser` in Task Manager.
1. Register the program to run automatically on user logon with one of the following techniques. Be sure to change the example path below if you chose a different installation directory in step 2.
1. Register the program to run automatically on user logon with any **one** of the following techniques. Be sure to change the example path below if you chose a different installation directory in step 2.
- Run this program with the `--autostart-on-logon` argument
```ps1
.\AuthenticatorChooser --autostart-on-logon
```
- Import a `.reg` file
```reg
Windows Registry Editor Version 5.00
Expand All @@ -70,3 +78,6 @@ Internally, this program uses [Microsoft UI Automation](https://learn.microsoft.
Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Run -Name AuthenticatorChooser -Value """C:\Program Files\AuthenticatorChooser\AuthenticatorChooser.exe"""
```
- Use `regedit.exe` interactively to go to the `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run` key, and then add a new String value with the Name `AuthenticatorChooser` and the Value `"C:\Program Files\AuthenticatorChooser\AuthenticatorChooser.exe"`.
## Demo
To test with a sample FIDO authentication prompt, visit [WebAuthn.io](https://webauthn.io) and click the **Authenticate** button.

0 comments on commit bcd10b1

Please sign in to comment.