Add steam ticket parser
This commit is contained in:
@@ -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
|
||||||
{
|
{
|
||||||
@@ -22,6 +23,9 @@ namespace Prospect.Server.Api.Controllers
|
|||||||
var publisherId = "850902E5B40508ED";
|
var publisherId = "850902E5B40508ED";
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.SteamTicket))
|
if (!string.IsNullOrEmpty(request.SteamTicket))
|
||||||
|
{
|
||||||
|
var ticket = AppTicket.Parse(request.SteamTicket);
|
||||||
|
if (ticket.IsValid && ticket.HasValidSignature)
|
||||||
{
|
{
|
||||||
return Ok(new ClientResponse<FServerLoginResult>
|
return Ok(new ClientResponse<FServerLoginResult>
|
||||||
{
|
{
|
||||||
@@ -72,6 +76,7 @@ namespace Prospect.Server.Api.Controllers
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user