Skip to content

Commit

Permalink
Added ability to save OTP Token URI
Browse files Browse the repository at this point in the history
  • Loading branch information
trowgundam committed Aug 24, 2021
1 parent c477afd commit 23504a1
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 10 deletions.
34 changes: 34 additions & 0 deletions src/XIVLauncher/Accounts/XivAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,40 @@ public string Password
Log.Information($"Set Password SaveCredentials: {b}");
}
}

[JsonIgnore]
public string OtpUri
{
get
{
var credentials = CredentialManager.GetCredentials($"FINAL FANTASY XIV-{UserName.ToLower()}-OTP");

return credentials != null ? credentials.Password : string.Empty;
}
set
{
// TODO: Remove logging here after making sure fix was good
// This will throw when the account doesn't actually exist
try
{
var a = CredentialManager.RemoveCredentials($"FINAL FANTASY XIV-{UserName.ToLower()}-OTP");

Log.Information($"Set Password RemoveCredentials: {a}");
}
catch (Win32Exception)
{
// ignored
}

var b = CredentialManager.SaveCredentials($"FINAL FANTASY XIV-{UserName.ToLower()}-OTP", new NetworkCredential
{
UserName = UserName,
Password = value
});

Log.Information($"Set Password SaveCredentials: {b}");
}
}

public bool SavePassword { get; set; }
public bool UseSteamServiceAccount { get; set; }
Expand Down
1 change: 1 addition & 0 deletions src/XIVLauncher/Windows/AccountSwitcher.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<MenuItem Header="{Binding AccountSwitcherCreateShortcutLoc}" Click="CreateDesktopShortcut_OnClick" Foreground="{DynamicResource MaterialDesignBody}"/>
<MenuItem Header="{Binding RemoveLoc}" Click="RemoveAccount_OnClick" Foreground="{DynamicResource MaterialDesignBody}"/>
<MenuItem x:Name="AccountEntrySavePasswordCheck" IsCheckable="True" Header="{Binding AccountSwitcherDontSavePasswordLoc}" Checked="DontSavePassword_OnChecked" Unchecked="DontSavePassword_OnUnchecked" Foreground="{DynamicResource MaterialDesignBody}"/>
<MenuItem Header="{Binding ModifyOtpUriLoc}" Click="ModifyOtpUri_Click" Foreground="{DynamicResource MaterialDesignBody}" />
</ContextMenu>
</ListView.ContextMenu>

Expand Down
13 changes: 13 additions & 0 deletions src/XIVLauncher/Windows/AccountSwitcher.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,18 @@ private void DontSavePassword_OnUnchecked(object sender, RoutedEventArgs e)
account.SavePassword = true;
_accountManager.Save();
}

private void ModifyOtpUri_Click(object sender, RoutedEventArgs e)
{
if (!(AccountListView.SelectedItem is AccountSwitcherEntry selectedEntry))
return;

var otpDialog = new OtpUriSetupWindow(selectedEntry.Account.OtpUri);
otpDialog.ShowDialog();

var account = _accountManager.Accounts.First(a => a.Id == selectedEntry.Account.Id);
account.OtpUri = otpDialog.Result;
_accountManager.Save();
}
}
}
81 changes: 71 additions & 10 deletions src/XIVLauncher/Windows/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public partial class MainWindow : Window

private AccountManager _accountManager;

private bool _previousLoginFailure = false; // To prevent locking peoples accounts, prevent the auto OTP, just in case
private bool _isLoggingIn;

public MainWindow()
Expand Down Expand Up @@ -460,23 +461,65 @@ private void PrepareLogin(bool autoLogin, bool startGame = true)
var otp = "";
if (OtpCheckBox.IsChecked == true && !hasValidCache)
{
var otpDialog = new OtpInputDialog();
otpDialog.ShowDialog();
if (!string.IsNullOrWhiteSpace(_accountManager?.CurrentAccount?.OtpUri) && !_previousLoginFailure)
{
try
{
OtpNet.Totp totp;

if (Uri.TryCreate(_accountManager.CurrentAccount.OtpUri, UriKind.Absolute, out Uri uri))
{
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);

if (!query.AllKeys.Contains("secret"))
{
throw new Exception("No Secret");
}

var secretKey = OtpNet.Base32Encoding.ToBytes(query["secret"]);
if (!query.AllKeys.Contains("period") || !int.TryParse(query["period"], out int period))
period = 30;
if (!query.AllKeys.Contains("digits") || !int.TryParse(query["digits"], out int digits))
digits = 6;
if (!query.AllKeys.Contains("algorithm") || !Enum.TryParse(query["algorithm"], true, out OtpNet.OtpHashMode algorithm))
algorithm = OtpNet.OtpHashMode.Sha1;

if (otpDialog.Result == null)
totp = new OtpNet.Totp(secretKey, step: period, mode: algorithm, totpSize: digits);

}
else
{
var secretKey = OtpNet.Base32Encoding.ToBytes(_accountManager.CurrentAccount.OtpUri);
totp = new OtpNet.Totp(secretKey);
}
otp = totp.ComputeTotp();
}
catch (Exception)
{
otp = ""; // Just ignore whatever happened, the prompt will come up instead.
}
}

