mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-10 06:34:19 +00:00
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:
parent
298c83bc09
commit
6de68ac626
6 changed files with 118 additions and 41 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
PKHeX.Core/Saves/Encryption/Providers/IMd5Provider.cs
Normal file
16
PKHeX.Core/Saves/Encryption/Providers/IMd5Provider.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue