From de3d705845b1843ff2320b32bdfa66d44334e97c Mon Sep 17 00:00:00 2001 From: jesterret Date: Sat, 16 Nov 2019 03:46:52 +0100 Subject: [PATCH] Implementation of ISteamUser.GetAuthSessionTicket for SteamKit --- .../Handlers/SteamAuthTicket/Callbacks.cs | 82 +++++++ .../SteamAuthTicket/SteamAuthTicket.cs | 210 ++++++++++++++++++ .../Handlers/SteamAuthTicket/TicketInfo.cs | 37 +++ .../Steam/SteamClient/SteamClient.cs | 1 + 4 files changed, 330 insertions(+) create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/Callbacks.cs create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/SteamAuthTicket.cs create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/TicketInfo.cs diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/Callbacks.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/Callbacks.cs new file mode 100644 index 000000000..00f347314 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/Callbacks.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using SteamKit2.Internal; + +namespace SteamKit2 +{ + public sealed partial class SteamAuthTicket + { + /// + /// This callback is fired when Steam accepts our auth ticket as valid. + /// + internal sealed class TicketAcceptedCallback : CallbackMsg + { + /// + /// of AppIDs of the games that have generated tickets. + /// + public List AppIDs { get; private set; } + /// + /// of CRC32 hashes of activated tickets. + /// + public List ActiveTicketsCRC { get; private set; } + /// + /// Number of message in sequence. + /// + public uint MessageSequence { get; private set; } + + internal TicketAcceptedCallback( JobID jobId, CMsgClientAuthListAck body ) + { + JobID = jobId; + AppIDs = body.app_ids; + ActiveTicketsCRC = body.ticket_crc; + MessageSequence = body.message_sequence; + } + } + + /// + /// This callback is fired when generated ticket was successfully used to authenticate user. + /// + internal sealed class TicketAuthCompleteCallback : CallbackMsg + { + /// + /// Steam response to authentication request. + /// + public EAuthSessionResponse AuthSessionResponse { get; } + /// + /// Authentication state. + /// + public uint State { get; } + /// + /// ID of the game the token was generated for. + /// + public GameID GameID { get; } + /// + /// of the game owner. + /// + public SteamID OwnerSteamID { get; } + /// + /// of the game server. + /// + public SteamID SteamID { get; } + /// + /// CRC of the ticket. + /// + public uint TicketCRC { get; } + /// + /// Sequence of the ticket. + /// + public uint TicketSequence { get; } + + internal TicketAuthCompleteCallback( JobID targetJobID, CMsgClientTicketAuthComplete body ) + { + JobID = targetJobID; + AuthSessionResponse = ( EAuthSessionResponse )body.eauth_session_response; + State = body.estate; + GameID = body.game_id; + OwnerSteamID = body.owner_steam_id; + SteamID = body.steam_id; + TicketCRC = body.ticket_crc; + TicketSequence = body.ticket_sequence; + } + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/SteamAuthTicket.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/SteamAuthTicket.cs new file mode 100644 index 000000000..a7d4ef838 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/SteamAuthTicket.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using SteamKit2.Internal; + +namespace SteamKit2 +{ + /// + /// This handler generates auth session ticket and handles it's verification by steam. + /// + public sealed partial class SteamAuthTicket : ClientMsgHandler + { + private readonly Dictionary> dispatchMap; + private readonly ConcurrentQueue gameConnectTokens = new ConcurrentQueue(); + private readonly ConcurrentDictionary> ticketsByGame = new ConcurrentDictionary>(); + + internal SteamAuthTicket() + { + dispatchMap = new Dictionary> + { + { EMsg.ClientAuthListAck, HandleTicketAcknowledged }, + { EMsg.ClientTicketAuthComplete, HandleTicketAuthComplete }, + { EMsg.ClientGameConnectTokens, HandleGameConnectTokens }, + { EMsg.ClientLogOnResponse, HandleLogOnResponse } + }; + } + + /// + /// Generate session ticket, and verify it with steam servers. + /// + /// The appid to request the ticket of. + /// null if user isn't fully logged in, doesn't own the game, or steam deemed ticket invalid; otherwise instance. + public async Task GetAuthSessionTicket( uint appid ) + { + // not logged in + if ( Client.CellID == null ) + { + return null; + } + + var apps = Client.GetHandler()!; + var appTicket = await apps.GetAppOwnershipTicket( appid ); + // user doesn't own the game + if ( appTicket.Result != EResult.OK ) + { + return null; + } + + if ( gameConnectTokens.TryDequeue( out var token ) ) + { + byte[] authToken = CreateAuthToken( token ); + var ticketTask = await VerifyTicket( appid, authToken, out var crc ); + // verify the ticket is on the list of accepted tickets + // didn't happen on my testing, but I don't think it hurts to check + if ( ticketTask.ActiveTicketsCRC.Any( x => x == crc ) ) + { + return new TicketInfo( this, appid, crc, BuildTicket( authToken, appTicket.Ticket ) ); + } + } + return null; + } + internal bool CancelAuthTicket( TicketInfo ticket ) + { + if(ticketsByGame.TryGetValue(ticket.AppID, out var values)) + { + if ( values.RemoveAll( x => x.ticket_crc == ticket.CRC ) > 0 ) + { + SendTickets(); + } + } + return false; + } + + private byte[] BuildTicket( byte[] authToken, byte[] appTicket ) + { + using ( var stream = new MemoryStream( authToken.Length + 4 + appTicket.Length ) ) + { + using ( var writer = new BinaryWriter( stream ) ) + { + writer.Write( authToken ); + writer.Write( appTicket.Length ); + writer.Write( appTicket ); + } + return stream.ToArray(); + } + } + private byte[] CreateAuthToken( byte[] gameConnectToken ) + { + const int sessionSize = + 4 + // unknown 1 + 4 + // unknown 2 + 4 + // external IP + 4 + // padding + 4 + // connection time + 4; // connection count + + // We checked that we're connected before calling this function + uint ipAddress = NetHelpers.GetIPAddress( Client.PublicIP! ); + int connectionTime = ( int )( ( DateTime.UtcNow - serverTime ).TotalMilliseconds ); + using ( var stream = new MemoryStream( 4 + gameConnectToken.Length + 4 + sessionSize ) ) + { + using ( var writer = new BinaryWriter( stream ) ) + { + writer.Write( gameConnectToken.Length ); + writer.Write( gameConnectToken.ToArray() ); + + writer.Write( sessionSize ); + writer.Write( 1 ); + writer.Write( 2 ); + + writer.Write( ipAddress ); + writer.Write( 0 ); // padding + writer.Write( connectionTime ); // in milliseconds + writer.Write( 1 ); // single client connected + } + + return stream.ToArray(); + } + } + + private AsyncJob VerifyTicket( uint appid, byte[] authToken, out uint crc ) + { + crc = Crc32.Compute( authToken ); + var items = ticketsByGame.GetOrAdd( appid, new List() ); + + // add ticket to specified games list + items.Add( new CMsgAuthTicket + { + gameid = appid, + ticket = authToken, + ticket_crc = crc + } ); + return SendTickets(); + } + private AsyncJob SendTickets() + { + var auth = new ClientMsgProtobuf( EMsg.ClientAuthList ); + auth.Body.tokens_left = ( uint )gameConnectTokens.Count; + // all registered games + auth.Body.app_ids.AddRange( ticketsByGame.Keys ); + // flatten all registered per-game tickets + auth.Body.tickets.AddRange( ticketsByGame.Values.SelectMany( x => x ) ); + auth.SourceJobID = Client.GetNextJobID(); + Client.Send( auth ); + return new AsyncJob( Client, auth.SourceJobID ); + } + + /// + /// Handles a client message. This should not be called directly. + /// + /// The packet message that contains the data. + public override void HandleMsg( IPacketMsg packetMsg ) + { + if ( packetMsg == null ) + { + throw new ArgumentNullException( nameof( packetMsg ) ); + } + + if ( dispatchMap.TryGetValue( packetMsg.MsgType, out var handlerFunc ) ) + { + handlerFunc( packetMsg ); + } + } + + #region ClientMsg Handlers + private void HandleLogOnResponse( IPacketMsg packetMsg ) + { + var body = new ClientMsgProtobuf( packetMsg ).Body; + // just grabbing server time + serverTime = DateUtils.DateTimeFromUnixTime( body.rtime32_server_time ); + } + private void HandleGameConnectTokens( IPacketMsg packetMsg ) + { + var body = new ClientMsgProtobuf( packetMsg ).Body; + + // add tokens + foreach ( var tok in body.tokens ) + { + gameConnectTokens.Enqueue( tok ); + } + + // keep only required amount, discard old entries + while ( gameConnectTokens.Count > body.max_tokens_to_keep ) + { + gameConnectTokens.TryDequeue( out _ ); + } + } + private void HandleTicketAuthComplete( IPacketMsg packetMsg ) + { + // ticket successfully used to authorize user + var complete = new ClientMsgProtobuf( packetMsg ); + var inUse = new TicketAuthCompleteCallback( complete.TargetJobID, complete.Body ); + Client.PostCallback( inUse ); + } + private void HandleTicketAcknowledged( IPacketMsg packetMsg ) + { + // ticket acknowledged as valid by steam + var authAck = new ClientMsgProtobuf( packetMsg ); + var acknowledged = new TicketAcceptedCallback( authAck.TargetJobID, authAck.Body ); + Client.PostCallback( acknowledged ); + } + #endregion + + private DateTime serverTime; + + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/TicketInfo.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/TicketInfo.cs new file mode 100644 index 000000000..5d2a6c1a4 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/TicketInfo.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SteamKit2 +{ + /// + /// Represents a valid authorized session ticket. + /// + public class TicketInfo : IDisposable + { + internal uint AppID { get; } + internal uint CRC { get; } + /// + /// Bytes of the valid Session Ticket + /// + public byte[] Ticket { get; } + + internal TicketInfo( SteamAuthTicket handler, uint appid, uint crc, byte[] ticket ) + { + _handler = handler; + AppID = appid; + CRC = crc; + Ticket = ticket; + } + + /// + /// Tell steam we no longer use the ticket. + /// + public void Dispose() + { + _handler.CancelAuthTicket( this ); + } + + private readonly SteamAuthTicket _handler; + } +} diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index b6029725f..e813a3160 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -92,6 +92,7 @@ public SteamClient( SteamConfiguration configuration, string identifier ) this.AddHandler( new SteamScreenshots() ); this.AddHandler( new SteamMatchmaking() ); this.AddHandler( new SteamNetworking() ); + this.AddHandler( new SteamAuthTicket() ); using ( var process = Process.GetCurrentProcess() ) {