Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite LegacyAdapterMiddleware + Mutliple digest keys + HMAC digests #562

Merged
merged 4 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Refresh.GameServer/Authentication/Token.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Xml.Serialization;
using Bunkum.Core.Authentication;
using JetBrains.Annotations;
using MongoDB.Bson;
using Realms;
using Refresh.GameServer.Types.UserData;
Expand Down Expand Up @@ -47,4 +48,10 @@ public TokenGame TokenGame
public string IpAddress { get; set; }

public GameUser User { get; set; }

/// <summary>
/// The digest key to use with this token, determined from the first game request created by this token
/// </summary>
[CanBeNull] public string Digest { get; set; }
public bool IsHmacDigest { get; set; }
}
5 changes: 4 additions & 1 deletion Refresh.GameServer/Configuration/GameServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Refresh.GameServer.Configuration;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class GameServerConfig : Config
{
public override int CurrentConfigVersion => 16;
public override int CurrentConfigVersion => 17;
public override int Version { get; set; } = 0;

protected override void Migrate(int oldVer, dynamic oldConfig) {}
Expand Down Expand Up @@ -49,4 +49,7 @@ protected override void Migrate(int oldVer, dynamic oldConfig) {}
/// Whether to print the room state whenever a `FindBestRoom` match returns no results
/// </summary>
public bool PrintRoomStateWhenNoFoundRooms { get; set; } = true;

public string[] Sha1DigestKeys = ["CustomServerDigest"];
public string[] HmacDigestKeys = ["CustomServerDigest"];
}
9 changes: 9 additions & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ public void DenyIpVerificationRequest(GameUser user, string ipAddress)
});
}

public void SetTokenDigestInfo(Token token, string digest, bool isHmacDigest)
{
this.Write(() =>
{
token.Digest = digest;
token.IsHmacDigest = isHmacDigest;
});
}

public DatabaseList<GameIpVerificationRequest> GetIpVerificationRequestsForUser(GameUser user, int count, int skip)
=> new(this.GameIpVerificationRequests.Where(r => r.User == user), skip, count);
}
4 changes: 1 addition & 3 deletions Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
this._time = time;
}

protected override ulong SchemaVersion => 133;
protected override ulong SchemaVersion => 134;

protected override string Filename => "refreshGameServer.realm";

Expand Down Expand Up @@ -358,8 +358,6 @@ protected override void Migrate(Migration migration, ulong oldVersion)
oldSubjects.Add(new GamePhotoSubject(user, subject.DisplayName, bounds));
}

Console.WriteLine(JsonConvert.SerializeObject(oldSubjects));

newPhoto.Subjects = oldSubjects;
}
}
Expand Down
180 changes: 117 additions & 63 deletions Refresh.GameServer/Middlewares/DigestMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,114 +1,168 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using Bunkum.Listener.Request;
using Bunkum.Core.Database;
using Bunkum.Core.Endpoints.Middlewares;
using Refresh.Common.Extensions;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints;

namespace Refresh.GameServer.Middlewares;

