Misc refactoring

add docs, move some data fetching to more appropriate class
remove old XP memecrypto support handling, was previously removed due to
net standard/core split
refactor memecrypto to handle multiple save sizes (USUM won't be the
same size save file); placeholder -1 for USUM size
This commit is contained in:
Kurt 2017-09-16 11:38:58 -07:00
parent 599a67a5a0
commit 7efd771bf4
10 changed files with 158 additions and 230 deletions

View file

@ -567,7 +567,7 @@ namespace PKHeX.Core
/// <param name="gen">Generation to get location names for.</param>
/// <param name="bankID">BankID used to choose the text bank.</param>
/// <returns>List of location names.</returns>
public static string[] GetLocationNames(int gen, int bankID)
private static string[] GetLocationNames(int gen, int bankID)
{
switch (gen)
{
@ -611,5 +611,51 @@ namespace PKHeX.Core
return null;
}
}
/// <summary>
/// Gets the location name for the specified parameters.
/// </summary>
/// <param name="eggmet">Location is from the <see cref="PKM.Egg_Location"/></param>
/// <param name="locval">Location value</param>
/// <param name="format">Current <see cref="PKM.Format"/></param>
/// <param name="generation"><see cref="PKM.GenNumber"/> of origin</param>
/// <returns>Location name</returns>
public static string GetLocationName(bool eggmet, int locval, int format, int generation)
{
int gen = -1;
int bankID = 0;
if (format == 2)
gen = 2;
else if (format == 3)
gen = 3;
else if (generation == 4 && (eggmet || format == 4)) // 4
{
const int size = 1000;
bankID = locval / size;
gen = 4;
locval %= size;
}
else // 5-7+
{
const int size = 10000;
bankID = locval / size;
int g = generation;
if (g >= 5)
gen = g;
else if (format >= 5)
gen = format;
locval %= size;
if (bankID >= 3)
locval -= 1;
}
var bank = GetLocationNames(gen, bankID);
if (bank == null || bank.Length <= locval)
return string.Empty;
return bank[locval];
}
}
}

View file

