From 7081b725a831b3cedc33e26ed0365b1384db4436 Mon Sep 17 00:00:00 2001 From: jvyden Date: Thu, 27 Jan 2022 16:50:08 -0500 Subject: [PATCH] Implement PSN ticket reading --- .gitignore | 1 + .../LighthouseServerTest.cs | 3 +- .../Controllers/LoginController.cs | 28 ++-- ProjectLighthouse/Database.cs | 15 +-- .../Extensions/BinaryReaderExtensions.cs | 4 + ProjectLighthouse/Types/LoginData.cs | 45 ------- ProjectLighthouse/Types/Platform.cs | 3 + ProjectLighthouse/Types/Tickets/DataHeader.cs | 7 + ProjectLighthouse/Types/Tickets/DataType.cs | 11 ++ ProjectLighthouse/Types/Tickets/NPTicket.cs | 125 ++++++++++++++++++ .../Types/Tickets/SectionHeader.cs | 7 + .../Types/Tickets/SectionType.cs | 7 + .../Types/Tickets/TicketReader.cs | 61 +++++++++ ProjectLighthouse/Types/Version.cs | 17 +++ 14 files changed, 266 insertions(+), 68 deletions(-) delete mode 100644 ProjectLighthouse/Types/LoginData.cs create mode 100644 ProjectLighthouse/Types/Tickets/DataHeader.cs create mode 100644 ProjectLighthouse/Types/Tickets/DataType.cs create mode 100644 ProjectLighthouse/Types/Tickets/NPTicket.cs create mode 100644 ProjectLighthouse/Types/Tickets/SectionHeader.cs create mode 100644 ProjectLighthouse/Types/Tickets/SectionType.cs create mode 100644 ProjectLighthouse/Types/Tickets/TicketReader.cs create mode 100644 ProjectLighthouse/Types/Version.cs diff --git a/.gitignore b/.gitignore index 1e2fd1a7..d6ee8a61 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ gitVersion.txt gitRemotes.txt gitUnpushed.txt logs/* +npTicket* # MSBuild stuff bin/ diff --git a/ProjectLighthouse.Tests/LighthouseServerTest.cs b/ProjectLighthouse.Tests/LighthouseServerTest.cs index 6a926c23..7b37d188 100644 --- a/ProjectLighthouse.Tests/LighthouseServerTest.cs +++ b/ProjectLighthouse.Tests/LighthouseServerTest.cs @@ -37,7 +37,8 @@ public class LighthouseServerTest await database.CreateUser($"{username}{number}", HashHelper.BCryptHash($"unitTestPassword{number}")); } - string stringContent = $"{LoginData.UsernamePrefix}{username}{number}{(char)0x00}"; + //TODO: generate actual tickets + string stringContent = $"unitTestTicket{username}{number}"; HttpResponseMessage response = await this.Client.PostAsync ($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent)); diff --git a/ProjectLighthouse/Controllers/LoginController.cs b/ProjectLighthouse/Controllers/LoginController.cs index 04a9d7df..c063e814 100644 --- a/ProjectLighthouse/Controllers/LoginController.cs +++ b/ProjectLighthouse/Controllers/LoginController.cs @@ -8,8 +8,10 @@ using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Settings; +using LBPUnion.ProjectLighthouse.Types.Tickets; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using IOFile = System.IO.File; namespace LBPUnion.ProjectLighthouse.Controllers; @@ -26,25 +28,29 @@ public class LoginController : ControllerBase } [HttpPost] - public async Task Login([FromQuery] string? titleId) + public async Task Login() { - titleId ??= ""; + MemoryStream ms = new(); + await this.Request.Body.CopyToAsync(ms); + byte[] loginData = ms.ToArray(); - string body = await new StreamReader(this.Request.Body).ReadToEndAsync(); + #if DEBUG + await IOFile.WriteAllBytesAsync($"npTicket-{TimestampHelper.TimestampMillis}.txt", loginData); + #endif - LoginData? loginData; + NPTicket? npTicket; try { - loginData = LoginData.CreateFromString(body); + npTicket = NPTicket.CreateFromBytes(loginData); } catch { - loginData = null; + npTicket = null; } - if (loginData == null) + if (npTicket == null) { - Logger.Log("loginData was null, rejecting login", LoggerLevelLogin.Instance); + Logger.Log("npTicket was null, rejecting login", LoggerLevelLogin.Instance); return this.BadRequest(); } @@ -60,11 +66,11 @@ public class LoginController : ControllerBase // Get an existing token from the IP & username GameToken? token = await this.database.GameTokens.Include (t => t.User) - .FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == loginData.Username && !t.Used); + .FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.Username && !t.Used); if (token == null) // If we cant find an existing token, try to generate a new one { - token = await this.database.AuthenticateUser(loginData, ipAddress, titleId); + token = await this.database.AuthenticateUser(npTicket, ipAddress); if (token == null) { Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance); @@ -129,7 +135,7 @@ public class LoginController : ControllerBase return this.StatusCode(403, ""); } - Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance); + Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client", LoggerLevelLogin.Instance); // After this point we are now considering this session as logged in. // We just logged in with the token. Mark it as used so someone else doesnt try to use it, diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 844fd6e5..365319fd 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -1,15 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using Kettu; using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Categories; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Reviews; using LBPUnion.ProjectLighthouse.Types.Settings; +using LBPUnion.ProjectLighthouse.Types.Tickets; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; @@ -67,9 +66,9 @@ public class Database : DbContext } #nullable enable - public async Task AuthenticateUser(LoginData loginData, string userLocation, string titleId = "") + public async Task AuthenticateUser(NPTicket npTicket, string userLocation) { - User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username); + User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username); if (user == null) return null; GameToken gameToken = new() @@ -78,15 +77,9 @@ public class Database : DbContext User = user, UserId = user.UserId, UserLocation = userLocation, - GameVersion = GameVersionHelper.FromTitleId(titleId), + GameVersion = npTicket.GameVersion, }; - if (gameToken.GameVersion == GameVersion.Unknown) - { - Logger.Log($"Unknown GameVersion for TitleId {titleId}", LoggerLevelLogin.Instance); - return null; - } - this.GameTokens.Add(gameToken); await this.SaveChangesAsync(); diff --git a/ProjectLighthouse/Helpers/Extensions/BinaryReaderExtensions.cs b/ProjectLighthouse/Helpers/Extensions/BinaryReaderExtensions.cs index feacec19..4a83bf42 100644 --- a/ProjectLighthouse/Helpers/Extensions/BinaryReaderExtensions.cs +++ b/ProjectLighthouse/Helpers/Extensions/BinaryReaderExtensions.cs @@ -23,6 +23,10 @@ public static class BinaryReaderExtensions public static int ReadInt32BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(int)).Reverse(), 0); + public static ulong ReadUInt64BE(this BinaryReader binRdr) => BitConverter.ToUInt32(binRdr.ReadBytesRequired(sizeof(ulong)).Reverse(), 0); + + public static long ReadInt64BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(long)).Reverse(), 0); + public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount) { byte[] result = binRdr.ReadBytes(byteCount); diff --git a/ProjectLighthouse/Types/LoginData.cs b/ProjectLighthouse/Types/LoginData.cs deleted file mode 100644 index ac57ac78..00000000 --- a/ProjectLighthouse/Types/LoginData.cs +++ /dev/null @@ -1,45 +0,0 @@ -#nullable enable -using System; -using System.IO; -using System.Text; -using LBPUnion.ProjectLighthouse.Helpers; - -namespace LBPUnion.ProjectLighthouse.Types; - -/// -/// The data sent from POST /LOGIN. -/// -public class LoginData -{ - - public static readonly string UsernamePrefix = Encoding.ASCII.GetString - ( - new byte[] - { - 0x04, 0x00, 0x20, - } - ); - - public string Username { get; set; } = null!; - - /// - /// Converts a X-I-5 Ticket into `LoginData`. - /// https://www.psdevwiki.com/ps3/X-I-5-Ticket - /// - public static LoginData? CreateFromString(string str) - { - str = str.Replace("\b", ""); // Remove backspace characters - - using MemoryStream ms = new(Encoding.ASCII.GetBytes(str)); - using BinaryReader reader = new(ms); - - if (!str.Contains(UsernamePrefix)) return null; - - LoginData loginData = new(); - - reader.BaseStream.Position = str.IndexOf(UsernamePrefix, StringComparison.Ordinal) + UsernamePrefix.Length; - loginData.Username = BinaryHelper.ReadString(reader).Replace("\0", string.Empty); - - return loginData; - } -} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Platform.cs b/ProjectLighthouse/Types/Platform.cs index 74fb1608..b863f285 100644 --- a/ProjectLighthouse/Types/Platform.cs +++ b/ProjectLighthouse/Types/Platform.cs @@ -5,4 +5,7 @@ public enum Platform PS3 = 0, RPCS3 = 1, Vita = 2, + PSP = 3, + UnitTest = 4, + Unknown = -1, } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Tickets/DataHeader.cs b/ProjectLighthouse/Types/Tickets/DataHeader.cs new file mode 100644 index 00000000..940f980c --- /dev/null +++ b/ProjectLighthouse/Types/Tickets/DataHeader.cs @@ -0,0 +1,7 @@ +namespace LBPUnion.ProjectLighthouse.Types.Tickets; + +public struct DataHeader +{ + public DataType Type; + public ushort Length; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Tickets/DataType.cs b/ProjectLighthouse/Types/Tickets/DataType.cs new file mode 100644 index 00000000..92da0543 --- /dev/null +++ b/ProjectLighthouse/Types/Tickets/DataType.cs @@ -0,0 +1,11 @@ +namespace LBPUnion.ProjectLighthouse.Types.Tickets; + +public enum DataType : byte +{ + Empty = 0x00, + UInt32 = 0x01, + UInt64 = 0x02, + String = 0x04, + Timestamp = 0x07, + Binary = 0x08, +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Tickets/NPTicket.cs b/ProjectLighthouse/Types/Tickets/NPTicket.cs new file mode 100644 index 00000000..dd1f1f3b --- /dev/null +++ b/ProjectLighthouse/Types/Tickets/NPTicket.cs @@ -0,0 +1,125 @@ +#nullable enable +using System; +using System.IO; +using System.Text; +using Kettu; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Helpers.Extensions; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Settings; + +namespace LBPUnion.ProjectLighthouse.Types.Tickets; + +/// +/// A PSN ticket, typically sent from PS3/RPCN +/// +public class NPTicket +{ + public string Username { get; set; } + + private Version ticketVersion { get; set; } + + public Platform Platform { get; set; } + + public uint IssuerId { get; set; } + public ulong IssuedDate { get; set; } + public ulong ExpireDate { get; set; } + + public GameVersion GameVersion { get; set; } + + /// + /// https://www.psdevwiki.com/ps3/X-I-5-Ticket + /// + public static NPTicket? CreateFromBytes(byte[] data) + { + #if DEBUG + if (data[0] == 'u' && ServerStatics.IsUnitTesting) + { + string dataStr = Encoding.UTF8.GetString(data); + if (dataStr.StartsWith("unitTestTicket")) + { + NPTicket npTicket = new() + { + IssuerId = 0, + ticketVersion = new Version(0, 0), + Platform = Platform.UnitTest, + GameVersion = GameVersion.LittleBigPlanet2, + ExpireDate = 0, + IssuedDate = 0, + }; + + npTicket.Username = dataStr.Substring(14); + + return npTicket; + } + } + #endif + try + { + NPTicket npTicket = new(); + using MemoryStream ms = new(data); + using TicketReader reader = new(ms); + + npTicket.ticketVersion = reader.ReadTicketVersion(); + + reader.ReadBytes(4); // Skip header + + reader.ReadUInt16BE(); // Ticket length, we don't care about this + + if (npTicket.ticketVersion != "2.1") throw new NotImplementedException(); + + #if DEBUG + SectionHeader bodyHeader = reader.ReadSectionHeader(); + Logger.Log($"bodyHeader.Type is {bodyHeader.Type}", LoggerLevelLogin.Instance); + #else + reader.ReadSectionHeader(); + #endif + + reader.ReadTicketString(); // "Serial id", but its apparently not what we're looking for + + npTicket.IssuerId = reader.ReadTicketUInt32(); + npTicket.IssuedDate = reader.ReadTicketUInt64(); + npTicket.ExpireDate = reader.ReadTicketUInt64(); + + reader.ReadTicketUInt64(); // PSN User id, we don't care about this + + npTicket.Username = reader.ReadTicketString(); + + reader.ReadTicketString(); // Country + reader.ReadTicketString(); // Domain + + // Title ID, kinda.. + // Data: "UP9000-BCUS98245_00 + string titleId = reader.ReadTicketString(); + titleId = titleId.Substring(7); // Trim UP9000- + titleId = titleId.Substring(0, titleId.Length - 3); // Trim _00 at the end + + #if DEBUG + Logger.Log($"titleId is {titleId}", LoggerLevelLogin.Instance); + #endif + + npTicket.GameVersion = GameVersionHelper.FromTitleId(titleId); // Finally, convert it to GameVersion + + // Production PSN Issuer ID: 0x100 + // RPCN Issuer ID: 0x33333333 + npTicket.Platform = npTicket.IssuerId switch + { + 0x100 => Platform.PS3, + 0x33333333 => Platform.RPCS3, + _ => Platform.Unknown, + }; + + if (npTicket.Platform == Platform.Unknown) + { + Logger.Log($"", LoggerLevelLogin.Instance); + return null; + } + + return npTicket; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Tickets/SectionHeader.cs b/ProjectLighthouse/Types/Tickets/SectionHeader.cs new file mode 100644 index 00000000..5242fe59 --- /dev/null +++ b/ProjectLighthouse/Types/Tickets/SectionHeader.cs @@ -0,0 +1,7 @@ +namespace LBPUnion.ProjectLighthouse.Types.Tickets; + +public struct SectionHeader +{ + public SectionType Type; + public ushort Length; +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Tickets/SectionType.cs b/ProjectLighthouse/Types/Tickets/SectionType.cs new file mode 100644 index 00000000..c7bb43a5 --- /dev/null +++ b/ProjectLighthouse/Types/Tickets/SectionType.cs @@ -0,0 +1,7 @@ +namespace LBPUnion.ProjectLighthouse.Types.Tickets; + +public enum SectionType : byte +{ + Body = 0x00, + Footer = 0x02, +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Tickets/TicketReader.cs b/ProjectLighthouse/Types/Tickets/TicketReader.cs new file mode 100644 index 00000000..47256adb --- /dev/null +++ b/ProjectLighthouse/Types/Tickets/TicketReader.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using System.IO; +using System.Text; +using JetBrains.Annotations; +using LBPUnion.ProjectLighthouse.Helpers.Extensions; + +namespace LBPUnion.ProjectLighthouse.Types.Tickets; + +public class TicketReader : BinaryReader +{ + public TicketReader([NotNull] Stream input) : base(input) + {} + + public Version ReadTicketVersion() => new(this.ReadByte() >> 4, this.ReadByte()); + + public SectionHeader ReadSectionHeader() + { + this.ReadByte(); + + SectionHeader sectionHeader = new(); + sectionHeader.Type = (SectionType)this.ReadByte(); + sectionHeader.Length = this.ReadUInt16BE(); + + return sectionHeader; + } + + public DataHeader ReadDataHeader() + { + DataHeader dataHeader = new(); + dataHeader.Type = (DataType)this.ReadUInt16BE(); + dataHeader.Length = this.ReadUInt16BE(); + + return dataHeader; + } + + public byte[] ReadTicketBinary() + { + DataHeader dataHeader = this.ReadDataHeader(); + Debug.Assert(dataHeader.Type == DataType.Binary || dataHeader.Type == DataType.String); + + return this.ReadBytes(dataHeader.Length); + } + + public string ReadTicketString() => Encoding.UTF8.GetString(this.ReadTicketBinary()).TrimEnd('\0'); + + public uint ReadTicketUInt32() + { + DataHeader dataHeader = this.ReadDataHeader(); + Debug.Assert(dataHeader.Type == DataType.UInt32); + + return this.ReadUInt32BE(); + } + + public ulong ReadTicketUInt64() + { + DataHeader dataHeader = this.ReadDataHeader(); + Debug.Assert(dataHeader.Type == DataType.UInt64 || dataHeader.Type == DataType.Timestamp); + + return this.ReadUInt64BE(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Version.cs b/ProjectLighthouse/Types/Version.cs new file mode 100644 index 00000000..eb9f3649 --- /dev/null +++ b/ProjectLighthouse/Types/Version.cs @@ -0,0 +1,17 @@ +namespace LBPUnion.ProjectLighthouse.Types; + +public class Version +{ + public int Major { get; set; } + public int Minor { get; set; } + + public Version(int major, int minor) + { + this.Major = major; + this.Minor = minor; + } + + public override string ToString() => $"{this.Major}.{this.Minor}"; + + public static implicit operator string(Version v) => v.ToString(); +} \ No newline at end of file