Add steam ticket parser

This commit is contained in:
AeonLucid
2021-10-31 20:55:51 +01:00
parent 07cd3e9855
commit d37cb86b78
7 changed files with 277 additions and 40 deletions
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Prospect.Server.Api.Models.Client; using Prospect.Server.Api.Models.Client;
using Prospect.Server.Api.Models.Client.Data; using Prospect.Server.Api.Models.Client.Data;
using Prospect.Server.Steam;
namespace Prospect.Server.Api.Controllers namespace Prospect.Server.Api.Controllers
{ {
@@ -23,54 +24,58 @@ namespace Prospect.Server.Api.Controllers
if (!string.IsNullOrEmpty(request.SteamTicket)) if (!string.IsNullOrEmpty(request.SteamTicket))
{ {
return Ok(new ClientResponse<FServerLoginResult> var ticket = AppTicket.Parse(request.SteamTicket);
if (ticket.IsValid && ticket.HasValidSignature)
{ {
Code = 200, return Ok(new ClientResponse<FServerLoginResult>
Status = "OK",
Data = new FServerLoginResult
{ {
EntityToken = new FEntityTokenResponse Code = 200,
Status = "OK",
Data = new FServerLoginResult
{ {
Entity = new FEntityKey EntityToken = new FEntityTokenResponse
{ {
Id = playerIdTwo, Entity = new FEntityKey
Type = "title_player_account", {
TypeString = "title_player_account" Id = playerIdTwo,
Type = "title_player_account",
TypeString = "title_player_account"
},
EntityToken = "RW50aXR5VG9rZW4=",
TokenExpiration = DateTime.UtcNow.AddDays(1),
}, },
EntityToken = "RW50aXR5VG9rZW4=", InfoResultPayload = new FGetPlayerCombinedInfoResultPayload
TokenExpiration = DateTime.UtcNow.AddDays(1),
},
InfoResultPayload = new FGetPlayerCombinedInfoResultPayload
{
CharacterInventories = new List<object>(),
PlayerProfile = new FPlayerProfileModel
{ {
DisplayName = playerName, CharacterInventories = new List<object>(),
PlayerId = playerIdOne, PlayerProfile = new FPlayerProfileModel
PublisherId = publisherId, {
TitleId = titleId DisplayName = playerName,
PlayerId = playerIdOne,
PublisherId = publisherId,
TitleId = titleId
},
UserDataVersion = 0,
UserInventory = new List<object>(),
UserReadOnlyDataVersion = 0
}, },
UserDataVersion = 0, LastLoginTime = DateTime.UtcNow,
UserInventory = new List<object>(), NewlyCreated = false,
UserReadOnlyDataVersion = 0 PlayFabId = playerIdOne,
}, SessionTicket = "SOME",
LastLoginTime = DateTime.UtcNow, SettingsForUser = new FUserSettings
NewlyCreated = false, {
PlayFabId = playerIdOne, GatherDeviceInfo = true,
SessionTicket = "SOME", GatherFocusInfo = true,
SettingsForUser = new FUserSettings NeedsAttribution = false,
{ },
GatherDeviceInfo = true, TreatmentAssignment = new FTreatmentAssignment
GatherFocusInfo = true, {
NeedsAttribution = false, Variables = new List<FVariable>(),
}, Variants = new List<string>()
TreatmentAssignment = new FTreatmentAssignment }
{
Variables = new List<FVariable>(),
Variants = new List<string>()
} }
} });
}); }
} }
return BadRequest(new ClientResponse return BadRequest(new ClientResponse
@@ -8,4 +8,8 @@
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Prospect.Server.Steam\Prospect.Server.Steam.csproj" />
</ItemGroup>
</Project> </Project>
+11
View File
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Prospect.Server.Steam
{
public class AppDlc
{
public uint AppId { get; set; }
public List<uint> Licenses { get; set; }
}
}
+167
View File
@@ -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()
{
}
/// <summary>
/// Full appticket, with a GC token and session header.
/// </summary>
public byte[] AuthTicket { get; set; }
public ulong GcToken { get; set; }
public DateTimeOffset TokenGenerated { get; set; }
private IPAddress SessionExternalIp { get; set; }
/// <summary>
/// Time the client has been connected to Steam in ms.
/// </summary>
public uint ClientConnectionTime { get; set; }
/// <summary>
/// How many servers the client has connected to.
/// </summary>
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<uint> Licenses { get; set; }
public List<AppDlc> 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<uint>();
var licenseCount = ticketReader.ReadUInt16();
for (var i = 0; i < licenseCount; i++)
{
result.Licenses.Add(ticketReader.ReadUInt32());
}
result.Dlc = new List<AppDlc>();
var dlcCount = ticketReader.ReadUInt16();
for (var i = 0; i < dlcCount; i++)
{
var dlc = new AppDlc();
dlc.AppId = ticketReader.ReadUInt32();
dlc.Licenses = new List<uint>();
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;
}
}
}
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
+33
View File
@@ -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;
}
}
}
+10
View File
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Launcher", "Prospe
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Server.Api", "Prospect.Server.Api\Prospect.Server.Api.csproj", "{A539B020-8BEC-497F-9176-DC2665D927A1}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Server.Api", "Prospect.Server.Api\Prospect.Server.Api.csproj", "{A539B020-8BEC-497F-9176-DC2665D927A1}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Server.Steam", "Prospect.Server.Steam\Prospect.Server.Steam.csproj", "{CF1CFAE8-B0B3-401A-9706-35AF54249E67}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64 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|x64.Build.0 = Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE