diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb50c50e0..e52716d9b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,7 @@ env: NET_CORE_VERSION: net7.0 NET_FRAMEWORK_VERSION: net481 NODE_JS_VERSION: 'lts/*' - PLUGINS: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.SteamTokenDumper + PLUGINS: ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper jobs: publish-asf-ui: diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj new file mode 100644 index 000000000..a330ad16e --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj @@ -0,0 +1,37 @@ + + + Library + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Strings.Designer.cs + + + + + + True + Strings.resx + True + + + diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/AssemblyInfo.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/AssemblyInfo.cs new file mode 100644 index 000000000..6ad4b4efb --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/AssemblyInfo.cs @@ -0,0 +1,24 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +[assembly: CLSCompliant(false)] diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs new file mode 100644 index 000000000..2f6880070 --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Commands.cs @@ -0,0 +1,311 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.Localization; +using ArchiSteamFarm.Steam; +using Newtonsoft.Json; +using SteamKit2; +using SteamKit2.Internal; + +namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; + +internal static class Commands { + private const byte MaxFinalizationAttempts = 900 / Steam.Security.MobileAuthenticator.CodeInterval; + + internal static async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) { + ArgumentNullException.ThrowIfNull(bot); + + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + if (string.IsNullOrEmpty(message)) { + throw new ArgumentNullException(nameof(message)); + } + + if ((args == null) || (args.Length == 0)) { + throw new ArgumentNullException(nameof(args)); + } + + if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + switch (args.Length) { + case 1: + switch (args[0].ToUpperInvariant()) { + case "2FAINIT": + return await ResponseTwoFactorInit(access, bot).ConfigureAwait(false); + } + + break; + default: + switch (args[0].ToUpperInvariant()) { + case "2FAINIT": + return await ResponseTwoFactorInit(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false); + case "2FASMS" when args.Length > 2: + return await ResponseTwoFactorFinalize(access, args[1], Utilities.GetArgsAsText(message, 2), steamID).ConfigureAwait(false); + case "2FASMS": + return await ResponseTwoFactorFinalize(access, bot, args[1]).ConfigureAwait(false); + } + + break; + } + + return null; + } + + private static async Task ResponseTwoFactorFinalize(EAccess access, Bot bot, string smsCode) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentNullException.ThrowIfNull(bot); + + if (string.IsNullOrEmpty(smsCode)) { + throw new ArgumentNullException(nameof(smsCode)); + } + + if (access < EAccess.Master) { + return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null; + } + + if (bot.HasMobileAuthenticator) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator))); + } + + if (!bot.IsConnectedAndLoggedOn) { + return bot.Commands.FormatBotResponse(Strings.BotNotConnected); + } + + string maFilePath = bot.GetFilePath(Bot.EFileType.MobileAuthenticator); + string maFilePendingPath = $"{maFilePath}.PENDING"; + + if (!File.Exists(maFilePendingPath)) { + return bot.Commands.FormatBotResponse(Strings.NothingFound); + } + + string json; + + try { + json = await File.ReadAllTextAsync(maFilePendingPath).ConfigureAwait(false); + } catch (Exception e) { + bot.ArchiLogger.LogGenericException(e); + + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message)); + } + + if (string.IsNullOrEmpty(json)) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); + } + + Steam.Security.MobileAuthenticator? mobileAuthenticator = JsonConvert.DeserializeObject(json); + + if (mobileAuthenticator == null) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(json))); + } + + mobileAuthenticator.Init(bot); + + MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler(); + + if (mobileAuthenticatorHandler == null) { + throw new InvalidOperationException(nameof(mobileAuthenticatorHandler)); + } + + ulong steamTime = await mobileAuthenticator.GetSteamTime().ConfigureAwait(false); + + bool successFinalizing = false; + + for (byte i = 0; i < MaxFinalizationAttempts; i++) { + if (i > 0) { + steamTime += Steam.Security.MobileAuthenticator.CodeInterval; + } + + string? code = mobileAuthenticator.GenerateTokenForTime(steamTime); + + if (string.IsNullOrEmpty(code)) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticator.GenerateTokenForTime))); + } + + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + CTwoFactor_FinalizeAddAuthenticator_Response? response = await mobileAuthenticatorHandler.FinalizeAuthenticator(bot.SteamID, smsCode, code!, steamTime).ConfigureAwait(false); + + if (response == null) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(mobileAuthenticatorHandler.FinalizeAuthenticator))); + } + + if (response.want_more) { + // OK, whatever + continue; + } + + if (!response.success) { + EResult result = (EResult) response.status; + + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result)); + } + + successFinalizing = true; + + break; + } + + if (!successFinalizing) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, MaxFinalizationAttempts)); + } + + if (!bot.TryImportAuthenticator(mobileAuthenticator)) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.TryImportAuthenticator))); + } + + string maFileFinishedPath = $"{maFilePath}.NEW"; + + try { + File.Move(maFilePendingPath, maFileFinishedPath, true); + } catch (Exception e) { + bot.ArchiLogger.LogGenericException(e); + + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message)); + } + + return bot.Commands.FormatBotResponse(Strings.Done); + } + + private static async Task ResponseTwoFactorFinalize(EAccess access, string botNames, string smsCode, ulong steamID = 0) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + if (string.IsNullOrEmpty(botNames)) { + throw new ArgumentNullException(nameof(botNames)); + } + + if (string.IsNullOrEmpty(smsCode)) { + throw new ArgumentNullException(nameof(smsCode)); + } + + if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null; + } + + IList results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorFinalize(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot, smsCode))).ConfigureAwait(false); + + List responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!); + + return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; + } + + private static async Task ResponseTwoFactorInit(EAccess access, Bot bot) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentNullException.ThrowIfNull(bot); + + if (access < EAccess.Master) { + return access > EAccess.None ? bot.Commands.FormatBotResponse(Strings.ErrorAccessDenied) : null; + } + + if (bot.HasMobileAuthenticator) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(bot.HasMobileAuthenticator))); + } + + if (!bot.IsConnectedAndLoggedOn) { + return bot.Commands.FormatBotResponse(Strings.BotNotConnected); + } + + MobileAuthenticatorHandler? mobileAuthenticatorHandler = bot.GetHandler(); + + if (mobileAuthenticatorHandler == null) { + throw new InvalidOperationException(nameof(mobileAuthenticatorHandler)); + } + + string deviceID = $"android:{Guid.NewGuid()}"; + + CTwoFactor_AddAuthenticator_Response? response = await mobileAuthenticatorHandler.AddAuthenticator(bot.SteamID, deviceID).ConfigureAwait(false); + + if (response == null) { + return bot.Commands.FormatBotResponse(Strings.WarningFailed); + } + + EResult result = (EResult) response.status; + + if (result != EResult.OK) { + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, result)); + } + + MaFileData maFileData = new(response, deviceID); + + string maFilePendingPath = $"{bot.GetFilePath(Bot.EFileType.MobileAuthenticator)}.PENDING"; + string json = JsonConvert.SerializeObject(maFileData, Formatting.Indented); + + try { + await File.WriteAllTextAsync(maFilePendingPath, json).ConfigureAwait(false); + } catch (Exception e) { + bot.ArchiLogger.LogGenericException(e); + + return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, e.Message)); + } + + return bot.Commands.FormatBotResponse(Strings.Done); + } + + private static async Task ResponseTwoFactorInit(EAccess access, string botNames, ulong steamID = 0) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + if (string.IsNullOrEmpty(botNames)) { + throw new ArgumentNullException(nameof(botNames)); + } + + if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return access >= EAccess.Owner ? Steam.Interaction.Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames)) : null; + } + + IList results = await Utilities.InParallel(bots.Select(bot => ResponseTwoFactorInit(Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), bot))).ConfigureAwait(false); + + List responses = new(results.Where(static result => !string.IsNullOrEmpty(result))!); + + return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; + } +} diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/README.md b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/README.md new file mode 100644 index 000000000..4d3bb618e --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/README.md @@ -0,0 +1,3 @@ +This directory contains ASF strings for display and localization purposes. + +All strings used by ASF can be found in main `Strings.resx` file, and that's also the only `resx` file that should be modified - all other `resx` files are managed automatically and should not be touched. Please visit **[localization](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Localization)** section on the wiki if you want to improve translation of other files. diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/Strings.Designer.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/Strings.Designer.cs new file mode 100644 index 000000000..d3cb13946 --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/Strings.Designer.cs @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/Strings.resx b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/Strings.resx new file mode 100644 index 000000000..81ec80f70 --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/Localization/Strings.resx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs new file mode 100644 index 000000000..0f0d93973 --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MaFileData.cs @@ -0,0 +1,81 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Newtonsoft.Json; +using SteamKit2.Internal; + +namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; + +internal sealed class MaFileData { + [JsonProperty("account_name", Required = Required.Always)] + internal readonly string AccountName; + + [JsonProperty("device_id", Required = Required.Always)] + internal readonly string DeviceID; + + [JsonProperty("identity_secret", Required = Required.Always)] + internal readonly string IdentitySecret; + + [JsonProperty("revocation_code", Required = Required.Always)] + internal readonly string RevocationCode; + + [JsonProperty("secret_1", Required = Required.Always)] + internal readonly string Secret1; + + [JsonProperty("serial_number", Required = Required.Always)] + internal readonly ulong SerialNumber; + + [JsonProperty("server_time", Required = Required.Always)] + internal readonly ulong ServerTime; + + [JsonProperty("shared_secret", Required = Required.Always)] + internal readonly string SharedSecret; + + [JsonProperty("status", Required = Required.Always)] + internal readonly int Status; + + [JsonProperty("token_gid", Required = Required.Always)] + internal readonly string TokenGid; + + [JsonProperty("uri", Required = Required.Always)] + internal readonly string Uri; + + internal MaFileData(CTwoFactor_AddAuthenticator_Response data, string deviceID) { + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrEmpty(deviceID)) { + throw new ArgumentNullException(nameof(deviceID)); + } + + AccountName = data.account_name; + DeviceID = deviceID; + IdentitySecret = Convert.ToBase64String(data.identity_secret); + RevocationCode = data.revocation_code; + Secret1 = Convert.ToBase64String(data.secret_1); + SerialNumber = data.serial_number; + ServerTime = data.server_time; + SharedSecret = Convert.ToBase64String(data.shared_secret); + Status = data.status; + TokenGid = data.token_gid; + Uri = data.uri; + } +} diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorHandler.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorHandler.cs new file mode 100644 index 000000000..6343533d3 --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorHandler.cs @@ -0,0 +1,138 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Threading.Tasks; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.NLog; +using SteamKit2; +using SteamKit2.Internal; + +namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; + +internal sealed class MobileAuthenticatorHandler : ClientMsgHandler { + private readonly ArchiLogger ArchiLogger; + private readonly SteamUnifiedMessages.UnifiedService UnifiedTwoFactorService; + + internal MobileAuthenticatorHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnifiedMessages) { + ArgumentNullException.ThrowIfNull(steamUnifiedMessages); + + ArchiLogger = archiLogger ?? throw new ArgumentNullException(nameof(archiLogger)); + UnifiedTwoFactorService = steamUnifiedMessages.CreateService(); + } + + public override void HandleMsg(IPacketMsg packetMsg) => ArgumentNullException.ThrowIfNull(packetMsg); + + internal async Task AddAuthenticator(ulong steamID, string deviceID) { + if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + if (string.IsNullOrEmpty(deviceID)) { + throw new ArgumentNullException(nameof(deviceID)); + } + + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected) { + return null; + } + + CTwoFactor_AddAuthenticator_Request request = new() { + authenticator_type = 1, + authenticator_time = Utilities.GetUnixTime(), + device_identifier = deviceID, + sms_phone_id = "1", + steamid = steamID + }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedTwoFactorService.SendMessage(x => x.AddAuthenticator(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return null; + } + + if (response.Result != EResult.OK) { + return null; + } + + CTwoFactor_AddAuthenticator_Response body = response.GetDeserializedResponse(); + + return body; + } + + internal async Task FinalizeAuthenticator(ulong steamID, string activationCode, string authenticatorCode, ulong authenticatorTime) { + if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + if (string.IsNullOrEmpty(activationCode)) { + throw new ArgumentNullException(nameof(activationCode)); + } + + if (string.IsNullOrEmpty(authenticatorCode)) { + throw new ArgumentNullException(nameof(authenticatorCode)); + } + + if (authenticatorTime <= 0) { + throw new ArgumentOutOfRangeException(nameof(authenticatorTime)); + } + + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected) { + return null; + } + + CTwoFactor_FinalizeAddAuthenticator_Request request = new() { + activation_code = activationCode, + authenticator_code = authenticatorCode, + authenticator_time = authenticatorTime, + steamid = steamID + }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedTwoFactorService.SendMessage(x => x.FinalizeAddAuthenticator(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return null; + } + + if (response.Result != EResult.OK) { + return null; + } + + CTwoFactor_FinalizeAddAuthenticator_Response body = response.GetDeserializedResponse(); + + return body; + } +} diff --git a/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs new file mode 100644 index 000000000..7655c974e --- /dev/null +++ b/ArchiSteamFarm.OfficialPlugins.MobileAuthenticator/MobileAuthenticatorPlugin.cs @@ -0,0 +1,91 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Composition; +using System.Threading.Tasks; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.Localization; +using ArchiSteamFarm.Plugins; +using ArchiSteamFarm.Plugins.Interfaces; +using ArchiSteamFarm.Steam; +using Newtonsoft.Json; +using SteamKit2; + +namespace ArchiSteamFarm.OfficialPlugins.MobileAuthenticator; + +[Export(typeof(IPlugin))] +internal sealed class MobileAuthenticatorPlugin : OfficialPlugin, IBotCommand2, IBotSteamClient { + [JsonProperty] + public override string Name => nameof(MobileAuthenticatorPlugin); + + [JsonProperty] + public override Version Version => typeof(MobileAuthenticatorPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version)); + + public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) { + ArgumentNullException.ThrowIfNull(bot); + + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + if (string.IsNullOrEmpty(message)) { + throw new ArgumentNullException(nameof(message)); + } + + if ((args == null) || (args.Length == 0)) { + throw new ArgumentNullException(nameof(args)); + } + + if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) { + throw new ArgumentOutOfRangeException(nameof(steamID)); + } + + return await Commands.OnBotCommand(bot, access, message, args, steamID).ConfigureAwait(false); + } + + public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) { + ArgumentNullException.ThrowIfNull(bot); + ArgumentNullException.ThrowIfNull(callbackManager); + + return Task.CompletedTask; + } + + public Task?> OnBotSteamHandlersInit(Bot bot) { + ArgumentNullException.ThrowIfNull(bot); + + SteamUnifiedMessages? steamUnifiedMessages = bot.GetHandler(); + + if (steamUnifiedMessages == null) { + throw new InvalidOperationException(nameof(steamUnifiedMessages)); + } + + return Task.FromResult?>(new HashSet(1) { new MobileAuthenticatorHandler(bot.ArchiLogger, steamUnifiedMessages) }); + } + + public override Task OnLoaded() { + Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager); + + return Task.CompletedTask; + } +} diff --git a/ArchiSteamFarm.sln b/ArchiSteamFarm.sln index da08c73f0..441a15454 100644 --- a/ArchiSteamFarm.sln +++ b/ArchiSteamFarm.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlug EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.CustomPlugins.SignInWithSteam", "ArchiSteamFarm.CustomPlugins.SignInWithSteam\ArchiSteamFarm.CustomPlugins.SignInWithSteam.csproj", "{27E35B1D-AA85-4D54-978B-520BF88DA0B7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm.OfficialPlugins.MobileAuthenticator", "ArchiSteamFarm.OfficialPlugins.MobileAuthenticator\ArchiSteamFarm.OfficialPlugins.MobileAuthenticator.csproj", "{8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,5 +71,11 @@ Global {27E35B1D-AA85-4D54-978B-520BF88DA0B7}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {27E35B1D-AA85-4D54-978B-520BF88DA0B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {27E35B1D-AA85-4D54-978B-520BF88DA0B7}.Release|Any CPU.Build.0 = Release|Any CPU + {8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU + {8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.DebugFast|Any CPU.Build.0 = Debug|Any CPU + {8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D85BCCA-4DE6-4CC0-B015-E2E89E8E8AA3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ArchiSteamFarm/AssemblyInfo.cs b/ArchiSteamFarm/AssemblyInfo.cs index fcfd686e0..fa858e3b3 100644 --- a/ArchiSteamFarm/AssemblyInfo.cs +++ b/ArchiSteamFarm/AssemblyInfo.cs @@ -27,9 +27,11 @@ using System.Runtime.CompilerServices; #if ASF_SIGNED_BUILD [assembly: InternalsVisibleTo("ArchiSteamFarm.Tests, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")] [assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.ItemsMatcher, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")] +[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.MobileAuthenticator, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")] [assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper, PublicKey=002400000480000014020000060200000024000052534131001000000100010099f0e5961ec7497fd7de1cba2b8c5eff3b18c1faf3d7a8d56e063359c7f928b54b14eae24d23d9d3c1a5db7ceca82edb6956d43e8ea2a0b7223e6e6836c0b809de43fde69bf33fba73cf669e71449284d477333d4b6e54fb69f7b6c4b4811b8fe26e88975e593cffc0e321490a50500865c01e50ab87c8a943b2a788af47dc20f2b860062b7b6df25477e471a744485a286b435cea2df3953cbb66febd8db73f3ccb4588886373141d200f749ba40bb11926b668cc15f328412dd0b0b835909229985336eb4a34f47925558dc6dc3910ea09c1aad5c744833f26ad9de727559d393526a7a29b3383de87802a034ead8ecc2d37340a5fa9b406774446256337d77e3c9e8486b5e732097e238312deaf5b4efcc04df8ecb986d90ee12b4a8a9a00319cc25cb91fd3e36a3cc39e501f83d14eb1e1a6fa6a1365483d99f4cefad1ea5dec204dad958e2a9a93add19781a8aa7bac71747b11d156711eafd1e873e19836eb573fa5cde284739df09b658ed40c56c7b5a7596840774a7065864e6c2af7b5a8bf7a2d238de83d77891d98ef5a4a58248c655a1c7c97c99e01d9928dc60c629eeb523356dc3686e3f9a1a30ffcd0268cd03718292f21d839fce741f4c1163001ab5b654c37d862998962a05e8028e061c611384772777ef6a49b00ebb4f228308e61b2afe408b33db2d82c4f385e26d7438ec0a183c64eeca4138cbc3dc2")] #else [assembly: InternalsVisibleTo("ArchiSteamFarm.Tests")] [assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.ItemsMatcher")] +[assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.MobileAuthenticator")] [assembly: InternalsVisibleTo("ArchiSteamFarm.OfficialPlugins.SteamTokenDumper")] #endif diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 1a21d1dd5..6cee0dfc3 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -577,6 +577,9 @@ public sealed class Bot : IAsyncDisposable, IDisposable { return GetFilePath(BotName, fileType); } + [PublicAPI] + public T? GetHandler() where T : ClientMsgHandler => SteamClient.GetHandler(); + [PublicAPI] public static HashSet GetItemsForFullSets(IReadOnlyCollection inventory, IReadOnlyDictionary<(uint RealAppID, Asset.EType Type, Asset.ERarity Rarity), (uint SetsToExtract, byte ItemsPerSet)> amountsToExtract, ushort maxItems = Trading.MaxItemsPerTrade) { if ((inventory == null) || (inventory.Count == 0)) { diff --git a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs index 97a445253..723e2d16c 100644 --- a/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs +++ b/ArchiSteamFarm/Steam/Security/MobileAuthenticator.cs @@ -41,8 +41,7 @@ namespace ArchiSteamFarm.Steam.Security; public sealed class MobileAuthenticator : IDisposable { internal const byte BackupCodeDigits = 7; internal const byte CodeDigits = 5; - - private const byte CodeInterval = 30; + internal const byte CodeInterval = 30; // For how many minutes we can assume that SteamTimeDifference is correct private const byte SteamTimeTTL = 15; @@ -83,6 +82,66 @@ public sealed class MobileAuthenticator : IDisposable { return GenerateTokenForTime(time); } + internal string? GenerateTokenForTime(ulong time) { + if (time == 0) { + throw new ArgumentOutOfRangeException(nameof(time)); + } + + if (Bot == null) { + throw new InvalidOperationException(nameof(Bot)); + } + + if (string.IsNullOrEmpty(SharedSecret)) { + throw new InvalidOperationException(nameof(SharedSecret)); + } + + byte[] sharedSecret; + + try { + sharedSecret = Convert.FromBase64String(SharedSecret); + } catch (FormatException e) { + Bot.ArchiLogger.LogGenericException(e); + Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(SharedSecret))); + + return null; + } + + byte[] timeArray = BitConverter.GetBytes(time / CodeInterval); + + if (BitConverter.IsLittleEndian) { + Array.Reverse(timeArray); + } + +#pragma warning disable CA5350 // This is actually a fair warning, but there is nothing we can do about Steam using weak cryptographic algorithms + byte[] hash = HMACSHA1.HashData(sharedSecret, timeArray); +#pragma warning restore CA5350 // This is actually a fair warning, but there is nothing we can do about Steam using weak cryptographic algorithms + + // The last 4 bits of the mac say where the code starts + int start = hash[^1] & 0x0f; + + // Extract those 4 bytes + byte[] bytes = new byte[4]; + + Array.Copy(hash, start, bytes, 0, 4); + + if (BitConverter.IsLittleEndian) { + Array.Reverse(bytes); + } + + // Build the alphanumeric code + uint fullCode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; + + // ReSharper disable once BuiltInTypeReferenceStyleForMemberAccess - required for .NET Framework + return String.Create( + CodeDigits, fullCode, static (buffer, state) => { + for (byte i = 0; i < CodeDigits; i++) { + buffer[i] = CodeCharacters[(byte) (state % CodeCharacters.Count)]; + state /= (byte) CodeCharacters.Count; + } + } + ); + } + internal async Task?> GetConfirmations() { if (Bot == null) { throw new InvalidOperationException(nameof(Bot)); @@ -195,6 +254,44 @@ public sealed class MobileAuthenticator : IDisposable { return result; } + internal async Task GetSteamTime() { + if (Bot == null) { + throw new InvalidOperationException(nameof(Bot)); + } + + int? steamTimeDifference = SteamTimeDifference; + + if (steamTimeDifference.HasValue && (DateTime.UtcNow.Subtract(LastSteamTimeCheck).TotalMinutes < SteamTimeTTL)) { + return Utilities.MathAdd(Utilities.GetUnixTime(), steamTimeDifference.Value); + } + + await TimeSemaphore.WaitAsync().ConfigureAwait(false); + + try { + steamTimeDifference = SteamTimeDifference; + + if (steamTimeDifference.HasValue && (DateTime.UtcNow.Subtract(LastSteamTimeCheck).TotalMinutes < SteamTimeTTL)) { + return Utilities.MathAdd(Utilities.GetUnixTime(), steamTimeDifference.Value); + } + + ulong serverTime = await Bot.ArchiWebHandler.GetServerTime().ConfigureAwait(false); + + if (serverTime == 0) { + return Utilities.GetUnixTime(); + } + + // We assume that the difference between times will be within int range, therefore we accept underflow here (for subtraction), and since we cast that result to int afterwards, we also accept overflow for the cast itself + steamTimeDifference = unchecked((int) (serverTime - Utilities.GetUnixTime())); + + SteamTimeDifference = steamTimeDifference; + LastSteamTimeCheck = DateTime.UtcNow; + } finally { + TimeSemaphore.Release(); + } + + return Utilities.MathAdd(Utilities.GetUnixTime(), steamTimeDifference.Value); + } + internal async Task HandleConfirmations(IReadOnlyCollection confirmations, bool accept) { if ((confirmations == null) || (confirmations.Count == 0)) { throw new ArgumentNullException(nameof(confirmations)); @@ -335,104 +432,6 @@ public sealed class MobileAuthenticator : IDisposable { return Convert.ToBase64String(hash); } - private string? GenerateTokenForTime(ulong time) { - if (time == 0) { - throw new ArgumentOutOfRangeException(nameof(time)); - } - - if (Bot == null) { - throw new InvalidOperationException(nameof(Bot)); - } - - if (string.IsNullOrEmpty(SharedSecret)) { - throw new InvalidOperationException(nameof(SharedSecret)); - } - - byte[] sharedSecret; - - try { - sharedSecret = Convert.FromBase64String(SharedSecret); - } catch (FormatException e) { - Bot.ArchiLogger.LogGenericException(e); - Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(SharedSecret))); - - return null; - } - - byte[] timeArray = BitConverter.GetBytes(time / CodeInterval); - - if (BitConverter.IsLittleEndian) { - Array.Reverse(timeArray); - } - -#pragma warning disable CA5350 // This is actually a fair warning, but there is nothing we can do about Steam using weak cryptographic algorithms - byte[] hash = HMACSHA1.HashData(sharedSecret, timeArray); -#pragma warning restore CA5350 // This is actually a fair warning, but there is nothing we can do about Steam using weak cryptographic algorithms - - // The last 4 bits of the mac say where the code starts - int start = hash[^1] & 0x0f; - - // Extract those 4 bytes - byte[] bytes = new byte[4]; - - Array.Copy(hash, start, bytes, 0, 4); - - if (BitConverter.IsLittleEndian) { - Array.Reverse(bytes); - } - - // Build the alphanumeric code - uint fullCode = BitConverter.ToUInt32(bytes, 0) & 0x7fffffff; - - // ReSharper disable once BuiltInTypeReferenceStyleForMemberAccess - required for .NET Framework - return String.Create( - CodeDigits, fullCode, static (buffer, state) => { - for (byte i = 0; i < CodeDigits; i++) { - buffer[i] = CodeCharacters[(byte) (state % CodeCharacters.Count)]; - state /= (byte) CodeCharacters.Count; - } - } - ); - } - - private async Task GetSteamTime() { - if (Bot == null) { - throw new InvalidOperationException(nameof(Bot)); - } - - int? steamTimeDifference = SteamTimeDifference; - - if (steamTimeDifference.HasValue && (DateTime.UtcNow.Subtract(LastSteamTimeCheck).TotalMinutes < SteamTimeTTL)) { - return Utilities.MathAdd(Utilities.GetUnixTime(), steamTimeDifference.Value); - } - - await TimeSemaphore.WaitAsync().ConfigureAwait(false); - - try { - steamTimeDifference = SteamTimeDifference; - - if (steamTimeDifference.HasValue && (DateTime.UtcNow.Subtract(LastSteamTimeCheck).TotalMinutes < SteamTimeTTL)) { - return Utilities.MathAdd(Utilities.GetUnixTime(), steamTimeDifference.Value); - } - - ulong serverTime = await Bot.ArchiWebHandler.GetServerTime().ConfigureAwait(false); - - if (serverTime == 0) { - return Utilities.GetUnixTime(); - } - - // We assume that the difference between times will be within int range, therefore we accept underflow here (for subtraction), and since we cast that result to int afterwards, we also accept overflow for the cast itself - steamTimeDifference = unchecked((int) (serverTime - Utilities.GetUnixTime())); - - SteamTimeDifference = steamTimeDifference; - LastSteamTimeCheck = DateTime.UtcNow; - } finally { - TimeSemaphore.Release(); - } - - return Utilities.MathAdd(Utilities.GetUnixTime(), steamTimeDifference.Value); - } - private static async Task LimitConfirmationsRequestsAsync() { if (ASF.ConfirmationsSemaphore == null) { throw new InvalidOperationException(nameof(ASF.ConfirmationsSemaphore)); diff --git a/Dockerfile b/Dockerfile index 9869e5fb4..1e8038b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,11 +18,12 @@ ARG TARGETOS ENV DOTNET_CLI_TELEMETRY_OPTOUT true ENV DOTNET_NOLOGO true ENV NET_CORE_VERSION net7.0 -ENV PLUGINS ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.SteamTokenDumper +ENV PLUGINS ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper WORKDIR /app COPY --from=build-node /app/ASF-ui/dist ASF-ui/dist COPY ArchiSteamFarm ArchiSteamFarm COPY ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.ItemsMatcher +COPY ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.MobileAuthenticator COPY ArchiSteamFarm.OfficialPlugins.SteamTokenDumper ArchiSteamFarm.OfficialPlugins.SteamTokenDumper COPY resources resources COPY .editorconfig .editorconfig diff --git a/Dockerfile.Service b/Dockerfile.Service index 9434885c7..c8b063f3b 100644 --- a/Dockerfile.Service +++ b/Dockerfile.Service @@ -18,11 +18,12 @@ ARG TARGETOS ENV DOTNET_CLI_TELEMETRY_OPTOUT true ENV DOTNET_NOLOGO true ENV NET_CORE_VERSION net7.0 -ENV PLUGINS ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.SteamTokenDumper +ENV PLUGINS ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.SteamTokenDumper WORKDIR /app COPY --from=build-node /app/ASF-ui/dist ASF-ui/dist COPY ArchiSteamFarm ArchiSteamFarm COPY ArchiSteamFarm.OfficialPlugins.ItemsMatcher ArchiSteamFarm.OfficialPlugins.ItemsMatcher +COPY ArchiSteamFarm.OfficialPlugins.MobileAuthenticator ArchiSteamFarm.OfficialPlugins.MobileAuthenticator COPY ArchiSteamFarm.OfficialPlugins.SteamTokenDumper ArchiSteamFarm.OfficialPlugins.SteamTokenDumper COPY resources resources COPY .editorconfig .editorconfig