From b3162ce39021123cc9703dac340ac4cc43c8adf3 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Fri, 11 Dec 2015 22:53:28 +0100 Subject: [PATCH] Squashed commit of ASF 2FA Check https://github.com/JustArchi/ArchiSteamFarm/wiki/Escrow for more info --- ArchiSteamFarm.sln | 6 + ArchiSteamFarm/App.config | 6 +- ArchiSteamFarm/ArchiSteamFarm.csproj | 9 +- ArchiSteamFarm/ArchiWebHandler.cs | 4 +- ArchiSteamFarm/Bot.cs | 196 +++++++++++++++- ArchiSteamFarm/Program.cs | 19 +- ArchiSteamFarm/SteamTradeOffer.cs | 11 +- ArchiSteamFarm/Trading.cs | 1 + ArchiSteamFarm/config/example.xml | 7 + README.md | 34 +-- SteamAuth/APIEndpoints.cs | 12 + SteamAuth/AuthenticatorLinker.cs | 240 +++++++++++++++++++ SteamAuth/Confirmation.cs | 15 ++ SteamAuth/Properties/AssemblyInfo.cs | 36 +++ SteamAuth/SessionData.cs | 39 ++++ SteamAuth/SteamAuth.csproj | 69 ++++++ SteamAuth/SteamAuth.sln | 28 +++ SteamAuth/SteamGuardAccount.cs | 333 +++++++++++++++++++++++++++ SteamAuth/SteamWeb.cs | 81 +++++++ SteamAuth/TimeAligner.cs | 56 +++++ SteamAuth/UserLogin.cs | 240 +++++++++++++++++++ SteamAuth/Util.cs | 24 ++ SteamAuth/packages.config | 4 + 23 files changed, 1441 insertions(+), 29 deletions(-) create mode 100644 SteamAuth/APIEndpoints.cs create mode 100644 SteamAuth/AuthenticatorLinker.cs create mode 100644 SteamAuth/Confirmation.cs create mode 100644 SteamAuth/Properties/AssemblyInfo.cs create mode 100644 SteamAuth/SessionData.cs create mode 100644 SteamAuth/SteamAuth.csproj create mode 100644 SteamAuth/SteamAuth.sln create mode 100644 SteamAuth/SteamGuardAccount.cs create mode 100644 SteamAuth/SteamWeb.cs create mode 100644 SteamAuth/TimeAligner.cs create mode 100644 SteamAuth/UserLogin.cs create mode 100644 SteamAuth/Util.cs create mode 100644 SteamAuth/packages.config diff --git a/ArchiSteamFarm.sln b/ArchiSteamFarm.sln index 38d3f9215fc32..82055061cbe2a 100644 --- a/ArchiSteamFarm.sln +++ b/ArchiSteamFarm.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm", "ArchiSteamFarm\ArchiSteamFarm.csproj", "{35AF7887-08B9-40E8-A5EA-797D8B60B30C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth\SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Debug|Any CPU.Build.0 = Debug|Any CPU {35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Release|Any CPU.ActiveCfg = Release|Any CPU {35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Release|Any CPU.Build.0 = Release|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ArchiSteamFarm/App.config b/ArchiSteamFarm/App.config index fe6842709f546..5d95367ce4e84 100644 --- a/ArchiSteamFarm/App.config +++ b/ArchiSteamFarm/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index 54b67b4ccb640..557d71292ea6a 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -9,7 +9,7 @@ Properties ArchiSteamFarm ArchiSteamFarm - v4.5 + v4.5.2 512 false publish\ @@ -26,6 +26,7 @@ 1.0.0.%2a false true + AnyCPU @@ -123,6 +124,12 @@ PreserveNewest + + + {5ad0934e-f6c4-4ae5-83af-c788313b2a87} + SteamAuth + + diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 3f47c78c7165c..34cc148833461 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -192,7 +192,9 @@ internal List GetTradeOffers() { is_our_offer = trade["is_our_offer"].AsBoolean(), time_created = trade["time_created"].AsInteger(), time_updated = trade["time_updated"].AsInteger(), - from_real_time_trade = trade["from_real_time_trade"].AsBoolean() + from_real_time_trade = trade["from_real_time_trade"].AsBoolean(), + escrow_end_date = trade["escrow_end_date"].AsInteger(), + confirmation_method = (SteamTradeOffer.ETradeOfferConfirmationMethod) trade["confirmation_method"].AsInteger() }; foreach (KeyValue item in trade["items_to_give"].Children) { tradeOffer.items_to_give.Add(new SteamItem { diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 27b97b05b1d14..183ce561acea4 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -22,6 +22,8 @@ limitations under the License. */ +using Newtonsoft.Json; +using SteamAuth; using SteamKit2; using System; using System.Collections.Concurrent; @@ -37,13 +39,13 @@ internal sealed class Bot { private static readonly ConcurrentDictionary Bots = new ConcurrentDictionary(); - private readonly string ConfigFile, SentryFile; + private readonly string ConfigFile, LoginKeyFile, MobileAuthenticatorFile, SentryFile; internal readonly string BotName; private bool LoggedInElsewhere = false; private bool IsRunning = false; - private string AuthCode, TwoFactorAuth; + private string AuthCode, LoginKey, TwoFactorAuth; internal ArchiHandler ArchiHandler { get; private set; } internal ArchiWebHandler ArchiWebHandler { get; private set; } @@ -51,6 +53,7 @@ internal sealed class Bot { internal CardsFarmer CardsFarmer { get; private set; } internal SteamClient SteamClient { get; private set; } internal SteamFriends SteamFriends { get; private set; } + internal SteamGuardAccount SteamGuardAccount { get; private set; } internal SteamUser SteamUser { get; private set; } internal Trading Trading { get; private set; } @@ -64,6 +67,7 @@ internal sealed class Bot { internal ulong SteamMasterID { get; private set; } = 0; internal ulong SteamMasterClanID { get; private set; } = 0; internal bool CardDropsRestricted { get; private set; } = false; + internal bool UseAsfAsMobileAuthenticator { get; private set; } = false; internal bool ShutdownOnFarmingFinished { get; private set; } = false; internal HashSet Blacklist { get; private set; } = new HashSet { 303700, 335590, 368020 }; internal bool Statistics { get; private set; } = true; @@ -106,6 +110,8 @@ internal Bot(string botName) { BotName = botName; ConfigFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".xml"); + LoginKeyFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".key"); + MobileAuthenticatorFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".auth"); SentryFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".bin"); if (!ReadConfig()) { @@ -132,10 +138,15 @@ internal Bot(string botName) { CallbackManager.Subscribe(OnFriendsList); CallbackManager.Subscribe(OnFriendMsg); + if (UseAsfAsMobileAuthenticator && File.Exists(MobileAuthenticatorFile)) { + SteamGuardAccount = JsonConvert.DeserializeObject(File.ReadAllText(MobileAuthenticatorFile)); + } + SteamUser = SteamClient.GetHandler(); CallbackManager.Subscribe(OnAccountInfo); CallbackManager.Subscribe(OnLoggedOff); CallbackManager.Subscribe(OnLoggedOn); + CallbackManager.Subscribe(OnLoginKey); CallbackManager.Subscribe(OnMachineAuth); CallbackManager.Subscribe(OnNotification); @@ -149,6 +160,90 @@ internal Bot(string botName) { var fireAndForget = Task.Run(async () => await Start().ConfigureAwait(false)); } + internal void AcceptAllConfirmations() { + if (SteamGuardAccount == null) { + return; + } + + foreach (Confirmation confirmation in SteamGuardAccount.FetchConfirmations()) { + if (SteamGuardAccount.AcceptConfirmation(confirmation)) { + Logging.LogGenericInfo(BotName, "Accepting confirmation: Success!"); + } else { + Logging.LogGenericWarning(BotName, "Accepting confirmation: Failed!"); + } + } + } + + private bool LinkMobileAuthenticator() { + if (SteamGuardAccount != null) { + return false; + } + + Logging.LogGenericNotice(BotName, "Linking new ASF MobileAuthenticator..."); + UserLogin userLogin = new UserLogin(SteamLogin, SteamPassword); + LoginResult loginResult; + while ((loginResult = userLogin.DoLogin()) != LoginResult.LoginOkay) { + switch (loginResult) { + case LoginResult.NeedEmail: + userLogin.EmailCode = Program.GetUserInput(BotName, Program.EUserInputType.SteamGuard); + break; + default: + Logging.LogGenericError(BotName, "Unhandled situation: " + loginResult); + return false; + } + } + + AuthenticatorLinker authenticatorLinker = new AuthenticatorLinker(userLogin.Session); + + AuthenticatorLinker.LinkResult linkResult = authenticatorLinker.AddAuthenticator(); + switch (linkResult) { + case AuthenticatorLinker.LinkResult.AwaitingFinalization: + Logging.LogGenericInfo(BotName, "OK: " + linkResult); + break; + case AuthenticatorLinker.LinkResult.MustProvidePhoneNumber: + while (linkResult == AuthenticatorLinker.LinkResult.MustProvidePhoneNumber) { + authenticatorLinker.PhoneNumber = Program.GetUserInput(BotName, Program.EUserInputType.PhoneNumber); + linkResult = authenticatorLinker.AddAuthenticator(); + } + break; + default: + Logging.LogGenericError(BotName, "Unhandled situation: " + linkResult); + return false; + } + + SteamGuardAccount = authenticatorLinker.LinkedAccount; + + try { + File.WriteAllText(MobileAuthenticatorFile, JsonConvert.SerializeObject(SteamGuardAccount)); + } catch (Exception e) { + Logging.LogGenericException(BotName, e); + return false; + } + + AuthenticatorLinker.FinalizeResult finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(Program.GetUserInput(BotName, Program.EUserInputType.SMS)); + if (finalizeResult != AuthenticatorLinker.FinalizeResult.Success) { + Logging.LogGenericError(BotName, "Unhandled situation: " + finalizeResult); + DelinkMobileAuthenticator(); + return false; + } + + Logging.LogGenericInfo(BotName, "Successfully linked ASF as new mobile authenticator for this account!"); + Program.GetUserInput(BotName, Program.EUserInputType.RevocationCode, SteamGuardAccount.RevocationCode); + return true; + } + + private bool DelinkMobileAuthenticator() { + if (SteamGuardAccount == null) { + return false; + } + + bool result = SteamGuardAccount.DeactivateAuthenticator(); + SteamGuardAccount = null; + File.Delete(MobileAuthenticatorFile); + + return result; + } + private bool ReadConfig() { if (!File.Exists(ConfigFile)) { return false; @@ -196,6 +291,9 @@ private bool ReadConfig() { case "SteamMasterClanID": SteamMasterClanID = ulong.Parse(value); break; + case "UseAsfAsMobileAuthenticator": + UseAsfAsMobileAuthenticator = bool.Parse(value); + break; case "CardDropsRestricted": CardDropsRestricted = bool.Parse(value); break; @@ -316,6 +414,59 @@ private void ResponseStatus(ulong steamID, string botName = null) { SendMessageToUser(steamID, "Currently " + Bots.Count + " bots are running"); } + private void Response2FA(ulong steamID, string botName = null) { + if (steamID == 0) { + return; + } + + Bot bot; + + if (string.IsNullOrEmpty(botName)) { + bot = this; + } else { + if (!Bots.TryGetValue(botName, out bot)) { + SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!"); + return; + } + } + + if (bot.SteamGuardAccount == null) { + SendMessageToUser(steamID, "That bot doesn't have ASF 2FA enabled!"); + return; + } + + long timeLeft = 30 - TimeAligner.GetSteamTime() % 30; + SendMessageToUser(steamID, "2FA Token: " + bot.SteamGuardAccount.GenerateSteamGuardCode() + " (expires in " + timeLeft + " seconds)"); + } + + private void Response2FAOff(ulong steamID, string botName = null) { + if (steamID == 0) { + return; + } + + Bot bot; + + if (string.IsNullOrEmpty(botName)) { + bot = this; + } else { + if (!Bots.TryGetValue(botName, out bot)) { + SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!"); + return; + } + } + + if (bot.SteamGuardAccount == null) { + SendMessageToUser(steamID, "That bot doesn't have ASF 2FA enabled!"); + return; + } + + if (bot.DelinkMobileAuthenticator()) { + SendMessageToUser(steamID, "Done! Bot is no longer using ASF 2FA"); + } else { + SendMessageToUser(steamID, "Something went wrong!"); + } + } + private void ResponseStart(ulong steamID, string botNameToStart) { if (steamID == 0 || string.IsNullOrEmpty(botNameToStart)) { return; @@ -367,6 +518,10 @@ private void OnConnected(SteamClient.ConnectedCallback callback) { Logging.LogGenericInfo(BotName, "Connected to Steam!"); + if (File.Exists(LoginKeyFile)) { + LoginKey = File.ReadAllText(LoginKeyFile); + } + byte[] sentryHash = null; if (File.Exists(SentryFile)) { byte[] sentryFileContent = File.ReadAllBytes(SentryFile); @@ -377,7 +532,7 @@ private void OnConnected(SteamClient.ConnectedCallback callback) { SteamLogin = Program.GetUserInput(BotName, Program.EUserInputType.Login); } - if (SteamPassword.Equals("null")) { + if (SteamPassword.Equals("null") && string.IsNullOrEmpty(LoginKey)) { SteamPassword = Program.GetUserInput(BotName, Program.EUserInputType.Password); } @@ -385,8 +540,10 @@ private void OnConnected(SteamClient.ConnectedCallback callback) { Username = SteamLogin, Password = SteamPassword, AuthCode = AuthCode, + LoginKey = LoginKey, TwoFactorCode = TwoFactorAuth, - SentryFileHash = sentryHash + SentryFileHash = sentryHash, + ShouldRememberPassword = true }); } @@ -477,6 +634,12 @@ private async void OnFriendMsg(SteamFriends.FriendMsgCallback callback) { if (!message.Contains(" ")) { switch (message) { + case "!2fa": + Response2FA(steamID); + break; + case "!2faoff": + Response2FAOff(steamID); + break; case "!exit": await ShutdownAllBots().ConfigureAwait(false); break; @@ -498,6 +661,12 @@ private async void OnFriendMsg(SteamFriends.FriendMsgCallback callback) { } else { string[] args = message.Split(' '); switch (args[0]) { + case "!2fa": + Response2FA(steamID, args[1]); + break; + case "!2faoff": + Response2FAOff(steamID, args[1]); + break; case "!redeem": ArchiHandler.RedeemKey(args[1]); break; @@ -549,7 +718,11 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) { AuthCode = Program.GetUserInput(SteamLogin, Program.EUserInputType.SteamGuard); break; case EResult.AccountLoginDeniedNeedTwoFactor: - TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication); + if (SteamGuardAccount == null) { + TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication); + } else { + TwoFactorAuth = SteamGuardAccount.GenerateSteamGuardCode(); + } break; case EResult.InvalidPassword: Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result + ", will retry after a longer while"); @@ -560,6 +733,10 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) { case EResult.OK: Logging.LogGenericInfo(BotName, "Successfully logged on!"); + if (UseAsfAsMobileAuthenticator && TwoFactorAuth == null && SteamGuardAccount == null) { + LinkMobileAuthenticator(); + } + // Reset one-time-only access tokens AuthCode = null; TwoFactorAuth = null; @@ -600,6 +777,15 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) { } } + private void OnLoginKey(SteamUser.LoginKeyCallback callback) { + if (callback == null) { + return; + } + + File.WriteAllText(LoginKeyFile, callback.LoginKey); + SteamUser.AcceptNewLoginKey(callback); + } + private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) { if (callback == null) { return; diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index d9785219cd819..a7b28fd3ae8bf 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -34,8 +34,11 @@ internal static class Program { internal enum EUserInputType { Login, Password, + PhoneNumber, + SMS, SteamGuard, SteamParentalPIN, + RevocationCode, TwoFactorAuthentication, } @@ -98,7 +101,7 @@ internal static async Task LimitSteamRequestsAsync() { SteamSemaphore.Release(); } - internal static string GetUserInput(string botLogin, EUserInputType userInputType) { + internal static string GetUserInput(string botLogin, EUserInputType userInputType, string extraInformation = null) { string result; lock (ConsoleLock) { switch (userInputType) { @@ -108,12 +111,23 @@ internal static string GetUserInput(string botLogin, EUserInputType userInputTyp case EUserInputType.Password: Console.Write("<" + botLogin + "> Please enter your password: "); break; + case EUserInputType.PhoneNumber: + Console.Write("<" + botLogin + "> Please enter your full phone number (e.g. +1234567890): "); + break; + case EUserInputType.SMS: + Console.Write("<" + botLogin + "> Please enter SMS code sent on your mobile: "); + break; case EUserInputType.SteamGuard: Console.Write("<" + botLogin + "> Please enter the auth code sent to your email: "); break; case EUserInputType.SteamParentalPIN: Console.Write("<" + botLogin + "> Please enter steam parental PIN: "); break; + case EUserInputType.RevocationCode: + Console.WriteLine("<" + botLogin + "> PLEASE WRITE DOWN YOUR REVOCATION CODE: " + extraInformation); + Console.WriteLine("<" + botLogin + "> THIS IS THE ONLY WAY TO NOT GET LOCKED OUT OF YOUR ACCOUNT!"); + Console.Write("<" + botLogin + "> Hit enter once ready..."); + break; case EUserInputType.TwoFactorAuthentication: Console.Write("<" + botLogin + "> Please enter your 2 factor auth code from your authenticator app: "); break; @@ -121,8 +135,7 @@ internal static string GetUserInput(string botLogin, EUserInputType userInputTyp result = Console.ReadLine(); Console.Clear(); // For security purposes } - result = result.Trim(); // Get rid of all whitespace characters - return result; + return result.Trim(); // Get rid of all whitespace characters } internal static async void OnBotShutdown(Bot bot) { diff --git a/ArchiSteamFarm/SteamTradeOffer.cs b/ArchiSteamFarm/SteamTradeOffer.cs index 087cc7d26e6ea..ddbea6aaf3bec 100644 --- a/ArchiSteamFarm/SteamTradeOffer.cs +++ b/ArchiSteamFarm/SteamTradeOffer.cs @@ -39,7 +39,14 @@ internal enum ETradeOfferState { Declined, InvalidItems, EmailPending, - EmailCanceled + EmailCanceled, + OnHold + } + + internal enum ETradeOfferConfirmationMethod { + Invalid, + Email, + MobileApp } internal string tradeofferid { get; set; } @@ -53,6 +60,8 @@ internal enum ETradeOfferState { internal int time_created { get; set; } internal int time_updated { get; set; } internal bool from_real_time_trade { get; set; } + internal int escrow_end_date { get; set; } + internal ETradeOfferConfirmationMethod confirmation_method { get; set; } // Extra private ulong _OtherSteamID64 = 0; diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 92cb1a8a64328..4bc686d76a0d4 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -62,6 +62,7 @@ private async Task ParseActiveTrades() { } await Task.WhenAll(tasks).ConfigureAwait(false); + Bot.AcceptAllConfirmations(); } private async Task ParseTrade(SteamTradeOffer tradeOffer) { diff --git a/ArchiSteamFarm/config/example.xml b/ArchiSteamFarm/config/example.xml index d959f94c91c06..3a9b9d102b3c9 100644 --- a/ArchiSteamFarm/config/example.xml +++ b/ArchiSteamFarm/config/example.xml @@ -49,6 +49,13 @@ + + + + + + + diff --git a/README.md b/README.md index b841a7083c1d6..a23c1163f4d17 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ ASF is a C# application that allows you to farm steam cards using multiple steam **Core features:** - - Automatically farm available games using any number of active accounts - - Automatically accept friend requests sent from master - - Automatically accept all trades coming from master - - Automatically accept all steam cd-keys sent via chat from master - - Possibility to choose the most efficient cards farming algorithm, based on given account - - SteamGuard / SteamParental / 2FA support - - Update notifications - - Full Mono support, cross-OS compatibility +- Automatically farm available games using any number of active accounts +- Automatically accept friend requests sent from master +- Automatically accept all trades coming from master +- Automatically accept all steam cd-keys sent via chat from master +- Possibility to choose the most efficient cards farming algorithm, based on given account +- SteamGuard / SteamParental / 2FA support +- Update notifications +- Full Mono support, cross-OS compatibility **Setting up:** @@ -26,13 +26,17 @@ ASF doesn't require and doesn't interfere in any way with Steam client, which me **Current Commands:** - - `!exit` Stops whole ASF - - `!farm` Restarts cards farming module. ASF automatically executes that if any cd-key is successfully claimed - - `!redeem ` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat - - `!start ` Starts given bot instance, after it was ```!stop```pped - - `!status` Prints current status of ASF - - `!stop` Stops current bot instance - - `!stop ` Stops given bot instance +- `!2fa` Generates temporary 2FA token for current bot instance +- `!2fa ` Generates temporary 2FA token for given bot instance +- `!2faoff` Deactivates 2FA for current bot instance +- `!2faoff ` Deactivates 2FA for given bot instance +- `!exit` Stops whole ASF +- `!farm` Restarts cards farming module. ASF automatically executes that if any cd-key is successfully claimed +- `!redeem ` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat +- `!start ` Starts given bot instance +- `!status` Prints current status of ASF +- `!stop` Stops current bot instance +- `!stop ` Stops given bot instance > Commands can be executed via a private chat with your bot. > Remember that bot accepts commands only from ```SteamMasterID```. That property can be configured in the config. diff --git a/SteamAuth/APIEndpoints.cs b/SteamAuth/APIEndpoints.cs new file mode 100644 index 0000000000000..b64ab0b0de23d --- /dev/null +++ b/SteamAuth/APIEndpoints.cs @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000000000..a30c6a00e8d24 --- /dev/null +++ b/SteamAuth/AuthenticatorLinker.cs @@ -0,0 +1,240 @@ +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 || 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) + { + bool smsCodeGood = false; + + var postData = new NameValueCollection(); + postData.Add("steamid", _session.SteamID.ToString()); + postData.Add("access_token", _session.OAuthToken); + postData.Add("activation_code", smsCode); + postData.Add("authenticator_code", ""); + int tries = 0; + while (tries <= 30) + { + postData.Set("authenticator_code", tries == 0 ? "" : LinkedAccount.GenerateSteamGuardCode()); + postData.Add("authenticator_time", TimeAligner.GetSteamTime().ToString()); + + if(smsCodeGood) + postData.Set("activation_code", ""); + + 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) + { + smsCodeGood = true; + tries++; + continue; + } + + this.LinkedAccount.FullyEnrolled = true; + return FinalizeResult.Success; + } + + return FinalizeResult.GeneralFailure; + } + + private bool _addPhoneNumber() + { + string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax?op=add_phone_number&arg=" + WebUtility.UrlEncode(PhoneNumber), "GET", null, _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"); + string response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "GET", 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!) + } + + 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; } + } + + private string _generateDeviceID() + { + using (var sha1 = new SHA1Managed()) + { + RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider(); + byte[] randomBytes = new byte[8]; + secureRandom.GetBytes(randomBytes); + + byte[] hashedBytes = sha1.ComputeHash(randomBytes); + return "android:" + BitConverter.ToString(hashedBytes).Replace("-", ""); + } + } + } +} diff --git a/SteamAuth/Confirmation.cs b/SteamAuth/Confirmation.cs new file mode 100644 index 0000000000000..7b437d4ee3c90 --- /dev/null +++ b/SteamAuth/Confirmation.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SteamAuth +{ + public class Confirmation + { + public string ConfirmationID; + public string ConfirmationKey; + public string ConfirmationDescription; + } +} diff --git a/SteamAuth/Properties/AssemblyInfo.cs b/SteamAuth/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000..603107ccdc35c --- /dev/null +++ b/SteamAuth/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SteamAuth")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SteamAuth")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5ad0934e-f6c4-4ae5-83af-c788313b2a87")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// 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("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SteamAuth/SessionData.cs b/SteamAuth/SessionData.cs new file mode 100644 index 0000000000000..40905b92ef475 --- /dev/null +++ b/SteamAuth/SessionData.cs @@ -0,0 +1,39 @@ +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")); + } + } +} diff --git a/SteamAuth/SteamAuth.csproj b/SteamAuth/SteamAuth.csproj new file mode 100644 index 0000000000000..099d98843e0fd --- /dev/null +++ b/SteamAuth/SteamAuth.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87} + Library + Properties + SteamAuth + SteamAuth + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.8.0.1-beta3\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SteamAuth/SteamAuth.sln b/SteamAuth/SteamAuth.sln new file mode 100644 index 0000000000000..b132b1343236f --- /dev/null +++ b/SteamAuth/SteamAuth.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBed", "..\TestBed\TestBed.csproj", "{8A732227-C090-4011-9F0A-51180CFE6271}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU + {8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/SteamAuth/SteamGuardAccount.cs b/SteamAuth/SteamGuardAccount.cs new file mode 100644 index 0000000000000..054b95e22a988 --- /dev/null +++ b/SteamAuth/SteamGuardAccount.cs @@ -0,0 +1,333 @@ +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; + +namespace SteamAuth +{ + public class SteamGuardAccount + { + [JsonProperty("shared_secret")] + public string SharedSecret { get; set; } + + [JsonProperty("serial_number")] + public string SerialNumber { get; set; } + + [JsonProperty("revocation_code")] + public string RevocationCode { get; set; } + + [JsonProperty("uri")] + public string URI { get; set; } + + [JsonProperty("server_time")] + public long ServerTime { get; set; } + + [JsonProperty("account_name")] + public string AccountName { get; set; } + + [JsonProperty("token_gid")] + public string TokenGID { get; set; } + + [JsonProperty("identity_secret")] + public string IdentitySecret { get; set; } + + [JsonProperty("secret_1")] + public string Secret1 { get; set; } + + [JsonProperty("status")] + public int Status { 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; } + + 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() + { + var postData = new NameValueCollection(); + postData.Add("steamid", this.Session.SteamID.ToString()); + postData.Add("steamguard_scheme", "2"); + 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 e) + { + 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 e) + { + 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() + { + ConfirmationDescription = confDesc, + ConfirmationID = confID, + ConfirmationKey = confKey + }; + ret.Add(conf); + } + + return ret.ToArray(); + } + + 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 e) + { + return false; + } + } + + private bool _sendConfirmationAjax(Confirmation conf, string op) + { + string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/ajaxop"; + string queryString = "?op=" + op + "&"; + queryString += _generateConfirmationQueryParams(op); + queryString += "&cid=" + conf.ConfirmationID + "&ck=" + conf.ConfirmationKey; + 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; + } + + private string _generateConfirmationQueryParams(string tag) + { + 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 e) + { + 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; } + } + } +} diff --git a/SteamAuth/SteamWeb.cs b/SteamAuth/SteamWeb.cs new file mode 100644 index 0000000000000..6f681b3044ee4 --- /dev/null +++ b/SteamAuth/SteamWeb.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; + +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 ex) + { + return null; + } + } + } +} diff --git a/SteamAuth/TimeAligner.cs b/SteamAuth/TimeAligner.cs new file mode 100644 index 0000000000000..0a4d4f98eb212 --- /dev/null +++ b/SteamAuth/TimeAligner.cs @@ -0,0 +1,56 @@ +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 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 e) + { + 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 new file mode 100644 index 0000000000000..2021a621fd07d --- /dev/null +++ b/SteamAuth/UserLogin.cs @@ -0,0 +1,240 @@ +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) 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.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.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; + } + + return LoginResult.GeneralFailure; + } + + 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; } + + 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, + } +} diff --git a/SteamAuth/Util.cs b/SteamAuth/Util.cs new file mode 100644 index 0000000000000..0574e7d5869c2 --- /dev/null +++ b/SteamAuth/Util.cs @@ -0,0 +1,24 @@ +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; + } + } +} diff --git a/SteamAuth/packages.config b/SteamAuth/packages.config new file mode 100644 index 0000000000000..43da9234aa9ba --- /dev/null +++ b/SteamAuth/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file