From 9f1c5d2a5a4a3279729371aaeb6dfc81b74d55b2 Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 8 Sep 2023 10:31:36 -0500 Subject: [PATCH 1/6] Add Yubico.YubiKey dependency --- src/XIVLauncher/XIVLauncher.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/XIVLauncher/XIVLauncher.csproj b/src/XIVLauncher/XIVLauncher.csproj index 07899d518..f85318634 100644 --- a/src/XIVLauncher/XIVLauncher.csproj +++ b/src/XIVLauncher/XIVLauncher.csproj @@ -86,6 +86,7 @@ + From e25075c9abb5f5d70a08f8aa1ea2e9037688520a Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 8 Sep 2023 10:32:48 -0500 Subject: [PATCH 2/6] Add YubiAuth and fix improper DLL import --- src/XIVLauncher/Accounts/YubiAuth.cs | 125 ++++++++++++++++++++++++ src/XIVLauncher/Directory.Build.targets | 31 ++++++ 2 files changed, 156 insertions(+) create mode 100644 src/XIVLauncher/Accounts/YubiAuth.cs create mode 100644 src/XIVLauncher/Directory.Build.targets diff --git a/src/XIVLauncher/Accounts/YubiAuth.cs b/src/XIVLauncher/Accounts/YubiAuth.cs new file mode 100644 index 000000000..559fb0796 --- /dev/null +++ b/src/XIVLauncher/Accounts/YubiAuth.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Serilog; +using Yubico.YubiKey; +using Yubico.YubiKey.Oath; +using Yubico.PlatformInterop; +using System.Runtime.InteropServices; + +namespace XIVLauncher.Accounts +{ + public class YubiAuth + { + private const string ISSUER = "XIVLauncher"; + private const string ACCOUNT_NAME = "FFXIV"; + private const CredentialType AUTH_TYPE = CredentialType.Totp; + private const CredentialPeriod TIME_PERIOD = CredentialPeriod.Period30; + private const byte NUM_DIGITS = 6; + + private static IYubiKeyDevice _yubiKey; + public YubiKeyDeviceListener DeviceListener; + public YubiAuth() + { + FindYubiDevice(); + DeviceListener = YubiKeyDeviceListener.Instance; + } + + //Generates generic credential + public Credential BuildCredential() + { + var credentialTotp = new Credential + { + Issuer = ISSUER, + AccountName = ACCOUNT_NAME, + Type = AUTH_TYPE, + Period = TIME_PERIOD, + Digits = NUM_DIGITS, + }; + return credentialTotp; + } + + //Generates credential to be put onto user's YubiKey + public Credential BuildCredential(string key, bool useTouch) + { + var credentialTotp = new Credential + { + Issuer = ISSUER, + AccountName = ACCOUNT_NAME, + Type = AUTH_TYPE, + Period = TIME_PERIOD, + Secret = key, + Digits = NUM_DIGITS, + RequiresTouch = useTouch, + }; + return credentialTotp; + } + + //Finds YubiKey(s) that are plugged into a USB port + public void FindYubiDevice() + { + //Find YubiKey device + try + { + if (_yubiKey == null) + { + IEnumerable keys = YubiKeyDevice.FindByTransport(Transport.UsbSmartCard); + SetYubiDevice(keys.First()); + } + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + Log.Debug("No YubiKey device was detected"); + Log.Debug(ex.ToString()); + } + + } + + public IYubiKeyDevice GetYubiDevice() + { + return _yubiKey; + } + + public void SetYubiDevice(IYubiKeyDevice yubiKeyDevice) + { + _yubiKey = yubiKeyDevice; + } + + //Checks for existing credentials on user's YubiKey that match the XIVLauncher generic credential + public bool CheckForCredential(OathSession session) + { + try + { + IList creds = session.GetCredentials().Where(credential => credential.Issuer == ISSUER && credential.AccountName == ACCOUNT_NAME).ToList(); + if (creds != null && creds.Count != 0) + { + return true; + } + return false; + } + catch (SCardException) + { + Log.Error("YubiKey was removed during GET operation."); + return false; + } + } + + //Adds the user's credential onto their YubiKey device + public void CreateEntry(Credential cred) + { + try + { + new OathSession(_yubiKey).AddCredential(cred); + Log.Debug("Successfully created new credential " + cred.Name); + } + catch (SCardException) + { + Log.Error("YubiKey was removed during ADD operation"); + } + + } + + } + + +} diff --git a/src/XIVLauncher/Directory.Build.targets b/src/XIVLauncher/Directory.Build.targets new file mode 100644 index 000000000..ece0b1a9b --- /dev/null +++ b/src/XIVLauncher/Directory.Build.targets @@ -0,0 +1,31 @@ + + + + + + + build + + + Yubico.NativeShims.dll + Always + false + + + + + + + + + build + + + Yubico.NativeShims.dll + Always + false + + + + + From cb6ab5eca19b89b14502a86d06a06bb40bd454c4 Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 8 Sep 2023 10:34:36 -0500 Subject: [PATCH 3/6] Implement YubiKey setup and authentication --- .../Settings/ILauncherSettingsV3.cs | 1 + .../Windows/OtpInputDialog.xaml.cs | 155 +++++++++++ .../Windows/SecurityKeySetupDialog.xaml | 75 ++++++ .../Windows/SecurityKeySetupDialog.xaml.cs | 242 ++++++++++++++++++ src/XIVLauncher/Windows/SettingsControl.xaml | 27 ++ .../Windows/SettingsControl.xaml.cs | 25 ++ .../ViewModel/OtpInputDialogViewModel.cs | 6 + .../SecurityKeySetupDialogViewModel.cs | 34 +++ .../ViewModel/SettingsControlViewModel.cs | 8 + 9 files changed, 573 insertions(+) create mode 100644 src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml create mode 100644 src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml.cs create mode 100644 src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs diff --git a/src/XIVLauncher/Settings/ILauncherSettingsV3.cs b/src/XIVLauncher/Settings/ILauncherSettingsV3.cs index 0c015d3a2..27908df97 100644 --- a/src/XIVLauncher/Settings/ILauncherSettingsV3.cs +++ b/src/XIVLauncher/Settings/ILauncherSettingsV3.cs @@ -21,6 +21,7 @@ public interface ILauncherSettingsV3 bool InGameAddonEnabled { get; set; } DalamudLoadMethod? InGameAddonLoadMethod { get; set; } bool OtpServerEnabled { get; set; } + bool OtpYubiKeyEnabled { get; set; } ClientLanguage? Language { get; set; } LauncherLanguage? LauncherLanguage { get; set; } string CurrentAccountId { get; set; } diff --git a/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs b/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs index 0d27b80c1..55f7abfbc 100644 --- a/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs +++ b/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -9,8 +10,13 @@ using System.Windows.Media.Animation; using System.Windows.Threading; using Serilog; +using XIVLauncher.Accounts; using XIVLauncher.Common.Http; using XIVLauncher.Windows.ViewModel; +using Yubico.PlatformInterop; +using Yubico.YubiKey; +using Yubico.YubiKey.Oath; +using Yubico.YubiKey.Oath.Commands; namespace XIVLauncher.Windows { @@ -28,6 +34,9 @@ public partial class OtpInputDialog : Window private OtpListener _otpListener; private bool _ignoreCurrentOtp; + private YubiAuth _yubiAuth; + private readonly object _lock = new(); + private Thread _yubiThread; public OtpInputDialog() { InitializeComponent(); @@ -50,6 +59,17 @@ public OtpInputDialog() _otpListener = new OtpListener("legacy-" + AppUtil.GetAssemblyVersion()); _otpListener.OnOtpReceived += TryAcceptOtp; + if (App.Settings.OtpYubiKeyEnabled) + { + _yubiAuth = new YubiAuth(); + if (_yubiAuth.DeviceListener != null) + { + _yubiAuth.DeviceListener.Arrived += OnYubiKeyArrived; + _yubiAuth.DeviceListener.Removed += OnYubiKeyRemoved; + } + AttemptYubiAuth(); + } + try { // Start Listen @@ -72,6 +92,15 @@ public void Reset() OtpTextBox.Text = ""; OtpTextBox.Focus(); } + public void ResetPrompt() + { + Dispatcher.Invoke(() => + { + OtpInputPrompt.Text = ViewModel.OtpInputPromptLoc; + OtpInputPrompt.Foreground = _otpInputPromptDefaultBrush; + OtpTextBox.Focus(); + }); + } public void IgnoreCurrentResult(string reason) { @@ -114,6 +143,7 @@ public void TryAcceptOtp(string otp) else { _otpListener?.Stop(); + CleanupYubi(); DialogResult = true; Hide(); } @@ -124,9 +154,19 @@ private void Cancel() { OnResult?.Invoke(null); _otpListener?.Stop(); + CleanupYubi(); DialogResult = false; Hide(); } + private void CleanupYubi() + { + if (_yubiAuth != null) + { + _yubiAuth.DeviceListener.Arrived -= OnYubiKeyArrived; + _yubiAuth.DeviceListener.Removed -= OnYubiKeyRemoved; + _yubiThread?.Abort(); + } + } private void OtpInputDialog_OnMouseMove(object sender, MouseEventArgs e) { @@ -175,6 +215,121 @@ private void PasteButton_OnClick(object sender, RoutedEventArgs e) this.OtpTextBox.Text = Clipboard.GetText(); TryAcceptOtp(this.OtpTextBox.Text); } + private void OnYubiKeyArrived(object sender, YubiKeyDeviceEventArgs e) + { + Log.Debug("YubiKey found! " + e.Device.ToString()); + _yubiAuth.SetYubiDevice(e.Device); + AttemptYubiAuth(); + } + + private void OnYubiKeyRemoved(object sender, YubiKeyDeviceEventArgs e) + { + Log.Debug("YubiKey removed!"); + _yubiAuth.SetYubiDevice(null); + ResetPrompt(); + } + + private void AttemptYubiAuth() + { + if (_yubiAuth.GetYubiDevice() == null) + { + return; + } + + OathSession session = new OathSession(_yubiAuth.GetYubiDevice()); + var totp = _yubiAuth.BuildCredential(); + + try + { + //Set prompt respectively + Dispatcher.Invoke(() => + { + if (_yubiAuth.CheckForCredential(session) == true) + { + OtpInputPrompt.Text = ViewModel.OtpInputPromptYubiLoc; + OtpInputPrompt.Foreground = Brushes.LightGreen; + Storyboard myStoryboard = (Storyboard)OtpInputPrompt.Resources["InvalidShake"]; + Storyboard.SetTarget(myStoryboard.Children.ElementAt(0), OtpInputPrompt); + myStoryboard.Begin(); + OtpTextBox.Focus(); + } + else + { + OtpInputPrompt.Text = ViewModel.OtpInputPromptYubiBadLoc; + OtpInputPrompt.Foreground = Brushes.Red; + Storyboard myStoryboard = (Storyboard)OtpInputPrompt.Resources["InvalidShake"]; + Storyboard.SetTarget(myStoryboard.Children.ElementAt(0), OtpInputPrompt); + myStoryboard.Begin(); + OtpTextBox.Focus(); + return; + } + }); + + } + catch (SCardException) + { + Log.Error("YubiKey was removed while performing operation."); + return; + } + + + byte retries = 0; + _yubiThread = new Thread(() => + { + //Handle touch-based authentication, gives user three attempts to touch YubiKey + //Attempts are approximately 15 seconds by defualt + while (retries < 3 && _yubiAuth.GetYubiDevice() != null) + { + CalculateCredentialResponse ccr = null; + try + { + //Attempts to generate otp and then login + CalculateCredentialCommand ccd = new CalculateCredentialCommand(totp, ResponseFormat.Truncated); + ccr = session.Connection.SendCommand(ccd); + + Log.Debug("Status: " + ccr.Status); + Log.Debug("Status Message: " + ccr.StatusMessage); + Log.Debug("Data: " + ccr.GetData().Value); + TryAcceptOtp(ccr.GetData().Value); + break; + } + catch (InvalidOperationException ex) + { + Log.Debug(ex.Message); + + retries++; + + //Handle authentication timeout + if (ccr != null) + { + Log.Debug("Status: " + ccr.Status); + Log.Debug("Status Message: " + ccr.StatusMessage); + } + + if (retries == 3) + { + Dispatcher.Invoke(() => + { + OtpInputPrompt.Text = ViewModel.OtpInputPromptYubiTimeoutLoc; + OtpInputPrompt.Foreground = Brushes.Red; + Storyboard myStoryboard = (Storyboard)OtpInputPrompt.Resources["InvalidShake"]; + Storyboard.SetTarget(myStoryboard.Children.ElementAt(0), OtpInputPrompt); + myStoryboard.Begin(); + OtpTextBox.Focus(); + }); + break; + } + } + catch (SCardException) + { + Log.Error("YubiKey was removed during authentication attempt."); + break; + } + } + }); + _yubiThread.Start(); + + } public void OpenShortcutInfo_MouseUp(object sender, RoutedEventArgs e) { diff --git a/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml b/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml new file mode 100644 index 000000000..6d2199238 --- /dev/null +++ b/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/XIVLauncher/Windows/SettingsControl.xaml.cs b/src/XIVLauncher/Windows/SettingsControl.xaml.cs index 82c7b053f..1efbf224b 100644 --- a/src/XIVLauncher/Windows/SettingsControl.xaml.cs +++ b/src/XIVLauncher/Windows/SettingsControl.xaml.cs @@ -87,6 +87,12 @@ public void ReloadSettings() OtpServerCheckBox.IsChecked = App.Settings.OtpServerEnabled; + if (OtpServerCheckBox.IsChecked == true) + OtpYubiKeyCheckBox.IsChecked = App.Settings.OtpYubiKeyEnabled; + else + OtpYubiKeyCheckBox.IsChecked = false; + + LaunchArgsTextBox.Text = App.Settings.AdditionalLaunchArgs; DpiAwarenessComboBox.SelectedIndex = (int) App.Settings.DpiAwareness.GetValueOrDefault(DpiAwareness.Unaware); @@ -137,6 +143,8 @@ private void AcceptButton_Click(object sender, RoutedEventArgs e) App.Settings.OtpServerEnabled = OtpServerCheckBox.IsChecked == true; + App.Settings.OtpYubiKeyEnabled = OtpYubiKeyCheckBox.IsChecked == true; + App.Settings.AdditionalLaunchArgs = LaunchArgsTextBox.Text; App.Settings.DpiAwareness = (DpiAwareness) DpiAwarenessComboBox.SelectedIndex; @@ -435,6 +443,23 @@ private void LearnMoreButton_OnClick(object sender, RoutedEventArgs e) { PlatformHelpers.OpenBrowser("https://goatcorp.github.io/faq/mobile_otp"); } + private void LearnMoreYubiKeyButton_OnClick(object sender, RoutedEventArgs e) + { + PlatformHelpers.OpenBrowser("https://goatcorp.github.io/faq/yubikey"); + } + private void YubiKeySetupAuthButton_OnClick(object sender, RoutedEventArgs e) + { + var authKey = string.Empty; + authKey = SecurityKeySetupDialog.AskForKey((securityKeySetupDialog, result) => + { + if (securityKeySetupDialog.DialogResult == true) + { + CustomMessageBox.Show(Loc.Localize("SetupAuthSuccess", $"YubiKey account \"{result}\" has been successfully created.\nYou may now use this YubiKey alongside XIVLauncher."), + "XIVLauncher - YubiKey Setup", image: MessageBoxImage.Information, showDiscordLink: false, showHelpLinks: false); + } + }, Window.GetWindow(this)); + + } private void IsFreeTrialCheckbox_OnClick(object sender, RoutedEventArgs e) { diff --git a/src/XIVLauncher/Windows/ViewModel/OtpInputDialogViewModel.cs b/src/XIVLauncher/Windows/ViewModel/OtpInputDialogViewModel.cs index 625eca5f6..2e7febbcb 100644 --- a/src/XIVLauncher/Windows/ViewModel/OtpInputDialogViewModel.cs +++ b/src/XIVLauncher/Windows/ViewModel/OtpInputDialogViewModel.cs @@ -17,6 +17,9 @@ private void SetupLoc() OtpOneClickHintLoc = Loc.Localize("OtpOneClickHint", "Or use the app!\r\nClick here to learn more!"); OtpInputPromptBadLoc = Loc.Localize("OtpInputPromptBad", "Enter a valid OTP key.\nIt is 6 digits long."); PasteButtonLoc = Loc.Localize("PasteButton", "Click here to paste from the clipboard."); + OtpInputPromptYubiLoc = Loc.Localize("OtpInputPromptYubi", "Found a valid Yubikey, touch it to login."); + OtpInputPromptYubiBadLoc = Loc.Localize("OtpInputPromptYubiBad", "Yubikey has not been setup yet.\nCheck Settings to complete setup."); + OtpInputPromptYubiTimeoutLoc = Loc.Localize("OtpInputPromptYubiTimeout", "YubiKey timed out\nEnter your OTP or reinsert the YubiKey."); } public string OtpInputPromptLoc { get; private set; } @@ -25,5 +28,8 @@ private void SetupLoc() public string OtpOneClickHintLoc { get; private set; } public string OtpInputPromptBadLoc { get; private set; } public string PasteButtonLoc { get; private set; } + public string OtpInputPromptYubiLoc { get; private set; } + public string OtpInputPromptYubiBadLoc { get; private set; } + public string OtpInputPromptYubiTimeoutLoc { get; private set; } } } diff --git a/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs b/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs new file mode 100644 index 000000000..b30354a4d --- /dev/null +++ b/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs @@ -0,0 +1,34 @@ +using CheapLoc; + +namespace XIVLauncher.Windows.ViewModel +{ + class SecurityKeySetupDialogViewModel + { + public SecurityKeySetupDialogViewModel() + { + SetupLoc(); + } + + private void SetupLoc() + { + KeySetupInputPromptInsertLoc = Loc.Localize("SetupInputPromptInsert", "Please insert your YubiKey now."); + KeySetupInputPromptYubiLoc = Loc.Localize("SetupInputPromptYubi", "Found a Yubikey.\nPlease enter your Authentication Key."); + CancelWithShortcutLoc = Loc.Localize("CancelWithShortcut", "_Cancel"); + OkLoc = Loc.Localize("OK", "OK"); + KeySetupOnClickHintLoc = Loc.Localize("SetupOnClickHint", "Don't know what this is?\n Check out the FAQ!"); + KeySetupInputPromptBadLoc = Loc.Localize("SetupInputPromptBad", "Enter a valid Authentication Key.\nKey needs to be 32 characters long."); + KeySetupCheckBoxLoc = Loc.Localize("SetupCheckBox", "Require Touch?"); + KeySetupTooltipLoc = Loc.Localize("SetupTooltip", "If checked, makes your YubiKey device require touch in order to authenticate."); + + } + public string KeySetupInputPromptInsertLoc { get; private set; } + public string KeySetupInputPromptYubiLoc { get; private set; } + public string CancelWithShortcutLoc { get; private set; } + public string OkLoc { get; private set; } + public string KeySetupOnClickHintLoc { get; private set; } + public string KeySetupInputPromptBadLoc { get; private set; } + public string KeySetupCheckBoxLoc { get; private set; } + public string KeySetupTooltipLoc { get; private set; } + + } +} diff --git a/src/XIVLauncher/Windows/ViewModel/SettingsControlViewModel.cs b/src/XIVLauncher/Windows/ViewModel/SettingsControlViewModel.cs index 4378119e4..5550d6594 100644 --- a/src/XIVLauncher/Windows/ViewModel/SettingsControlViewModel.cs +++ b/src/XIVLauncher/Windows/ViewModel/SettingsControlViewModel.cs @@ -72,6 +72,10 @@ private void SetupLoc() OtpServerTooltipLoc = Loc.Localize("OtpServerTooltip", "This will allow you to send your OTP code to XIVLauncher directly from your phone.\nClick \"Learn more\" to see how to set this up."); LearnMoreLoc = Loc.Localize("LearnMore", "Learn More"); OtpLearnMoreTooltipLoc = Loc.Localize("OtpLearnMoreTooltipLoc", "Open a guide in your web browser."); + OtpYubiKeyCheckBoxLoc = Loc.Localize("OtpYubikeyCheckBox", "Enable YubiKey-based authentication"); + OtpYubiKeyTooltipLoc = Loc.Localize("OtpYubikeyTooltip", "This will allow you to input your OTP code to XIVLauncher directly from touching your YubiKey.\nClick \"Learn more\" to see how to set this up."); + OtpSetupAuthLoc = Loc.Localize("OtpSetupAuth", "Setup Auth"); + OtpSetupAuthTooltipLoc = Loc.Localize("OtpSetupAuthTooltip", "Sets up a physical YubiKey device."); AdditionalArgumentsLoc = Loc.Localize("AdditionalArguments", "Additional launch arguments"); ChooseDpiAwarenessLoc = Loc.Localize("ChooseDpiAwareness", "Game DPI Awareness"); DpiAwarenessAwareLoc = Loc.Localize("DpiAwarenessAware", "Aware"); @@ -179,6 +183,10 @@ private void SetupLoc() public string OtpServerTooltipLoc { get; private set; } public string LearnMoreLoc { get; private set; } public string OtpLearnMoreTooltipLoc { get; private set; } + public string OtpYubiKeyCheckBoxLoc { get; private set; } + public string OtpYubiKeyTooltipLoc { get; private set; } + public string OtpSetupAuthLoc { get; private set; } + public string OtpSetupAuthTooltipLoc { get; private set; } public string AdditionalArgumentsLoc { get; private set; } public string ChooseDpiAwarenessLoc { get; private set; } public string ChooseDpiAwarenessHintLoc { get; private set; } From 5a7bbfec2ed0d4f2d3df00498afb1b92c0661324 Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 8 Sep 2023 10:41:25 -0500 Subject: [PATCH 4/6] Update license file --- src/XIVLauncher/Resources/LICENSE.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/XIVLauncher/Resources/LICENSE.txt b/src/XIVLauncher/Resources/LICENSE.txt index b7d748730..d70ede45f 100644 --- a/src/XIVLauncher/Resources/LICENSE.txt +++ b/src/XIVLauncher/Resources/LICENSE.txt @@ -724,4 +724,5 @@ System.ValueTuple https://github.com/dotnet/corefx/blob/ aria2 https://github.com/aria2/aria2/blob/master/COPYING Spooky https://github.com/Yortw/Spooky/blob/master/LICENSE AriaNet https://creativecommons.org/licenses/by-nc-sa/3.0/au/ -SharedMemory https://github.com/spazzarama/SharedMemory/blob/master/LICENSE.md \ No newline at end of file +SharedMemory https://github.com/spazzarama/SharedMemory/blob/master/LICENSE.md +Yubico.NET.SDK https://github.com/Yubico/Yubico.NET.SDK/blob/develop/LICENSE.txt From 6c303f49553d3abce17ea50289af793999d050ca Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 8 Sep 2023 12:32:56 -0500 Subject: [PATCH 5/6] Fix message not appearing --- src/XIVLauncher/Windows/OtpInputDialog.xaml.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs b/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs index 55f7abfbc..028235870 100644 --- a/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs +++ b/src/XIVLauncher/Windows/OtpInputDialog.xaml.cs @@ -261,7 +261,7 @@ private void AttemptYubiAuth() Storyboard.SetTarget(myStoryboard.Children.ElementAt(0), OtpInputPrompt); myStoryboard.Begin(); OtpTextBox.Focus(); - return; + throw new InvalidOperationException("Unable to find valid YubiKey credential."); } }); @@ -272,6 +272,11 @@ private void AttemptYubiAuth() return; } + catch (InvalidOperationException e) + { + Log.Error(e.Message); + return; + } byte retries = 0; _yubiThread = new Thread(() => From eb08b2e271774c85d048278fef2947548f8a6997 Mon Sep 17 00:00:00 2001 From: Cam Date: Fri, 8 Sep 2023 12:33:26 -0500 Subject: [PATCH 6/6] Add support for multiple accounts --- src/XIVLauncher/Accounts/YubiAuth.cs | 21 +++++++++++--- .../Windows/SecurityKeySetupDialog.xaml.cs | 29 +++++++++++++++++++ .../Windows/ViewModel/MainWindowViewModel.cs | 1 + .../SecurityKeySetupDialogViewModel.cs | 3 +- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/XIVLauncher/Accounts/YubiAuth.cs b/src/XIVLauncher/Accounts/YubiAuth.cs index 559fb0796..f10215954 100644 --- a/src/XIVLauncher/Accounts/YubiAuth.cs +++ b/src/XIVLauncher/Accounts/YubiAuth.cs @@ -5,7 +5,6 @@ using Yubico.YubiKey; using Yubico.YubiKey.Oath; using Yubico.PlatformInterop; -using System.Runtime.InteropServices; namespace XIVLauncher.Accounts { @@ -16,6 +15,7 @@ public class YubiAuth private const CredentialType AUTH_TYPE = CredentialType.Totp; private const CredentialPeriod TIME_PERIOD = CredentialPeriod.Period30; private const byte NUM_DIGITS = 6; + private static string username = ""; private static IYubiKeyDevice _yubiKey; public YubiKeyDeviceListener DeviceListener; @@ -25,13 +25,26 @@ public YubiAuth() DeviceListener = YubiKeyDeviceListener.Instance; } + public static void SetUsername(string name) + { + username = name; + } + public static string GetUsername() + { + return username; + } + public string GetAccountName() + { + return ACCOUNT_NAME + "-" + username; + } + //Generates generic credential public Credential BuildCredential() { var credentialTotp = new Credential { Issuer = ISSUER, - AccountName = ACCOUNT_NAME, + AccountName = GetAccountName(), Type = AUTH_TYPE, Period = TIME_PERIOD, Digits = NUM_DIGITS, @@ -45,7 +58,7 @@ public Credential BuildCredential(string key, bool useTouch) var credentialTotp = new Credential { Issuer = ISSUER, - AccountName = ACCOUNT_NAME, + AccountName = GetAccountName(), Type = AUTH_TYPE, Period = TIME_PERIOD, Secret = key, @@ -90,7 +103,7 @@ public bool CheckForCredential(OathSession session) { try { - IList creds = session.GetCredentials().Where(credential => credential.Issuer == ISSUER && credential.AccountName == ACCOUNT_NAME).ToList(); + IList creds = session.GetCredentials().Where(credential => credential.Issuer == ISSUER && credential.AccountName == GetAccountName()).ToList(); if (creds != null && creds.Count != 0) { return true; diff --git a/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml.cs b/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml.cs index d447762d4..2994c7eda 100644 --- a/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml.cs +++ b/src/XIVLauncher/Windows/SecurityKeySetupDialog.xaml.cs @@ -25,6 +25,8 @@ public partial class SecurityKeySetupDialog : Window private SecurityKeySetupDialogViewModel ViewModel => DataContext as SecurityKeySetupDialogViewModel; + private bool _usernameProvided; + private YubiAuth _yubiAuth; public SecurityKeySetupDialog() @@ -57,6 +59,17 @@ public SecurityKeySetupDialog() KeySetupTextBox.Focus(); }); } + + AccountManager accountManager = new AccountManager(App.Settings); + + var savedAccount = accountManager.CurrentAccount; + + if (savedAccount != null) + { + YubiAuth.SetUsername(savedAccount.UserName); + _usernameProvided = true; + } + SetupYubiListener(); return base.ShowDialog(); } @@ -123,6 +136,22 @@ private void CleanupYubi() public void TryAcceptKey(string key) { + if (!_usernameProvided) + { + Log.Error("Didn't receive a username."); + Dispatcher.Invoke(() => + { + KeySetupInputPrompt.Text = ViewModel.KeySetupUsernameLoc; + KeySetupInputPrompt.Foreground = Brushes.Red; + Storyboard myStoryboard = (Storyboard)KeySetupInputPrompt.Resources["InvalidShake"]; + Storyboard.SetTarget(myStoryboard.Children.ElementAt(0), KeySetupInputPrompt); + myStoryboard.Begin(); + KeySetupTextBox.Focus(); + }); + + return; + } + if (key.Length < 32 || Regex.IsMatch(key, "[^A-Za-z2-7=]+")) { Log.Error("Malformed Authentication Key: {Key}", key); diff --git a/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs b/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs index 33e49959b..ec7605097 100644 --- a/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs +++ b/src/XIVLauncher/Windows/ViewModel/MainWindowViewModel.cs @@ -219,6 +219,7 @@ private async Task Login(string username, string password, bool isOtp, bool isSt } username = username.Replace(" ", string.Empty); // Remove whitespace + YubiAuth.SetUsername(username); if (Repository.Ffxiv.GetVer(App.Settings.GamePath) == Constants.BASE_GAME_VERSION && App.Settings.UniqueIdCacheEnabled) diff --git a/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs b/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs index b30354a4d..cedd2e335 100644 --- a/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs +++ b/src/XIVLauncher/Windows/ViewModel/SecurityKeySetupDialogViewModel.cs @@ -19,7 +19,7 @@ private void SetupLoc() KeySetupInputPromptBadLoc = Loc.Localize("SetupInputPromptBad", "Enter a valid Authentication Key.\nKey needs to be 32 characters long."); KeySetupCheckBoxLoc = Loc.Localize("SetupCheckBox", "Require Touch?"); KeySetupTooltipLoc = Loc.Localize("SetupTooltip", "If checked, makes your YubiKey device require touch in order to authenticate."); - + KeySetupUsernameLoc = Loc.Localize("SetupUsername", "Unable to verify username\nPlease login using XIVLauncher at least once."); } public string KeySetupInputPromptInsertLoc { get; private set; } public string KeySetupInputPromptYubiLoc { get; private set; } @@ -29,6 +29,7 @@ private void SetupLoc() public string KeySetupInputPromptBadLoc { get; private set; } public string KeySetupCheckBoxLoc { get; private set; } public string KeySetupTooltipLoc { get; private set; } + public string KeySetupUsernameLoc { get; private set; } } }