Implement PSN ticket reading

This commit is contained in:
jvyden 2022-01-27 16:50:08 -05:00
parent ef84bf1d50
commit 7081b725a8
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
14 changed files with 266 additions and 68 deletions

1
.gitignore vendored
View file

@ -28,6 +28,7 @@ gitVersion.txt
gitRemotes.txt
gitUnpushed.txt
logs/*
npTicket*
# MSBuild stuff
bin/

View file

@ -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));

View file

@ -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<IActionResult> Login([FromQuery] string? titleId)
public async Task<IActionResult> 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,

View file

@ -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<GameToken?> AuthenticateUser(LoginData loginData, string userLocation, string titleId = "")
public async Task<GameToken?> 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();

View file

@ -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);

View file

@ -1,45 +0,0 @@
#nullable enable
using System;
using System.IO;
using System.Text;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Types;
/// <summary>
/// The data sent from POST /LOGIN.
/// </summary>
public class LoginData
{
public static readonly string UsernamePrefix = Encoding.ASCII.GetString
(
new byte[]
{
0x04, 0x00, 0x20,
}
);
public string Username { get; set; } = null!;
/// <summary>
/// Converts a X-I-5 Ticket into `LoginData`.
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
/// </summary>
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;
}
}

View file

@ -5,4 +5,7 @@ public enum Platform
PS3 = 0,
RPCS3 = 1,
Vita = 2,
PSP = 3,
UnitTest = 4,
Unknown = -1,
}

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public struct DataHeader
{
public DataType Type;
public ushort Length;
}

View file

@ -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,
}

View file

@ -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;
/// <summary>
/// A PSN ticket, typically sent from PS3/RPCN
/// </summary>
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; }
/// <summary>
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
/// </summary>
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;
}
}
}

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public struct SectionHeader
{
public SectionType Type;
public ushort Length;
}

View file

@ -0,0 +1,7 @@
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
public enum SectionType : byte
{
Body = 0x00,
Footer = 0x02,
}

View file

@ -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();
}
}

View file

@ -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();
}