feat: allow external consumers to specify AES implementation (#4311)

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.
This commit is contained in:
Arley Pádua 2024-07-02 16:12:03 +02:00 committed by GitHub
parent 298c83bc09
commit 6de68ac626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 41 deletions

View file

@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using static System.Buffers.Binary.BinaryPrimitives;
@ -78,24 +77,22 @@ public static class HomeCrypto
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>

View file

@ -74,7 +74,7 @@ public readonly ref struct MemeKey
{
var slice = sig.Slice(i, chunk);
Xor(temp, slice);
aes.DecryptEcb(temp, temp, PaddingMode.None);
aes.DecryptEcb(temp, temp);
temp.CopyTo(slice);
}
@ -88,7 +88,7 @@ public readonly ref struct MemeKey
{
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);
}
@ -104,7 +104,7 @@ public readonly ref struct MemeKey
{
var slice = sig.Slice(i, chunk);
Xor(slice, temp);
aes.EncryptEcb(slice, slice, PaddingMode.None);
aes.EncryptEcb(slice, slice);
slice.CopyTo(temp);
}
@ -118,24 +118,20 @@ public readonly ref struct MemeKey
{
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>

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -59,7 +59,7 @@ public sealed class SAV8BS : SaveFile, ISaveFileRevision, ITrainerStatRecord, IE
// 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]
@ -174,27 +174,33 @@ public sealed class SAV8BS : SaveFile, ISaveFileRevision, ITrainerStatRecord, IE
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);
}
public override bool ChecksumsValid
{
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;
}
}
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)
{
Span<byte> computed = stackalloc byte[HashLength];
ComputeHash(data, computed);
return computed.SequenceEqual(currentHash);
}
#endregion
protected override PB8 GetPKM(byte[] data) => new(data);