Squashed commit of ASF 2FA

Check https://github.com/JustArchi/ArchiSteamFarm/wiki/Escrow for more info
This commit is contained in:
JustArchi
2015-12-11 22:53:28 +01:00
parent 9a2a37f2fc
commit b3162ce390
23 changed files with 1441 additions and 29 deletions

View File

@@ -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

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
</startup>
</configuration>
</configuration>

View File

@@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ArchiSteamFarm</RootNamespace>
<AssemblyName>ArchiSteamFarm</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
@@ -26,6 +26,7 @@
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -123,6 +124,12 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SteamAuth\SteamAuth.csproj">
<Project>{5ad0934e-f6c4-4ae5-83af-c788313b2a87}</Project>
<Name>SteamAuth</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>

View File

@@ -192,7 +192,9 @@ namespace ArchiSteamFarm {
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 {

View File

@@ -22,6 +22,8 @@
*/
using Newtonsoft.Json;
using SteamAuth;
using SteamKit2;
using System;
using System.Collections.Concurrent;
@@ -37,13 +39,13 @@ namespace ArchiSteamFarm {
private static readonly ConcurrentDictionary<string, Bot> Bots = new ConcurrentDictionary<string, Bot>();
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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<uint> Blacklist { get; private set; } = new HashSet<uint> { 303700, 335590, 368020 };
internal bool Statistics { get; private set; } = true;
@@ -106,6 +110,8 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
CallbackManager.Subscribe<SteamFriends.FriendMsgCallback>(OnFriendMsg);
if (UseAsfAsMobileAuthenticator && File.Exists(MobileAuthenticatorFile)) {
SteamGuardAccount = JsonConvert.DeserializeObject<SteamGuardAccount>(File.ReadAllText(MobileAuthenticatorFile));
}
SteamUser = SteamClient.GetHandler<SteamUser>();
CallbackManager.Subscribe<SteamUser.AccountInfoCallback>(OnAccountInfo);
CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
CallbackManager.Subscribe<SteamUser.LoginKeyCallback>(OnLoginKey);
CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
CallbackManager.Subscribe<ArchiHandler.NotificationCallback>(OnNotification);
@@ -149,6 +160,90 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
Username = SteamLogin,
Password = SteamPassword,
AuthCode = AuthCode,
LoginKey = LoginKey,
TwoFactorCode = TwoFactorAuth,
SentryFileHash = sentryHash
SentryFileHash = sentryHash,
ShouldRememberPassword = true
});
}
@@ -477,6 +634,12 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
} 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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
}
}
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;

View File

