using System; using System.IO; using System.Runtime.CompilerServices; using System.Security.Cryptography; using static System.Buffers.Binary.BinaryPrimitives; namespace PKHeX.Core; /// /// Logic related to Encrypting and Decrypting Pokémon Home entity data. /// public static class HomeCrypto { public const int Version1 = 1; public const int Version2 = 2; public const int SIZE_1HEADER = 0x10; // 16 public const int SIZE_1CORE = 0xC8; // 200 public const int SIZE_1GAME_PB7 = 0x3B; // 59 public const int SIZE_1GAME_PK8 = 0x44; // 68 public const int SIZE_1GAME_PA8 = 0x3C; // 60 public const int SIZE_1GAME_PB8 = 0x2B; // 43 public const int SIZE_1STORED = 0x1EE; // 494 public const int SIZE_2CORE = 0xC4; // 196 public const int SIZE_2GAME_PB7 = 0x3F; // 63 public const int SIZE_2GAME_PK8 = 0x48; // 72 public const int SIZE_2GAME_PA8 = 0x40; // 64 public const int SIZE_2GAME_PB8 = 0x2F; // 47 public const int SIZE_2GAME_PK9 = 0x3D; // 61 public const int SIZE_2STORED = 0x23A; // 570 public const int SIZE_STORED = SIZE_2STORED; public const int SIZE_CORE = SIZE_2CORE; public const int VersionLatest = Version2; public static bool IsKnownVersion(ushort version) => version is Version1 or Version2; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetEncryptionKey(Span key, ulong seed) { WriteUInt64BigEndian(key, seed ^ 0x6B7B5966193DB88B); WriteUInt64BigEndian(key.Slice(8, 8), seed & 0x937EC53BF8856E87); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetEncryptionIv(Span iv, ulong seed) { WriteUInt64BigEndian(iv, seed ^ 0x5F4ED4E84975D976); WriteUInt64BigEndian(iv.Slice(8, 8), seed | 0xE3CDA917EA9E489C); } /// /// Encryption and Decryption are asymmetrical operations, but we reuse the same method and pivot off the inputs. /// /// Data to crypt, not in place. /// Encryption or Decryption mode /// New array with result data. /// if the format is not supported. public static byte[] Crypt(ReadOnlySpan data, bool decrypt = true) { var format = ReadUInt16LittleEndian(data); if (!IsKnownVersion(format)) throw new ArgumentException($"Unrecognized format: {format}"); ulong seed = ReadUInt64LittleEndian(data.Slice(2, 8)); var key = new byte[0x10]; SetEncryptionKey(key, seed); var iv = new byte[0x10]; SetEncryptionIv(iv, seed); 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); return result; } private static void Crypt(ReadOnlySpan data, byte[] key, byte[] iv, byte[] result, ushort dataSize, 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); } /// /// Decrypts the input data into a new array if it is encrypted, and updates the reference. /// /// Format encryption check public static void DecryptIfEncrypted(ref byte[] data) { var span = data.AsSpan(); var format = ReadUInt16LittleEndian(span); if (IsKnownVersion(format)) { if (GetIsEncrypted(span, format)) data = Crypt(span); } else { throw new ArgumentException($"Unrecognized format: {format}"); } } /// /// Converts the input data into their encrypted state. /// public static byte[] Encrypt(ReadOnlySpan pk) { var result = Crypt(pk, false); RefreshChecksum(result, result); return result; } private static void RefreshChecksum(ReadOnlySpan encrypted, Span dest) { var chk = GetChecksum1(encrypted); WriteUInt32LittleEndian(dest[0xA..0xE], chk); } /// /// Calculates the checksum of format 1 data. /// public static uint GetChecksum1(ReadOnlySpan encrypted) => GetCHK(encrypted[SIZE_1HEADER..]); /// /// Checks if the format 1 data is encrypted. /// /// True if encrypted. public static bool GetIsEncrypted(ReadOnlySpan data, ushort format) => format switch { Version1 => IsEncryptedCore1(data), Version2 => IsEncryptedCore2(data), _ => throw new ArgumentException($"Unrecognized format: {format}"), }; private static bool IsEncryptedCore1(ReadOnlySpan data) { var core = data.Slice(SIZE_1HEADER + 2, SIZE_1CORE); // Strings should be \0000 terminated if decrypted. // Any non-zero value is a sign of encryption. if (ReadUInt16LittleEndian(core[0xB5..]) != 0) // OT return true; // OT_Name final terminator should be 0 if decrypted. if (ReadUInt16LittleEndian(core[0x60..]) != 0) // Nick return true; // Nickname final terminator should be 0 if decrypted. if (ReadUInt16LittleEndian(core[0x88..]) != 0) // HT return true; // HT_Name final terminator should be 0 if decrypted. //// Fall back to checksum. //return ReadUInt32LittleEndian(data[0xA..0xE]) == GetChecksum1(data); return false; // 64 bits checked is enough to feel safe about this check. } private static bool IsEncryptedCore2(ReadOnlySpan data) { var core = data.Slice(SIZE_1HEADER + 2, SIZE_2CORE); if (ReadUInt16LittleEndian(core[0xB1..]) != 0) return true; // OT_Name final terminator should be 0 if decrypted. if (ReadUInt16LittleEndian(core[0x5C..]) != 0) return true; // Nickname final terminator should be 0 if decrypted. if (ReadUInt16LittleEndian(core[0x84..]) != 0) return true; // HT_Name final terminator should be 0 if decrypted. //// Fall back to checksum. //return ReadUInt32LittleEndian(data[0xA..0xE]) == GetChecksum1(data); return false; // 64 bits checked is enough to feel safe about this check. } /// /// Gets the checksum of an Pokémon's AES-encrypted data. /// /// AES-Encrypted Pokémon data. public static uint GetCHK(ReadOnlySpan data) { uint chk = 0; for (var i = 0; i < data.Length; i += 100) { var chunkSize = Math.Min(data.Length - i, 100); var span = data.Slice(i, chunkSize); chk ^= Checksums.CRC32Invert(span); } return chk; } }