public class DigestMiddleware : IMiddleware
{
// Should be 19 characters (or less maybe?)
// Length was taken from PS3 and PS4 digest keys
private const string DigestKey = "CustomServerDigest";
private readonly GameServerConfig _config;

public static string CalculateDigest(string url, Stream body, string auth, short? exeVersion, short? dataVersion)
public DigestMiddleware(GameServerConfig config)
{
using MemoryStream ms = new();

if (!url.StartsWith($"{GameEndpointAttribute.BaseRoute}upload/"))
this._config = config;
}

public record PspVersionInfo(short ExeVersion, short DataVersion) {}

public static string CalculateDigest(
string digest,
string route,
Stream body,
string auth,
PspVersionInfo? pspVersionInfo,
bool isUpload,
bool hmacDigest)
{
// Init a MemoryStream with the known final capacity capacity
using MemoryStream ms = new((int)(auth.Length + route.Length + digest.Length + (isUpload ? 0 : body.Length)) + (pspVersionInfo == null ? 0 : 4));

// If this is not an upload endpoint, then we need to copy the body of the request into the digest calculation
if (!isUpload)
{
// get request body
body.CopyTo(ms);
body.Seek(0, SeekOrigin.Begin);
}

ms.WriteString(auth);
ms.WriteString(url);
if (exeVersion.HasValue)
ms.WriteString(route);
if (pspVersionInfo != null)
{
byte[] bytes = BitConverter.GetBytes(exeVersion.Value);
Span<byte> bytes = stackalloc byte[2];

BitConverter.TryWriteBytes(bytes, pspVersionInfo.ExeVersion);
// If we are on a big endian system, we need to flip the bytes
if(!BitConverter.IsLittleEndian)
Array.Reverse(bytes);
bytes.Reverse();
ms.Write(bytes);
}
if (dataVersion.HasValue)
{
byte[] bytes = BitConverter.GetBytes(dataVersion.Value);

BitConverter.TryWriteBytes(bytes, pspVersionInfo.DataVersion);
// If we are on a big endian system, we need to flip the bytes
if(!BitConverter.IsLittleEndian)
Array.Reverse(bytes);
bytes.Reverse();
ms.Write(bytes);
}
ms.WriteString(DigestKey);
}
if(!hmacDigest)
ms.WriteString(digest);

ms.Position = 0;
using SHA1 sha = SHA1.Create();
string digestResponse = Convert.ToHexString(sha.ComputeHash(ms)).ToLower();

return digestResponse;
if (hmacDigest)
{
using HMACSHA1 hmac = new(Encoding.UTF8.GetBytes(digest));
return Convert.ToHexString(hmac.ComputeHash(ms)).ToLower();
}

using SHA1 sha = SHA1.Create();
return Convert.ToHexString(sha.ComputeHash(ms)).ToLower();
}

// Referenced from Project Lighthouse
// https://github.com/LBPUnion/ProjectLighthouse/blob/d16132f67f82555ef636c0dabab5aabf36f57556/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs
// https://github.com/LBPUnion/ProjectLighthouse/blob/19ea44e0e2ff5f2ebae8d9dfbaf0f979720bd7d9/ProjectLighthouse/Helpers/CryptoHelper.cs#L35
private bool VerifyDigestRequest(ListenerContext context, short? exeVersion, short? dataVersion)
public void HandleRequest(ListenerContext context, Lazy<IDatabaseContext> database, Action next)
{
string url = context.Uri.AbsolutePath;
string auth = context.Cookies["MM_AUTH"] ?? string.Empty;
string route = context.Uri.AbsolutePath;

//If this isn't an LBP endpoint, dont do digest
if (!route.StartsWith(GameEndpointAttribute.BaseRoute) && !route.StartsWith(LegacyAdapterMiddleware.OldBaseRoute))
{
next();
return;
}

PspVersionInfo? pspVersionInfo = null;
// Try to acquire the exe and data version, this is only accounted for in the client digests, not the server digests
if (short.TryParse(context.RequestHeaders["X-Exe-V"], out short exeVer) &&
short.TryParse(context.RequestHeaders["X-Data-V"], out short dataVer))
pspVersionInfo = new PspVersionInfo(exeVer, dataVer);

bool isUpload = url.StartsWith($"{GameEndpointAttribute.BaseRoute}upload/");
string auth = context.Cookies["MM_AUTH"] ?? string.Empty;
bool isUpload = route.StartsWith($"{LegacyAdapterMiddleware.OldBaseRoute}upload/") || route.StartsWith($"{GameEndpointAttribute.BaseRoute}upload/");

MemoryStream body = isUpload ? new MemoryStream(0) : context.InputStream;
// For upload requests, the X-Digest-B header is in use instead by the client
string digestHeader = isUpload ? "X-Digest-B" : "X-Digest-A";
string clientDigest = context.RequestHeaders[digestHeader] ?? string.Empty;

string expectedDigest = CalculateDigest(url, body, auth, isUpload ? null : exeVersion, isUpload ? null : dataVersion);

context.ResponseHeaders["X-Digest-B"] = expectedDigest;
if (clientDigest == expectedDigest) return true;
// Pass through the client's digest right back to the digest B response
context.ResponseHeaders["X-Digest-B"] = clientDigest;

return false;
}

