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"
|
dotnet-version: "7.0.x"
|
||||||
|
|
||||||
- name: Compile
|
- 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
|
- name: Run tests on ProjectLighthouse.Tests
|
||||||
continue-on-error: true
|
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
|
- name: Run tests on ProjectLighthouse.Tests.GameApiTests
|
||||||
continue-on-error: true
|
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
|
- name: Run tests on ProjectLighthouse.Tests.WebsiteTests
|
||||||
if: ${{ matrix.os.webTest }}
|
if: ${{ matrix.os.webTest }}
|
||||||
continue-on-error: true
|
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.
|
# Attempt to upload results even if test fails.
|
||||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
# 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;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||||
using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup;
|
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()
|
public DatabaseFactAttribute()
|
||||||
{
|
{
|
||||||
ServerConfiguration.Instance = new ServerConfiguration();
|
ServerConfiguration.Instance = new ServerConfiguration
|
||||||
ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
|
{
|
||||||
|
DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse",
|
||||||
|
};
|
||||||
if (!ServerStatics.DbConnected) this.Skip = "Database not available";
|
if (!ServerStatics.DbConnected) this.Skip = "Database not available";
|
||||||
else
|
else
|
||||||
lock(migrateLock)
|
lock(migrateLock)
|
||||||
|
|
|
@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||||
using LBPUnion.ProjectLighthouse.Serialization;
|
using LBPUnion.ProjectLighthouse.Serialization;
|
||||||
|
using LBPUnion.ProjectLighthouse.Tickets;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -25,27 +26,38 @@ public class LighthouseServerTest<TStartup> where TStartup : class
|
||||||
this.Server = new TestServer(new WebHostBuilder().UseStartup<TStartup>());
|
this.Server = new TestServer(new WebHostBuilder().UseStartup<TStartup>());
|
||||||
this.Client = this.Server.CreateClient();
|
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();
|
if (number == -1) number = new Random().Next();
|
||||||
|
|
||||||
const string username = "unitTestUser";
|
const string username = "unitTestUser";
|
||||||
|
|
||||||
if (createUser)
|
if (createUser)
|
||||||
{
|
{
|
||||||
await using Database database = new();
|
await using Database database = new();
|
||||||
if (await database.Users.FirstOrDefaultAsync(u => u.Username == $"{username}{number}") == null)
|
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;
|
user.LinkedPsnId = (ulong)number;
|
||||||
await database.SaveChangesAsync();
|
await database.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: generate actual tickets
|
return $"{username}{number}";
|
||||||
string stringContent = $"unitTestTicket{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
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,33 +46,65 @@ public class NPTicket
|
||||||
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
|
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
private byte[] ticketSignature { 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; }
|
public GameVersion GameVersion { get; set; }
|
||||||
|
|
||||||
private static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
|
private static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
|
||||||
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
|
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
|
||||||
|
|
||||||
private static readonly ECDomainParameters secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
|
public static readonly ECDomainParameters Secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
|
||||||
private static readonly ECDomainParameters secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
|
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("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16),
|
||||||
new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 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("39c62d061d4ee35c5f3f7531de0af3cf918346526edac727", 16),
|
||||||
new BigInteger("a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86", 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()
|
private bool ValidateSignature()
|
||||||
{
|
{
|
||||||
ECPublicKeyParameters pubKey = new(this.getPublicKey(), this.getCurveParams());
|
string identifierHex = Convert.ToHexString(this.ticketSignatureIdentifier);
|
||||||
string algo = this.IsRpcn() ? "SHA-224" : "SHA-1";
|
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.Init(false, pubKey);
|
||||||
|
|
||||||
signer.BlockUpdate(this.ticketBody);
|
signer.BlockUpdate(this.ticketBody);
|
||||||
|
@ -80,18 +112,6 @@ public class NPTicket
|
||||||
return signer.VerifySignature(this.ticketSignature);
|
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
|
// Sometimes psn signatures have one or two extra empty bytes
|
||||||
// This is slow but it's better than carelessly chopping 0's
|
// This is slow but it's better than carelessly chopping 0's
|
||||||
private static byte[] ParseSignature(byte[] signature)
|
private static byte[] ParseSignature(byte[] signature)
|
||||||
|
@ -136,14 +156,7 @@ public class NPTicket
|
||||||
|
|
||||||
reader.ReadSectionHeader(); // footer header
|
reader.ReadSectionHeader(); // footer header
|
||||||
|
|
||||||
byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier
|
npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary();
|
||||||
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.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
||||||
return true;
|
return true;
|
||||||
|
@ -175,14 +188,7 @@ public class NPTicket
|
||||||
|
|
||||||
reader.ReadSectionHeader(); // footer header
|
reader.ReadSectionHeader(); // footer header
|
||||||
|
|
||||||
byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier
|
npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary();
|
||||||
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.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
||||||
return true;
|
return true;
|
||||||
|
@ -216,9 +222,14 @@ public class NPTicket
|
||||||
|
|
||||||
if (!parsedSuccessfully) return false;
|
if (!parsedSuccessfully) return false;
|
||||||
|
|
||||||
npTicket.ticketBody = npTicket.IsRpcn()
|
npTicket.ticketBody = Convert.ToHexString(npTicket.ticketSignatureIdentifier) switch
|
||||||
? data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray()
|
{
|
||||||
: data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray();
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -229,28 +240,6 @@ public class NPTicket
|
||||||
public static NPTicket? CreateFromBytes(byte[] data)
|
public static NPTicket? CreateFromBytes(byte[] data)
|
||||||
{
|
{
|
||||||
NPTicket npTicket = new();
|
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
|
try
|
||||||
{
|
{
|
||||||
using MemoryStream ms = new(data);
|
using MemoryStream ms = new(data);
|
||||||
|
@ -263,14 +252,14 @@ public class NPTicket
|
||||||
return null;
|
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;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,12 +288,13 @@ public class NPTicket
|
||||||
{
|
{
|
||||||
0x100 => Platform.PS3,
|
0x100 => Platform.PS3,
|
||||||
0x33333333 => Platform.RPCS3,
|
0x33333333 => Platform.RPCS3,
|
||||||
|
0x74657374 => Platform.UnitTest,
|
||||||
_ => Platform.Unknown,
|
_ => Platform.Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (npTicket.Platform == Platform.PS3 && npTicket.GameVersion == GameVersion.LittleBigPlanetVita) npTicket.Platform = Platform.Vita;
|
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);
|
Logger.Warn($"Could not determine platform from IssuerId {npTicket.IssuerId} decimal", LogArea.Login);
|
||||||
return null;
|
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