diff --git a/src/Prospect.Server.Api/Controllers/ClientController.cs b/src/Prospect.Server.Api/Controllers/ClientController.cs index c807999..a010d3b 100644 --- a/src/Prospect.Server.Api/Controllers/ClientController.cs +++ b/src/Prospect.Server.Api/Controllers/ClientController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Prospect.Server.Api.Models.Client; using Prospect.Server.Api.Models.Client.Data; +using Prospect.Server.Steam; namespace Prospect.Server.Api.Controllers { @@ -23,54 +24,58 @@ namespace Prospect.Server.Api.Controllers if (!string.IsNullOrEmpty(request.SteamTicket)) { - return Ok(new ClientResponse + var ticket = AppTicket.Parse(request.SteamTicket); + if (ticket.IsValid && ticket.HasValidSignature) { - Code = 200, - Status = "OK", - Data = new FServerLoginResult + return Ok(new ClientResponse { - EntityToken = new FEntityTokenResponse + Code = 200, + Status = "OK", + Data = new FServerLoginResult { - Entity = new FEntityKey + EntityToken = new FEntityTokenResponse { - Id = playerIdTwo, - Type = "title_player_account", - TypeString = "title_player_account" + Entity = new FEntityKey + { + Id = playerIdTwo, + Type = "title_player_account", + TypeString = "title_player_account" + }, + EntityToken = "RW50aXR5VG9rZW4=", + TokenExpiration = DateTime.UtcNow.AddDays(1), }, - EntityToken = "RW50aXR5VG9rZW4=", - TokenExpiration = DateTime.UtcNow.AddDays(1), - }, - InfoResultPayload = new FGetPlayerCombinedInfoResultPayload - { - CharacterInventories = new List(), - PlayerProfile = new FPlayerProfileModel + InfoResultPayload = new FGetPlayerCombinedInfoResultPayload { - DisplayName = playerName, - PlayerId = playerIdOne, - PublisherId = publisherId, - TitleId = titleId + CharacterInventories = new List(), + PlayerProfile = new FPlayerProfileModel + { + DisplayName = playerName, + PlayerId = playerIdOne, + PublisherId = publisherId, + TitleId = titleId + }, + UserDataVersion = 0, + UserInventory = new List(), + UserReadOnlyDataVersion = 0 }, - UserDataVersion = 0, - UserInventory = new List(), - UserReadOnlyDataVersion = 0 - }, - LastLoginTime = DateTime.UtcNow, - NewlyCreated = false, - PlayFabId = playerIdOne, - SessionTicket = "SOME", - SettingsForUser = new FUserSettings - { - GatherDeviceInfo = true, - GatherFocusInfo = true, - NeedsAttribution = false, - }, - TreatmentAssignment = new FTreatmentAssignment - { - Variables = new List(), - Variants = new List() + LastLoginTime = DateTime.UtcNow, + NewlyCreated = false, + PlayFabId = playerIdOne, + SessionTicket = "SOME", + SettingsForUser = new FUserSettings + { + GatherDeviceInfo = true, + GatherFocusInfo = true, + NeedsAttribution = false, + }, + TreatmentAssignment = new FTreatmentAssignment + { + Variables = new List(), + Variants = new List() + } } - } - }); + }); + } } return BadRequest(new ClientResponse diff --git a/src/Prospect.Server.Api/Prospect.Server.Api.csproj b/src/Prospect.Server.Api/Prospect.Server.Api.csproj index 6fab24b..ead6723 100644 --- a/src/Prospect.Server.Api/Prospect.Server.Api.csproj +++ b/src/Prospect.Server.Api/Prospect.Server.Api.csproj @@ -8,4 +8,8 @@ + + + + diff --git a/src/Prospect.Server.Steam/AppDlc.cs b/src/Prospect.Server.Steam/AppDlc.cs new file mode 100644 index 0000000..9bbdf41 --- /dev/null +++ b/src/Prospect.Server.Steam/AppDlc.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Prospect.Server.Steam +{ + public class AppDlc + { + public uint AppId { get; set; } + + public List Licenses { get; set; } + } +} \ No newline at end of file diff --git a/src/Prospect.Server.Steam/AppTicket.cs b/src/Prospect.Server.Steam/AppTicket.cs new file mode 100644 index 0000000..fa2dde9 --- /dev/null +++ b/src/Prospect.Server.Steam/AppTicket.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; + +namespace Prospect.Server.Steam +{ + public class AppTicket + { + private AppTicket() + { + + } + + /// + /// Full appticket, with a GC token and session header. + /// + public byte[] AuthTicket { get; set; } + + public ulong GcToken { get; set; } + + public DateTimeOffset TokenGenerated { get; set; } + + private IPAddress SessionExternalIp { get; set; } + + /// + /// Time the client has been connected to Steam in ms. + /// + public uint ClientConnectionTime { get; set; } + + /// + /// How many servers the client has connected to. + /// + public uint ClientConnectionCount { get; set; } + + public uint Version { get; set; } + + public ulong SteamId { get; set; } + + public uint AppId { get; set; } + + public IPAddress OwnershipTicketExternalIp { get; set; } + + public IPAddress OwnershipTicketInternalIp { get; set; } + + public uint OwnershipFlags { get; set; } + + public DateTimeOffset OwnershipTicketGenerated { get; set; } + + public DateTimeOffset OwnershipTicketExpires { get; set; } + + public List Licenses { get; set; } + + public List Dlc { get; set; } + + public byte[] Signature { get; set; } + + public bool IsExpired { get; set; } + + public bool HasValidSignature { get; set; } + + public bool IsValid { get; set; } + + public static AppTicket Parse(string ticketHex) + { + var result = new AppTicket(); + var ticketBytes = Convert.FromHexString(ticketHex); + + using (var stream = new MemoryStream(ticketBytes)) + using (var ticketReader = new BinaryReader(stream)) + { + // AuthTicket + var initialLength = ticketReader.ReadUInt32(); + if (initialLength == 20) + { + result.AuthTicket = ticketBytes.AsSpan(0, 52).ToArray(); + result.GcToken = ticketReader.ReadUInt64(); + + ticketReader.BaseStream.Position += 8; + + result.TokenGenerated = DateTimeOffset.FromUnixTimeSeconds(ticketReader.ReadUInt32()); + + if (ticketReader.ReadUInt32() != 24) + { + return null; + } + + ticketReader.BaseStream.Position += 8; + + result.SessionExternalIp = new IPAddress(BitConverter.GetBytes(ticketReader.ReadUInt32())); + + ticketReader.BaseStream.Position += 4; + + result.ClientConnectionTime = ticketReader.ReadUInt32(); + result.ClientConnectionCount = ticketReader.ReadUInt32(); + + if (ticketReader.ReadUInt32() + ticketReader.BaseStream.Position != ticketReader.BaseStream.Length) + { + return null; + } + } + else + { + ticketReader.BaseStream.Position -= 4; + } + + // Ownership ticket + var ownershipTicketOffset = ticketReader.BaseStream.Position; + var ownershipTicketLength = ticketReader.ReadUInt32(); + if (ownershipTicketOffset + ownershipTicketLength != ticketReader.BaseStream.Length && + ownershipTicketOffset + ownershipTicketLength + 128 != ticketReader.BaseStream.Length) + { + return null; + } + + result.Version = ticketReader.ReadUInt32(); + result.SteamId = ticketReader.ReadUInt64(); + result.AppId = ticketReader.ReadUInt32(); + result.OwnershipTicketExternalIp = new IPAddress(BitConverter.GetBytes(ticketReader.ReadUInt32())); + result.OwnershipTicketInternalIp = new IPAddress(BitConverter.GetBytes(ticketReader.ReadUInt32())); + result.OwnershipFlags = ticketReader.ReadUInt32(); + result.OwnershipTicketGenerated = DateTimeOffset.FromUnixTimeSeconds(ticketReader.ReadUInt32()); + result.OwnershipTicketExpires = DateTimeOffset.FromUnixTimeSeconds(ticketReader.ReadUInt32()); + result.Licenses = new List(); + + var licenseCount = ticketReader.ReadUInt16(); + for (var i = 0; i < licenseCount; i++) + { + result.Licenses.Add(ticketReader.ReadUInt32()); + } + + result.Dlc = new List(); + + var dlcCount = ticketReader.ReadUInt16(); + for (var i = 0; i < dlcCount; i++) + { + var dlc = new AppDlc(); + + dlc.AppId = ticketReader.ReadUInt32(); + dlc.Licenses = new List(); + + var dlcLicenseCount = ticketReader.ReadUInt16(); + for (var j = 0; j < dlcLicenseCount; j++) + { + dlc.Licenses.Add(ticketReader.ReadUInt32()); + } + + result.Dlc.Add(dlc); + } + + ticketReader.ReadUInt16(); + + if (ticketReader.BaseStream.Position + 128 == ticketReader.BaseStream.Length) + { + result.Signature = ticketBytes.AsSpan((int)ticketReader.BaseStream.Position, 128).ToArray(); + } + + var date = DateTimeOffset.UtcNow; + result.IsExpired = result.OwnershipTicketExpires < date; + result.HasValidSignature = result.Signature != null && SteamCrypto.VerifySignature(ticketBytes.AsSpan((int)ownershipTicketOffset, (int)ownershipTicketLength).ToArray(), result.Signature); + result.IsValid = !result.IsExpired && (result.Signature == null || result.HasValidSignature); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Prospect.Server.Steam/Prospect.Server.Steam.csproj b/src/Prospect.Server.Steam/Prospect.Server.Steam.csproj new file mode 100644 index 0000000..cbfa581 --- /dev/null +++ b/src/Prospect.Server.Steam/Prospect.Server.Steam.csproj @@ -0,0 +1,7 @@ + + + + net5.0 + + + diff --git a/src/Prospect.Server.Steam/SteamCrypto.cs b/src/Prospect.Server.Steam/SteamCrypto.cs new file mode 100644 index 0000000..1e3f005 --- /dev/null +++ b/src/Prospect.Server.Steam/SteamCrypto.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; + +namespace Prospect.Server.Steam +{ + internal static class SteamCrypto + { + private const string SystemCertificate = "-----BEGIN PUBLIC KEY-----\n" + + "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDf7BrWLBBmLBc1OhSwfFkRf53T\n" + + "2Ct64+AVzRkeRuh7h3SiGEYxqQMUeYKO6UWiSRKpI2hzic9pobFhRr3Bvr/WARvY\n" + + "gdTckPv+T1JzZsuVcNfFjrocejN1oWI0Rrtgt4Bo+hOneoo3S57G9F1fOpn5nsQ6\n" + + "6WOiu4gZKODnFMBCiQIBEQ==\n" + + "-----END PUBLIC KEY-----" ; + + public static bool VerifySignature(byte[] data, byte[] signature) + { + var rsa = new RSACryptoServiceProvider(); + var hash = new SHA1Managed(); + + rsa.ImportFromPem(SystemCertificate); + + var dataOk = rsa.VerifyData(data, CryptoConfig.MapNameToOID("SHA1")!, signature); + if (!dataOk) + { + return false; + } + + var dataHashed = hash.ComputeHash(data); + var dataVerified = rsa.VerifyHash(dataHashed, CryptoConfig.MapNameToOID("SHA1")!, signature); + + return dataVerified; + } + } +} \ No newline at end of file diff --git a/src/Prospect.sln b/src/Prospect.sln index d4d1cb2..624c722 100644 --- a/src/Prospect.sln +++ b/src/Prospect.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Launcher", "Prospe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Server.Api", "Prospect.Server.Api\Prospect.Server.Api.csproj", "{A539B020-8BEC-497F-9176-DC2665D927A1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Server.Steam", "Prospect.Server.Steam\Prospect.Server.Steam.csproj", "{CF1CFAE8-B0B3-401A-9706-35AF54249E67}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -39,6 +41,14 @@ Global {A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x64.Build.0 = Release|Any CPU {A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x86.ActiveCfg = Release|Any CPU {A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x86.Build.0 = Release|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Debug|x64.Build.0 = Debug|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Debug|x86.Build.0 = Debug|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Release|x64.ActiveCfg = Release|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Release|x64.Build.0 = Release|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Release|x86.ActiveCfg = Release|Any CPU + {CF1CFAE8-B0B3-401A-9706-35AF54249E67}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE