Skip to content

Commit

Permalink
ResourceHelper: Directly decrypt PSP assets
Browse files Browse the repository at this point in the history
Switches from an XOR blob to directly decrypting the data as we stream it in.
  • Loading branch information
Beyley committed Dec 28, 2023
1 parent d2a8aef commit 6129f75
Show file tree
Hide file tree
Showing 5 changed files with 26 additions and 129 deletions.
2 changes: 1 addition & 1 deletion Refresh.GameServer/Importing/ImageImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void ImportAsset(GameAsset asset, IDataStore dataStore)
break;
case GameAssetType.Mip: {
byte[] rawData = dataStore.GetDataFromStore(asset.IsPSP ? "psp/" + asset.AssetHash : asset.AssetHash);
byte[] data = ResourceHelper.PspDecrypt(rawData, this._pspKey.Value);
byte[] data = ResourceHelper.PspDecrypt(rawData, this.PSPKey.Value);

using MemoryStream dataStream = new(data);

Expand Down
20 changes: 15 additions & 5 deletions Refresh.GameServer/Importing/Importer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Bunkum.Core;
using NotEnoughLogs;
Expand All @@ -14,7 +15,7 @@ public abstract class Importer
{
private readonly Logger _logger;
protected readonly Stopwatch Stopwatch;
protected readonly Lazy<byte[]?> _pspKey;
protected readonly Lazy<byte[]?> PSPKey;

protected Importer(Logger? logger = null)
{
Expand All @@ -26,11 +27,20 @@ protected Importer(Logger? logger = null)
this._logger = logger;
this.Stopwatch = new Stopwatch();

this._pspKey = new(() =>
this.PSPKey = new(() =>
{
try
{
return File.ReadAllBytes("keys/psp");
//Read the key
byte[] key = File.ReadAllBytes("keys/psp");

//If the hash matches, return the read key
if (SHA1.HashData(key).AsSpan().SequenceEqual(new byte[] { 0x12, 0xb5, 0xa8, 0xb5, 0x91, 0x55, 0x24, 0x96, 0x00, 0xdf, 0x0e, 0x33, 0xf9, 0xc5, 0xa8, 0x76, 0xc1, 0x85, 0x43, 0xfe }))
return key;

//If the hash does not match, log an error and return null
this._logger.LogError(BunkumCategory.Digest, "PSP key failed to validate! Correct hash is 12b5a8b59155249600df0e33f9c5a876c18543fe");
return null;
}
catch(Exception e)
{
Expand Down Expand Up @@ -126,7 +136,7 @@ private static bool IsPspTga(ReadOnlySpan<byte> data)
private bool IsMip(Span<byte> rawData)
{
//If we dont have a key, then we cant determine the data type
if (this._pspKey.Value == null) return false;
if (this.PSPKey.Value == null) return false;

//Data less than this size isn't encrypted(?) and all Mip files uploaded to the server will be encrypted
//See https://github.com/ennuo/lbparc/blob/16ad36aa7f4eae2f7b406829e604082750f16fe1/tools/toggle.js#L33
Expand All @@ -135,7 +145,7 @@ private bool IsMip(Span<byte> rawData)
try
{
//Decrypt the data
ReadOnlySpan<byte> data = ResourceHelper.PspDecrypt(rawData, this._pspKey.Value);
ReadOnlySpan<byte> data = ResourceHelper.PspDecrypt(rawData, this.PSPKey.Value);

uint clutOffset = BinaryPrimitives.ReadUInt32LittleEndian(data[..4]);
uint width = BinaryPrimitives.ReadUInt32LittleEndian(data[4..8]);
Expand Down
2 changes: 1 addition & 1 deletion Refresh.GameServer/Refresh.GameServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<Nullable>enable</Nullable>
<Configurations>Debug;Release;DebugLocalBunkum</Configurations>
<Platforms>AnyCPU</Platforms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'DebugLocalBunkum' ">
Expand Down Expand Up @@ -66,6 +65,7 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net.Webhook" Version="3.13.0" />
<PackageReference Include="FastAes" Version="1.0.1" />
<PackageReference Include="IronCompress" Version="1.5.1" />
<PackageReference Include="MailKit" Version="4.3.0" />
<PackageReference Include="NPTicket" Version="3.1.0" />
Expand Down
82 changes: 9 additions & 73 deletions Refresh.GameServer/Resources/ResourceHelper.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Numerics;
using System.Reflection;
using System.Security.Cryptography;
using FastAes;
using IronCompress;
using Microsoft.Toolkit.HighPerformance;

namespace Refresh.GameServer.Resources;

Expand All @@ -15,61 +14,10 @@ public static Stream StreamFromResource(string name)
Assembly assembly = Assembly.GetExecutingAssembly();
return assembly.GetManifestResourceStream(name)!;
}

private static unsafe void XorVectorized(Span<byte> rawData, ReadOnlySpan<byte> key)
{
fixed (byte* dataPtr = rawData)
{
//Get the misalignment of the data
int misalignment = (int)((nint)dataPtr % sizeof(nint));

Span<byte> data = new(dataPtr, rawData.Length);

if (misalignment != 0)
{
//XOR the data before the aligned data
XorScalar(data[..misalignment], key);
}

//Get the span of data which is aligned to word boundaries
Span<byte> alignedData = data[misalignment..];

//Get a Vector<byte> of the data, offset by the misalignment, making this data byte-aligned
Span<Vector<byte>> alignedDataVec = alignedData.Cast<byte, Vector<byte>>();
//Get a Vector<byte> of the key, offset by the misalignment
ReadOnlySpan<Vector<byte>> keyVec = key[misalignment..].Cast<byte, Vector<byte>>();

//Iterate over every item in the data vector
for (int i = 0; i < alignedDataVec.Length; i++)
//XOR it with the key
alignedDataVec[i] ^= keyVec[i];

//Get the amount of bytes which are left over
int leftoverCount = data.Length - (alignedDataVec.Length * Vector<byte>.Count * sizeof(byte)) - misalignment;

//If theres no leftover bytes, return out
if (leftoverCount <= 0) return;

//Get the start index of the leftover bytes
int leftoverOffset = data.Length - leftoverCount;
//Slice the data to only have the leftover data
Span<byte> leftover = data[leftoverOffset..];

XorScalar(leftover, key[leftoverOffset..]);
}
}

private static void XorScalar(Span<byte> data, ReadOnlySpan<byte> key)
{
//Iterate over all bytes
for (int i = 0; i < data.Length; i++)
//Iterate over all data, and XOR it with the key
data[i] ^= key[i];
}

private static readonly ThreadLocal<Iron> Iron = new(() => new Iron(ArrayPool<byte>.Shared));

private static void Encrypt(Span<byte> data, ReadOnlySpan<byte> key)
private static void Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key)
{
throw new NotImplementedException(); //TODO
}
Expand All @@ -82,9 +30,13 @@ private static void Encrypt(Span<byte> data, ReadOnlySpan<byte> key)
/// <returns>The decrypted data.</returns>
public static byte[] PspDecrypt(Span<byte> data, ReadOnlySpan<byte> key)
{
//XOR the data
Xor(data, key);
byte[] initialCounter = new byte[16];
key[..8].CopyTo(initialCounter);

//Decrypt the data
AesCtr ctr = new(key, initialCounter);
ctr.DecryptBytes(data, data);

//Get the data buffer
Span<byte> buf = data[..^0x19];
//Get the info of the file
Expand All @@ -98,28 +50,12 @@ public static byte[] PspDecrypt(Span<byte> data, ReadOnlySpan<byte> key)
if (!signature.SequenceEqual(MD5.HashData(data[..^0x10]))) throw new InvalidDataException("Encrypted PSP asset hash does not match signature, likely corrupt");

//If the data is not compressed, just return a copy of the raw buffer
if (info[0x4] != 1)
{
Xor(data, key);
return buf.ToArray();
}
if (info[0x4] != 1) return buf.ToArray();

//Decompress the data
using IronCompressResult decompressed = Iron.Value!.Decompress(Codec.LZO, buf, (int)len);

Xor(data, key);

//Return a copy of the decompressed data
return decompressed.AsSpan().ToArray();
}

public static void Xor(Span<byte> data, ReadOnlySpan<byte> key)
{
if (key.Length < data.Length) throw new ArgumentException("Key must be as long or longer than the data!", nameof(key));

if (Vector<byte>.IsSupported && data.Length >= Vector<byte>.Count * sizeof(byte))
XorVectorized(data, key);
else
XorScalar(data, key);
}
}
49 changes: 0 additions & 49 deletions RefreshTests.GameServer/Tests/Resources/XorTests.cs

This file was deleted.

0 comments on commit 6129f75

Please sign in to comment.