private void SetDigestResponse(ListenerContext context)
{
string url = context.Uri.AbsolutePath;
string auth = context.Cookies["MM_AUTH"] ?? string.Empty;

string digestResponse = CalculateDigest(url, context.ResponseStream, auth, null, null);
next();

GameDatabaseContext gameDatabase = (GameDatabaseContext)database.Value;

Token? token = gameDatabase.GetTokenFromTokenData(auth, TokenType.Game);

context.ResponseHeaders["X-Digest-A"] = digestResponse;
}
// Make sure the digest calculation reads the whole response stream
context.ResponseStream.Seek(0, SeekOrigin.Begin);

public void HandleRequest(ListenerContext context, Lazy<IDatabaseContext> database, Action next)
{
//If this isn't an LBP endpoint, dont do digest
if (!context.Uri.AbsolutePath.StartsWith(GameEndpointAttribute.BaseRoute))
// If the digest is already saved on the token, use the token's digest
if (token is { Digest: not null })
{
next();
SetDigestResponse(context, CalculateDigest(token.Digest, route, context.ResponseStream, auth, null, isUpload, token.IsHmacDigest));
return;
}

short? exeVersion = null;
short? dataVersion = null;
if (short.TryParse(context.RequestHeaders["X-Exe-V"], out short exeVer))
(string digest, bool hmac)? foundDigest = this.FindBestKey(clientDigest, route, context.InputStream, auth, pspVersionInfo, isUpload, false) ??
this.FindBestKey(clientDigest, route, context.InputStream, auth, pspVersionInfo, isUpload, true);

if (foundDigest != null)
{
exeVersion = exeVer;
string digest = foundDigest.Value.digest;
bool hmac = foundDigest.Value.hmac;

SetDigestResponse(context, CalculateDigest(digest, route, context.ResponseStream, auth, null, isUpload, hmac));

if(token != null)
gameDatabase.SetTokenDigestInfo(token, digest, hmac);
}
if (short.TryParse(context.RequestHeaders["X-Data-V"], out short dataVer))
else
{
dataVersion = dataVer;
// If we were unable to find any digests, just use the first one specified as a backup
// TODO: once we have PS4 support, check if the token is a PS4 token
bool isPs4 = context.RequestHeaders["User-Agent"] == "MM CHTTPClient LBP3 01.26";
string firstDigest = isPs4 ? this._config.HmacDigestKeys[0] : this._config.Sha1DigestKeys[0];

SetDigestResponse(context, CalculateDigest(firstDigest, route, context.ResponseStream, auth, null, isUpload, isPs4));

if(token != null)
gameDatabase.SetTokenDigestInfo(token, firstDigest, isPs4);
}
}

this.VerifyDigestRequest(context, exeVersion, dataVersion);
Debug.Assert(context.InputStream.Position == 0); // should be at position 0 before we pass down the pipeline
private (string digest, bool hmac)? FindBestKey(string clientDigest, string route, MemoryStream inputStream, string auth, PspVersionInfo? pspVersionInfo, bool isUpload, bool hmac)
{
string[] keys = hmac ? this._config.HmacDigestKeys : this._config.Sha1DigestKeys;

next();
foreach (string digest in keys)
{
string calculatedClientDigest = CalculateDigest(digest, route, inputStream, auth, pspVersionInfo, isUpload, hmac);

// should be at position 0 before we try to set digest
context.ResponseStream.Seek(0, SeekOrigin.Begin);
this.SetDigestResponse(context);
// If the calculated client digest is invalid, then this isn't the digest the game is using, so check the next one
if (calculatedClientDigest != clientDigest)
continue;

// If they match, we found the client's digest
return (digest, hmac);
}

return null;
}

