From 20b2ef5700c72c447a0a938eeded1632eb32b285 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 7 Jan 2023 00:12:39 -0600 Subject: [PATCH] Rework CI and add game server login tests (#615) * Make tests run in release mode * Fix multiple command in ci * I forgot how yaml works * Shitty workaround * grr mondays * Add an NP ticket builder for unit tests * Add NP ticket unit testing * Fix range indexers for finding uid * Fix LoginTests * Validate unit test signatures * Make builder code follow same style conventions * Remove remnant of hardcoded issuer id --- .github/workflows/ci.yml | 13 +- .../Tests/AuthenticationTests.cs | 1 - .../Tests/LoginTests.cs | 97 +++++++++++ .../DatabaseFactAttribute.cs | 6 +- .../LighthouseServerTest.cs | 24 ++- ProjectLighthouse/Tickets/NPTicket.cs | 124 +++++++------- ProjectLighthouse/Tickets/TicketBuilder.cs | 160 ++++++++++++++++++ ProjectLighthouse/Tickets/Types/BinaryData.cs | 23 +++ ProjectLighthouse/Tickets/Types/BlobData.cs | 29 ++++ ProjectLighthouse/Tickets/Types/EmptyData.cs | 15 ++ ProjectLighthouse/Tickets/Types/StringData.cs | 29 ++++ ProjectLighthouse/Tickets/Types/TicketData.cs | 21 +++ .../Tickets/Types/TimestampData.cs | 26 +++ ProjectLighthouse/Tickets/Types/UInt32Data.cs | 26 +++ ProjectLighthouse/Tickets/Types/UInt64Data.cs | 26 +++ 15 files changed, 540 insertions(+), 80 deletions(-) create mode 100644 ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs create mode 100644 ProjectLighthouse/Tickets/TicketBuilder.cs create mode 100644 ProjectLighthouse/Tickets/Types/BinaryData.cs create mode 100644 ProjectLighthouse/Tickets/Types/BlobData.cs create mode 100644 ProjectLighthouse/Tickets/Types/EmptyData.cs create mode 100644 ProjectLighthouse/Tickets/Types/StringData.cs create mode 100644 ProjectLighthouse/Tickets/Types/TicketData.cs create mode 100644 ProjectLighthouse/Tickets/Types/TimestampData.cs create mode 100644 ProjectLighthouse/Tickets/Types/UInt32Data.cs create mode 100644 ProjectLighthouse/Tickets/Types/UInt64Data.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bea676b..8a1b3db9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,20 +40,25 @@ jobs: dotnet-version: "7.0.x" - name: Compile - run: dotnet build -c Debug + run: | + dotnet restore + dotnet build -c Release --no-restore + dotnet build -c Release --no-restore --no-dependencies ProjectLighthouse.Tests + dotnet build -c Release --no-restore --no-dependencies ProjectLighthouse.Tests.GameApiTests + dotnet build -c Release --no-restore --no-dependencies ProjectLighthouse.Tests.WebsiteTests - name: Run tests on ProjectLighthouse.Tests continue-on-error: true - run: dotnet test --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-Tests.trx" ProjectLighthouse.Tests + run: dotnet test -c Release --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-Tests.trx" ProjectLighthouse.Tests - name: Run tests on ProjectLighthouse.Tests.GameApiTests continue-on-error: true - run: dotnet test --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-GameApiTests.trx" ProjectLighthouse.Tests.GameApiTests + run: dotnet test -c Release --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-GameApiTests.trx" ProjectLighthouse.Tests.GameApiTests - name: Run tests on ProjectLighthouse.Tests.WebsiteTests if: ${{ matrix.os.webTest }} continue-on-error: true - run: dotnet test --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-WebsiteTests.trx" ProjectLighthouse.Tests.WebsiteTests + run: dotnet test -c Release --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-WebsiteTests.trx" ProjectLighthouse.Tests.WebsiteTests # Attempt to upload results even if test fails. # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs b/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs index b81102d5..a14feba4 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs @@ -1,7 +1,6 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs b/ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs new file mode 100644 index 00000000..4c47b529 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse; +using LBPUnion.ProjectLighthouse.Administration; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; +using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tickets; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Tests; + +public class LoginTests : LighthouseServerTest +{ + [Fact] + public async Task ShouldLoginWithGoodTicket() + { + string username = await this.CreateRandomUser(); + ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + byte[] ticketData = new TicketBuilder() + .SetUsername(username) + .SetUserId(userId) + .Build(); + HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); + Assert.True(response.IsSuccessStatusCode); + } + + [Fact] + public async Task ShouldNotLoginWithExpiredTicket() + { + string username = await this.CreateRandomUser(); + ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + byte[] ticketData = new TicketBuilder() + .SetUsername(username) + .SetUserId(userId) + .setExpirationTime((ulong)TimeHelper.TimestampMillis - 1000 * 60) + .Build(); + HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); + Assert.False(response.IsSuccessStatusCode); + Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + } + + [Fact] + public async Task ShouldNotLoginWithBadTitleId() + { + string username = await this.CreateRandomUser(); + ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + byte[] ticketData = new TicketBuilder() + .SetUsername(username) + .SetUserId(userId) + .SetTitleId("UP9000-BLUS30079_00") + .Build(); + HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); + Assert.False(response.IsSuccessStatusCode); + Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + } + + [Fact] + public async Task ShouldNotLoginWithBadSignature() + { + string username = await this.CreateRandomUser(); + ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + byte[] ticketData = new TicketBuilder() + .SetUsername(username) + .SetUserId(userId) + .Build(); + ticketData[^21] = 0; + HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); + Assert.False(response.IsSuccessStatusCode); + Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + } + + [Fact] + public async Task ShouldNotLoginIfBanned() + { + string username = await this.CreateRandomUser(); + ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + await using Database database = new(); + User user = await database.Users.FirstAsync(u => u.Username == username); + user.PermissionLevel = PermissionLevel.Banned; + await database.SaveChangesAsync(); + + byte[] ticketData = new TicketBuilder() + .SetUsername(username) + .SetUserId(userId) + .Build(); + HttpResponseMessage response = + await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); + Assert.False(response.IsSuccessStatusCode); + Assert.True(response.StatusCode == HttpStatusCode.Forbidden); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests/DatabaseFactAttribute.cs b/ProjectLighthouse.Tests/DatabaseFactAttribute.cs index 5af29a56..5f34d624 100644 --- a/ProjectLighthouse.Tests/DatabaseFactAttribute.cs +++ b/ProjectLighthouse.Tests/DatabaseFactAttribute.cs @@ -10,8 +10,10 @@ public sealed class DatabaseFactAttribute : FactAttribute public DatabaseFactAttribute() { - ServerConfiguration.Instance = new ServerConfiguration(); - ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; + ServerConfiguration.Instance = new ServerConfiguration + { + DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse", + }; if (!ServerStatics.DbConnected) this.Skip = "Database not available"; else lock(migrateLock) diff --git a/ProjectLighthouse.Tests/LighthouseServerTest.cs b/ProjectLighthouse.Tests/LighthouseServerTest.cs index a30352dc..0a0f9159 100644 --- a/ProjectLighthouse.Tests/LighthouseServerTest.cs +++ b/ProjectLighthouse.Tests/LighthouseServerTest.cs @@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Serialization; +using LBPUnion.ProjectLighthouse.Tickets; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; @@ -25,27 +26,38 @@ public class LighthouseServerTest where TStartup : class this.Server = new TestServer(new WebHostBuilder().UseStartup()); this.Client = this.Server.CreateClient(); } - public async Task AuthenticateResponse(int number = -1, bool createUser = true) + + public async Task CreateRandomUser(int number = -1, bool createUser = true) { if (number == -1) number = new Random().Next(); - const string username = "unitTestUser"; + if (createUser) { await using Database database = new(); if (await database.Users.FirstOrDefaultAsync(u => u.Username == $"{username}{number}") == null) { - User user = await database.CreateUser($"{username}{number}", CryptoHelper.BCryptHash($"unitTestPassword{number}")); + User user = await database.CreateUser($"{username}{number}", + CryptoHelper.BCryptHash($"unitTestPassword{number}")); user.LinkedPsnId = (ulong)number; await database.SaveChangesAsync(); } } - //TODO: generate actual tickets - string stringContent = $"unitTestTicket{username}{number}"; + return $"{username}{number}"; + } + + public async Task AuthenticateResponse(int number = -1, bool createUser = true) + { + string username = await this.CreateRandomUser(number, createUser); + + byte[] ticketData = new TicketBuilder() + .SetUsername($"{username}{number}") + .SetUserId((ulong)number) + .Build(); HttpResponseMessage response = await this.Client.PostAsync - ($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent)); + ($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new ByteArrayContent(ticketData)); return response; } diff --git a/ProjectLighthouse/Tickets/NPTicket.cs b/ProjectLighthouse/Tickets/NPTicket.cs index b3c1eae0..0a73351c 100644 --- a/ProjectLighthouse/Tickets/NPTicket.cs +++ b/ProjectLighthouse/Tickets/NPTicket.cs @@ -46,33 +46,65 @@ public class NPTicket private byte[] ticketBody { get; set; } = Array.Empty(); private byte[] ticketSignature { get; set; } = Array.Empty(); + private byte[] ticketSignatureIdentifier { get; set; } = Array.Empty(); public GameVersion GameVersion { get; set; } private static ECDomainParameters FromX9EcParams(X9ECParameters param) => new(param.Curve, param.G, param.N, param.H, param.GetSeed()); - private static readonly ECDomainParameters secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1")); - private static readonly ECDomainParameters secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1")); + public static readonly ECDomainParameters Secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1")); + public static readonly ECDomainParameters Secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1")); - private static readonly ECPoint rpcnPublic = secp224K1.Curve.CreatePoint( + private static readonly ECPoint rpcnPublic = Secp224K1.Curve.CreatePoint( new BigInteger("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16), new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 16)); - private static readonly ECPoint psnPublic = secp192R1.Curve.CreatePoint( + private static readonly ECPoint psnPublic = Secp192R1.Curve.CreatePoint( new BigInteger("39c62d061d4ee35c5f3f7531de0af3cf918346526edac727", 16), new BigInteger("a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86", 16)); - private ECDomainParameters getCurveParams() => this.IsRpcn() ? secp224K1 : secp192R1; + private static readonly ECPoint unitTestPublic = Secp192R1.Curve.CreatePoint( + new BigInteger("b6f3374bde4ec23a25e1508889e7d7e71870ba74daf8654f", 16), + new BigInteger("738de93dad0fffb5642045439afaaf8c6fda319a72d2a584", 16)); - private ECPoint getPublicKey() => this.IsRpcn() ? rpcnPublic : psnPublic; + internal class SignatureParams + { + public string HashAlgo { get; set; } + public ECPoint PublicKey { get; set; } + public ECDomainParameters CurveParams { get; set; } + + public SignatureParams(string hashAlgo, ECPoint pubKey, ECDomainParameters curve) + { + this.HashAlgo = hashAlgo; + this.PublicKey = pubKey; + this.CurveParams = curve; + } + } + + private readonly Dictionary signatureParamsMap = new() + { + //psn + { "719F1D4A", new SignatureParams("SHA-1", psnPublic, Secp192R1) }, + //rpcn + { "5250434E", new SignatureParams("SHA-224", rpcnPublic, Secp224K1) }, + //unit test + { "54455354", new SignatureParams("SHA-1", unitTestPublic, Secp192R1) }, + }; private bool ValidateSignature() { - ECPublicKeyParameters pubKey = new(this.getPublicKey(), this.getCurveParams()); - string algo = this.IsRpcn() ? "SHA-224" : "SHA-1"; + string identifierHex = Convert.ToHexString(this.ticketSignatureIdentifier); + if (!this.signatureParamsMap.ContainsKey(identifierHex)) + { + Logger.Warn($"Unknown signature identifier in ticket: {identifierHex}, platform={this.Platform}", LogArea.Login); + return false; + } - ISigner signer = SignerUtilities.GetSigner($"{algo}withECDSA"); + SignatureParams sigParams = this.signatureParamsMap[identifierHex]; + ECPublicKeyParameters pubKey = new(sigParams.PublicKey, sigParams.CurveParams); + + ISigner signer = SignerUtilities.GetSigner($"{sigParams.HashAlgo}withECDSA"); signer.Init(false, pubKey); signer.BlockUpdate(this.ticketBody); @@ -80,18 +112,6 @@ public class NPTicket return signer.VerifySignature(this.ticketSignature); } - private bool IsRpcn() => this.IssuerId == 0x33333333; - - private static readonly Dictionary identifierByPlatform = new() - { - { - Platform.RPCS3, "RPCN"u8.ToArray() - }, - { - Platform.PS3, new byte[]{ 0x71, 0x9F, 0x1D, 0x4A, } - }, - }; - // Sometimes psn signatures have one or two extra empty bytes // This is slow but it's better than carelessly chopping 0's private static byte[] ParseSignature(byte[] signature) @@ -136,14 +156,7 @@ public class NPTicket reader.ReadSectionHeader(); // footer header - byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier - Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3; - byte[] platformIdent = identifierByPlatform[platform]; - if (!ticketIdent.SequenceEqual(platformIdent)) - { - Logger.Warn(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ticketIdent)} == {Convert.ToHexString(platformIdent)}", LogArea.Login); - return false; - } + npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary(); npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary()); return true; @@ -175,14 +188,7 @@ public class NPTicket reader.ReadSectionHeader(); // footer header - byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier - Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3; - byte[] platformIdent = identifierByPlatform[platform]; - if (!ticketIdent.SequenceEqual(platformIdent)) - { - Logger.Warn(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ticketIdent)} == {Convert.ToHexString(platformIdent)}", LogArea.Login); - return false; - } + npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary(); npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary()); return true; @@ -216,9 +222,14 @@ public class NPTicket if (!parsedSuccessfully) return false; - npTicket.ticketBody = npTicket.IsRpcn() - ? data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray() - : data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray(); + npTicket.ticketBody = Convert.ToHexString(npTicket.ticketSignatureIdentifier) switch + { + // rpcn + "5250434E" => data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray(), + // psn and unit test + "719F1D4A" or "54455354" => data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray(), + _ => throw new ArgumentOutOfRangeException(nameof(npTicket)), + }; return true; } @@ -229,28 +240,6 @@ public class NPTicket public static NPTicket? CreateFromBytes(byte[] data) { NPTicket npTicket = new(); - #if DEBUG - if (data[0] == 'u' && ServerStatics.IsUnitTesting) - { - string dataStr = Encoding.UTF8.GetString(data); - if (dataStr.StartsWith("unitTestTicket")) - { - npTicket = new NPTicket - { - IssuerId = 0, - ticketVersion = new Version(0, 0), - Platform = Platform.UnitTest, - GameVersion = GameVersion.LittleBigPlanet2, - ExpireDate = 0, - IssuedDate = 0, - Username = dataStr["unitTestTicket".Length..], - UserId = ulong.Parse(dataStr["unitTestTicketunitTestUser".Length..]), - }; - - return npTicket; - } - } - #endif try { using MemoryStream ms = new(data); @@ -263,14 +252,14 @@ public class NPTicket return null; } - if ((long)npTicket.IssuedDate > TimeHelper.TimestampMillis) + if (npTicket.IssuedDate > (ulong)TimeHelper.TimestampMillis) { - Logger.Warn($"Ticket isn't valid yet from {npTicket.Username}", LogArea.Login); + Logger.Warn($"Ticket isn't valid yet from {npTicket.Username} ({npTicket.IssuedDate} > {(ulong)TimeHelper.TimestampMillis})", LogArea.Login); return null; } - if (TimeHelper.TimestampMillis > (long)npTicket.ExpireDate) + if ((ulong)TimeHelper.TimestampMillis > npTicket.ExpireDate) { - Logger.Warn($"Ticket has expired from {npTicket.Username}", LogArea.Login); + Logger.Warn($"Ticket has expired from {npTicket.Username} ({(ulong)TimeHelper.TimestampMillis} > {npTicket.ExpireDate}", LogArea.Login); return null; } @@ -299,12 +288,13 @@ public class NPTicket { 0x100 => Platform.PS3, 0x33333333 => Platform.RPCS3, + 0x74657374 => Platform.UnitTest, _ => Platform.Unknown, }; if (npTicket.Platform == Platform.PS3 && npTicket.GameVersion == GameVersion.LittleBigPlanetVita) npTicket.Platform = Platform.Vita; - if (npTicket.Platform == Platform.Unknown) + if (npTicket.Platform == Platform.Unknown || (npTicket.Platform == Platform.UnitTest && !ServerStatics.IsUnitTesting)) { Logger.Warn($"Could not determine platform from IssuerId {npTicket.IssuerId} decimal", LogArea.Login); return null; diff --git a/ProjectLighthouse/Tickets/TicketBuilder.cs b/ProjectLighthouse/Tickets/TicketBuilder.cs new file mode 100644 index 00000000..cb48741f --- /dev/null +++ b/ProjectLighthouse/Tickets/TicketBuilder.cs @@ -0,0 +1,160 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Tickets.Types; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; + +namespace LBPUnion.ProjectLighthouse.Tickets; + +// generates v2.1 tickets +public class TicketBuilder +{ + private static uint _idDispenser; + + private string Username { get; set; } = "test"; + private string TitleId { get; set; } = "UP9000-BCUS98245_00"; + private ulong IssueTime { get; set; } = (ulong)TimeHelper.TimestampMillis; + private ulong ExpirationTime { get; set; } = (ulong)TimeHelper.TimestampMillis + 15 * 60 * 1000; + private uint IssuerId { get; set; } = 0x74657374; + private ulong UserId { get; set; } + private string Country { get; set; } = "br"; + private string Domain { get; set; } = "un"; + private uint Status { get; set; } + + private static readonly BigInteger privateKey = new("0edab5a79f0fff1cafd80849f620363ca15bcb89197f153d", 16); + + public byte[] Build() + { + uint ticketId = _idDispenser++; + byte[] serialBytes = new byte[0x14]; + BinaryPrimitives.WriteUInt32BigEndian(serialBytes, ticketId); + + byte[] onlineId = new byte[0x20]; + Encoding.ASCII.GetBytes(this.Username).CopyTo(onlineId, 0); + + byte[] country = new byte[0x4]; + Encoding.ASCII.GetBytes(this.Country).CopyTo(country, 0); + + byte[] domain = new byte[0x4]; + Encoding.ASCII.GetBytes(this.Domain).CopyTo(domain, 0); + + byte[] titleId = new byte[0x18]; + Encoding.ASCII.GetBytes(this.TitleId).CopyTo(titleId, 0); + + List userData = new() + { + new BinaryData(serialBytes), + new UInt32Data(this.IssuerId), + new TimestampData(this.IssueTime), + new TimestampData(this.ExpirationTime), + new UInt64Data(this.UserId), + new StringData(onlineId), + new BinaryData(country), + new BinaryData(domain), + new BinaryData(titleId), + new UInt32Data(this.Status), + new EmptyData(), + new EmptyData(), + }; + TicketData userBlob = new BlobData(0, userData); + List footer = new() + { + new BinaryData("TEST"u8.ToArray()), + new BinaryData(new byte[0x38]), + }; + TicketData footerBlob = new BlobData(2, footer); + MemoryStream ms = new(); + BinaryWriter writer = new(ms); + writer.Write(new byte[]{0x21, 0x01, 0x00, 0x00,}); + int ticketLen = userBlob.Len() + footerBlob.Len() + 8; + byte[] lenAsBytes = new byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(lenAsBytes, (uint)ticketLen); + writer.Write(lenAsBytes); + userBlob.Write(writer); + footerBlob.Write(writer); + byte[] data = ms.ToArray(); + // sign all data besides the signature + byte[] ticketData = data.AsSpan()[..(data.Length-0x38)].ToArray(); + byte[] signature = SignData(ticketData); + if (signature.Length < 0x38) + { + Array.Resize(ref signature, 0x38); + } + ticketData = ticketData.Concat(signature).ToArray(); + return ticketData; + } + + private static byte[] SignData(byte[] data) + { + ECPrivateKeyParameters key = new(privateKey, NPTicket.Secp192R1); + + ISigner signer = SignerUtilities.GetSigner("SHA-1withECDSA"); + signer.Init(true, key); + + signer.BlockUpdate(data); + + return signer.GenerateSignature(); + } + + public TicketBuilder SetUsername(string username) + { + this.Username = username; + return this; + } + + public TicketBuilder SetTitleId(string titleId) + { + this.TitleId = titleId; + return this; + } + + public TicketBuilder setExpirationTime(ulong expirationTime) + { + this.ExpirationTime = expirationTime; + return this; + } + + public TicketBuilder SetIssueTime(ulong issueTime) + { + this.IssueTime = issueTime; + return this; + } + + public TicketBuilder SetIssuerId(ushort issuerId) + { + this.IssuerId = issuerId; + return this; + } + + public TicketBuilder SetUserId(ulong userId) + { + this.UserId = userId; + return this; + } + + public TicketBuilder SetCountry(string country) + { + this.Country = country; + return this; + } + + public TicketBuilder SetDomain(string domain) + { + this.Domain = domain; + return this; + } + + public TicketBuilder SetStatus(uint status) + { + this.Status = status; + return this; + } + +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/BinaryData.cs b/ProjectLighthouse/Tickets/Types/BinaryData.cs new file mode 100644 index 00000000..66c3d797 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/BinaryData.cs @@ -0,0 +1,23 @@ +using System.IO; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class BinaryData : TicketData +{ + private readonly byte[] val; + + public BinaryData(byte[] val) + { + this.val = val; + } + + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + writer.Write(this.val); + } + + public override short Id() => 8; + + public override short Len() => (short)this.val.Length; +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/BlobData.cs b/ProjectLighthouse/Tickets/Types/BlobData.cs new file mode 100644 index 00000000..0493de6d --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/BlobData.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class BlobData : TicketData +{ + private readonly byte id; + private readonly List data; + + public BlobData(byte id, List data) + { + this.id = id; + this.data = data; + } + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + foreach (TicketData d in this.data) + { + d.Write(writer); + } + } + + public override short Id() => (short)(0x3000 | this.id); + + public override short Len() => (short)this.data.Sum(d => d.Len()+4); +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/EmptyData.cs b/ProjectLighthouse/Tickets/Types/EmptyData.cs new file mode 100644 index 00000000..42938883 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/EmptyData.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class EmptyData : TicketData +{ + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + } + + public override short Id() => 0; + + public override short Len() => 0; +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/StringData.cs b/ProjectLighthouse/Tickets/Types/StringData.cs new file mode 100644 index 00000000..8a9fae74 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/StringData.cs @@ -0,0 +1,29 @@ +using System.IO; +using System.Text; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class StringData : TicketData +{ + private readonly byte[] val; + + public StringData(string val) + { + this.val = Encoding.ASCII.GetBytes(val); + } + + public StringData(byte[] val) + { + this.val = val; + } + + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + writer.Write(this.val); + } + + public override short Id() => 4; + + public override short Len() => (short)this.val.Length; +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/TicketData.cs b/ProjectLighthouse/Tickets/Types/TicketData.cs new file mode 100644 index 00000000..b638ef49 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/TicketData.cs @@ -0,0 +1,21 @@ +using System.Buffers.Binary; +using System.IO; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public abstract class TicketData +{ + public void WriteHeader(BinaryWriter writer) + { + byte[] id = new byte[2]; + byte[] len = new byte[2]; + BinaryPrimitives.WriteUInt16BigEndian(id, (ushort)this.Id()); + BinaryPrimitives.WriteUInt16BigEndian(len, (ushort)this.Len()); + writer.Write(id); + writer.Write(len); + } + + public abstract void Write(BinaryWriter writer); + public abstract short Id(); + public abstract short Len(); +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/TimestampData.cs b/ProjectLighthouse/Tickets/Types/TimestampData.cs new file mode 100644 index 00000000..a08b5dd8 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/TimestampData.cs @@ -0,0 +1,26 @@ +using System.Buffers.Binary; +using System.IO; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class TimestampData : TicketData +{ + private readonly ulong val; + + public TimestampData(ulong val) + { + this.val = val; + } + + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + byte[] data = new byte[8]; + BinaryPrimitives.WriteUInt64BigEndian(data, this.val); + writer.Write(data); + } + + public override short Id() => 7; + + public override short Len() => 8; +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/UInt32Data.cs b/ProjectLighthouse/Tickets/Types/UInt32Data.cs new file mode 100644 index 00000000..52302924 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/UInt32Data.cs @@ -0,0 +1,26 @@ +using System.Buffers.Binary; +using System.IO; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class UInt32Data : TicketData +{ + private readonly uint val; + + public UInt32Data(uint val) + { + this.val = val; + } + + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + byte[] data = new byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(data, this.val); + writer.Write(data); + } + + public override short Id() => 1; + + public override short Len() => 4; +} \ No newline at end of file diff --git a/ProjectLighthouse/Tickets/Types/UInt64Data.cs b/ProjectLighthouse/Tickets/Types/UInt64Data.cs new file mode 100644 index 00000000..b9bf6af9 --- /dev/null +++ b/ProjectLighthouse/Tickets/Types/UInt64Data.cs @@ -0,0 +1,26 @@ +using System.Buffers.Binary; +using System.IO; + +namespace LBPUnion.ProjectLighthouse.Tickets.Types; + +public class UInt64Data : TicketData +{ + private readonly ulong val; + + public UInt64Data(ulong val) + { + this.val = val; + } + + public override void Write(BinaryWriter writer) + { + this.WriteHeader(writer); + byte[] data = new byte[8]; + BinaryPrimitives.WriteUInt64BigEndian(data, this.val); + writer.Write(data); + } + + public override short Id() => 2; + + public override short Len() => 8; +} \ No newline at end of file