@ -168,15 +168,18 @@ namespace PKHeX.Core
};
#endregion
/// <summary>
/// Species name lists indexed by the <see cref="PKM.Language"/> value.
/// </summary>
public static readonly string[][] SpeciesLang =
{
Util.GetSpeciesList("ja"), // none
Util.GetSpeciesList("ja"), // 0 (unused, invalid)
Util.GetSpeciesList("ja"), // 1
Util.GetSpeciesList("en"), // 2
Util.GetSpeciesList("fr"), // 3
Util.GetSpeciesList("it"), // 4
Util.GetSpeciesList("de"), // 5
Util.GetSpeciesList("es"), // none
Util.GetSpeciesList("es"), // 6 (reserved for Gen3 KO?, unused)
Util.GetSpeciesList("es"), // 7
Util.GetSpeciesList("ko"), // 8
Util.GetSpeciesList("zh"), // 9 Simplified
@ -570,6 +573,14 @@ namespace PKHeX.Core
ivs[i] = (ivs[i] & 0x1E) + hpivs[type, i];
return ivs;
}
/// <summary>
/// Hidden Power IV values (even or odd) to achieve a specified Hidden Power Type
/// </summary>
/// <remarks>
/// There are other IV combinations to achieve the same Hidden Power Type.
/// These are just precomputed for fast modification.
/// </remarks>
public static readonly int[,] hpivs = {
{ 1, 1, 0, 0, 0, 0 }, // Fighting
{ 0, 0, 0, 0, 0, 1 }, // Flying
@ -801,46 +812,19 @@ namespace PKHeX.Core
}
// Extensions
/// <summary>
/// Gets the Location Name for the <see cref="PKM"/>
/// </summary>
/// <param name="pk">PKM to fetch data for</param>
/// <param name="eggmet">Location requested is the egg obtained location, not met location.</param>
/// <returns>Location string</returns>
public static string GetLocationString(this PKM pk, bool eggmet)
{
if (pk.Format < 2)
return "";
int gen = -1;
int bankID = 0;
int locval = eggmet ? pk.Egg_Location : pk.Met_Location;
if (pk.Format == 2)
gen = 2;
else if (pk.Format == 3)
gen = 3;
else if (pk.Gen4 && (eggmet || pk.Format == 4)) // 4
{
const int size = 1000;
bankID = locval/size;
gen = 4;
locval %= size;
}
else // 5-7+
{
const int size = 10000;
bankID = locval/size;
int g = pk.GenNumber;
if (g >= 5)
gen = g;
else if (pk.Format >= 5)
gen = pk.Format;
locval %= size;
if (bankID >= 3)
locval -= 1;
}
var bank = GameInfo.GetLocationNames(gen, bankID);
if (bank == null || bank.Length <= locval)
return "";
return bank[locval];
return GameInfo.GetLocationName(eggmet, locval, pk.Format, pk.GenNumber);
}
public static string[] GetQRLines(this PKM pkm)
{

View file

@ -1250,14 +1250,6 @@ namespace PKHeX.Core
{
var r = new StringBuilder();
// MemeCrypto check
if (RequiresMemeCrypto && !MemeCrypto.CanUseMemeCrypto())
{
r.AppendLine("Platform does not support required cryptography providers.");
r.AppendLine("Checksum will be broken until the file is saved using an OS without FIPS compliance enabled or a newer OS.");
r.AppendLine();
}
// FFFF checks
byte[] FFFF = Enumerable.Repeat((byte)0xFF, 0x200).ToArray();
for (int i = 0; i < Data.Length / 0x200; i++)

View file

@ -10,6 +10,7 @@ namespace PKHeX.Core
{
public const int BEEF = 0x42454546;
public const int SIZE_G7USUM = -1;
public const int SIZE_G7SM = 0x6BE00;
public const int SIZE_G6XY = 0x65600;
public const int SIZE_G6ORAS = 0x76000;
@ -47,10 +48,10 @@ namespace PKHeX.Core
SIZE_G1RAW, SIZE_G1BAT
};
public static readonly byte[] FOOTER_DSV = Encoding.ASCII.GetBytes("|-DESMUME SAVE-|");
public static readonly string[] HEADER_COLO = { "GC6J","GC6E","GC6P" }; // NTSC-J, NTSC-U, PAL
public static readonly string[] HEADER_XD = { "GXXJ","GXXE","GXXP" }; // NTSC-J, NTSC-U, PAL
public static readonly string[] HEADER_RSBOX = { "GPXJ","GPXE","GPXP" }; // NTSC-J, NTSC-U, PAL
private static readonly byte[] FOOTER_DSV = Encoding.ASCII.GetBytes("|-DESMUME SAVE-|");
internal static readonly string[] HEADER_COLO = { "GC6J","GC6E","GC6P" }; // NTSC-J, NTSC-U, PAL
internal static readonly string[] HEADER_XD = { "GXXJ","GXXE","GXXP" }; // NTSC-J, NTSC-U, PAL
internal static readonly string[] HEADER_RSBOX = { "GPXJ","GPXE","GPXP" }; // NTSC-J, NTSC-U, PAL
/// <summary>Determines the generation of the given save data.</summary>
/// <param name="data">Save data of which to determine the generation</param>
@ -562,11 +563,11 @@ namespace PKHeX.Core
}
/// <summary>
/// Retrieves the full path of the most recent file based on LastWriteTime.
/// Retrieves the full path of the most recent file based on <see cref="FileInfo.LastWriteTime"/>.
/// </summary>
/// <param name="folderPath">Folder to look within</param>
/// <param name="deep">Search all subfolders</param>
/// <param name="result">If this function returns true, full path of all save files that match criteria. If this function returns false, the error message, or null if the directory could not be found</param>
/// <param name="result">If this function returns true, full path of all <see cref="SaveFile"/> that match criteria. If this function returns false, the error message, or null if the directory could not be found</param>
/// <returns>Boolean indicating whether or not operation was successful.</returns>
public static bool GetSavesFromFolder(string folderPath, bool deep, out IEnumerable<string> result)
{
@ -684,7 +685,7 @@ namespace PKHeX.Core
}
public static byte[] Resign7(byte[] sav7)
{
return MemeCrypto.Resign(sav7, false);
return MemeCrypto.Resign(sav7);
}
/// <summary>Calculates the 32bit checksum over an input byte array. Used in GBA save files.</summary>
/// <param name="data">Input byte array</param>

View file

@ -3,7 +3,6 @@ using System.IO;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
namespace PKHeX.Core
{
@ -12,19 +11,17 @@ namespace PKHeX.Core
private static byte[] AESECBEncrypt(byte[] key, byte[] data)
{
using (var ms = new MemoryStream())
using (var aes = Aes.Create())
{
using (var aes = Util.GetAesProvider())
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
using (var cs = new CryptoStream(ms, aes.CreateEncryptor(key, new byte[0x10]), CryptoStreamMode.Write))
{
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
using (var cs = new CryptoStream(ms, aes.CreateEncryptor(key, new byte[0x10]), CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
return ms.ToArray();
}
}
}
@ -32,19 +29,17 @@ namespace PKHeX.Core
private static byte[] AESECBDecrypt(byte[] key, byte[] data)
{
using (var ms = new MemoryStream())
using (var aes = Aes.Create())
{
using (var aes = Util.GetAesProvider())
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(key, new byte[0x10]), CryptoStreamMode.Write))
{
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(key, new byte[0x10]), CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
return ms.ToArray();
}
return ms.ToArray();
}
}
}
@ -171,10 +166,9 @@ namespace PKHeX.Core
private static byte[] ReverseCrypt(byte[] input, int meme_ofs, int memeindex)
{
var output = (byte[])input.Clone();
var memekey = MemeKeys[memeindex];
using (var sha1 = Util.GetSHA1Provider())
using (var sha1 = SHA1.Create())
{
var enc = new byte[0x60];
Array.Copy(input, meme_ofs, enc, 0, 0x60);
@ -202,6 +196,7 @@ namespace PKHeX.Core
return null;
}
private const uint POKE = 0x454B4F50;
public static byte[] VerifyMemeData(byte[] input)
{
if (input.Length < 0x60)
@ -211,7 +206,7 @@ namespace PKHeX.Core
for (var i = input.Length - 8; i >= 0; i--)
{
if (BitConverter.ToUInt32(input, i) != 0x454B4F50 ||
if (BitConverter.ToUInt32(input, i) != POKE ||
BitConverter.ToUInt32(input, i + 4) >= MemeKeys.Length) continue;
meme_ofs = i - 0x60;
@ -236,7 +231,7 @@ namespace PKHeX.Core
if (input.Length < 0x60)
throw new ArgumentException("Bad Meme input!");
const int memeindex = 3;
using (var sha1 = Util.GetSHA1Provider())
using (var sha1 = SHA1.Create())
{
var key = sha1.ComputeHash(MemeKeys[memeindex].DER.Concat(input.Take(input.Length - 0x60)).ToArray()).Take(0x10).ToArray();
@ -251,66 +246,40 @@ namespace PKHeX.Core
}
/// <summary>
/// Resigns save data.
/// Resigns save data.
/// </summary>
/// <param name="sav7">The save data to resign.</param>
/// <param name="throwIfUnsupported">
/// If true, throw an <see cref="InvalidOperationException" /> if MemeCrypto is
/// unsupported. If false, calling this function will have no effect.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the current platform has FIPS mode enabled on a platform that
/// does not support the required crypto service providers.
/// </exception>
/// <returns>The resigned save data.</returns>
public static byte[] Resign(byte[] sav7, bool throwIfUnsupported = true)
public static byte[] Resign(byte[] sav7)
{
if (sav7 == null || sav7.Length != 0x6BE00)
if (sav7 == null || sav7.Length != SaveUtil.SIZE_G7SM && sav7.Length != SaveUtil.SIZE_G7USUM)
return null;
try
// Save Chunks are 0x200 bytes each; Memecrypto signature is 0x100 bytes into the 2nd to last chunk.
int ChecksumTableOffset = sav7.Length - 0x200;
int MemeCryptoOffset = ChecksumTableOffset - 0x100;
const int ChecksumSignatureLength = 0x140;
const int MemeCryptoSignatureLength = 0x80;
var outSav = (byte[])sav7.Clone();
using (var sha256 = SHA256.Create())
{
var outSav = (byte[])sav7.Clone();
// Store current signature
var CurSig = new byte[MemeCryptoSignatureLength];
Buffer.BlockCopy(sav7, MemeCryptoOffset, CurSig, 0, MemeCryptoSignatureLength);
using (var sha256 = Util.GetSHA256Provider())
{
var CurSig = new byte[0x80];
Array.Copy(sav7, 0x6BB00, CurSig, 0, 0x80);
var ChecksumTableSignature = new byte[ChecksumSignatureLength];
Buffer.BlockCopy(sav7, ChecksumTableOffset, ChecksumTableSignature, 0, ChecksumSignatureLength);
var ChecksumTable = new byte[0x140];
Array.Copy(sav7, 0x6BC00, ChecksumTable, 0, 0x140);
var newSig = new byte[MemeCryptoSignatureLength];
sha256.ComputeHash(ChecksumTableSignature).CopyTo(newSig, 0);
var memeSig = VerifyMemeData(CurSig);
if (memeSig != null)
Buffer.BlockCopy(memeSig, 0x20, newSig, 0x20, 0x60);
var newSig = new byte[0x80];
sha256.ComputeHash(ChecksumTable).CopyTo(newSig, 0);
var memeSig = VerifyMemeData(CurSig);
if (memeSig != null)
Array.Copy(memeSig, 0x20, newSig, 0x20, 0x60);
SignMemeData(newSig).CopyTo(outSav, 0x6BB00);
}
return outSav;
SignMemeData(newSig).CopyTo(outSav, MemeCryptoOffset);
}
catch (InvalidOperationException)
{
if (throwIfUnsupported)
{
throw;
}
return (byte[])sav7.Clone();
}
}
public static bool CanUseMemeCrypto()
{
try
{
Util.GetSHA256Provider();
}
catch (InvalidOperationException)
{
return false;
}
return true;
return outSav;
}
#region Meme Key Data
@ -400,13 +369,5 @@ namespace PKHeX.Core
x[i] = (byte)(b1[i] ^ b2[i]);
return x;
}
public static string ToHexString(this byte[] ba)
{
var hex = new StringBuilder(ba.Length * 2);
foreach (var b in ba)
hex.AppendFormat("{0:X2}", b);
return hex.ToString();
}
}
}

View file

@ -1,36 +0,0 @@
using System;
using System.Security.Cryptography;
namespace PKHeX.Core
{
public partial class Util
{
/// <summary>
/// Creates a new instance of <see cref="SHA1CryptoServiceProvider"/>, or <see cref="SHA1Managed"/> if not supported by the current platform.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if FIPS mode is enabled on a platform that does not support <see cref="SHA1CryptoServiceProvider"/>.</exception>
public static SHA1 GetSHA1Provider()
{
return SHA1.Create();
}
/// <summary>
/// Creates a new instance of <see cref="SHA256CryptoServiceProvider"/>, or <see cref="SHA256Managed"/> if not supported by the current platform.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if FIPS mode is enabled on a platform that does not support <see cref="SHA256CryptoServiceProvider"/>.</exception>
public static SHA256 GetSHA256Provider()
{
return SHA256.Create();
}
/// <summary>
/// Creates a new instance of <see cref="AesCryptoServiceProvider"/>, or <see cref="AesManaged"/> if not supported by the current platform.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if FIPS mode is enabled on a platform that does not support <see cref="AesCryptoServiceProvider"/>.</exception>
public static Aes GetAesProvider()
{
return Aes.Create();
}
}
}

View file

@ -743,7 +743,7 @@ namespace PKHeX.WinForms
if (sav == null || sav.Version == GameVersion.Invalid)
{ WinFormsUtil.Error("Invalid save file loaded. Aborting.", path); return; }
if (!SanityCheckSAV(ref sav, path))
if (!SanityCheckSAV(ref sav))
return;
StoreLegalSaveGameData(sav);
PKMUtil.Initialize(sav); // refresh sprite generator
@ -834,17 +834,8 @@ namespace PKHeX.WinForms
"If the path is a removable disk (SD card), please ensure the write protection switch is not set.");
return false;
}
private static bool SanityCheckSAV(ref SaveFile sav, string path)
private static bool SanityCheckSAV(ref SaveFile sav)
{
if (!string.IsNullOrWhiteSpace(path)) // If path is null, this is the default save
{
if (sav.RequiresMemeCrypto && !MemeCrypto.CanUseMemeCrypto())
{
WinFormsUtil.Error("Your platform does not support the required cryptography components.",
"In order to be able to save your changes, you must either upgrade to a newer version of Windows or disable FIPS compliance mode.");
// Don't abort loading; user can still view save and fix checksum on another platform.
}
}
// Finish setting up the save file.
if (sav.Generation == 1)
{

View file

@ -1,21 +1,33 @@
using PKHeX.Core;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using PKHeX.Core;
namespace PKHeX.WinForms
{
public static class PathUtilWindows
{
public static string Get3DSLocation()
/// <summary>
/// Gets the 3DS's root folder, usually from an inserted SD card.
/// </summary>
/// <param name="skipFirstDrive">Optional parameter to skip the first drive.
/// The first drive is usually the system hard drive, or can be a floppy disk drive (slower to check, never has expected data).</param>
/// <returns>Folder path pointing to the Nintendo 3DS folder.</returns>
public static string Get3DSLocation(bool skipFirstDrive = true)
{
try
{
string[] DriveList = Environment.GetLogicalDrives();
for (int i = 1; i < DriveList.Length; i++) // Skip first drive (some users still have floppy drives and would chew up time!)
IEnumerable<string> DriveList = Environment.GetLogicalDrives();
// Skip first drive (some users still have floppy drives and would chew up time!)
if (skipFirstDrive)
DriveList = DriveList.Skip(1);
foreach (var drive in DriveList)
{
string potentialPath = Path.Combine(DriveList[i], "Nintendo 3DS");
string potentialPath = Path.Combine(drive, "Nintendo 3DS");
if (Directory.Exists(potentialPath))
return potentialPath;
}
@ -29,41 +41,37 @@ namespace PKHeX.WinForms
/// </summary>
/// <param name="root">Root location of device</param>
/// <returns>List of possible 3DS save backup paths.</returns>
public static string[] Get3DSBackupPaths(string root)
public static IEnumerable<string> Get3DSBackupPaths(string root)
{
return new[]
{
Path.Combine(root, "saveDataBackup"),
Path.Combine(root, "filer", "UserSaveData"),
Path.Combine(root, "JKSV", "Saves"),
Path.Combine(root, "TWLSaveTool"),
Path.Combine(root, "fbi", "save"),
Path.Combine(root, "gm9", "out"),
Path.Combine(root, "3ds", "data", "JKSM", "Saves"),
};
yield return Path.Combine(root, "saveDataBackup");
yield return Path.Combine(root, "filer", "UserSaveData");
yield return Path.Combine(root, "JKSV", "Saves");
yield return Path.Combine(root, "TWLSaveTool");
yield return Path.Combine(root, "fbi", "save");
yield return Path.Combine(root, "gm9", "out");
}
/// <summary>
/// Detects a save file.
/// Finds a compatible save file that was most recently saved (by file write time).
/// </summary>
/// <param name="path">If this function returns true, full path of a save file or null if no path could be found. If this function returns false, this parameter will be set to the error message.</param>
/// <param name="extra">Paths to check in addition to the default paths</param>
/// <returns>A boolean indicating whether or not a file was detected</returns>
public static bool DetectSaveFile(out string path, params string[] extra)
{
var foldersToCheck = extra.Where(f => f?.Length > 0);
string path3DS = Path.GetPathRoot(Get3DSLocation());
List<string> possiblePaths = new List<string>();
List<string> foldersToCheck = new List<string>(extra.Where(f => f?.Length > 0));
path = null;
if (path3DS != null) // check for Homebrew/CFW backups
foldersToCheck.AddRange(Get3DSBackupPaths(path3DS));
foldersToCheck = foldersToCheck.Concat(Get3DSBackupPaths(path3DS));
foreach (var p in foldersToCheck)
path = null;
List<string> possiblePaths = new List<string>();
foreach (var folder in foldersToCheck)
{
if (!SaveUtil.GetSavesFromFolder(p, true, out IEnumerable<string> files))
if (!SaveUtil.GetSavesFromFolder(folder, true, out IEnumerable<string> files))
{
if (files != null) // Could be null if `p` doesn't exist
if (files != null) // can be null if folder doesn't exist
{
path = string.Join(Environment.NewLine, files); // `files` contains the error message
return false;

View file

@ -77,7 +77,6 @@
<Compile Include="PKM\PIDIVTests.cs" />
<Compile Include="PKM\PKMTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Saves\Substructures\MemeCryptoTests.cs" />
<Compile Include="Util\DateUtilTests.cs" />
</ItemGroup>
<ItemGroup>
@ -90,7 +89,9 @@
<Name>PKHeX.WinForms</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Folder Include="Saves\Substructures\" />
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>

View file

@ -1,20 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PKHeX.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PKHeX.Tests.Saves.Substructures
{
[TestClass]
public class MemeCryptoTests
{
[TestMethod]
public void CanUseMemeCrypto()
{
Assert.IsTrue(MemeCrypto.CanUseMemeCrypto());
}
}
}