if (string.IsNullOrEmpty(otp))
{
_isLoggingIn = false;
var otpDialog = new OtpInputDialog();
otpDialog.ShowDialog();

if (autoLogin)
if (otpDialog.Result == null)
{
CleanUp();
Environment.Exit(0);
_isLoggingIn = false;

if (autoLogin)
{
CleanUp();
Environment.Exit(0);
}

return;
}

return;
otp = otpDialog.Result;
}

otp = otpDialog.Result;
}

StartLogin(otp, startGame);
Expand Down Expand Up @@ -593,6 +636,24 @@ private async void StartLogin(string otp, bool startGame)
Log.Verbose(
$"[LR] {loginResult.State} {loginResult.PendingPatches != null} {loginResult.OauthLogin?.Playable}");

if (loginResult.State == Launcher.LoginState.NoOAuth)
{
var failedOauthMessage = Loc.Localize("LoginNoOauthMessage", "Could not login into your Square Enix account.\nThis could be caused by bad credentials or OTPs.\n\nPlease also check your email inbox for any messages from Square Enix - they might want you to reset your password due to \"suspicious activity\".\nThis is NOT caused by a security issue in XIVLauncher, it is merely a safety measure by Square Enix to prevent logins from new locations, in case your account is getting stolen.\nXIVLauncher and the official launcher will work fine again after resetting your password.");
if (App.Settings.AutologinEnabled)
{
failedOauthMessage += Loc.Localize("LoginNoOauthAutologinHint", "\n\nAuto-Login has been disabled.");
App.Settings.AutologinEnabled = false;
}

CustomMessageBox.Show(failedOauthMessage, Loc.Localize("LoginNoOauthTitle", "Login issue"), MessageBoxButton.OK, MessageBoxImage.Error);

_previousLoginFailure = true; // Prevents auto OTP being populated.
_isLoggingIn = false;
Show();
Activate();
return;
}

if (loginResult.State == Launcher.LoginState.NoService)
{
CustomMessageBox.Show(
Expand Down
59 changes: 59 additions & 0 deletions src/XIVLauncher/Windows/OtpUriSetupWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<Window x:Class="XIVLauncher.Windows.OtpUriSetupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XIVLauncher"
xmlns:components="clr-namespace:XIVLauncher.Xaml.Components"
xmlns:vm="clr-namespace:XIVLauncher.Windows.ViewModel"
mc:Ignorable="d"
Title="{Binding OtpUriSetupTitleLoc}" Height="241.747" Width="533.495"
d:DataContext="{d:DesignInstance Type={x:Type vm:OtpUriSetupWindowViewModel}}"
WindowStartupLocation="CenterScreen"
Closed="Window_Closed"
Icon="pack://application:,,,/Resources/dalamud_icon.ico" ResizeMode="NoResize"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
Background="{DynamicResource MaterialDesignPaper}"
TextElement.FontWeight="Medium"
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto">
<Grid>
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,10,0,0"
Text="{Binding OtpUriSetupDescriptionLoc}" />
<TextBox x:Name="OtpUriTextBox" Margin="10,0,0,80" Width="400"
TextChanged="OtpUriTextBox_TextChanged"
VerticalAlignment="Center" HorizontalAlignment="Left" />

<Label Margin="10,0,0,10" Width="200" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="{Binding SecretDescLoc}"/>
<CheckBox x:Name="SecretCheckBox" Content="{Binding UnknownValueLoc, Mode=OneTime}"
Margin="210,0,0,10" IsHitTestVisible="False" Foreground="Red"
Width="200" VerticalAlignment="Center" HorizontalAlignment="Left" />

<Label Margin="10,0,0,-30" Width="200" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="{Binding PeriodDescLoc}"/>
<CheckBox x:Name="PeriodCheckBox" Content="{Binding UnknownValueLoc, Mode=OneTime}"
Margin="210,0,0,-30" IsHitTestVisible="False" Foreground="Red"
Width="200" VerticalAlignment="Center" HorizontalAlignment="Left" />

<Label Margin="10,0,0,-70" Width="200" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="{Binding LengthDescloc}"/>
<CheckBox x:Name="LengthCheckBox" Content="{Binding UnknownValueLoc, Mode=OneTime}"
Margin="210,0,0,-70" IsHitTestVisible="False" Foreground="Red"
Width="200" VerticalAlignment="Center" HorizontalAlignment="Left" />

<Label Margin="10,0,0,-110" Width="200" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="{Binding AlgorithmDescLoc}"/>
<CheckBox x:Name="AlgorithmCheckBox" Content="{Binding UnknownValueLoc, Mode=OneTime}"
Margin="210,0,0,-110" IsHitTestVisible="False" Foreground="Red"
Width="200" VerticalAlignment="Center" HorizontalAlignment="Left" />

<Label Margin="10,0,0,-150" Width="200" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="{Binding OtpCodeDescLoc}"/>
<CheckBox x:Name="OtpCodeCheckBox" Content="{Binding UnknownValueLoc, Mode=OneTime}"
Margin="210,0,0,-150" IsHitTestVisible="False" Foreground="Red"
Width="200" VerticalAlignment="Center" HorizontalAlignment="Left" />

<Button Content="{Binding OkLoc}" Width="79" VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="0,0,10,10" Click="NextButton_Click" />
</Grid>
</Window>
Loading

0 comments on commit 23504a1

Please sign in to comment.