diff --git a/PKHeX.Core/PKM/HOME/HomeCrypto.cs b/PKHeX.Core/PKM/HOME/HomeCrypto.cs index c7d4c870fa3..de3e390315a 100644 --- a/PKHeX.Core/PKM/HOME/HomeCrypto.cs +++ b/PKHeX.Core/PKM/HOME/HomeCrypto.cs @@ -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 byte[] Crypt(ReadOnlySpan 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 data, byte[] key, byte[] iv, byte[] result, ushort dataSize, bool decrypt) + private static void Crypt(ReadOnlySpan data, Span 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); } /// diff --git a/PKHeX.Core/Saves/Encryption/MemeCrypto/MemeKey.cs b/PKHeX.Core/Saves/Encryption/MemeCrypto/MemeKey.cs index 9eb1529647b..113271a5881 100644 --- a/PKHeX.Core/Saves/Encryption/MemeCrypto/MemeKey.cs +++ b/PKHeX.Core/Saves/Encryption/MemeCrypto/MemeKey.cs @@ -74,7 +74,7 @@ private void AesDecrypt(ReadOnlySpan data, Span sig) { 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 @@ private void AesDecrypt(ReadOnlySpan data, Span 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); } @@ -104,7 +104,7 @@ private void AesEncrypt(Span data, Span sig) { 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 @@ private void AesEncrypt(Span data, Span 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 payload) + private IAesCryptographyProvider.IAes GetAesImpl(ReadOnlySpan 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); } /// diff --git a/PKHeX.Core/Saves/Encryption/Providers/IAesCryptographyProvider.cs b/PKHeX.Core/Saves/Encryption/Providers/IAesCryptographyProvider.cs new file mode 100644 index 00000000000..abb3cba87c6 --- /dev/null +++ b/PKHeX.Core/Saves/Encryption/Providers/IAesCryptographyProvider.cs @@ -0,0 +1,52 @@ +using System; +using System.Security.Cryptography; + +namespace PKHeX.Core; + +/// +/// Provide an implementation of the Aes algorithm +/// +/// +///

The property will use the .NET implementation that will return an implementation that is specific for each platform except browser (web assembly).

+///

This interface is intended to allow any runtime that's not supported to provide its own implementation.

+///

See more at https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/5.0/cryptography-apis-not-supported-on-blazor-webassembly

+///
+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 plaintext, Span destination); + void DecryptEcb(ReadOnlySpan ciphertext, Span destination); + void EncryptCbc(ReadOnlySpan plaintext, Span destination); + void DecryptCbc(ReadOnlySpan ciphertext, Span 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 plaintext, Span destination) => _aes.EncryptEcb(plaintext, destination, _aes.Padding); + public void DecryptEcb(ReadOnlySpan ciphertext, Span destination) => _aes.DecryptEcb(ciphertext, destination, _aes.Padding); + public void EncryptCbc(ReadOnlySpan plaintext, Span destination) => _aes.EncryptCbc(plaintext, _aes.IV, destination, _aes.Padding); + public void DecryptCbc(ReadOnlySpan ciphertext, Span destination) => _aes.DecryptCbc(ciphertext, _aes.IV, destination, _aes.Padding); + } + } +} diff --git a/PKHeX.Core/Saves/Encryption/Providers/IMd5Provider.cs b/PKHeX.Core/Saves/Encryption/Providers/IMd5Provider.cs new file mode 100644 index 00000000000..377391f3954 --- /dev/null +++ b/PKHeX.Core/Saves/Encryption/Providers/IMd5Provider.cs @@ -0,0 +1,16 @@ +using System; +using System.Security.Cryptography; + +namespace PKHeX.Core; + +public interface IMd5Provider +{ + void HashData(ReadOnlySpan source, Span destination); + + internal static readonly IMd5Provider Default = new DefaultMd5(); + + private sealed class DefaultMd5 : IMd5Provider + { + public void HashData(ReadOnlySpan source, Span destination) => MD5.HashData(source, destination); + } +} diff --git a/PKHeX.Core/Saves/Encryption/Providers/RuntimeCryptographyProvider.cs b/PKHeX.Core/Saves/Encryption/Providers/RuntimeCryptographyProvider.cs new file mode 100644 index 00000000000..2511fc5225a --- /dev/null +++ b/PKHeX.Core/Saves/Encryption/Providers/RuntimeCryptographyProvider.cs @@ -0,0 +1,10 @@ +namespace PKHeX.Core; + +/// +/// Holds the singleton instance that provides the AES implementation to the app running this library +/// +public static class RuntimeCryptographyProvider +{ + public static IAesCryptographyProvider Aes { get; set; } = IAesCryptographyProvider.Default; + public static IMd5Provider Md5 { get; set; } = IMd5Provider.Default; +} diff --git a/PKHeX.Core/Saves/SAV8BS.cs b/PKHeX.Core/Saves/SAV8BS.cs index 5e90756683d..c8362d43e04 100644 --- a/PKHeX.Core/Saves/SAV8BS.cs +++ b/PKHeX.Core/Saves/SAV8BS.cs @@ -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] @@ -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 CurrentHash => Data.AsSpan(HashOffset, HashLength); - private static void ComputeHash(ReadOnlySpan data, Span 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 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 data, ReadOnlySpan currentHash) + public override bool ChecksumsValid { - Span computed = stackalloc byte[HashLength]; - ComputeHash(data, computed); - return computed.SequenceEqual(currentHash); + get + { + // Cache existing checksum as computing will update it. + var current = CurrentHash; + Span 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);