private static void SetDigestResponse(ListenerContext context, string calculatedDigest)
=> context.ResponseHeaders["X-Digest-A"] = calculatedDigest;
}
jvyden marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 5 additions & 4 deletions Refresh.GameServer/Middlewares/LegacyAdapterMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
using Bunkum.Listener.Request;
using Bunkum.Core.Database;
using Bunkum.Core.Endpoints.Middlewares;
using Refresh.GameServer.Endpoints;

namespace Refresh.GameServer.Middlewares;

public class LegacyAdapterMiddleware : IMiddleware
{
private const string OldUrl = "/LITTLEBIGPLANETPS3_XML";
private const string NewUrl = "/lbp";
public const string OldBaseRoute = "/LITTLEBIGPLANETPS3_XML/";
private const string NewBaseRoute = GameEndpointAttribute.BaseRoute;

public void HandleRequest(ListenerContext context, Lazy<IDatabaseContext> database, Action next)
{
if (!context.Uri.AbsolutePath.StartsWith(OldUrl))
if (!context.Uri.AbsolutePath.StartsWith(OldBaseRoute))
{
next();
return;
}

context.Uri = new Uri(context.Uri, context.Uri.AbsolutePath.Replace(OldUrl, NewUrl));
context.Uri = new Uri(context.Uri, string.Concat(NewBaseRoute, context.Uri.AbsolutePath.AsSpan()[OldBaseRoute.Length..]));

next();
}
Expand Down
5 changes: 3 additions & 2 deletions Refresh.GameServer/RefreshGameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,13 @@ protected override void Initialize()

protected override void SetupMiddlewares()
{
this.Server.AddMiddleware<LegacyAdapterMiddleware>();
this.Server.AddMiddleware<WebsiteMiddleware>();
this.Server.AddMiddleware(new DeflateMiddleware(this._config!));
this.Server.AddMiddleware<DigestMiddleware>();
// Digest middleware must be run before LegacyAdapterMiddleware, because digest is based on the raw route, not the fixed route
this.Server.AddMiddleware(new DigestMiddleware(this._config!));
this.Server.AddMiddleware<CrossOriginMiddleware>();
this.Server.AddMiddleware<PspVersionMiddleware>();
this.Server.AddMiddleware<LegacyAdapterMiddleware>();
}

protected override void SetupConfiguration()
Expand Down
13 changes: 10 additions & 3 deletions Refresh.HttpsProxy/Middlewares/DigestMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@

namespace Refresh.HttpsProxy.Middlewares;

public class DigestMiddleware(ProxyConfig config) : IMiddleware
public class DigestMiddleware : IMiddleware
{
private readonly ProxyConfig _config;

public DigestMiddleware(ProxyConfig config)
{
this._config = config;
}

public string CalculatePs3Digest(string route, Stream body, string auth, bool isUpload)
{
using MemoryStream ms = new();
Expand All @@ -23,7 +30,7 @@ public string CalculatePs3Digest(string route, Stream body, string auth, bool is

ms.WriteString(auth);
ms.WriteString(route);
ms.WriteString(config.Ps3Digest);
ms.WriteString(this._config.Ps3Digest);

ms.Position = 0;
using SHA1 sha = SHA1.Create();
Expand All @@ -48,7 +55,7 @@ public string CalculatePs4Digest(string route, Stream body, string auth, bool is

ms.Position = 0;

using HMACSHA1 hmac = new(Encoding.UTF8.GetBytes(config.Ps4Digest));
using HMACSHA1 hmac = new(Encoding.UTF8.GetBytes(this._config.Ps4Digest));
string digestResponse = Convert.ToHexString(hmac.ComputeHash(ms)).ToLower();

return digestResponse;
Expand Down
Loading
Loading