diff --git a/TaikoSwitchDataTableDecryptor.sln b/TaikoSwitchDataTableDecryptor.sln new file mode 100644 index 0000000..2f4ad3b --- /dev/null +++ b/TaikoSwitchDataTableDecryptor.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.1525 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TaikoSwitchDataTableDecryptor", "TaikoSwitchDataTableDecryptor\TaikoSwitchDataTableDecryptor.vcxproj", "{2A0F9E61-C7B2-497B-B587-C580E0C17110}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zlib", "Dependencies\zlib\zlib.vcxproj", "{164A9D43-345C-407B-BF59-527386DED788}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{EE11261E-85ED-4445-AB6F-5C4F55B4E34B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Debug|x64.ActiveCfg = Debug|x64 + {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Debug|x64.Build.0 = Debug|x64 + {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Release|x64.ActiveCfg = Release|x64 + {2A0F9E61-C7B2-497B-B587-C580E0C17110}.Release|x64.Build.0 = Release|x64 + {164A9D43-345C-407B-BF59-527386DED788}.Debug|x64.ActiveCfg = Debug|x64 + {164A9D43-345C-407B-BF59-527386DED788}.Debug|x64.Build.0 = Debug|x64 + {164A9D43-345C-407B-BF59-527386DED788}.Release|x64.ActiveCfg = Release|x64 + {164A9D43-345C-407B-BF59-527386DED788}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {164A9D43-345C-407B-BF59-527386DED788} = {EE11261E-85ED-4445-AB6F-5C4F55B4E34B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {32848A13-A06E-49EA-B024-22472419BF2F} + EndGlobalSection +EndGlobal diff --git a/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj new file mode 100644 index 0000000..054a5cd --- /dev/null +++ b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj @@ -0,0 +1,102 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {2A0F9E61-C7B2-497B-B587-C580E0C17110} + TaikoSwitchDataTableDecryptor + 10.0.17763.0 + + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + + + + + + + + + + + + + $(ProjectDir)bin\$(Platform)-$(Configuration)\ + $(ProjectDir)bin-int\$(Platform)-$(Configuration)\ + + + $(ProjectDir)bin\$(Platform)-$(Configuration)\ + $(ProjectDir)bin-int\$(Platform)-$(Configuration)\ + + + + Level3 + Disabled + true + true + stdcpp17 + false + MultiThreadedDebug + $(ProjectDir)src;$(SolutionDir)Dependencies\zlib\include + + + Console + zlib.lib;Bcrypt.lib;%(AdditionalDependencies) + $(SolutionDir)Dependencies\zlib\bin\$(Platform)-$(Configuration) + + + + + Level3 + MaxSpeed + true + true + true + true + false + MultiThreaded + $(ProjectDir)src;$(SolutionDir)Dependencies\zlib\include + stdcpp17 + + + Console + true + true + zlib.lib;Bcrypt.lib;%(AdditionalDependencies) + $(SolutionDir)Dependencies\zlib\bin\$(Platform)-$(Configuration) + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj.filters b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj.filters new file mode 100644 index 0000000..1a1bb48 --- /dev/null +++ b/TaikoSwitchDataTableDecryptor/TaikoSwitchDataTableDecryptor.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/TaikoSwitchDataTableDecryptor/src/EntryPoint.cpp b/TaikoSwitchDataTableDecryptor/src/EntryPoint.cpp new file mode 100644 index 0000000..9db51a3 --- /dev/null +++ b/TaikoSwitchDataTableDecryptor/src/EntryPoint.cpp @@ -0,0 +1,107 @@ +#include "Types.h" +#include "Utilities.h" + +namespace PeepoHappy +{ + bool HasValidGZipHeader(const u8* fileContent, size_t fileSize) + { + if (fileSize <= Crypto::AesKeySize) + return false; + + constexpr std::array gzibMagic = { 0x1F, 0x8B }; + + const bool validMagic = (memcmp(fileContent, gzibMagic.data(), gzibMagic.size()) == 0); + const bool validCompressionMethod = (fileContent[2] == /*Z_DEFLATED*/ 0x08); + + return (validMagic && validCompressionMethod); + } + + bool DecompressAndWriteDataTableJsonFile(const u8* compressedData, size_t compressedDataSize, std::string_view jsonOutputFilePath) + { + auto decompressedBuffer = std::make_unique(IO::MaxDecompressedGameDataTableFileSize); + if (Compression::Inflate(compressedData, compressedDataSize, decompressedBuffer.get(), IO::MaxDecompressedGameDataTableFileSize)) + { + const size_t jsonLength = strnlen(reinterpret_cast(decompressedBuffer.get()), IO::MaxDecompressedGameDataTableFileSize); + const auto jsonString = std::string_view(reinterpret_cast(decompressedBuffer.get()), jsonLength); + + if (IO::WriteEntireFile(jsonOutputFilePath, reinterpret_cast(jsonString.data()), jsonString.size())) + return true; + } + + return false; + } + + int ReadAndWriteEncryptedAndOrCompressedBinToJsonFile(std::string_view encryptedAndOrCompressedInputFilePath, std::string_view jsonOutputFilePath) + { + const auto[fileContent, fileSize] = IO::ReadEntireFile(encryptedAndOrCompressedInputFilePath); + if (fileSize <= 8) + { + fprintf(stderr, "Bad file? :WidePeepoSad:"); + return EXIT_WIDEPEEPOSAD; + } + + if (HasValidGZipHeader(fileContent.get(), fileSize)) + { + if (!DecompressAndWriteDataTableJsonFile(fileContent.get(), fileSize, jsonOutputFilePath)) + return EXIT_WIDEPEEPOSAD; + } + else + { + std::array iv = {}; + memcpy(iv.data(), fileContent.get(), iv.size()); + + const size_t fileSizeWithoutIV = (fileSize - iv.size()); + const u8* fileContentWithoutIV = (fileContent.get() + iv.size()); + + auto decryptedBuffer = std::make_unique(fileSizeWithoutIV); + Crypto::DecryptAes128Cbc(fileContentWithoutIV, decryptedBuffer.get(), fileSizeWithoutIV, Crypto::DataTableAesKey, iv); + + if (!DecompressAndWriteDataTableJsonFile(decryptedBuffer.get(), fileSizeWithoutIV, jsonOutputFilePath)) + return EXIT_WIDEPEEPOSAD; + } + + return EXIT_WIDEPEEPOHAPPY; + } + + int ReadAndWriteJsonFileToCompressedBin(std::string_view jsonInputFilePath, std::string_view compressedOutputFilePath) + { + auto compressedBuffer = std::make_unique(IO::MaxDecompressedGameDataTableFileSize); + + const auto[fileContent, fileSize] = IO::ReadEntireFile(jsonInputFilePath); + const auto compressedSize = Compression::Deflate(fileContent.get(), fileSize, compressedBuffer.get(), IO::MaxDecompressedGameDataTableFileSize); + + if (compressedSize < 0) + return EXIT_WIDEPEEPOSAD; + + if (!IO::WriteEntireFile(compressedOutputFilePath, compressedBuffer.get(), compressedSize)) + return EXIT_WIDEPEEPOSAD; + + return EXIT_WIDEPEEPOHAPPY; + } + + int EntryPoint() + { + const auto[argc, argv] = UTF8::GetCommandLineArguments(); + + if (argc <= 1) + { + fprintf(stderr, "Insufficient arguments :WidePeepoSad:\n"); + return EXIT_WIDEPEEPOSAD; + } + + const auto inputPath = std::string_view(argv[1]); + if (IO::HasFileExtension(inputPath, ".bin")) + return ReadAndWriteEncryptedAndOrCompressedBinToJsonFile(inputPath, IO::ChangeFileExtension(inputPath, ".json")); + + if (IO::HasFileExtension(inputPath, ".json")) + return ReadAndWriteJsonFileToCompressedBin(inputPath, IO::ChangeFileExtension(inputPath, ".bin")); + + fprintf(stderr, "Unknown file extension\n"); + return EXIT_WIDEPEEPOSAD; + } +} + +int main() +{ + return PeepoHappy::EntryPoint(); +} diff --git a/TaikoSwitchDataTableDecryptor/src/Types.h b/TaikoSwitchDataTableDecryptor/src/Types.h new file mode 100644 index 0000000..cb87db8 --- /dev/null +++ b/TaikoSwitchDataTableDecryptor/src/Types.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +using i8 = int8_t; +using u8 = uint8_t; +using i16 = int16_t; +using u16 = uint16_t; +using i32 = int32_t; +using u32 = uint32_t; +using i64 = int64_t; +using u64 = uint64_t; +using f32 = float; +using f64 = double; + +struct NonCopyable +{ + NonCopyable() = default; + ~NonCopyable() = default; + + NonCopyable(const NonCopyable&) = delete; + NonCopyable& operator=(const NonCopyable&) = delete; +}; diff --git a/TaikoSwitchDataTableDecryptor/src/Utilities.cpp b/TaikoSwitchDataTableDecryptor/src/Utilities.cpp new file mode 100644 index 0000000..af62162 --- /dev/null +++ b/TaikoSwitchDataTableDecryptor/src/Utilities.cpp @@ -0,0 +1,317 @@ +#include "Utilities.h" +#include + +#define NOMINMAX +#include +#include + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((::NTSTATUS)(Status)) >= 0) +#endif // ! NT_SUCCESS + +namespace PeepoHappy +{ + namespace UTF8 + { + std::string Narrow(std::wstring_view inputString) + { + std::string utf8String; + const int utf8Length = ::WideCharToMultiByte(CP_UTF8, 0, inputString.data(), static_cast(inputString.size() + 1), nullptr, 0, nullptr, nullptr) - 1; + + if (utf8Length > 0) + { + utf8String.resize(utf8Length); + ::WideCharToMultiByte(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), utf8String.data(), utf8Length, nullptr, nullptr); + } + + return utf8String; + } + + std::wstring Widen(std::string_view inputString) + { + std::wstring utf16String; + const int utf16Length = ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size() + 1), nullptr, 0) - 1; + + if (utf16Length > 0) + { + utf16String.resize(utf16Length); + ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), utf16String.data(), utf16Length); + } + + return utf16String; + } + + std::pair GetCommandLineArguments() + { + static std::vector argvString; + static std::vector argvCStr; + + if (!argvString.empty() || !argvCStr.empty()) + return { static_cast(argvString.size()), argvCStr.data() }; + + int argc = 0; + auto argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + + argvString.reserve(argc); + argvCStr.reserve(argc); + + for (auto i = 0; i < argc; i++) + argvCStr.emplace_back(argvString.emplace_back(UTF8::Narrow(argv[i])).c_str()); + + ::LocalFree(argv); + return { argc, argvCStr.data() }; + } + + std::vector GetArgV() + { + return std::vector(); + } + + WideArg::WideArg(std::string_view inputString) + { + // NOTE: Length **without** null terminator + convertedLength = ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size() + 1), nullptr, 0) - 1; + + if (convertedLength <= 0) + { + stackBuffer[0] = L'\0'; + return; + } + + if (convertedLength < stackBuffer.size()) + { + ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), stackBuffer.data(), convertedLength); + stackBuffer[convertedLength] = L'\0'; + } + else + { + heapBuffer = std::make_unique(convertedLength + 1); + ::MultiByteToWideChar(CP_UTF8, 0, inputString.data(), static_cast(inputString.size()), heapBuffer.get(), convertedLength); + heapBuffer[convertedLength] = L'\0'; + } + } + + const wchar_t* WideArg::c_str() const + { + return (convertedLength < stackBuffer.size()) ? stackBuffer.data() : heapBuffer.get(); + } + } + + namespace IO + { + std::pair, size_t> ReadEntireFile(std::string_view filePath) + { + std::unique_ptr fileContent = nullptr; + size_t fileSize = 0; + + ::HANDLE fileHandle = ::CreateFileW(UTF8::WideArg(filePath).c_str(), GENERIC_READ, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (fileHandle != INVALID_HANDLE_VALUE) + { + ::LARGE_INTEGER largeIntegerFileSize = {}; + ::GetFileSizeEx(fileHandle, &largeIntegerFileSize); + + if (fileSize = static_cast(largeIntegerFileSize.QuadPart); fileSize > 0) + { + if (fileContent = std::make_unique(fileSize); fileContent != nullptr) + { + assert(fileSize < std::numeric_limits::max() && "No way that's ever gonna happen, right?"); + + DWORD bytesRead = 0; + ::ReadFile(fileHandle, fileContent.get(), static_cast(fileSize), &bytesRead, nullptr); + } + } + + ::CloseHandle(fileHandle); + } + + return { std::move(fileContent), fileSize }; + } + + bool WriteEntireFile(std::string_view filePath, const u8* fileContent, size_t fileSize) + { + if (filePath.empty() || fileContent == nullptr || fileSize == 0) + return false; + + ::HANDLE fileHandle = ::CreateFileW(UTF8::WideArg(filePath).c_str(), GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE), NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (fileHandle == INVALID_HANDLE_VALUE) + return false; + + assert(fileSize < std::numeric_limits::max() && "No way that's ever gonna happen, right?"); + + DWORD bytesWritten = 0; + ::WriteFile(fileHandle, fileContent, static_cast(fileSize), &bytesWritten, nullptr); + + ::CloseHandle(fileHandle); + return true; + } + + bool HasFileExtension(std::string_view filePath, std::string_view extensionToCheckFor) + { + assert(!extensionToCheckFor.empty() && extensionToCheckFor[0] == '.'); + + if (extensionToCheckFor.size() >= filePath.size()) + return false; + + const auto stringA = filePath.substr(filePath.size() - extensionToCheckFor.size()); + const auto stringB = extensionToCheckFor; + return std::equal(stringA.begin(), stringA.end(), stringB.begin(), stringB.end(), [](char a, char b) { return ::tolower(a) == ::tolower(b); }); + } + + std::string ChangeFileExtension(std::string_view filePath, std::string_view newExtension) + { + const size_t lastSeparator = filePath.find_last_of("./\\"); + if (lastSeparator != std::string_view::npos) + { + if (filePath[lastSeparator] == '.') + return std::string(filePath.substr(0, lastSeparator)) + std::string(newExtension); + } + + return std::string(filePath) + std::string(newExtension); + } + } + + namespace Crypto + { + bool DecryptAes128Cbc(const u8* inEncryptedData, u8* outDecryptedData, size_t inOutDataSize, std::array key, std::array iv) + { + bool successful = false; + ::NTSTATUS status = {}; + ::BCRYPT_ALG_HANDLE algorithmHandle = {}; + + status = ::BCryptOpenAlgorithmProvider(&algorithmHandle, BCRYPT_AES_ALGORITHM, nullptr, 0); + if (NT_SUCCESS(status)) + { + status = ::BCryptSetProperty(algorithmHandle, BCRYPT_CHAINING_MODE, reinterpret_cast(const_cast(BCRYPT_CHAIN_MODE_CBC)), sizeof(BCRYPT_CHAIN_MODE_CBC), 0); + if (NT_SUCCESS(status)) + { + ULONG keyObjectSize = {}; + ULONG copiedDataSize = {}; + + status = ::BCryptGetProperty(algorithmHandle, BCRYPT_OBJECT_LENGTH, reinterpret_cast(&keyObjectSize), sizeof(ULONG), &copiedDataSize, 0); + if (NT_SUCCESS(status)) + { + ::BCRYPT_KEY_HANDLE symmetricKeyHandle = {}; + auto keyObject = std::make_unique(keyObjectSize); + + status = ::BCryptGenerateSymmetricKey(algorithmHandle, &symmetricKeyHandle, keyObject.get(), keyObjectSize, key.data(), static_cast(key.size()), 0); + if (NT_SUCCESS(status)) + { + status = ::BCryptDecrypt(symmetricKeyHandle, const_cast(inEncryptedData), static_cast(inOutDataSize), nullptr, iv.data(), static_cast(iv.size()), outDecryptedData, static_cast(inOutDataSize), &copiedDataSize, 0); + if (NT_SUCCESS(status)) + { + successful = true; + } + else + { + fprintf(stderr, __FUNCTION__"(): BCryptDecrypt() failed with 0x%X", status); + } + + if (symmetricKeyHandle) + ::BCryptDestroyKey(symmetricKeyHandle); + } + else + { + fprintf(stderr, __FUNCTION__"(): BCryptGenerateSymmetricKey() failed with 0x%X", status); + } + } + else + { + fprintf(stderr, __FUNCTION__"(): BCryptGetProperty(BCRYPT_OBJECT_LENGTH) failed with 0x%X", status); + } + } + else + { + fprintf(stderr, __FUNCTION__"(): BCryptSetProperty(BCRYPT_CHAINING_MODE) failed with 0x%X", status); + } + + if (algorithmHandle) + ::BCryptCloseAlgorithmProvider(algorithmHandle, 0); + } + else + { + fprintf(stderr, __FUNCTION__"(): BCryptOpenAlgorithmProvider(BCRYPT_AES_ALGORITHM) failed with 0x%X", status); + } + + return successful; + } + } + + namespace Compression + { + bool Inflate(const u8* inCompressedData, size_t inDataSize, u8* outDecompressedData, size_t outDataSize) + { + z_stream zStream = {}; + zStream.zalloc = Z_NULL; + zStream.zfree = Z_NULL; + zStream.opaque = Z_NULL; + zStream.avail_in = static_cast(inDataSize); + zStream.next_in = static_cast(inCompressedData); + zStream.avail_out = static_cast(outDataSize); + zStream.next_out = static_cast(outDecompressedData); + + const int initResult = inflateInit2(&zStream, 31); + if (initResult != Z_OK) + return false; + + const int inflateResult = inflate(&zStream, Z_FINISH); + // assert(inflateResult == Z_STREAM_END && zStream.msg == nullptr); + + const int endResult = inflateEnd(&zStream); + if (endResult != Z_OK) + return false; + + return true; + } + + size_t Deflate(const u8* inData, size_t inDataSize, u8* outCompressedData, size_t outDataSize) + { + constexpr size_t chunkStepSize = 0x4000; + + z_stream zStream = {}; + zStream.zalloc = Z_NULL; + zStream.zfree = Z_NULL; + zStream.opaque = Z_NULL; + + int errorCode = deflateInit2(&zStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); + assert(errorCode == Z_OK); + + const u8* inDataReadHeader = static_cast(inData); + size_t remainingSize = inDataSize; + size_t compressedSize = 0; + + while (remainingSize > 0) + { + const size_t chunkSize = std::min(remainingSize, chunkStepSize); + + zStream.avail_in = static_cast(chunkSize); + zStream.next_in = reinterpret_cast(inDataReadHeader); + + inDataReadHeader += chunkSize; + remainingSize -= chunkSize; + + do + { + std::array outputBuffer; + + zStream.avail_out = chunkStepSize; + zStream.next_out = outputBuffer.data(); + + errorCode = deflate(&zStream, remainingSize == 0 ? Z_FINISH : Z_NO_FLUSH); + assert(errorCode != Z_STREAM_ERROR); + + const auto compressedChunkSize = chunkStepSize - zStream.avail_out; + memcpy(&outCompressedData[compressedSize], outputBuffer.data(), compressedChunkSize); + + compressedSize += compressedChunkSize; + } + while (zStream.avail_out == 0); + assert(zStream.avail_in == 0); + } + + deflateEnd(&zStream); + + assert(errorCode == Z_STREAM_END); + return compressedSize; + } + } +} diff --git a/TaikoSwitchDataTableDecryptor/src/Utilities.h b/TaikoSwitchDataTableDecryptor/src/Utilities.h new file mode 100644 index 0000000..d3e3541 --- /dev/null +++ b/TaikoSwitchDataTableDecryptor/src/Utilities.h @@ -0,0 +1,64 @@ +#pragma once +#include "Types.h" + +#define EXIT_WIDEPEEPOHAPPY EXIT_SUCCESS +#define EXIT_WIDEPEEPOSAD EXIT_FAILURE + +namespace PeepoHappy +{ + // NOTE: Following the "UTF-8 Everywhere" guidelines + namespace UTF8 + { + // NOTE: Convert UTF-16 to UTF-8 + std::string Narrow(std::wstring_view); + + // NOTE: Convert UTF-8 to UTF-16 + std::wstring Widen(std::string_view); + + // NOTE: To avoid needless heap allocations for temporary wchar_t C-API function arguments + // Example: DummyU16FuncW(UTF8::WideArg(stringU8).c_str(), ...) + class WideArg : NonCopyable + { + public: + WideArg(std::string_view); + const wchar_t* c_str() const; + + private: + std::unique_ptr heapBuffer; + std::array stackBuffer; + int convertedLength; + }; + + // NOTE: Includes the program file path as first argument + std::pair GetCommandLineArguments(); + } + + namespace IO + { + // NOTE: Sucks for modders, makes sense for them to do it though... + constexpr size_t MaxDecompressedGameDataTableFileSize = 0x200000; + + std::pair, size_t> ReadEntireFile(std::string_view filePath); + bool WriteEntireFile(std::string_view filePath, const u8* fileContent, size_t fileSize); + + bool HasFileExtension(std::string_view filePath, std::string_view extensionToCheckFor); + std::string ChangeFileExtension(std::string_view filePath, std::string_view newExtension); + } + + namespace Crypto + { + constexpr size_t AesKeySize = 16; + + // NOTE: Literally loaded directly into X8 right before calling nn::crypto::DecryptAes128Cbc() + // they couldn't even bother trying to "hide" it by adding a few pointer indirection or scrambling first :KEKL: + constexpr std::array DataTableAesKey = { 0x57, 0x39, 0x73, 0x35, 0x38, 0x73, 0x68, 0x43, 0x54, 0x70, 0x76, 0x75, 0x6A, 0x6B, 0x4A, 0x74, }; + + bool DecryptAes128Cbc(const u8* inEncryptedData, u8* outDecryptedData, size_t inOutDataSize, std::array key, std::array iv); + } + + namespace Compression + { + bool Inflate(const u8* inCompressedData, size_t inDataSize, u8* outDecompressedData, size_t outDataSize); + size_t Deflate(const u8* inData, size_t inDataSize, u8* outCompressedData, size_t outDataSize); + } +}