(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 000000000..6f681b304
--- /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 000000000..0a4d4f98e
--- /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 000000000..2021a621f
--- /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 000000000..0574e7d58
--- /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 000000000..43da9234a
--- /dev/null
+++ b/SteamAuth/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file