2015-12-11 22:53:28 +01:00
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
{
2016-01-28 23:54:25 +01:00
2015-12-11 22:53:28 +01:00
/// <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 ;
2015-12-16 01:30:02 +01:00
this . DeviceID = GenerateDeviceID ( ) ;
2015-12-11 22:53:28 +01:00
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 ) ;
2015-12-16 01:30:02 +01:00
if ( addAuthenticatorResponse = = null | | addAuthenticatorResponse . Response = = null )
{
return LinkResult . GeneralFailure ;
}
if ( addAuthenticatorResponse . Response . Status = = 29 )
{
return LinkResult . AuthenticatorPresent ;
}
if ( addAuthenticatorResponse . Response . Status ! = 1 )
2015-12-11 22:53:28 +01:00
{
return LinkResult . GeneralFailure ;
}
this . LinkedAccount = addAuthenticatorResponse . Response ;
LinkedAccount . Session = this . _session ;
LinkedAccount . DeviceID = this . DeviceID ;
return LinkResult . AwaitingFinalization ;
}
public FinalizeResult FinalizeAddAuthenticator ( string smsCode )
{
2016-01-28 23:54:25 +01:00
//The act of checking the SMS code is necessary for Steam to finalize adding the phone number to the account.
2016-02-16 08:11:07 +01:00
//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 ) )
2016-01-28 23:54:25 +01:00
{
return FinalizeResult . BadSMSCode ;
}
2015-12-11 22:53:28 +01:00
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 )
{
2016-01-28 23:54:25 +01:00
postData . Set ( "authenticator_code" , LinkedAccount . GenerateSteamGuardCode ( ) ) ;
postData . Set ( "authenticator_time" , TimeAligner . GetSteamTime ( ) . ToString ( ) ) ;
2015-12-11 22:53:28 +01:00
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 ;
}
2016-01-28 23:54:25 +01:00
if ( finalizeResponse . Response . Status = = 89 )
2015-12-11 22:53:28 +01:00
{
return FinalizeResult . BadSMSCode ;
}
2016-01-28 23:54:25 +01:00
if ( finalizeResponse . Response . Status = = 88 )
2015-12-11 22:53:28 +01:00
{
2016-01-28 23:54:25 +01:00
if ( tries > = 30 )
2015-12-11 22:53:28 +01:00
{
return FinalizeResult . UnableToGenerateCorrectCodes ;
}
}
if ( ! finalizeResponse . Response . Success )
{
return FinalizeResult . GeneralFailure ;
}
2016-01-28 23:54:25 +01:00
if ( finalizeResponse . Response . WantMore )
2015-12-11 22:53:28 +01:00
{
tries + + ;
continue ;
}
this . LinkedAccount . FullyEnrolled = true ;
return FinalizeResult . Success ;
}
return FinalizeResult . GeneralFailure ;
}
2016-01-28 23:54:25 +01:00
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 ;
}
2015-12-11 22:53:28 +01:00
private bool _addPhoneNumber ( )
{
2015-12-12 06:54:25 +01:00
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 ) ;
2015-12-11 22:53:28 +01:00
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" ) ;
2015-12-12 06:54:25 +01:00
postData . Add ( "sessionid" , _session . SessionID ) ;
string response = SteamWeb . Request ( APIEndpoints . COMMUNITY_BASE + "/steamguard/phoneajax" , "POST" , postData , _cookies ) ;
2015-12-11 22:53:28 +01:00
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
2015-12-16 01:30:02 +01:00
GeneralFailure , //General failure (really now!)
AuthenticatorPresent
2015-12-11 22:53:28 +01:00
}
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 ; }
}
2015-12-16 01:30:02 +01:00
public static string GenerateDeviceID ( )
2015-12-11 22:53:28 +01:00
{
using ( var sha1 = new SHA1Managed ( ) )
{
RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider ( ) ;
byte [ ] randomBytes = new byte [ 8 ] ;
secureRandom . GetBytes ( randomBytes ) ;
byte [ ] hashedBytes = sha1 . ComputeHash ( randomBytes ) ;
2015-12-16 01:30:02 +01:00
string random32 = BitConverter . ToString ( hashedBytes ) . Replace ( "-" , "" ) . Substring ( 0 , 32 ) . ToLower ( ) ;
return "android:" + SplitOnRatios ( random32 , new [ ] { 8 , 4 , 4 , 4 , 12 } , "-" ) ;
2015-12-11 22:53:28 +01:00
}
}
2015-12-16 01:30:02 +01:00
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 ;
}
2015-12-11 22:53:28 +01:00
}
}