mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-16 22:52:27 +00:00
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
This commit is contained in:
parent
337124690d
commit
20b2ef5700
15 changed files with 540 additions and 80 deletions
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
97
ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs
Normal file
97
ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs
Normal file
|
@ -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<GameServerTestStartup>
|
||||
{
|
||||
[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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<TStartup> where TStartup : class
|
|||
this.Server = new TestServer(new WebHostBuilder().UseStartup<TStartup>());
|
||||
this.Client = this.Server.CreateClient();
|
||||
}
|
||||
public async Task<HttpResponseMessage> AuthenticateResponse(int number = -1, bool createUser = true)
|
||||
|
||||
public async Task<string> 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<HttpResponseMessage> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,33 +46,65 @@ public class NPTicket
|
|||
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
|
||||
|
||||
private byte[] ticketSignature { get; set; } = Array.Empty<byte>();
|
||||
private byte[] ticketSignatureIdentifier { get; set; } = Array.Empty<byte>();
|
||||
|
||||
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<string, SignatureParams> 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<Platform, byte[]> 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;
|
||||
|
|
160
ProjectLighthouse/Tickets/TicketBuilder.cs
Normal file
160
ProjectLighthouse/Tickets/TicketBuilder.cs
Normal file
|
@ -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<TicketData> 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<TicketData> 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;
|
||||
}
|
||||
|
||||
}
|
23
ProjectLighthouse/Tickets/Types/BinaryData.cs
Normal file
23
ProjectLighthouse/Tickets/Types/BinaryData.cs
Normal file
|
@ -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;
|
||||
}
|
29
ProjectLighthouse/Tickets/Types/BlobData.cs
Normal file
29
ProjectLighthouse/Tickets/Types/BlobData.cs
Normal file
|
@ -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<TicketData> data;
|
||||
|
||||
public BlobData(byte id, List<TicketData> 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);
|
||||
}
|
15
ProjectLighthouse/Tickets/Types/EmptyData.cs
Normal file
15
ProjectLighthouse/Tickets/Types/EmptyData.cs
Normal file
|
@ -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;
|
||||
}
|
29
ProjectLighthouse/Tickets/Types/StringData.cs
Normal file
29
ProjectLighthouse/Tickets/Types/StringData.cs
Normal file
|
@ -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;
|
||||
}
|
21
ProjectLighthouse/Tickets/Types/TicketData.cs
Normal file
21
ProjectLighthouse/Tickets/Types/TicketData.cs
Normal file
|
@ -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();
|
||||
}
|
26
ProjectLighthouse/Tickets/Types/TimestampData.cs
Normal file
26
ProjectLighthouse/Tickets/Types/TimestampData.cs
Normal file
|
@ -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;
|
||||
}
|
26
ProjectLighthouse/Tickets/Types/UInt32Data.cs
Normal file
26
ProjectLighthouse/Tickets/Types/UInt32Data.cs
Normal file
|
@ -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;
|
||||
}
|
26
ProjectLighthouse/Tickets/Types/UInt64Data.cs
Normal file
26
ProjectLighthouse/Tickets/Types/UInt64Data.cs
Normal file
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue