diff --git a/ArchiSteamFarm.sln.DotSettings b/ArchiSteamFarm.sln.DotSettings index f28f7b744..de75fed61 100644 --- a/ArchiSteamFarm.sln.DotSettings +++ b/ArchiSteamFarm.sln.DotSettings @@ -3,6 +3,7 @@ ASF FA FS + HTML ID OK PIN diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index ded832197..9d1aa8e7c 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -109,6 +109,7 @@ + diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 747d99ab5..f0b5313ff 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -27,6 +27,7 @@ using HtmlAgilityPack; using SteamKit2; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Text; @@ -47,6 +48,7 @@ namespace ArchiSteamFarm { private readonly SemaphoreSlim SessionSemaphore = new SemaphoreSlim(1); private readonly WebBrowser WebBrowser; + private ulong SteamID; private DateTime LastSessionRefreshCheck = DateTime.MinValue; internal static void Init() { @@ -122,12 +124,9 @@ namespace ArchiSteamFarm { return false; } - ulong steamID = steamClient.SteamID; - if (steamID == 0) { - return false; - } + SteamID = steamClient.SteamID; - string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString())); + string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(SteamID.ToString())); // Generate an AES session key byte[] sessionKey = CryptoHelper.GenerateRandomBlock(32); @@ -154,7 +153,7 @@ namespace ArchiSteamFarm { try { authResult = iSteamUserAuth.AuthenticateUser( - steamid: steamID, + steamid: SteamID, sessionkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedSessionKey, 0, cryptedSessionKey.Length)), encrypted_loginkey: Encoding.ASCII.GetString(WebUtility.UrlEncodeToBytes(cryptedLoginKey, 0, cryptedLoginKey.Length)), method: WebRequestMethods.Http.Post, @@ -173,6 +172,7 @@ namespace ArchiSteamFarm { Logging.LogGenericInfo("Success!", Bot.BotName); + WebBrowser.CookieContainer.Add(new Cookie("steamid", SteamID.ToString(), "/", "." + SteamCommunityHost)); // TODO: Check if needed WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", "." + SteamCommunityHost)); string steamLogin = authResult["token"].Value; @@ -238,6 +238,80 @@ namespace ArchiSteamFarm { return await WebBrowser.UrlPostRetry(request, data).ConfigureAwait(false); } + internal async Task GetConfirmations(string deviceID, string confirmationHash, uint time) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0)) { + Logging.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time)); + return null; + } + + if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { + return null; + } + + string request = SteamCommunityURL + "/mobileconf/conf?p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf"; + return await WebBrowser.UrlGetToHtmlDocumentRetry(request).ConfigureAwait(false); + } + + internal async Task GetConfirmationDetails(string deviceID, string confirmationHash, uint time, uint confirmationID) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmationID == 0)) { + Logging.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmationID), Bot.BotName); + return null; + } + + string request = SteamCommunityURL + "/mobileconf/details/" + confirmationID + "?p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf"; + + string json = await WebBrowser.UrlGetToContentRetry(request).ConfigureAwait(false); + if (string.IsNullOrEmpty(json)) { + return null; + } + + Steam.ConfirmationDetails response; + + try { + response = JsonConvert.DeserializeObject(json); + } catch (JsonException e) { + Logging.LogGenericException(e, Bot.BotName); + return null; + } + + if (response != null) { + return response; + } + + Logging.LogNullError(nameof(response), Bot.BotName); + return null; + } + + internal async Task HandleConfirmation(string deviceID, string confirmationHash, uint time, uint confirmationID, ulong confirmationKey, bool accept) { + if (string.IsNullOrEmpty(deviceID) || string.IsNullOrEmpty(confirmationHash) || (time == 0) || (confirmationID == 0) || (confirmationKey == 0)) { + Logging.LogNullError(nameof(deviceID) + " || " + nameof(confirmationHash) + " || " + nameof(time) + " || " + nameof(confirmationID) + " || " + nameof(confirmationKey), Bot.BotName); + return false; + } + + string request = SteamCommunityURL + "/mobileconf/ajaxop?op=" + (accept ? "allow" : "cancel") + "&p=" + deviceID + "&a=" + SteamID + "&k=" + WebUtility.UrlEncode(confirmationHash) + "&t=" + time + "&m=android&tag=conf&cid=" + confirmationID + "&ck=" + confirmationKey; + + string json = await WebBrowser.UrlGetToContentRetry(request).ConfigureAwait(false); + if (string.IsNullOrEmpty(json)) { + return false; + } + + Steam.ConfirmationResponse response; + + try { + response = JsonConvert.DeserializeObject(json); + } catch (JsonException e) { + Logging.LogGenericException(e, Bot.BotName); + return false; + } + + if (response != null) { + return response.Success; + } + + Logging.LogNullError(nameof(response), Bot.BotName); + return false; + } + internal async Task> GetOwnedGames() { if (!await RefreshSessionIfNeeded().ConfigureAwait(false)) { return null; @@ -325,6 +399,31 @@ namespace ArchiSteamFarm { return result; } + internal uint GetServerTime() { + KeyValue response = null; + using (dynamic iTwoFactorService = WebAPI.GetInterface("ITwoFactorService", Bot.BotConfig.SteamApiKey)) { + iTwoFactorService.Timeout = Timeout; + + for (byte i = 0; (i < WebBrowser.MaxRetries) && (response == null); i++) { + try { + response = iTwoFactorService.QueryTime( + method: WebRequestMethods.Http.Post, + secure: !Program.GlobalConfig.ForceHttp + ); + } catch (Exception e) { + Logging.LogGenericException(e, Bot.BotName); + } + } + } + + if (response != null) { + return (uint) response["server_time"].AsUnsignedLong(); + } + + Logging.LogGenericWTF("Request failed even after " + WebBrowser.MaxRetries + " tries", Bot.BotName); + return 0; + } + internal async Task GetTradeHoldDuration(ulong tradeID) { if (tradeID == 0) { Logging.LogNullError(nameof(tradeID), Bot.BotName); diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 8057c133e..cd91e1ade 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -23,7 +23,6 @@ */ using Newtonsoft.Json; -using SteamAuth; using SteamKit2; using SteamKit2.Internal; using System; @@ -150,7 +149,17 @@ namespace ArchiSteamFarm { return; } - if (BotDatabase.SteamGuardAccount == null) { + // TODO: Converter code will be removed soon + if (BotDatabase.SteamGuardAccount != null) { + Logging.LogGenericWarning("Converting old ASF 2FA V2.0 format into new ASF 2FA V2.1 format..."); + BotDatabase.MobileAuthenticator = MobileAuthenticator.LoadFromSteamGuardAccount(BotDatabase.SteamGuardAccount); + BotDatabase.SteamGuardAccount = null; + Logging.LogGenericInfo("Done!"); + } + + if (BotDatabase.MobileAuthenticator != null) { + BotDatabase.MobileAuthenticator.Init(this); + } else { // Support and convert SDA files string maFilePath = botPath + ".maFile"; if (File.Exists(maFilePath)) { @@ -231,57 +240,17 @@ namespace ArchiSteamFarm { Start().Forget(); } - internal async Task AcceptConfirmations(bool confirm, Confirmation.ConfirmationType allowedConfirmationType = Confirmation.ConfirmationType.Unknown) { - if (BotDatabase.SteamGuardAccount == null) { - return true; + internal async Task AcceptConfirmations(bool accept) { + if (BotDatabase.MobileAuthenticator == null) { + return; } - bool result = false; - for (byte i = 0; (i < WebBrowser.MaxRetries) && !result; i++) { - result = true; - - try { - if (!await BotDatabase.SteamGuardAccount.RefreshSessionAsync().ConfigureAwait(false)) { - result = false; - continue; - } - - Confirmation[] confirmations = await BotDatabase.SteamGuardAccount.FetchConfirmationsAsync().ConfigureAwait(false); - if (confirmations == null) { - return true; - } - - foreach (Confirmation confirmation in confirmations.Where(confirmation => (allowedConfirmationType == Confirmation.ConfirmationType.Unknown) || (confirmation.ConfType == allowedConfirmationType))) { - if (confirm) { - if (BotDatabase.SteamGuardAccount.AcceptConfirmation(confirmation)) { - continue; - } - - result = false; - break; - } - - if (BotDatabase.SteamGuardAccount.DenyConfirmation(confirmation)) { - continue; - } - - result = false; - break; - } - } catch (SteamGuardAccount.WGTokenInvalidException) { - result = false; - } catch (Exception e) { - Logging.LogGenericException(e, BotName); - return false; - } + HashSet confirmations = await BotDatabase.MobileAuthenticator.GetConfirmations().ConfigureAwait(false); + if (confirmations == null) { + return; } - if (result) { - return true; - } - - Logging.LogGenericWTF("Could not accept confirmations even after " + WebBrowser.MaxRetries + " tries", BotName); - return false; + await confirmations.ForEachAsync(async confirmation => await BotDatabase.MobileAuthenticator.HandleConfirmation(confirmation, accept).ConfigureAwait(false)).ConfigureAwait(false); } internal async Task RefreshSession() { @@ -344,11 +313,9 @@ namespace ArchiSteamFarm { if (message.IndexOf(' ') < 0) { switch (message) { case "!2fa": - return Response2FA(steamID); + return await Response2FA(steamID).ConfigureAwait(false); case "!2fano": return await Response2FAConfirm(steamID, false).ConfigureAwait(false); - case "!2faoff": - return Response2FAOff(steamID); case "!2faok": return await Response2FAConfirm(steamID, true).ConfigureAwait(false); case "!exit": @@ -383,11 +350,9 @@ namespace ArchiSteamFarm { string[] args = message.Split((char[]) null, StringSplitOptions.RemoveEmptyEntries); switch (args[0]) { case "!2fa": - return Response2FA(steamID, args[1]); + return await Response2FA(steamID, args[1]).ConfigureAwait(false); case "!2fano": return await Response2FAConfirm(steamID, args[1], false).ConfigureAwait(false); - case "!2faoff": - return Response2FAOff(steamID, args[1]); case "!2faok": return await Response2FAConfirm(steamID, args[1], true).ConfigureAwait(false); case "!addlicense": @@ -438,7 +403,7 @@ namespace ArchiSteamFarm { } // 2FA tokens are expiring soon, don't use limiter when user is providing one - if ((TwoFactorCode == null) || (BotDatabase.SteamGuardAccount != null)) { + if ((TwoFactorCode == null) || (BotDatabase.MobileAuthenticator != null)) { await LimitLoginRequestsAsync().ConfigureAwait(false); } @@ -467,68 +432,38 @@ namespace ArchiSteamFarm { } private void ImportAuthenticator(string maFilePath) { - if ((BotDatabase.SteamGuardAccount != null) || !File.Exists(maFilePath)) { + if ((BotDatabase.MobileAuthenticator != null) || !File.Exists(maFilePath)) { return; } - Logging.LogGenericInfo("Converting SDA .maFile into ASF format...", BotName); + Logging.LogGenericInfo("Converting .maFile into ASF format...", BotName); try { - BotDatabase.SteamGuardAccount = JsonConvert.DeserializeObject(File.ReadAllText(maFilePath)); + BotDatabase.MobileAuthenticator = JsonConvert.DeserializeObject(File.ReadAllText(maFilePath)); File.Delete(maFilePath); - Logging.LogGenericInfo("Success!", BotName); } catch (Exception e) { Logging.LogGenericException(e, BotName); return; } - // If this is SDA file, then we should already have everything ready - if (BotDatabase.SteamGuardAccount.Session != null) { - Logging.LogGenericInfo("Successfully finished importing mobile authenticator!", BotName); + if (BotDatabase.MobileAuthenticator == null) { + Logging.LogNullError(nameof(BotDatabase.MobileAuthenticator)); return; } - // But here we're dealing with WinAuth authenticator - Logging.LogGenericInfo("ASF requires a few more steps to complete authenticator import...", BotName); + BotDatabase.MobileAuthenticator.Init(this); - if (!InitializeLoginAndPassword(true)) { - BotDatabase.SteamGuardAccount = null; - return; - } - - UserLogin userLogin = new UserLogin(BotConfig.SteamLogin, BotConfig.SteamPassword); - LoginResult loginResult; - while ((loginResult = userLogin.DoLogin()) != LoginResult.LoginOkay) { - switch (loginResult) { - case LoginResult.Need2FA: - userLogin.TwoFactorCode = Program.GetUserInput(Program.EUserInputType.TwoFactorAuthentication, BotName); - if (string.IsNullOrEmpty(userLogin.TwoFactorCode)) { - BotDatabase.SteamGuardAccount = null; - return; - } - - break; - default: - BotDatabase.SteamGuardAccount = null; - Logging.LogGenericError("Unhandled situation: " + loginResult, BotName); - return; + if (!BotDatabase.MobileAuthenticator.HasDeviceID) { + string deviceID = Program.GetUserInput(Program.EUserInputType.DeviceID, BotName); + if (string.IsNullOrEmpty(deviceID)) { + BotDatabase.MobileAuthenticator = null; + return; } + + BotDatabase.MobileAuthenticator.CorrectDeviceID(deviceID); + BotDatabase.Save(); } - if (userLogin.Session == null) { - BotDatabase.SteamGuardAccount = null; - Logging.LogGenericError("Session is invalid, linking can't be completed!", BotName); - return; - } - - BotDatabase.SteamGuardAccount.FullyEnrolled = true; - BotDatabase.SteamGuardAccount.Session = userLogin.Session; - - if (string.IsNullOrEmpty(BotDatabase.SteamGuardAccount.DeviceID)) { - BotDatabase.SteamGuardAccount.DeviceID = Program.GetUserInput(Program.EUserInputType.DeviceID, BotName); - } - - BotDatabase.Save(); Logging.LogGenericInfo("Successfully finished importing mobile authenticator!", BotName); } @@ -677,7 +612,7 @@ namespace ArchiSteamFarm { return "Trade offer failed due to error!"; } - await AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false); + await AcceptConfirmations(true).ConfigureAwait(false); return "Trade offer sent successfully!"; } @@ -699,7 +634,7 @@ namespace ArchiSteamFarm { return null; } - private string Response2FA(ulong steamID) { + private async Task Response2FA(ulong steamID) { if (steamID == 0) { Logging.LogNullError(nameof(steamID)); return null; @@ -709,15 +644,15 @@ namespace ArchiSteamFarm { return null; } - if (BotDatabase.SteamGuardAccount == null) { + if (BotDatabase.MobileAuthenticator == null) { return "That bot doesn't have ASF 2FA enabled!"; } - long timeLeft = 30 - TimeAligner.GetSteamTime() % 30; - return "2FA Token: " + BotDatabase.SteamGuardAccount.GenerateSteamGuardCode() + " (expires in " + timeLeft + " seconds)"; + byte timeLeft = (byte) (30 - await BotDatabase.MobileAuthenticator.GetSteamTime().ConfigureAwait(false) % 30); + return "2FA Token: " + await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false) + " (expires in " + timeLeft + " seconds)"; } - private static string Response2FA(ulong steamID, string botName) { + private static async Task Response2FA(ulong steamID, string botName) { if ((steamID == 0) || string.IsNullOrEmpty(botName)) { Logging.LogNullError(nameof(steamID) + " || " + nameof(botName)); return null; @@ -725,42 +660,7 @@ namespace ArchiSteamFarm { Bot bot; if (Bots.TryGetValue(botName, out bot)) { - return bot.Response2FA(steamID); - } - - if (IsOwner(steamID)) { - return "Couldn't find any bot named " + botName + "!"; - } - - return null; - } - - private string Response2FAOff(ulong steamID) { - if (steamID == 0) { - Logging.LogNullError(nameof(steamID)); - return null; - } - - if (!IsMaster(steamID)) { - return null; - } - - if (BotDatabase.SteamGuardAccount == null) { - return "That bot doesn't have ASF 2FA enabled!"; - } - - return DelinkMobileAuthenticator() ? "Done! Bot is no longer using ASF 2FA" : "Something went wrong during delinking mobile authenticator!"; - } - - private static string Response2FAOff(ulong steamID, string botName) { - if ((steamID == 0) || string.IsNullOrEmpty(botName)) { - Logging.LogNullError(nameof(steamID) + " || " + nameof(botName)); - return null; - } - - Bot bot; - if (Bots.TryGetValue(botName, out bot)) { - return bot.Response2FAOff(steamID); + return await bot.Response2FA(steamID).ConfigureAwait(false); } if (IsOwner(steamID)) { @@ -780,7 +680,7 @@ namespace ArchiSteamFarm { return null; } - if (BotDatabase.SteamGuardAccount == null) { + if (BotDatabase.MobileAuthenticator == null) { return "That bot doesn't have ASF 2FA enabled!"; } @@ -1393,101 +1293,6 @@ namespace ArchiSteamFarm { } } - private void LinkMobileAuthenticator() { - if (BotDatabase.SteamGuardAccount != null) { - return; - } - - Logging.LogGenericInfo("Linking new ASF MobileAuthenticator...", BotName); - - if (!InitializeLoginAndPassword(true)) { - return; - } - - UserLogin userLogin = new UserLogin(BotConfig.SteamLogin, BotConfig.SteamPassword); - LoginResult loginResult; - while ((loginResult = userLogin.DoLogin()) != LoginResult.LoginOkay) { - switch (loginResult) { - case LoginResult.NeedEmail: - userLogin.EmailCode = Program.GetUserInput(Program.EUserInputType.SteamGuard, BotName); - if (string.IsNullOrEmpty(userLogin.EmailCode)) { - return; - } - - break; - default: - Logging.LogGenericError("Unhandled situation: " + loginResult, BotName); - return; - } - } - - AuthenticatorLinker authenticatorLinker = new AuthenticatorLinker(userLogin.Session); - - AuthenticatorLinker.LinkResult linkResult; - while ((linkResult = authenticatorLinker.AddAuthenticator()) != AuthenticatorLinker.LinkResult.AwaitingFinalization) { - switch (linkResult) { - case AuthenticatorLinker.LinkResult.MustProvidePhoneNumber: - authenticatorLinker.PhoneNumber = Program.GetUserInput(Program.EUserInputType.PhoneNumber, BotName); - if (string.IsNullOrEmpty(authenticatorLinker.PhoneNumber)) { - return; - } - - break; - default: - Logging.LogGenericError("Unhandled situation: " + linkResult, BotName); - return; - } - } - - BotDatabase.SteamGuardAccount = authenticatorLinker.LinkedAccount; - - string sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName); - if (string.IsNullOrEmpty(sms)) { - Logging.LogGenericWarning("Aborted!", BotName); - DelinkMobileAuthenticator(); - return; - } - - AuthenticatorLinker.FinalizeResult finalizeResult; - while ((finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(sms)) != AuthenticatorLinker.FinalizeResult.Success) { - switch (finalizeResult) { - case AuthenticatorLinker.FinalizeResult.BadSMSCode: - sms = Program.GetUserInput(Program.EUserInputType.SMS, BotName); - if (string.IsNullOrEmpty(sms)) { - Logging.LogGenericWarning("Aborted!", BotName); - DelinkMobileAuthenticator(); - return; - } - - break; - default: - Logging.LogGenericError("Unhandled situation: " + finalizeResult, BotName); - DelinkMobileAuthenticator(); - return; - } - } - - // Ensure that we also save changes made by finalization step (if any) - BotDatabase.Save(); - - Logging.LogGenericInfo("Successfully linked ASF as new mobile authenticator for this account!", BotName); - Program.GetUserInput(Program.EUserInputType.RevocationCode, BotName, BotDatabase.SteamGuardAccount.RevocationCode); - } - - private bool DelinkMobileAuthenticator() { - if (BotDatabase.SteamGuardAccount == null) { - return false; - } - - // Try to deactivate authenticator, and assume we're safe to remove if it wasn't fully enrolled yet (even if request fails) - if (!BotDatabase.SteamGuardAccount.DeactivateAuthenticator() && BotDatabase.SteamGuardAccount.FullyEnrolled) { - return false; - } - - BotDatabase.SteamGuardAccount = null; - return true; - } - private void JoinMasterChat() { if (!SteamClient.IsConnected || (BotConfig.SteamMasterClanID == 0)) { return; @@ -1524,7 +1329,7 @@ namespace ArchiSteamFarm { } } - private void OnConnected(SteamClient.ConnectedCallback callback) { + private async void OnConnected(SteamClient.ConnectedCallback callback) { if (callback == null) { Logging.LogNullError(nameof(callback), BotName); return; @@ -1561,8 +1366,8 @@ namespace ArchiSteamFarm { Logging.LogGenericInfo("Logging in...", BotName); // If we have ASF 2FA enabled, we can always provide TwoFactorCode, and save a request - if (BotDatabase.SteamGuardAccount != null) { - TwoFactorCode = BotDatabase.SteamGuardAccount.GenerateSteamGuardCode(); + if (BotDatabase.MobileAuthenticator != null) { + TwoFactorCode = await BotDatabase.MobileAuthenticator.GenerateToken().ConfigureAwait(false); } if (Program.GlobalConfig.HackIgnoreMachineID) { @@ -1642,7 +1447,7 @@ namespace ArchiSteamFarm { Logging.LogGenericInfo("Reconnecting...", BotName); // 2FA tokens are expiring soon, don't use limiter when user is providing one - if ((TwoFactorCode == null) || (BotDatabase.SteamGuardAccount != null)) { + if ((TwoFactorCode == null) || (BotDatabase.MobileAuthenticator != null)) { await LimitLoginRequestsAsync().ConfigureAwait(false); } @@ -1818,7 +1623,7 @@ namespace ArchiSteamFarm { break; case EResult.AccountLoginDeniedNeedTwoFactor: - if (BotDatabase.SteamGuardAccount == null) { + if (BotDatabase.MobileAuthenticator == null) { TwoFactorCode = Program.GetUserInput(Program.EUserInputType.TwoFactorAuthentication, BotName); if (string.IsNullOrEmpty(TwoFactorCode)) { Stop(); @@ -1841,13 +1646,11 @@ namespace ArchiSteamFarm { Program.GlobalDatabase.CellID = callback.CellID; } - if (BotDatabase.SteamGuardAccount == null) { + if (BotDatabase.MobileAuthenticator == null) { // Support and convert SDA files string maFilePath = Path.Combine(Program.ConfigDirectory, callback.ClientSteamID.ConvertToUInt64() + ".maFile"); if (File.Exists(maFilePath)) { ImportAuthenticator(maFilePath); - } else if ((TwoFactorCode == null) && BotConfig.UseAsfAsMobileAuthenticator) { - LinkMobileAuthenticator(); } } diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index acf4cc7cc..417eee9cb 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -83,9 +83,6 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] internal bool DistributeKeys { get; private set; } = false; - [JsonProperty(Required = Required.DisallowNull)] - internal bool UseAsfAsMobileAuthenticator { get; private set; } = false; - [JsonProperty(Required = Required.DisallowNull)] internal bool ShutdownOnFarmingFinished { get; private set; } = false; diff --git a/ArchiSteamFarm/BotDatabase.cs b/ArchiSteamFarm/BotDatabase.cs index e86986208..2f7a79ed3 100644 --- a/ArchiSteamFarm/BotDatabase.cs +++ b/ArchiSteamFarm/BotDatabase.cs @@ -30,6 +30,9 @@ using System.IO; namespace ArchiSteamFarm { internal sealed class BotDatabase { + [JsonProperty] + private string _LoginKey; + internal string LoginKey { get { return _LoginKey; @@ -44,6 +47,27 @@ namespace ArchiSteamFarm { } } + [JsonProperty] + private MobileAuthenticator _MobileAuthenticator; + + internal MobileAuthenticator MobileAuthenticator { + get { + return _MobileAuthenticator; + } + set { + if (_MobileAuthenticator == value) { + return; + } + + _MobileAuthenticator = value; + Save(); + } + } + + // TODO: Converter code will be removed soon + [JsonProperty] + private SteamGuardAccount _SteamGuardAccount; + internal SteamGuardAccount SteamGuardAccount { get { return _SteamGuardAccount; @@ -58,12 +82,6 @@ namespace ArchiSteamFarm { } } - [JsonProperty] - private string _LoginKey; - - [JsonProperty] - private SteamGuardAccount _SteamGuardAccount; - private string FilePath; internal static BotDatabase Load(string filePath) { diff --git a/ArchiSteamFarm/JSON/Steam.cs b/ArchiSteamFarm/JSON/Steam.cs index b25f7032e..a8b6fd4cf 100644 --- a/ArchiSteamFarm/JSON/Steam.cs +++ b/ArchiSteamFarm/JSON/Steam.cs @@ -25,6 +25,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net; +using HtmlAgilityPack; using Newtonsoft.Json; using SteamKit2; @@ -328,5 +330,90 @@ namespace ArchiSteamFarm.JSON { [JsonProperty(PropertyName = "them", Required = Required.Always)] internal ItemList ItemsToReceive { get; } = new ItemList(); } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + internal sealed class ConfirmationResponse { + [JsonProperty(PropertyName = "success", Required = Required.Always)] + internal bool Success { get; private set; } + } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + internal sealed class ConfirmationDetails { + [JsonProperty(PropertyName = "success", Required = Required.Always)] + internal bool Success { get; private set; } + + private ulong _OtherSteamID64; + internal ulong OtherSteamID64 { + get { + if (_OtherSteamID64 != 0) { + return _OtherSteamID64; + } + + if (OtherSteamID3 == 0) { + Logging.LogNullError(nameof(OtherSteamID3)); + return 0; + } + + _OtherSteamID64 = new SteamID(OtherSteamID3, EUniverse.Public, EAccountType.Individual); + return _OtherSteamID64; + } + } + + [JsonProperty(PropertyName = "html", Required = Required.Always)] + private string HTML; + + private uint _OtherSteamID3; + private uint OtherSteamID3 { + get { + if (_OtherSteamID3 != 0) { + return _OtherSteamID3; + } + + if (HtmlDocument == null) { + Logging.LogNullError(nameof(HtmlDocument)); + return 0; + } + + HtmlNode htmlNode = HtmlDocument.DocumentNode.SelectSingleNode("//a/@data-miniprofile"); + if (htmlNode == null) { + Logging.LogNullError(nameof(htmlNode)); + return 0; + } + + string miniProfile = htmlNode.GetAttributeValue("data-miniprofile", null); + if (string.IsNullOrEmpty(miniProfile)) { + Logging.LogNullError(nameof(miniProfile)); + return 0; + } + + if (uint.TryParse(miniProfile, out _OtherSteamID3) && (_OtherSteamID3 != 0)) { + return _OtherSteamID3; + } + + Logging.LogNullError(nameof(_OtherSteamID3)); + return 0; + } + } + + private HtmlDocument _HtmlDocument; + private HtmlDocument HtmlDocument { + get { + if (_HtmlDocument != null) { + return _HtmlDocument; + } + + if (string.IsNullOrEmpty(HTML)) { + Logging.LogNullError(nameof(HTML)); + return null; + } + + _HtmlDocument = new HtmlDocument(); + _HtmlDocument.LoadHtml(WebUtility.HtmlDecode(HTML)); + return _HtmlDocument; + } + } + } } } diff --git a/ArchiSteamFarm/MobileAuthenticator.cs b/ArchiSteamFarm/MobileAuthenticator.cs new file mode 100644 index 000000000..053776a27 --- /dev/null +++ b/ArchiSteamFarm/MobileAuthenticator.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ArchiSteamFarm.JSON; +using HtmlAgilityPack; +using Newtonsoft.Json; +using SteamAuth; + +namespace ArchiSteamFarm { + internal sealed class MobileAuthenticator { + internal sealed class Confirmation { + internal readonly uint ID; + internal readonly ulong Key; + + internal Confirmation(uint id, ulong key) { + if ((id == 0) || (key == 0)) { + throw new ArgumentNullException(nameof(id) + " || " + nameof(key)); + } + + ID = id; + Key = key; + } + } + + private const byte TokenDigits = 5; + + private static readonly byte[] TokenCharacters = { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 }; + private static readonly SemaphoreSlim TimeSemaphore = new SemaphoreSlim(1); + + private static short SteamTimeDifference; + + internal bool HasDeviceID => !string.IsNullOrEmpty(DeviceID); + + [JsonProperty(PropertyName = "shared_secret", Required = Required.DisallowNull)] + private string SharedSecret; + + [JsonProperty(PropertyName = "revocation_code", Required = Required.DisallowNull)] + private string RevocationCode; + + [JsonProperty(PropertyName = "identity_secret", Required = Required.DisallowNull)] + private string IdentitySecret; + + [JsonProperty(PropertyName = "device_id")] + private string DeviceID; + + private Bot Bot; + + internal static MobileAuthenticator LoadFromSteamGuardAccount(SteamGuardAccount sga) { + if (sga != null) { + return new MobileAuthenticator { + SharedSecret = sga.SharedSecret, + RevocationCode = sga.RevocationCode, + IdentitySecret = sga.IdentitySecret, + DeviceID = sga.DeviceID + }; + } + + Logging.LogNullError(nameof(sga)); + return null; + } + + private MobileAuthenticator() { + + } + + internal void Init(Bot bot) { + if (bot == null) { + throw new ArgumentNullException(nameof(bot)); + } + + Bot = bot; + } + + internal void CorrectDeviceID(string deviceID) { + if (string.IsNullOrEmpty(deviceID)) { + Logging.LogNullError(nameof(deviceID), Bot.BotName); + return; + } + + DeviceID = deviceID; + } + + internal async Task HandleConfirmation(Confirmation confirmation, bool accept) { + if (confirmation == null) { + Logging.LogNullError(nameof(confirmation), Bot.BotName); + return false; + } + + uint time = await GetSteamTime().ConfigureAwait(false); + if (time == 0) { + Logging.LogNullError(nameof(time), Bot.BotName); + return false; + } + + string confirmationHash = GenerateConfirmationKey(time, "conf"); + if (!string.IsNullOrEmpty(confirmationHash)) { + return await Bot.ArchiWebHandler.HandleConfirmation(DeviceID, confirmationHash, time, confirmation.ID, confirmation.Key, accept); + } + + Logging.LogNullError(nameof(confirmationHash), Bot.BotName); + return false; + } + + internal async Task GetConfirmationDetails(Confirmation confirmation) { + if (confirmation == null) { + Logging.LogNullError(nameof(confirmation), Bot.BotName); + return null; + } + + uint time = await GetSteamTime().ConfigureAwait(false); + if (time == 0) { + Logging.LogNullError(nameof(time), Bot.BotName); + return null; + } + + string confirmationHash = GenerateConfirmationKey(time, "conf"); + if (!string.IsNullOrEmpty(confirmationHash)) { + return await Bot.ArchiWebHandler.GetConfirmationDetails(DeviceID, confirmationHash, time, confirmation.ID); + } + + Logging.LogNullError(nameof(confirmationHash), Bot.BotName); + return null; + } + + internal async Task GenerateToken() { + uint time = await GetSteamTime().ConfigureAwait(false); + if (time != 0) { + return GenerateTokenForTime(time); + } + + Logging.LogNullError(nameof(time), Bot.BotName); + return null; + } + + internal async Task> GetConfirmations() { + uint time = await GetSteamTime().ConfigureAwait(false); + if (time == 0) { + Logging.LogNullError(nameof(time)); + return null; + } + + string confirmationHash = GenerateConfirmationKey(time, "conf"); + if (string.IsNullOrEmpty(confirmationHash)) { + Logging.LogNullError(nameof(confirmationHash), Bot.BotName); + return null; + } + + HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetConfirmations(DeviceID, confirmationHash, time); + if (htmlDocument == null) { + return null; + } + + HtmlNodeCollection confirmations = htmlDocument.DocumentNode.SelectNodes("//div[@class='mobileconf_list_entry']"); + if (confirmations == null) { + return null; + } + + HashSet result = new HashSet(); + foreach (HtmlNode confirmation in confirmations) { + string idString = confirmation.GetAttributeValue("data-confid", null); + if (string.IsNullOrEmpty(idString)) { + Logging.LogNullError(nameof(idString)); + continue; + } + + uint id; + if (!uint.TryParse(idString, out id) || (id == 0)) { + Logging.LogNullError(nameof(id)); + continue; + } + + string keyString = confirmation.GetAttributeValue("data-key", null); + if (string.IsNullOrEmpty(keyString)) { + Logging.LogNullError(nameof(keyString)); + continue; + } + + ulong key; + if (!ulong.TryParse(keyString, out key) || (key == 0)) { + Logging.LogNullError(nameof(key)); + continue; + } + + result.Add(new Confirmation(id, key)); + } + + return result; + } + + internal async Task GetSteamTime() { + if (SteamTimeDifference != 0) { + return (uint) (Utilities.GetUnixTime() + SteamTimeDifference); + } + + await TimeSemaphore.WaitAsync().ConfigureAwait(false); + + if (SteamTimeDifference == 0) { + uint serverTime = Bot.ArchiWebHandler.GetServerTime(); + if (serverTime != 0) { + SteamTimeDifference = (short) (serverTime - Utilities.GetUnixTime()); + } + } + + TimeSemaphore.Release(); + return (uint) (Utilities.GetUnixTime() + SteamTimeDifference); + } + + private string GenerateTokenForTime(long time) { + if (time == 0) { + Logging.LogNullError(nameof(time), Bot.BotName); + return null; + } + + byte[] sharedSecretArray = Convert.FromBase64String(SharedSecret); + byte[] timeArray = new byte[8]; + + time /= 30L; + + for (int i = 8; i > 0; i--) { + timeArray[i - 1] = (byte) time; + time >>= 8; + } + + byte[] hashedData; + using (HMACSHA1 hmacGenerator = new HMACSHA1(sharedSecretArray, true)) { + hashedData = hmacGenerator.ComputeHash(timeArray); + } + + byte b = (byte) (hashedData[19] & 0xF); + int codePoint = ((hashedData[b] & 0x7F) << 24) | ((hashedData[b + 1] & 0xFF) << 16) | ((hashedData[b + 2] & 0xFF) << 8) | (hashedData[b + 3] & 0xFF); + + byte[] codeArray = new byte[5]; + for (int i = 0; i < 5; ++i) { + codeArray[i] = TokenCharacters[codePoint % TokenCharacters.Length]; + codePoint /= TokenCharacters.Length; + } + + return Encoding.UTF8.GetString(codeArray); + } + + private string GenerateConfirmationKey(uint time, string tag = null) { + if (time == 0) { + Logging.LogNullError(nameof(time), Bot.BotName); + return null; + } + + byte[] b64Secret = Convert.FromBase64String(IdentitySecret); + + int bufferSize = 8; + if (string.IsNullOrEmpty(tag) == false) { + bufferSize += Math.Min(32, tag.Length); + } + + byte[] buffer = new byte[bufferSize]; + + byte[] timeArray = BitConverter.GetBytes((long) time); + if (BitConverter.IsLittleEndian) { + Array.Reverse(timeArray); + } + + Array.Copy(timeArray, buffer, 8); + if (string.IsNullOrEmpty(tag) == false) { + Array.Copy(Encoding.UTF8.GetBytes(tag), 0, buffer, 8, bufferSize - 8); + } + + byte[] hash; + using (HMACSHA1 hmac = new HMACSHA1(b64Secret, true)) { + hash = hmac.ComputeHash(buffer); + } + + return Convert.ToBase64String(hash, Base64FormattingOptions.None); + } + } +} diff --git a/ArchiSteamFarm/Properties/AssemblyInfo.cs b/ArchiSteamFarm/Properties/AssemblyInfo.cs index 82ed1580b..c63947b2b 100644 --- a/ArchiSteamFarm/Properties/AssemblyInfo.cs +++ b/ArchiSteamFarm/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.6.0")] -[assembly: AssemblyFileVersion("2.0.6.0")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 6274b0276..dfc84d455 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -95,7 +95,7 @@ namespace ArchiSteamFarm { } await tradeOffers.ForEachAsync(ParseTrade).ConfigureAwait(false); - await Bot.AcceptConfirmations(true, Confirmation.ConfirmationType.Trade).ConfigureAwait(false); + await Bot.AcceptConfirmations(true).ConfigureAwait(false); } private async Task ParseTrade(Steam.TradeOffer tradeOffer) { diff --git a/ArchiSteamFarm/Utilities.cs b/ArchiSteamFarm/Utilities.cs index bd829d0d3..7e3ee8480 100644 --- a/ArchiSteamFarm/Utilities.cs +++ b/ArchiSteamFarm/Utilities.cs @@ -62,6 +62,8 @@ namespace ArchiSteamFarm { return cookies.Count == 0 ? null : (from Cookie cookie in cookies where cookie.Name.Equals(name) select cookie.Value).FirstOrDefault(); } + internal static uint GetUnixTime() => (uint) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + internal static Task SleepAsync(int miliseconds) { if (miliseconds >= 0) { return Task.Delay(miliseconds); diff --git a/ArchiSteamFarm/config/example.json b/ArchiSteamFarm/config/example.json index c027773bd..d27bb6519 100644 --- a/ArchiSteamFarm/config/example.json +++ b/ArchiSteamFarm/config/example.json @@ -16,7 +16,6 @@ "SteamTradeMatcher": false, "ForwardKeysToOtherBots": false, "DistributeKeys": false, - "UseAsfAsMobileAuthenticator": false, "ShutdownOnFarmingFinished": false, "SendOnFarmingFinished": false, "SteamTradeToken": null, diff --git a/ConfigGenerator/BotConfig.cs b/ConfigGenerator/BotConfig.cs index 66acf1db3..2bcef6837 100644 --- a/ConfigGenerator/BotConfig.cs +++ b/ConfigGenerator/BotConfig.cs @@ -87,9 +87,6 @@ namespace ConfigGenerator { [JsonProperty(Required = Required.DisallowNull)] public bool DistributeKeys { get; set; } = false; - [JsonProperty(Required = Required.DisallowNull)] - public bool UseAsfAsMobileAuthenticator { get; set; } = false; - [JsonProperty(Required = Required.DisallowNull)] public bool ShutdownOnFarmingFinished { get; set; } = false; diff --git a/SteamAuth/APIEndpoints.cs b/SteamAuth/APIEndpoints.cs deleted file mode 100644 index b64ab0b0d..000000000 --- a/SteamAuth/APIEndpoints.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SteamAuth -{ - public static class APIEndpoints - { - public const string STEAMAPI_BASE = "https://api.steampowered.com"; - public const string COMMUNITY_BASE = "https://steamcommunity.com"; - public const string MOBILEAUTH_BASE = STEAMAPI_BASE + "/IMobileAuthService/%s/v0001"; - public static string MOBILEAUTH_GETWGTOKEN = MOBILEAUTH_BASE.Replace("%s", "GetWGToken"); - public const string TWO_FACTOR_BASE = STEAMAPI_BASE + "/ITwoFactorService/%s/v0001"; - public static string TWO_FACTOR_TIME_QUERY = TWO_FACTOR_BASE.Replace("%s", "QueryTime"); - } -} diff --git a/SteamAuth/AuthenticatorLinker.cs b/SteamAuth/AuthenticatorLinker.cs deleted file mode 100644 index d5a99c513..000000000 --- a/SteamAuth/AuthenticatorLinker.cs +++ /dev/null @@ -1,292 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; - -namespace SteamAuth -{ - /// - /// Handles the linking process for a new mobile authenticator. - /// - public class AuthenticatorLinker - { - - - /// - /// Set to register a new phone number when linking. If a phone number is not set on the account, this must be set. If a phone number is set on the account, this must be null. - /// - public string PhoneNumber = null; - - /// - /// Randomly-generated device ID. Should only be generated once per linker. - /// - public string DeviceID { get; private set; } - - /// - /// After the initial link step, if successful, this will be the SteamGuard data for the account. PLEASE save this somewhere after generating it; it's vital data. - /// - public SteamGuardAccount LinkedAccount { get; private set; } - - /// - /// True if the authenticator has been fully finalized. - /// - public bool Finalized = false; - - private SessionData _session; - private CookieContainer _cookies; - - public AuthenticatorLinker(SessionData session) - { - this._session = session; - this.DeviceID = GenerateDeviceID(); - - this._cookies = new CookieContainer(); - session.AddCookies(_cookies); - } - - public LinkResult AddAuthenticator() - { - bool hasPhone = _hasPhoneAttached(); - if (hasPhone && PhoneNumber != null) - return LinkResult.MustRemovePhoneNumber; - if (!hasPhone && PhoneNumber == null) - return LinkResult.MustProvidePhoneNumber; - - if (!hasPhone) - { - if (!_addPhoneNumber()) - { - return LinkResult.GeneralFailure; - } - } - - var postData = new NameValueCollection(); - postData.Add("access_token", _session.OAuthToken); - postData.Add("steamid", _session.SteamID.ToString()); - postData.Add("authenticator_type", "1"); - postData.Add("device_identifier", this.DeviceID); - postData.Add("sms_phone_id", "1"); - - string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/AddAuthenticator/v0001", "POST", postData); - if (response == null) return LinkResult.GeneralFailure; - - var addAuthenticatorResponse = JsonConvert.DeserializeObject(response); - if (addAuthenticatorResponse == null || addAuthenticatorResponse.Response == null) - { - return LinkResult.GeneralFailure; - } - - if (addAuthenticatorResponse.Response.Status == 29) - { - return LinkResult.AuthenticatorPresent; - } - - if (addAuthenticatorResponse.Response.Status != 1) - { - return LinkResult.GeneralFailure; - } - - this.LinkedAccount = addAuthenticatorResponse.Response; - LinkedAccount.Session = this._session; - LinkedAccount.DeviceID = this.DeviceID; - - return LinkResult.AwaitingFinalization; - } - - public FinalizeResult FinalizeAddAuthenticator(string smsCode) - { - //The act of checking the SMS code is necessary for Steam to finalize adding the phone number to the account. - //Of course, we only want to check it if we're adding a phone number in the first place... - - if (!String.IsNullOrEmpty(this.PhoneNumber) && !this._checkSMSCode(smsCode)) - { - return FinalizeResult.BadSMSCode; - } - - var postData = new NameValueCollection(); - postData.Add("steamid", _session.SteamID.ToString()); - postData.Add("access_token", _session.OAuthToken); - postData.Add("activation_code", smsCode); - int tries = 0; - while (tries <= 30) - { - postData.Set("authenticator_code", LinkedAccount.GenerateSteamGuardCode()); - postData.Set("authenticator_time", TimeAligner.GetSteamTime().ToString()); - - string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/FinalizeAddAuthenticator/v0001", "POST", postData); - if (response == null) return FinalizeResult.GeneralFailure; - - var finalizeResponse = JsonConvert.DeserializeObject(response); - - if (finalizeResponse == null || finalizeResponse.Response == null) - { - return FinalizeResult.GeneralFailure; - } - - if (finalizeResponse.Response.Status == 89) - { - return FinalizeResult.BadSMSCode; - } - - if (finalizeResponse.Response.Status == 88) - { - if (tries >= 30) - { - return FinalizeResult.UnableToGenerateCorrectCodes; - } - } - - if (!finalizeResponse.Response.Success) - { - return FinalizeResult.GeneralFailure; - } - - if (finalizeResponse.Response.WantMore) - { - tries++; - continue; - } - - this.LinkedAccount.FullyEnrolled = true; - return FinalizeResult.Success; - } - - return FinalizeResult.GeneralFailure; - } - - private bool _checkSMSCode(string smsCode) - { - var postData = new NameValueCollection(); - postData.Add("op", "check_sms_code"); - postData.Add("arg", smsCode); - postData.Add("sessionid", _session.SessionID); - - string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies); - if (response == null) return false; - - var addPhoneNumberResponse = JsonConvert.DeserializeObject(response); - return addPhoneNumberResponse.Success; - } - - private bool _addPhoneNumber() - { - var postData = new NameValueCollection(); - postData.Add("op", "add_phone_number"); - postData.Add("arg", PhoneNumber); - postData.Add("sessionid", _session.SessionID); - - string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies); - if (response == null) return false; - - var addPhoneNumberResponse = JsonConvert.DeserializeObject(response); - return addPhoneNumberResponse.Success; - } - - private bool _hasPhoneAttached() - { - var postData = new NameValueCollection(); - postData.Add("op", "has_phone"); - postData.Add("arg", "null"); - postData.Add("sessionid", _session.SessionID); - - string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies); - if (response == null) return false; - - var hasPhoneResponse = JsonConvert.DeserializeObject(response); - return hasPhoneResponse.HasPhone; - } - - public enum LinkResult - { - MustProvidePhoneNumber, //No phone number on the account - MustRemovePhoneNumber, //A phone number is already on the account - AwaitingFinalization, //Must provide an SMS code - GeneralFailure, //General failure (really now!) - AuthenticatorPresent - } - - public enum FinalizeResult - { - BadSMSCode, - UnableToGenerateCorrectCodes, - Success, - GeneralFailure - } - - private class AddAuthenticatorResponse - { - [JsonProperty("response")] - public SteamGuardAccount Response { get; set; } - } - - private class FinalizeAuthenticatorResponse - { - [JsonProperty("response")] - public FinalizeAuthenticatorInternalResponse Response { get; set; } - - internal class FinalizeAuthenticatorInternalResponse - { - [JsonProperty("status")] - public int Status { get; set; } - - [JsonProperty("server_time")] - public long ServerTime { get; set; } - - [JsonProperty("want_more")] - public bool WantMore { get; set; } - - [JsonProperty("success")] - public bool Success { get; set; } - } - } - - private class HasPhoneResponse - { - [JsonProperty("has_phone")] - public bool HasPhone { get; set; } - } - - private class AddPhoneResponse - { - [JsonProperty("success")] - public bool Success { get; set; } - } - - public static string GenerateDeviceID() - { - using (var sha1 = new SHA1Managed()) - { - RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider(); - byte[] randomBytes = new byte[8]; - secureRandom.GetBytes(randomBytes); - - byte[] hashedBytes = sha1.ComputeHash(randomBytes); - string random32 = BitConverter.ToString(hashedBytes).Replace("-", "").Substring(0, 32).ToLower(); - - return "android:" + SplitOnRatios(random32, new[] { 8, 4, 4, 4, 12 }, "-"); - } - } - - private static string SplitOnRatios(string str, int[] ratios, string intermediate) - { - string result = ""; - - int pos = 0; - for (int index = 0; index < ratios.Length; index++) - { - result += str.Substring(pos, ratios[index]); - pos = ratios[index]; - - if (index < ratios.Length - 1) - result += intermediate; - } - - return result; - } - } -} diff --git a/SteamAuth/Confirmation.cs b/SteamAuth/Confirmation.cs deleted file mode 100644 index 1035446ea..000000000 --- a/SteamAuth/Confirmation.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SteamAuth -{ - public class Confirmation - { - public string ID; - public string Key; - public string Description; - - public ConfirmationType ConfType - { - get - { - if (String.IsNullOrEmpty(Description)) return ConfirmationType.Unknown; - if (Description.StartsWith("Confirm ")) return ConfirmationType.GenericConfirmation; - if (Description.StartsWith("Trade with ")) return ConfirmationType.Trade; - if (Description.StartsWith("Sell -")) return ConfirmationType.MarketSellTransaction; - - return ConfirmationType.Unknown; - } - } - - public enum ConfirmationType - { - GenericConfirmation, - Trade, - MarketSellTransaction, - Unknown - } - } -} diff --git a/SteamAuth/SessionData.cs b/SteamAuth/SessionData.cs deleted file mode 100644 index 692ae60a9..000000000 --- a/SteamAuth/SessionData.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Net; - -namespace SteamAuth -{ - public class SessionData - { - public string SessionID { get; set; } - - public string SteamLogin { get; set; } - - public string SteamLoginSecure { get; set; } - - public string WebCookie { get; set; } - - public string OAuthToken { get; set; } - - public ulong SteamID { get; set; } - - public void AddCookies(CookieContainer cookies) - { - cookies.Add(new Cookie("mobileClientVersion", "0 (2.1.3)", "/", ".steamcommunity.com")); - cookies.Add(new Cookie("mobileClient", "android", "/", ".steamcommunity.com")); - - cookies.Add(new Cookie("steamid", SteamID.ToString(), "/", ".steamcommunity.com")); - cookies.Add(new Cookie("steamLogin", SteamLogin, "/", ".steamcommunity.com") - { - HttpOnly = true - }); - - cookies.Add(new Cookie("steamLoginSecure", SteamLoginSecure, "/", ".steamcommunity.com") - { - HttpOnly = true, - Secure = true - }); - cookies.Add(new Cookie("Steam_Language", "english", "/", ".steamcommunity.com")); - cookies.Add(new Cookie("dob", "", "/", ".steamcommunity.com")); - cookies.Add(new Cookie("sessionid", this.SessionID, "/", ".steamcommunity.com")); - } - } -} diff --git a/SteamAuth/SteamAuth.csproj b/SteamAuth/SteamAuth.csproj index e04e946b4..d0124244c 100644 --- a/SteamAuth/SteamAuth.csproj +++ b/SteamAuth/SteamAuth.csproj @@ -46,16 +46,8 @@ - - - - - - - - diff --git a/SteamAuth/SteamGuardAccount.cs b/SteamAuth/SteamGuardAccount.cs index 8af6b7ce3..6d1019964 100644 --- a/SteamAuth/SteamGuardAccount.cs +++ b/SteamAuth/SteamGuardAccount.cs @@ -1,456 +1,43 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace SteamAuth -{ - public class SteamGuardAccount - { - [JsonProperty("shared_secret")] - public string SharedSecret { get; set; } +namespace SteamAuth { + // TODO: Converter code will be removed soon + public class SteamGuardAccount { + [JsonProperty("shared_secret")] + public string SharedSecret { get; set; } - [JsonProperty("serial_number")] - public string SerialNumber { get; set; } + [JsonProperty("serial_number")] + public string SerialNumber { get; set; } - [JsonProperty("revocation_code")] - public string RevocationCode { get; set; } + [JsonProperty("revocation_code")] + public string RevocationCode { get; set; } - [JsonProperty("uri")] - public string URI { get; set; } + [JsonProperty("uri")] + public string URI { get; set; } - [JsonProperty("server_time")] - public long ServerTime { get; set; } + [JsonProperty("server_time")] + public long ServerTime { get; set; } - [JsonProperty("account_name")] - public string AccountName { get; set; } + [JsonProperty("account_name")] + public string AccountName { get; set; } - [JsonProperty("token_gid")] - public string TokenGID { get; set; } + [JsonProperty("token_gid")] + public string TokenGID { get; set; } - [JsonProperty("identity_secret")] - public string IdentitySecret { get; set; } + [JsonProperty("identity_secret")] + public string IdentitySecret { get; set; } - [JsonProperty("secret_1")] - public string Secret1 { get; set; } + [JsonProperty("secret_1")] + public string Secret1 { get; set; } - [JsonProperty("status")] - public int Status { get; set; } + [JsonProperty("status")] + public int Status { get; set; } - [JsonProperty("device_id")] - public string DeviceID { get; set; } + [JsonProperty("device_id")] + public string DeviceID { get; set; } - /// - /// Set to true if the authenticator has actually been applied to the account. - /// - [JsonProperty("fully_enrolled")] - public bool FullyEnrolled { get; set; } + [JsonProperty("fully_enrolled")] + public bool FullyEnrolled { get; set; } - public SessionData Session { get; set; } - - private static byte[] steamGuardCodeTranslations = new byte[] { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 }; - - public bool DeactivateAuthenticator(int scheme = 2) - { - var postData = new NameValueCollection(); - postData.Add("steamid", this.Session.SteamID.ToString()); - postData.Add("steamguard_scheme", scheme.ToString()); - postData.Add("revocation_code", this.RevocationCode); - postData.Add("access_token", this.Session.OAuthToken); - - try - { - string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/RemoveAuthenticator/v0001", "POST", postData); - var removeResponse = JsonConvert.DeserializeObject(response); - - if (removeResponse == null || removeResponse.Response == null || !removeResponse.Response.Success) return false; - return true; - } - catch (Exception) - { - return false; - } - } - - public string GenerateSteamGuardCode() - { - return GenerateSteamGuardCodeForTime(TimeAligner.GetSteamTime()); - } - - public string GenerateSteamGuardCodeForTime(long time) - { - if (this.SharedSecret == null || this.SharedSecret.Length == 0) - { - return ""; - } - - byte[] sharedSecretArray = Convert.FromBase64String(this.SharedSecret); - byte[] timeArray = new byte[8]; - - time /= 30L; - - for (int i = 8; i > 0; i--) - { - timeArray[i - 1] = (byte)time; - time >>= 8; - } - - HMACSHA1 hmacGenerator = new HMACSHA1(); - hmacGenerator.Key = sharedSecretArray; - byte[] hashedData = hmacGenerator.ComputeHash(timeArray); - byte[] codeArray = new byte[5]; - try - { - byte b = (byte)(hashedData[19] & 0xF); - int codePoint = (hashedData[b] & 0x7F) << 24 | (hashedData[b + 1] & 0xFF) << 16 | (hashedData[b + 2] & 0xFF) << 8 | (hashedData[b + 3] & 0xFF); - - for (int i = 0; i < 5; ++i) - { - codeArray[i] = steamGuardCodeTranslations[codePoint % steamGuardCodeTranslations.Length]; - codePoint /= steamGuardCodeTranslations.Length; - } - } - catch (Exception) - { - return null; //Change later, catch-alls are bad! - } - return Encoding.UTF8.GetString(codeArray); - } - - public Confirmation[] FetchConfirmations() - { - string url = this.GenerateConfirmationURL(); - - CookieContainer cookies = new CookieContainer(); - this.Session.AddCookies(cookies); - - string response = SteamWeb.Request(url, "GET", null, cookies); - - /*So you're going to see this abomination and you're going to be upset. - It's understandable. But the thing is, regex for HTML -- while awful -- makes this way faster than parsing a DOM, plus we don't need another library. - And because the data is always in the same place and same format... It's not as if we're trying to naturally understand HTML here. Just extract strings. - I'm sorry. */ - - Regex confIDRegex = new Regex("data-confid=\"(\\d+)\""); - Regex confKeyRegex = new Regex("data-key=\"(\\d+)\""); - Regex confDescRegex = new Regex("
((Confirm|Trade with|Sell -) .+)
"); - - if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response))) - { - if (response == null || !response.Contains("
Nothing to confirm
")) - { - throw new WGTokenInvalidException(); - } - - return new Confirmation[0]; - } - - MatchCollection confIDs = confIDRegex.Matches(response); - MatchCollection confKeys = confKeyRegex.Matches(response); - MatchCollection confDescs = confDescRegex.Matches(response); - - List ret = new List(); - for (int i = 0; i < confIDs.Count; i++) - { - string confID = confIDs[i].Groups[1].Value; - string confKey = confKeys[i].Groups[1].Value; - string confDesc = confDescs[i].Groups[1].Value; - Confirmation conf = new Confirmation() - { - Description = confDesc, - ID = confID, - Key = confKey - }; - ret.Add(conf); - } - - return ret.ToArray(); - } - - public async Task FetchConfirmationsAsync() - { - string url = this.GenerateConfirmationURL(); - - CookieContainer cookies = new CookieContainer(); - this.Session.AddCookies(cookies); - - string response = await SteamWeb.RequestAsync(url, "GET", null, cookies); - - /*So you're going to see this abomination and you're going to be upset. - It's understandable. But the thing is, regex for HTML -- while awful -- makes this way faster than parsing a DOM, plus we don't need another library. - And because the data is always in the same place and same format... It's not as if we're trying to naturally understand HTML here. Just extract strings. - I'm sorry. */ - - Regex confIDRegex = new Regex("data-confid=\"(\\d+)\""); - Regex confKeyRegex = new Regex("data-key=\"(\\d+)\""); - Regex confDescRegex = new Regex("
((Confirm|Trade with|Sell -) .+)
"); - - if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response))) - { - if (response == null || !response.Contains("
Nothing to confirm
")) - { - throw new WGTokenInvalidException(); - } - - return new Confirmation[0]; - } - - MatchCollection confIDs = confIDRegex.Matches(response); - MatchCollection confKeys = confKeyRegex.Matches(response); - MatchCollection confDescs = confDescRegex.Matches(response); - - List ret = new List(); - for (int i = 0; i < confIDs.Count; i++) - { - string confID = confIDs[i].Groups[1].Value; - string confKey = confKeys[i].Groups[1].Value; - string confDesc = confDescs[i].Groups[1].Value; - Confirmation conf = new Confirmation() - { - Description = confDesc, - ID = confID, - Key = confKey - }; - ret.Add(conf); - } - - return ret.ToArray(); - } - - public long GetConfirmationTradeOfferID(Confirmation conf) - { - var confDetails = _getConfirmationDetails(conf); - if (confDetails == null || !confDetails.Success) return -1; - - Regex tradeOfferIDRegex = new Regex("
"); - if(!tradeOfferIDRegex.IsMatch(confDetails.HTML)) return -1; - return long.Parse(tradeOfferIDRegex.Match(confDetails.HTML).Groups[1].Value); - } - - public bool AcceptConfirmation(Confirmation conf) - { - return _sendConfirmationAjax(conf, "allow"); - } - - public bool DenyConfirmation(Confirmation conf) - { - return _sendConfirmationAjax(conf, "cancel"); - } - - /// - /// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed. - /// - /// - public bool RefreshSession() - { - string url = APIEndpoints.MOBILEAUTH_GETWGTOKEN; - NameValueCollection postData = new NameValueCollection(); - postData.Add("access_token", this.Session.OAuthToken); - - string response = SteamWeb.Request(url, "POST", postData); - if (response == null) return false; - - try - { - var refreshResponse = JsonConvert.DeserializeObject(response); - if (refreshResponse == null || refreshResponse.Response == null || String.IsNullOrEmpty(refreshResponse.Response.Token)) - return false; - - string token = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.Token; - string tokenSecure = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.TokenSecure; - - this.Session.SteamLogin = token; - this.Session.SteamLoginSecure = tokenSecure; - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed. - /// - /// - public async Task RefreshSessionAsync() - { - string url = APIEndpoints.MOBILEAUTH_GETWGTOKEN; - NameValueCollection postData = new NameValueCollection(); - postData.Add("access_token", this.Session.OAuthToken); - - string response = await SteamWeb.RequestAsync(url, "POST", postData); - if (response == null) return false; - - try - { - var refreshResponse = JsonConvert.DeserializeObject(response); - if (refreshResponse == null || refreshResponse.Response == null || String.IsNullOrEmpty(refreshResponse.Response.Token)) - return false; - - string token = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.Token; - string tokenSecure = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.TokenSecure; - - this.Session.SteamLogin = token; - this.Session.SteamLoginSecure = tokenSecure; - return true; - } - catch (Exception) - { - return false; - } - } - - private ConfirmationDetailsResponse _getConfirmationDetails(Confirmation conf) - { - string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/details/" + conf.ID + "?"; - string queryString = GenerateConfirmationQueryParams("details"); - url += queryString; - - CookieContainer cookies = new CookieContainer(); - this.Session.AddCookies(cookies); - string referer = GenerateConfirmationURL(); - - string response = SteamWeb.Request(url, "GET", null, cookies, null); - if (String.IsNullOrEmpty(response)) return null; - - var confResponse = JsonConvert.DeserializeObject(response); - if (confResponse == null) return null; - return confResponse; - } - - private bool _sendConfirmationAjax(Confirmation conf, string op) - { - string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/ajaxop"; - string queryString = "?op=" + op + "&"; - queryString += GenerateConfirmationQueryParams(op); - queryString += "&cid=" + conf.ID + "&ck=" + conf.Key; - url += queryString; - - CookieContainer cookies = new CookieContainer(); - this.Session.AddCookies(cookies); - string referer = GenerateConfirmationURL(); - - string response = SteamWeb.Request(url, "GET", null, cookies, null); - if (response == null) return false; - - SendConfirmationResponse confResponse = JsonConvert.DeserializeObject(response); - return confResponse.Success; - } - - public string GenerateConfirmationURL(string tag = "conf") - { - string endpoint = APIEndpoints.COMMUNITY_BASE + "/mobileconf/conf?"; - string queryString = GenerateConfirmationQueryParams(tag); - return endpoint + queryString; - } - - public string GenerateConfirmationQueryParams(string tag) - { - if (String.IsNullOrEmpty(DeviceID)) - throw new ArgumentException("Device ID is not present"); - - long time = TimeAligner.GetSteamTime(); - return "p=" + this.DeviceID + "&a=" + this.Session.SteamID.ToString() + "&k=" + _generateConfirmationHashForTime(time, tag) + "&t=" + time + "&m=android&tag=" + tag; - } - - private string _generateConfirmationHashForTime(long time, string tag) - { - byte[] decode = Convert.FromBase64String(this.IdentitySecret); - int n2 = 8; - if (tag != null) - { - if (tag.Length > 32) - { - n2 = 8 + 32; - } - else - { - n2 = 8 + tag.Length; - } - } - byte[] array = new byte[n2]; - int n3 = 8; - while (true) - { - int n4 = n3 - 1; - if (n3 <= 0) - { - break; - } - array[n4] = (byte)time; - time >>= 8; - n3 = n4; - } - if (tag != null) - { - Array.Copy(Encoding.UTF8.GetBytes(tag), 0, array, 8, n2 - 8); - } - - try - { - HMACSHA1 hmacGenerator = new HMACSHA1(); - hmacGenerator.Key = decode; - byte[] hashedData = hmacGenerator.ComputeHash(array); - string encodedData = Convert.ToBase64String(hashedData, Base64FormattingOptions.None); - string hash = WebUtility.UrlEncode(encodedData); - return hash; - } - catch (Exception) - { - return null; //Fix soon: catch-all is BAD! - } - } - - //TODO: Determine how to detect an invalid session. - public class WGTokenInvalidException : Exception - { - } - - private class RefreshSessionDataResponse - { - [JsonProperty("response")] - public RefreshSessionDataInternalResponse Response { get; set; } - internal class RefreshSessionDataInternalResponse - { - [JsonProperty("token")] - public string Token { get; set; } - - [JsonProperty("token_secure")] - public string TokenSecure { get; set; } - } - } - - private class RemoveAuthenticatorResponse - { - [JsonProperty("response")] - public RemoveAuthenticatorInternalResponse Response { get; set; } - - internal class RemoveAuthenticatorInternalResponse - { - [JsonProperty("success")] - public bool Success { get; set; } - } - } - - private class SendConfirmationResponse - { - [JsonProperty("success")] - public bool Success { get; set; } - } - - private class ConfirmationDetailsResponse - { - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("html")] - public string HTML { get; set; } - } - } + } } diff --git a/SteamAuth/SteamWeb.cs b/SteamAuth/SteamWeb.cs deleted file mode 100644 index 1b52cf658..000000000 --- a/SteamAuth/SteamWeb.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.IO; -using System.Net; -using System.Threading.Tasks; - -namespace SteamAuth -{ - public class SteamWeb - { - /// - /// Perform a mobile login request - /// - /// API url - /// GET or POST - /// Name-data pairs - /// current cookie container - /// response body - public static string MobileLoginRequest(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null) - { - return Request(url, method, data, cookies, headers, APIEndpoints.COMMUNITY_BASE + "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client"); - } - - public static string Request(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, string referer = APIEndpoints.COMMUNITY_BASE) - { - string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", WebUtility.UrlEncode(key), WebUtility.UrlEncode(data[key]))))); - if (method == "GET") - { - url += (url.Contains("?") ? "&" : "?") + query; - } - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); - request.Method = method; - request.Accept = "text/javascript, text/html, application/xml, text/xml, */*"; - request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"; - request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; - request.Referer = referer; - - if (headers != null) - { - request.Headers.Add(headers); - } - - if (cookies != null) - { - request.CookieContainer = cookies; - } - - if (method == "POST") - { - request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8"; - request.ContentLength = query.Length; - - StreamWriter requestStream = new StreamWriter(request.GetRequestStream()); - requestStream.Write(query); - requestStream.Close(); - } - - try - { - using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) - { - if (response.StatusCode != HttpStatusCode.OK) - { - return null; - } - - using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) - { - string responseData = responseStream.ReadToEnd(); - return responseData; - } - } - } - catch (WebException) - { - return null; - } - } - - public static async Task RequestAsync(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, string referer = APIEndpoints.COMMUNITY_BASE) - { - string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", WebUtility.UrlEncode(key), WebUtility.UrlEncode(data[key]))))); - if (method == "GET") - { - url += (url.Contains("?") ? "&" : "?") + query; - } - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); - request.Method = method; - request.Accept = "text/javascript, text/html, application/xml, text/xml, */*"; - request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"; - request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; - request.Referer = referer; - - if (headers != null) - { - request.Headers.Add(headers); - } - - if (cookies != null) - { - request.CookieContainer = cookies; - } - - if (method == "POST") - { - request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8"; - request.ContentLength = query.Length; - - StreamWriter requestStream = new StreamWriter(request.GetRequestStream()); - requestStream.Write(query); - requestStream.Close(); - } - - try - { - using (HttpWebResponse response = (HttpWebResponse) await request.GetResponseAsync()) { - - if (response.StatusCode != HttpStatusCode.OK) { - return null; - } - - using (StreamReader responseStream = new StreamReader(response.GetResponseStream())) { - string responseData = responseStream.ReadToEnd(); - return responseData; - } - } - } - catch (WebException) - { - return null; - } - } - } -} diff --git a/SteamAuth/TimeAligner.cs b/SteamAuth/TimeAligner.cs deleted file mode 100644 index 11489cedd..000000000 --- a/SteamAuth/TimeAligner.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Net; -using Newtonsoft.Json; - -namespace SteamAuth -{ - /// - /// Class to help align system time with the Steam server time. Not super advanced; probably not taking some things into account that it should. - /// Necessary to generate up-to-date codes. In general, this will have an error of less than a second, assuming Steam is operational. - /// - public class TimeAligner - { - private static bool _aligned = false; - private static int _timeDifference = 0; - - public static long GetSteamTime() - { - if (!TimeAligner._aligned) - { - TimeAligner.AlignTime(); - } - return Util.GetSystemUnixTime() + _timeDifference; - } - - public static async Task GetSteamTimeAsync() - { - if (!TimeAligner._aligned) - { - await TimeAligner.AlignTimeAsync(); - } - return Util.GetSystemUnixTime() + _timeDifference; - } - - public static void AlignTime() - { - long currentTime = Util.GetSystemUnixTime(); - using (WebClient client = new WebClient()) - { - try - { - string response = client.UploadString(APIEndpoints.TWO_FACTOR_TIME_QUERY, "steamid=0"); - TimeQuery query = JsonConvert.DeserializeObject(response); - TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime); - TimeAligner._aligned = true; - } - catch (WebException) - { - return; - } - } - } - - public static async Task AlignTimeAsync() - { - long currentTime = Util.GetSystemUnixTime(); - WebClient client = new WebClient(); - try - { - string response = await client.UploadStringTaskAsync(new Uri(APIEndpoints.TWO_FACTOR_TIME_QUERY), "steamid=0"); - TimeQuery query = JsonConvert.DeserializeObject(response); - TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime); - TimeAligner._aligned = true; - } - catch (WebException) - { - return; - } - } - - internal class TimeQuery - { - [JsonProperty("response")] - internal TimeQueryResponse Response { get; set; } - - internal class TimeQueryResponse - { - [JsonProperty("server_time")] - public long ServerTime { get; set; } - } - - } - } -} diff --git a/SteamAuth/UserLogin.cs b/SteamAuth/UserLogin.cs deleted file mode 100644 index a298d2166..000000000 --- a/SteamAuth/UserLogin.cs +++ /dev/null @@ -1,252 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Specialized; -using System.Net; -using System.Security.Cryptography; -using System.Text; - -namespace SteamAuth -{ - /// - /// Handles logging the user into the mobile Steam website. Necessary to generate OAuth token and session cookies. - /// - public class UserLogin - { - public string Username; - public string Password; - public ulong SteamID; - - public bool RequiresCaptcha; - public string CaptchaGID = null; - public string CaptchaText = null; - - public bool RequiresEmail; - public string EmailDomain = null; - public string EmailCode = null; - - public bool Requires2FA; - public string TwoFactorCode = null; - - public SessionData Session = null; - public bool LoggedIn = false; - - private CookieContainer _cookies = new CookieContainer(); - - public UserLogin(string username, string password) - { - this.Username = username; - this.Password = password; - } - - public LoginResult DoLogin() - { - var postData = new NameValueCollection(); - var cookies = _cookies; - string response = null; - - if (cookies.Count == 0) - { - //Generate a SessionID - cookies.Add(new Cookie("mobileClientVersion", "0 (2.1.3)", "/", ".steamcommunity.com")); - cookies.Add(new Cookie("mobileClient", "android", "/", ".steamcommunity.com")); - cookies.Add(new Cookie("Steam_Language", "english", "/", ".steamcommunity.com")); - - NameValueCollection headers = new NameValueCollection(); - headers.Add("X-Requested-With", "com.valvesoftware.android.steam.community"); - - SteamWeb.MobileLoginRequest("https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client", "GET", null, cookies, headers); - } - - postData.Add("username", this.Username); - response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/getrsakey", "POST", postData, cookies); - if (response == null || response.Contains("\nAn error occurred while processing your request.")) return LoginResult.GeneralFailure; - - var rsaResponse = JsonConvert.DeserializeObject(response); - - if (!rsaResponse.Success) - { - return LoginResult.BadRSA; - } - - RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider(); - byte[] encryptedPasswordBytes; - using (var rsaEncryptor = new RSACryptoServiceProvider()) - { - var passwordBytes = Encoding.ASCII.GetBytes(this.Password); - var rsaParameters = rsaEncryptor.ExportParameters(false); - rsaParameters.Exponent = Util.HexStringToByteArray(rsaResponse.Exponent); - rsaParameters.Modulus = Util.HexStringToByteArray(rsaResponse.Modulus); - rsaEncryptor.ImportParameters(rsaParameters); - encryptedPasswordBytes = rsaEncryptor.Encrypt(passwordBytes, false); - } - - string encryptedPassword = Convert.ToBase64String(encryptedPasswordBytes); - - postData.Clear(); - postData.Add("username", this.Username); - postData.Add("password", encryptedPassword); - - postData.Add("twofactorcode", this.TwoFactorCode ?? ""); - - postData.Add("captchagid", this.RequiresCaptcha ? this.CaptchaGID : "-1"); - postData.Add("captcha_text", this.RequiresCaptcha ? this.CaptchaText : ""); - - postData.Add("emailsteamid", (this.Requires2FA || this.RequiresEmail) ? this.SteamID.ToString() : ""); - postData.Add("emailauth", this.RequiresEmail ? this.EmailCode : ""); - - postData.Add("rsatimestamp", rsaResponse.Timestamp); - postData.Add("remember_login", "false"); - postData.Add("oauth_client_id", "DE45CD61"); - postData.Add("oauth_scope", "read_profile write_profile read_client write_client"); - postData.Add("loginfriendlyname", "#login_emailauth_friendlyname_mobile"); - postData.Add("donotcache", Util.GetSystemUnixTime().ToString()); - - response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/dologin", "POST", postData, cookies); - if (response == null) return LoginResult.GeneralFailure; - - var loginResponse = JsonConvert.DeserializeObject(response); - - if (loginResponse.Message != null && loginResponse.Message.Contains("Incorrect login")) - { - return LoginResult.BadCredentials; - } - - if (loginResponse.CaptchaNeeded) - { - this.RequiresCaptcha = true; - this.CaptchaGID = loginResponse.CaptchaGID; - return LoginResult.NeedCaptcha; - } - - if (loginResponse.EmailAuthNeeded) - { - this.RequiresEmail = true; - this.SteamID = loginResponse.EmailSteamID; - return LoginResult.NeedEmail; - } - - if (loginResponse.TwoFactorNeeded && !loginResponse.Success) - { - this.Requires2FA = true; - return LoginResult.Need2FA; - } - - if (loginResponse.Message != null && loginResponse.Message.Contains("too many login failures")) - { - return LoginResult.TooManyFailedLogins; - } - - if (loginResponse.OAuthData == null || loginResponse.OAuthData.OAuthToken == null || loginResponse.OAuthData.OAuthToken.Length == 0) - { - return LoginResult.GeneralFailure; - } - - if (!loginResponse.LoginComplete) - { - return LoginResult.BadCredentials; - } - else - { - var readableCookies = cookies.GetCookies(new Uri("https://steamcommunity.com")); - var oAuthData = loginResponse.OAuthData; - - SessionData session = new SessionData(); - session.OAuthToken = oAuthData.OAuthToken; - session.SteamID = oAuthData.SteamID; - session.SteamLogin = session.SteamID + "%7C%7C" + oAuthData.SteamLogin; - session.SteamLoginSecure = session.SteamID + "%7C%7C" + oAuthData.SteamLoginSecure; - session.WebCookie = oAuthData.Webcookie; - session.SessionID = readableCookies["sessionid"].Value; - this.Session = session; - this.LoggedIn = true; - return LoginResult.LoginOkay; - } - } - - private class LoginResponse - { - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("login_complete")] - public bool LoginComplete { get; set; } - - [JsonProperty("oauth")] - public string OAuthDataString { get; set; } - - public OAuth OAuthData - { - get - { - return OAuthDataString != null ? JsonConvert.DeserializeObject(OAuthDataString) : null; - } - } - - [JsonProperty("captcha_needed")] - public bool CaptchaNeeded { get; set; } - - [JsonProperty("captcha_gid")] - public string CaptchaGID { get; set; } - - [JsonProperty("emailsteamid")] - public ulong EmailSteamID { get; set; } - - [JsonProperty("emailauth_needed")] - public bool EmailAuthNeeded { get; set; } - - [JsonProperty("requires_twofactor")] - public bool TwoFactorNeeded { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } - - internal class OAuth - { - [JsonProperty("steamid")] - public ulong SteamID { get; set; } - - [JsonProperty("oauth_token")] - public string OAuthToken { get; set; } - - [JsonProperty("wgtoken")] - public string SteamLogin { get; set; } - - [JsonProperty("wgtoken_secure")] - public string SteamLoginSecure { get; set; } - - [JsonProperty("webcookie")] - public string Webcookie { get; set; } - } - } - - private class RSAResponse - { - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("publickey_exp")] - public string Exponent { get; set; } - - [JsonProperty("publickey_mod")] - public string Modulus { get; set; } - - [JsonProperty("timestamp")] - public string Timestamp { get; set; } - - [JsonProperty("steamid")] - public ulong SteamID { get; set; } - } - } - - public enum LoginResult - { - LoginOkay, - GeneralFailure, - BadRSA, - BadCredentials, - NeedCaptcha, - Need2FA, - NeedEmail, - TooManyFailedLogins, - } -} diff --git a/SteamAuth/Util.cs b/SteamAuth/Util.cs deleted file mode 100644 index 0574e7d58..000000000 --- a/SteamAuth/Util.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Net; - -namespace SteamAuth -{ - public class Util - { - public static long GetSystemUnixTime() - { - return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; - } - - public static byte[] HexStringToByteArray(string hex) - { - int hexLen = hex.Length; - byte[] ret = new byte[hexLen / 2]; - for (int i = 0; i < hexLen; i += 2) - { - ret[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); - } - return ret; - } - } -}