Skip to content

Commit

Permalink
feat: allow external consumers to specify AES implementation (#4311)
Browse files Browse the repository at this point in the history
Allow external consumers to specify AES/MD5 implementation

HOME: Replace direct usage of transforms with built-in wrapper methods for easier API replacement.
BDSP: Replace incremental hash with one-liner for easier API replacement. Handle dirtying manually.
  • Loading branch information
arleypadua authored Jul 2, 2024
1 parent 298c83b commit 6de68ac
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 40 deletions.
25 changes: 11 additions & 14 deletions PKHeX.Core/PKM/HOME/HomeCrypto.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using static System.Buffers.Binary.BinaryPrimitives;
Expand Down Expand Up @@ -78,24 +77,22 @@ public static byte[] Crypt(ReadOnlySpan<byte> data, bool decrypt = true)
var dataSize = ReadUInt16LittleEndian(data[0xE..0x10]);
var result = new byte[SIZE_1HEADER + dataSize];
data[..SIZE_1HEADER].CopyTo(result); // header
Crypt(data, key, iv, result, dataSize, decrypt);

var input = data.Slice(SIZE_1HEADER, dataSize);
var output = result.AsSpan(SIZE_1HEADER, dataSize);
Crypt(input, output, key, iv, decrypt);

return result;
}

private static void Crypt(ReadOnlySpan<byte> data, byte[] key, byte[] iv, byte[] result, ushort dataSize, bool decrypt)
private static void Crypt(ReadOnlySpan<byte> data, Span<byte> result, byte[] key, byte[] iv, bool decrypt)
{
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None; // Handle PKCS7 manually.

var tmp = data[SIZE_1HEADER..].ToArray();
using var ms = new MemoryStream(tmp);
using var transform = decrypt ? aes.CreateDecryptor(key, iv) : aes.CreateEncryptor(key, iv);
using var cs = new CryptoStream(ms, transform, CryptoStreamMode.Read);

var size = cs.Read(result, SIZE_1HEADER, dataSize);
System.Diagnostics.Debug.Assert(SIZE_1HEADER + size == data.Length);
// Handle PKCS7 manually.
using var aes = RuntimeCryptographyProvider.Aes.Create(key, CipherMode.CBC, PaddingMode.None, iv);
if (decrypt)
aes.DecryptCbc(data, result);
else
aes.EncryptCbc(data, result);
}

/// <summary>
Expand Down
16 changes: 6 additions & 10 deletions PKHeX.Core/Saves/Encryption/MemeCrypto/MemeKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private void AesDecrypt(ReadOnlySpan<byte> data, Span<byte> sig)
{
var slice = sig.Slice(i, chunk);
Xor(temp, slice);
aes.DecryptEcb(temp, temp, PaddingMode.None);
aes.DecryptEcb(temp, temp);
temp.CopyTo(slice);
}

Expand All @@ -88,7 +88,7 @@ private void AesDecrypt(ReadOnlySpan<byte> data, Span<byte> sig)
{
var slice = sig.Slice(i, chunk);
slice.CopyTo(temp);
aes.DecryptEcb(slice, slice, PaddingMode.None);
aes.DecryptEcb(slice, slice);
Xor(slice, nextXor);
temp.CopyTo(nextXor);
}
Expand All @@ -104,7 +104,7 @@ private void AesEncrypt(Span<byte> data, Span<byte> sig)
{
var slice = sig.Slice(i, chunk);
Xor(slice, temp);
aes.EncryptEcb(slice, slice, PaddingMode.None);
aes.EncryptEcb(slice, slice);
slice.CopyTo(temp);
}

Expand All @@ -118,24 +118,20 @@ private void AesEncrypt(Span<byte> data, Span<byte> sig)
{
var slice = sig.Slice(i, chunk);
slice.CopyTo(nextXor);
aes.EncryptEcb(slice, slice, PaddingMode.None);
aes.EncryptEcb(slice, slice);
Xor(slice, temp);
nextXor.CopyTo(temp);
}
}

private Aes GetAesImpl(ReadOnlySpan<byte> payload)
private IAesCryptographyProvider.IAes GetAesImpl(ReadOnlySpan<byte> payload)
{
// The C# implementation of AES isn't fully allocation-free, so some allocation on key & implementation is needed.
var key = GetAesKey(payload);

// Don't dispose in this method, let the consumer dispose.
var aes = Aes.Create();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
aes.Key = key;
// no IV -- all zero.
return aes;
return RuntimeCryptographyProvider.Aes.Create(key, CipherMode.ECB, PaddingMode.None);
}

/// <summary>
Expand Down
52 changes: 52 additions & 0 deletions PKHeX.Core/Saves/Encryption/Providers/IAesCryptographyProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Security.Cryptography;

namespace PKHeX.Core;

/// <summary>
/// Provide an implementation of the Aes algorithm
/// </summary>
/// <remarks>
/// <p>The <see cref="Default"/> property will use the .NET implementation that will return an implementation that is specific for each platform except browser (web assembly).</p>
/// <p>This interface is intended to allow any runtime that's not supported to provide its own implementation.</p>
/// <p>See more at https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/5.0/cryptography-apis-not-supported-on-blazor-webassembly</p>
/// </remarks>
public interface IAesCryptographyProvider
{
IAes Create(byte[] key, CipherMode mode, PaddingMode padding, byte[]? iv = null);

internal static readonly IAesCryptographyProvider Default = new DefaultAes();

public interface IAes : IDisposable
{
void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination);
void DecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination);
void EncryptCbc(ReadOnlySpan<byte> plaintext, Span<byte> destination);
void DecryptCbc(ReadOnlySpan<byte> ciphertext, Span<byte> destination);
}

