Files
ArchiSteamFarm/SteamAuth/AuthenticatorLinker.cs
2016-02-16 08:11:07 +01:00

293 lines
10 KiB
C#

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)
{
return LinkResult.GeneralFailure;
}
if (addAuthenticatorResponse.Response.Status == 29)
{
return LinkResult.AuthenticatorPresent;
}
if (addAuthenticatorResponse.Response.Status != 1)
{
return LinkResult.GeneralFailure;
}
this.LinkedAccount = addAuthenticatorResponse.Response;
LinkedAccount.Session = this._session;
LinkedAccount.DeviceID = this.DeviceID;
return LinkResult.AwaitingFinalization;
}
public FinalizeResult FinalizeAddAuthenticator(string smsCode)
{
//The act of checking the SMS code is necessary for Steam to finalize adding the phone number to the account.
//Of course, we only want to check it if we're adding a phone number in the first place...
if (!String.IsNullOrEmpty(this.PhoneNumber) && !this._checkSMSCode(smsCode))
{
return FinalizeResult.BadSMSCode;
}
var postData = new NameValueCollection();
postData.Add("steamid", _session.SteamID.ToString());
postData.Add("access_token", _session.OAuthToken);
postData.Add("activation_code", smsCode);
int tries = 0;
while (tries <= 30)
{
postData.Set("authenticator_code", LinkedAccount.GenerateSteamGuardCode());
postData.Set("authenticator_time", TimeAligner.GetSteamTime().ToString());
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/FinalizeAddAuthenticator/v0001", "POST", postData);
if (response == null) return FinalizeResult.GeneralFailure;
var finalizeResponse = JsonConvert.DeserializeObject<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)
{
tries++;
continue;
}
this.LinkedAccount.FullyEnrolled = true;
return FinalizeResult.Success;
}
return FinalizeResult.GeneralFailure;
}
private bool _checkSMSCode(string smsCode)
{
var postData = new NameValueCollection();
postData.Add("op", "check_sms_code");
postData.Add("arg", smsCode);
postData.Add("sessionid", _session.SessionID);
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
if (response == null) return false;
var addPhoneNumberResponse = JsonConvert.DeserializeObject<AddPhoneResponse>(response);
return addPhoneNumberResponse.Success;
}
private bool _addPhoneNumber()
{
var postData = new NameValueCollection();
postData.Add("op", "add_phone_number");
postData.Add("arg", PhoneNumber);
postData.Add("sessionid", _session.SessionID);
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
if (response == null) return false;
var addPhoneNumberResponse = JsonConvert.DeserializeObject<AddPhoneResponse>(response);
return addPhoneNumberResponse.Success;
}
private bool _hasPhoneAttached()
{
var postData = new NameValueCollection();
postData.Add("op", "has_phone");
postData.Add("arg", "null");
postData.Add("sessionid", _session.SessionID);
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
if (response == null) return false;
var hasPhoneResponse = JsonConvert.DeserializeObject<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!)
AuthenticatorPresent
}
public enum FinalizeResult
{
BadSMSCode,
UnableToGenerateCorrectCodes,
Success,
GeneralFailure
}
private class AddAuthenticatorResponse
{
[JsonProperty("response")]
public SteamGuardAccount Response { get; set; }
}
private class FinalizeAuthenticatorResponse
{
[JsonProperty("response")]
public FinalizeAuthenticatorInternalResponse Response { get; set; }
internal class FinalizeAuthenticatorInternalResponse
{
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("server_time")]
public long ServerTime { get; set; }
[JsonProperty("want_more")]
public bool WantMore { get; set; }
[JsonProperty("success")]
public bool Success { get; set; }
}
}
private class HasPhoneResponse
{
[JsonProperty("has_phone")]
public bool HasPhone { get; set; }
}
private class AddPhoneResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
}
public static string GenerateDeviceID()
{
using (var sha1 = new SHA1Managed())
{
RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider();
byte[] randomBytes = new byte[8];
secureRandom.GetBytes(randomBytes);
byte[] hashedBytes = sha1.ComputeHash(randomBytes);
string random32 = BitConverter.ToString(hashedBytes).Replace("-", "").Substring(0, 32).ToLower();
return "android:" + SplitOnRatios(random32, new[] { 8, 4, 4, 4, 12 }, "-");
}
}
private static string SplitOnRatios(string str, int[] ratios, string intermediate)
{
string result = "";
int pos = 0;
for (int index = 0; index < ratios.Length; index++)
{
result += str.Substring(pos, ratios[index]);
pos = ratios[index];
if (index < ratios.Length - 1)
result += intermediate;
}
return result;
}
}
}