@@ -34,8 +34,11 @@ namespace ArchiSteamFarm {
internal enum EUserInputType {
Login,
Password,
PhoneNumber,
SMS,
SteamGuard,
SteamParentalPIN,
RevocationCode,
TwoFactorAuthentication,
}
@@ -98,7 +101,7 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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 @@ namespace ArchiSteamFarm {
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) {

View File

@@ -39,7 +39,14 @@ namespace ArchiSteamFarm {
Declined,
InvalidItems,
EmailPending,
EmailCanceled
EmailCanceled,
OnHold
}
internal enum ETradeOfferConfirmationMethod {
Invalid,
Email,
MobileApp
}
internal string tradeofferid { get; set; }
@@ -53,6 +60,8 @@ namespace ArchiSteamFarm {
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;

View File

@@ -62,6 +62,7 @@ namespace ArchiSteamFarm {
}
await Task.WhenAll(tasks).ConfigureAwait(false);
Bot.AcceptAllConfirmations();
}
private async Task ParseTrade(SteamTradeOffer tradeOffer) {

View File

@@ -49,6 +49,13 @@
<!-- TIP: If you don't have your own farming group, most likely you don't want to change it, 0 means there is no master group defined -->
<SteamMasterClanID type="ulong" value="0"/>
<!-- This switch defines if you want to use built-in ASF two-factor-authentication (ASF 2FA) for this account -->
<!-- The one and only purpose for this function is automating steam logins and steam trades, so you won't need to enter 2FA codes all the time -->
<!-- This however defeats the whole purpose of two-factor-auth, and should be used on alt accounts ONLY -->
<!-- You should read full documentation, along with explanation here: https://github.com/JustArchi/ArchiSteamFarm/wiki/Escrow -->
<!-- WARNING, this option can potentially LOCK OUT YOUR ACCOUNT! If you don't fully understand this feature, DON'T USE IT! -->
<UseAsfAsMobileAuthenticator type="bool" value="false"/>
<!-- This switch defines if the account has card drops restricted -->
<!-- Restricted card drops means that the account doesn't receive any steam cards until it plays the game for at least 2 hours -->
<!-- As there is no magical way to detect it by ASF, I made this option config-based switch -->

View File

@@ -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 <KEY>` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat
- `!start <BOT>` Starts given bot instance, after it was ```!stop```pped
- `!status` Prints current status of ASF
- `!stop` Stops current bot instance
- `!stop <BOT>` Stops given bot instance
- `!2fa` Generates temporary 2FA token for current bot instance
- `!2fa <BOT>` Generates temporary 2FA token for given bot instance
- `!2faoff` Deactivates 2FA for current bot instance
- `!2faoff <BOT>` 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 <KEY>` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat
- `!start <BOT>` Starts given bot instance
- `!status` Prints current status of ASF
- `!stop` Stops current bot instance
- `!stop <BOT>` 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.

12
SteamAuth/APIEndpoints.cs Normal file
View File

@@ -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");
}
}

View File

@@ -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
{
/// <summary>
/// Handles the linking process for a new mobile authenticator.
/// </summary>
public class AuthenticatorLinker
{
/// <summary>
/// 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.
/// </summary>
public string PhoneNumber = null;
/// <summary>
/// Randomly-generated device ID. Should only be generated once per linker.
/// </summary>
public string DeviceID { get; private set; }
/// <summary>
/// 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.
/// </summary>
public SteamGuardAccount LinkedAccount { get; private set; }
/// <summary>
/// True if the authenticator has been fully finalized.
/// </summary>
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<AddAuthenticatorResponse>(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<FinalizeAuthenticatorResponse>(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<AddPhoneResponse>(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<HasPhoneResponse>(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("-", "");
}
}
}
}

15
SteamAuth/Confirmation.cs Normal file
View File

@@ -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;
}
}

View File

@@ -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")]

39
SteamAuth/SessionData.cs Normal file
View File

@@ -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"));
}
}
}

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>SteamAuth</RootNamespace>
<AssemblyName>SteamAuth</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="APIEndpoints.cs" />
<Compile Include="AuthenticatorLinker.cs" />
<Compile Include="Confirmation.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SessionData.cs" />
<Compile Include="SteamGuardAccount.cs" />
<Compile Include="SteamWeb.cs" />
<Compile Include="TimeAligner.cs" />
<Compile Include="UserLogin.cs" />
<Compile Include="Util.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

28
SteamAuth/SteamAuth.sln Normal file
View File

@@ -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

View File

@@ -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; }
/// <summary>
/// Set to true if the authenticator has actually been applied to the account.
/// </summary>
[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<RemoveAuthenticatorResponse>(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("<div>((Confirm|Trade with|Sell -) .+)</div>");
if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response)))
{
if (response == null || !response.Contains("<div>Nothing to confirm</div>"))
{
throw new WGTokenInvalidException();
}
return new Confirmation[0];
}
MatchCollection confIDs = confIDRegex.Matches(response);
MatchCollection confKeys = confKeyRegex.Matches(response);
MatchCollection confDescs = confDescRegex.Matches(response);
List<Confirmation> ret = new List<Confirmation>();
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");
}
/// <summary>
/// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed.
/// </summary>
/// <returns></returns>
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<RefreshSessionDataResponse>(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<SendConfirmationResponse>(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; }
}
}
}

81
SteamAuth/SteamWeb.cs Normal file
View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
namespace SteamAuth
{
public class SteamWeb
{
/// <summary>
/// Perform a mobile login request
/// </summary>
/// <param name="url">API url</param>
/// <param name="method">GET or POST</param>
/// <param name="data">Name-data pairs</param>
/// <param name="cookies">current cookie container</param>
/// <returns>response body</returns>
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;
}
}
}
}

56
SteamAuth/TimeAligner.cs Normal file
View File

@@ -0,0 +1,56 @@
using System.Net;
using Newtonsoft.Json;
namespace SteamAuth
{
/// <summary>
/// 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.
/// </summary>
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<TimeQuery>(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; }
}
}
}
}

240
SteamAuth/UserLogin.cs Normal file
View File

@@ -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
{
/// <summary>
/// Handles logging the user into the mobile Steam website. Necessary to generate OAuth token and session cookies.
/// </summary>
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<RSAResponse>(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<LoginResponse>(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<OAuth>(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,
}
}

24
SteamAuth/Util.cs Normal file
View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="8.0.1-beta3" targetFramework="net452" />
</packages>