private sealed class DefaultAes : IAesCryptographyProvider
{
public IAes Create(byte[] key, CipherMode mode, PaddingMode padding, byte[]? iv = null) => new AesSession(key, mode, padding, iv);

private class AesSession : IAes
{
private readonly Aes _aes = Aes.Create();

public AesSession(byte[] key, CipherMode mode, PaddingMode padding, byte[]? iv)
{
_aes.Mode = mode;
_aes.Padding = padding;
_aes.Key = key;
if (iv != null)
_aes.IV = iv;
}

public void Dispose() => _aes.Dispose();
public void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination) => _aes.EncryptEcb(plaintext, destination, _aes.Padding);
public void DecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination) => _aes.DecryptEcb(ciphertext, destination, _aes.Padding);
public void EncryptCbc(ReadOnlySpan<byte> plaintext, Span<byte> destination) => _aes.EncryptCbc(plaintext, _aes.IV, destination, _aes.Padding);
public void DecryptCbc(ReadOnlySpan<byte> ciphertext, Span<byte> destination) => _aes.DecryptCbc(ciphertext, _aes.IV, destination, _aes.Padding);
}
}
}
16 changes: 16 additions & 0 deletions PKHeX.Core/Saves/Encryption/Providers/IMd5Provider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Security.Cryptography;

namespace PKHeX.Core;

public interface IMd5Provider
{
void HashData(ReadOnlySpan<byte> source, Span<byte> destination);

internal static readonly IMd5Provider Default = new DefaultMd5();

private sealed class DefaultMd5 : IMd5Provider
{
public void HashData(ReadOnlySpan<byte> source, Span<byte> destination) => MD5.HashData(source, destination);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PKHeX.Core;

/// <summary>
/// Holds the singleton instance that provides the AES implementation to the app running this library
/// </summary>
public static class RuntimeCryptographyProvider
{
public static IAesCryptographyProvider Aes { get; set; } = IAesCryptographyProvider.Default;
public static IMd5Provider Md5 { get; set; } = IMd5Provider.Default;
}
38 changes: 22 additions & 16 deletions PKHeX.Core/Saves/SAV8BS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public SAV8BS(byte[] data, bool exportable = true) : base(data, exportable)
// 0x96340 - _DENDOU_SAVEDATA; DENDOU_RECORD[30], POKEMON_DATA_INSIDE[6], ushort[4] ?
// BadgeSaveData; byte[8]
// BoukenNote; byte[24]
// TV_DATA (int[48], TV_STR_DATA[42]), (int[37], bool[37])*2, (int[8], int[8]), TV_STR_DATA[10]; 144 128bit zeroed (900 bytes?)?
// TV_DATA (int[48], TV_STR_DATA[42]), (int[37], bool[37])*2, (int[8], int[8]), TV_STR_DATA[10]; 144 128bit zeroed (900 bytes?)?
UgSaveData = new UgSaveData8b(this, Raw.Slice(0x9A89C, 0x27A0));
// 0x9D03C - GMS_DATA // size: 0x31304, (GMS_POINT_DATA[650], ushort, ushort, byte)?; substructure GMS_POINT_HISTORY_DATA[5]
// 0xCE340 - PLAYER_NETWORK_DATA; bcatFlagArray byte[1300]
Expand Down Expand Up @@ -174,27 +174,33 @@ public override StorageSlotSource GetBoxSlotFlags(int index)
private const int HashLength = MD5.HashSizeInBytes;
private const int HashOffset = SaveUtil.SIZE_G8BDSP - HashLength;
private Span<byte> CurrentHash => Data.AsSpan(HashOffset, HashLength);
private static void ComputeHash(ReadOnlySpan<byte> data, Span<byte> dest)

// Checksum is stored in the middle of the save file, and is zeroed before computing.
protected override void SetChecksums()
{
using var h = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
h.AppendData(data[..HashOffset]);
Span<byte> zeroes = stackalloc byte[HashLength]; // Hash is zeroed prior to computing over the payload. Treat it as zero.
h.AppendData(zeroes);
h.AppendData(data[(HashOffset + HashLength)..]);
h.GetCurrentHash(dest);
var current = CurrentHash;
current.Clear();
RuntimeCryptographyProvider.Md5.HashData(Data, current);
}

protected override void SetChecksums() => ComputeHash(Data, CurrentHash);
public override bool ChecksumsValid => GetIsHashValid(Data, CurrentHash);
public override string ChecksumInfo => !ChecksumsValid ? "MD5 Hash Invalid" : string.Empty;

public static bool GetIsHashValid(ReadOnlySpan<byte> data, ReadOnlySpan<byte> currentHash)
public override bool ChecksumsValid
{
Span<byte> computed = stackalloc byte[HashLength];
ComputeHash(data, computed);
return computed.SequenceEqual(currentHash);
get
{
// Cache existing checksum as computing will update it.
var current = CurrentHash;
Span<byte> exist = stackalloc byte[HashLength];
current.CopyTo(exist);
SetChecksums();
var result = current.SequenceEqual(exist);
if (!result)
exist.CopyTo(current); // restore original bad checksum
return result;
}
}

public override string ChecksumInfo => !ChecksumsValid ? "MD5 Hash Invalid" : string.Empty;

#endregion

protected override PB8 GetPKM(byte[] data) => new(data);
Expand Down

0 comments on commit 6de68ac

Please sign in to comment.