diff --git a/.gitignore b/.gitignore index d8ed9b1..4558df5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ subprojects/xxHash-0.8.2 subprojects/safetyhook subprojects/zxing-cpp-2.2.1 subprojects/stb +subprojects/zlib +libtomcrypt-1.18.2 dist.7z .vscode out/ \ No newline at end of file diff --git a/Makefile b/Makefile index 6be9a04..36e77e4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: @strip build/bnusio.dll setup: - @meson setup build --cross cross-mingw-64.txt + @meson setup build --cross cross-mingw-64.txt --wipe dist-no-7z: all @mkdir -p out/ @@ -11,5 +11,5 @@ dist-no-7z: all @cp -r dist/* out/ dist: dist-no-7z - @cd out && 7z a -t7z ../dist.7z . + @cd out && 7zz a -t7z ../dist.7z . @rm -rf out diff --git a/README.md b/README.md index afeb92e..d9c2a81 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ country_code = "JPN" # JPN00: JPN 00.18 # JPN08: JPN 08.18 # JPN39: JPN 39.06 -# CHN00: CHN 00.32 (based JPN 32.09) +# CHN00: CHN 00.32 (based on JPN 32.09) version = "auto" # window resolution res = { x = 1920, y = 1080 } @@ -41,7 +41,7 @@ vsync = false unlock_songs = true [patches.chn00] - # sync test mode language to attract etc. + # sync test mode language to attract etc fix_language = false # show demo movie demo_movie = true @@ -52,10 +52,13 @@ unlock_songs = true [audio] # wasapi shared mode +# allows you to have multiple audio sources at once at a cost of having higher latency wasapi_shared = true # use asio asio = false # asio driver name +# to find it, open up regedit then navigate to HKEY_LOCAL_MACHINE\SOFTWARE\ASIO +# the name is case sensitive ! asio_driver = "" [card_reader] @@ -92,4 +95,15 @@ analog_input = false auto_ime = false # use jp layout scan code (if using jp layout keyboard, must be set to true) jp_layout = false + +[layeredfs] +# replace assets from the game using a layered file system. +# For example if you want to edit the wordlist, add your edited version like so : +# DumpFolder\Data_mods\x64\datatable\wordlist.bin +enabled = false +# AES encryption keys needed to dynamically encrypt datatable files and fumens +# keys need to be provided in an hexlified form. A missing or incorrect key will crash the game ! +# keys are not needed if you provide already encrypted files +datatable_key = "" +fumen_key = "" ``` diff --git a/dist/config.toml b/dist/config.toml index f67baf7..0b05ab5 100644 --- a/dist/config.toml +++ b/dist/config.toml @@ -44,4 +44,9 @@ analog_input = false [keyboard] auto_ime = false -jp_layout = false \ No newline at end of file +jp_layout = false + +[layeredfs] +enabled = false +datatable_key = "" +fumen_key = "" \ No newline at end of file diff --git a/meson.build b/meson.build index d1552d1..35b6104 100644 --- a/meson.build +++ b/meson.build @@ -40,6 +40,10 @@ stb = subproject('stb') opt_var.add_cmake_defines({'BUILD_EXAMPLES': false}) zxing_proj = cmake.subproject('zxing', options: opt_var) zxing_dep = zxing_proj.dependency('ZXing') +zlib_proj = subproject('zlib') +zlib_dep = zlib_proj.get_variable('zlib_dep') +libtomcrypt = subproject('libtomcrypt') +libtomcrypt_dep = libtomcrypt.get_variable('tomcrypt_dep') library( 'bnusio', @@ -48,6 +52,8 @@ library( tomlc99.get_variable('tomlc99_lib'), sdl2.get_variable('sdl2'), xxhash.get_variable('xxhash'), + zlib_proj.get_variable('zlib_lib'), + libtomcrypt.get_variable('tomcryptlib'), ], include_directories: [ 'src', @@ -55,11 +61,16 @@ library( tomlc99.get_variable('tomlc99_inc'), sdl2.get_variable('core_inc'), xxhash.get_variable('inc'), + zlib_proj.get_variable('zlib_inc'), + libtomcrypt.get_variable('core_inc'), ], - dependencies: [stb.get_variable('stb_dep'), + dependencies: [ + stb.get_variable('stb_dep'), zxing_dep, safetyhook_dep, zydis_dep, + zlib_dep, + libtomcrypt_dep, ], sources : [ 'src/dllmain.cpp', diff --git a/src/dllmain.cpp b/src/dllmain.cpp index 0c80673..39e7d61 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -3,6 +3,9 @@ #include "helpers.h" #include "patches/patches.h" #include "poll.h" +#include +#include +#define POLY 0x82f63b78 GameVersion gameVersion = GameVersion::UNKNOWN; std::vector plugins; @@ -23,6 +26,160 @@ char chipId1[33] = "00000000000000000000000000000001"; char chipId2[33] = "00000000000000000000000000000002"; bool autoIME = false; bool jpLayout = false; +bool useLayeredFs = false; +std::string datatableKey = "0000000000000000000000000000000000000000000000000000000000000000"; +std::string fumenKey = "0000000000000000000000000000000000000000000000000000000000000000"; + +uint32_t crc32c(uint32_t crc, const unsigned char *buf, size_t len) +{ + int k; + + crc = ~crc; + while (len--) { + crc ^= *buf++; + for (k = 0; k < 8; k++) + crc = (crc >> 1) ^ (POLY & (0 - (crc & 1))); + } + return ~crc; +} + +bool checkCRC(const std::string &path, uint32_t crc){ + if (std::filesystem::exists(path)){ + std::filesystem::path crc_path = path; + crc_path.replace_extension(".crc"); + std::ifstream crc_file(crc_path, std::ios::binary); + std::string crc_content((std::istreambuf_iterator(crc_file)), std::istreambuf_iterator()); + return std::stoul(crc_content) != crc; + } + return 1; +} + +void create_directories(const std::string &path) { + size_t pos = 0; + std::string delimiter = "\\"; + std::string current_path; + + while ((pos = path.find(delimiter, pos)) != std::string::npos) { + current_path = path.substr(0, pos++); + if (!current_path.empty() && !CreateDirectory(current_path.c_str(), NULL) && GetLastError() != ERROR_ALREADY_EXISTS) { + throw std::runtime_error("Error creating directory: " + current_path); + } + } + if (!path.empty() && !CreateDirectory(path.c_str(), NULL) && GetLastError() != ERROR_ALREADY_EXISTS) { + throw std::runtime_error("Error creating directory: " + path); + } +} + +void write_file(const std::string &filename, const std::vector &data, uint32_t original_crc) { + std::string::size_type pos = filename.find_last_of("\\"); + if (pos != std::string::npos) { + std::string directory = filename.substr(0, pos); + create_directories(directory); + } + + std::filesystem::path crc_path = filename; + crc_path.replace_extension(".crc"); + std::ofstream crc_file(crc_path.string()); + crc_file << std::to_string(original_crc); + crc_file.close(); + + std::ofstream file(filename, std::ios::binary); + file.write(reinterpret_cast(data.data()), data.size()); +} + +std::vector gzip_compress(const std::vector& data) { + z_stream deflate_stream; + deflate_stream.zalloc = Z_NULL; + deflate_stream.zfree = Z_NULL; + deflate_stream.opaque = Z_NULL; + deflate_stream.avail_in = data.size(); + deflate_stream.next_in = const_cast(data.data()); + + deflateInit2(&deflate_stream, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); + + std::vector compressed_data; + compressed_data.resize(deflateBound(&deflate_stream, data.size())); + + deflate_stream.avail_out = compressed_data.size(); + deflate_stream.next_out = compressed_data.data(); + + deflate(&deflate_stream, Z_FINISH); + deflateEnd(&deflate_stream); + + compressed_data.resize(deflate_stream.total_out); + return compressed_data; +} + +// Function to pad data according to PKCS7 +std::vector pad_data(const std::vector &data, size_t block_size) { + size_t padding = block_size - (data.size() % block_size); + std::vector padded_data = data; + padded_data.insert(padded_data.end(), padding, static_cast(padding)); + return padded_data; +} + +std::vector hex_to_bytes(const std::string &hex) { + std::vector bytes; + for (size_t i = 0; i < hex.length(); i += 2) { + uint8_t byte = static_cast(std::stoi(hex.substr(i, 2), nullptr, 16)); + bytes.push_back(byte); + } + return bytes; +} + +std::vector encrypt_file(const std::string &input_file, const std::string &hex_key) { + // Convert the key from hex to bytes + std::vector key = hex_to_bytes(hex_key); + + // Generate the 128 bits IV + std::vector iv(16); + for (size_t i = 0; i < iv.size(); ++i) iv[i] = static_cast(i); + + // Read the entire file into memory + std::ifstream file(input_file, std::ios::binary); + + std::vector data((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + // Compress the data + std::vector compressed_data = gzip_compress(data); + + // Pad the compressed data + std::vector padded_data = pad_data(compressed_data, 16); + + // Encrypt the data + symmetric_CBC cbc; + if (cbc_start(find_cipher("aes"), iv.data(), key.data(), key.size(), 0, &cbc) != CRYPT_OK) { + throw std::runtime_error("Error initializing CBC"); + } + + std::vector encrypted_data(padded_data.size()); + if (cbc_encrypt(padded_data.data(), encrypted_data.data(), padded_data.size(), &cbc) != CRYPT_OK) { + throw std::runtime_error("Error during encryption"); + } + + cbc_done(&cbc); + + // Return IV concatenated with the encrypted data + encrypted_data.insert(encrypted_data.begin(), iv.begin(), iv.end()); + return encrypted_data; +} + +bool is_fumen_encrypted(const std::string& filename) { + std::ifstream file(filename, std::ios::binary); + file.seekg(0x210, std::ios::beg); + std::vector buffer(32); + file.read(reinterpret_cast(buffer.data()), buffer.size()); + + // Check if the read bytes match the expected pattern + std::vector expected_bytes = { + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 + }; + + return buffer != expected_bytes; +} HOOK (i32, ShowMouse, PROC_ADDRESS ("user32.dll", "ShowCursor"), bool) { return originalShowMouse (true); } HOOK (i32, ExitWindows, PROC_ADDRESS ("user32.dll", "ExitWindowsEx")) { ExitProcess (0); } @@ -44,6 +201,109 @@ HOOK (i32, ws2_getaddrinfo, PROC_ADDRESS ("ws2_32.dll", "getaddrinfo"), const ch return originalws2_getaddrinfo (server.c_str (), service, hints, out); } +HOOK (HANDLE, CreateFileAHook, PROC_ADDRESS ("kernel32.dll", "CreateFileA"), LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { + std::filesystem::path path (lpFileName); + if (!path.is_absolute ()) path = std::filesystem::absolute (path); + auto originalDataFolder = std::filesystem::current_path ().parent_path ().parent_path () / "Data" / "x64"; + auto originalLayeredFsFolder = std::filesystem::current_path ().parent_path ().parent_path () / "Data_mods" / "x64"; + auto encryptedLayeredFsFolder = std::filesystem::current_path ().parent_path ().parent_path () / "Data_mods" / "x64_enc"; + + if (path.string ().find (originalDataFolder.string ()) == 0) { + auto newPath = path.string (); + auto encPath = path.string (); + newPath.replace (0, originalDataFolder.string ().length (), originalLayeredFsFolder.string ()); + encPath.replace (0, originalDataFolder.string ().length (), encryptedLayeredFsFolder.string ()); + + //The following code handles file redirection and if need be, file encryption. + //It's a bit of a mess but it works well ! -Kit + + if (std::filesystem::exists (newPath)) { //If a file exists in the datamod folder + if(is_fumen_encrypted(newPath)){ //And if it's an encrypted fumen or a different type of file, use it. + std::cout << "Redirecting " << std::filesystem::relative (path).string() << std::endl; + return originalCreateFileAHook (newPath.c_str (), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, + dwFlagsAndAttributes, hTemplateFile); + } + else{ //Otherwise if it's an unencrypted fumen. + if (!std::filesystem::exists(encPath)) { //We check if we don't already have a cached file. + if(fumenKey.length() == 64){ + std::cout << "Encrypting " << std::filesystem::relative (newPath) << std::endl; // If we don't we encrypt the file + std::ifstream crc_file(newPath, std::ios::binary); + std::vector crc_vector((std::istreambuf_iterator(crc_file)), std::istreambuf_iterator()); + uint32_t crc = crc32c(0, crc_vector.data(), crc_vector.size()); + write_file(encPath, encrypt_file(newPath, fumenKey), crc); // And we save it + } else { + std::cout << "Missing or invalid fumen key : " << std::filesystem::relative (newPath) << " couldn't be encrypted." << std::endl; + encPath = path.string(); + } + } else std::cout << "Using cached file for " << std::filesystem::relative (newPath) << std::endl; + return originalCreateFileAHook (encPath.c_str (), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, + dwFlagsAndAttributes, hTemplateFile); + } + } + + //We check separately for unencrypted json files. + std::filesystem::path json_path = newPath; + json_path.replace_extension(".json"); + if (std::filesystem::exists(json_path)) { //If a json file exists in the folder + bool crcBool = false; + + if (std::filesystem::exists(encPath)){ + std::ifstream crc_file(json_path, std::ios::binary); + std::vector crc_vector((std::istreambuf_iterator(crc_file)), std::istreambuf_iterator()); + uint32_t crc = crc32c(0, crc_vector.data(), crc_vector.size()); + crcBool = checkCRC(encPath, crc); + } + + if (!std::filesystem::exists(encPath) || crcBool) { //And if it hasn't been encrypted before + if(datatableKey.length() == 64){ + std::cout << "Encrypting " << std::filesystem::relative (json_path) << std::endl; //Encrypt the file + std::ifstream crc_file(json_path.string(), std::ios::binary); + std::vector crc_vector((std::istreambuf_iterator(crc_file)), std::istreambuf_iterator()); + uint32_t crc = crc32c(0, crc_vector.data(), crc_vector.size()); + write_file(encPath, encrypt_file(json_path.string(), datatableKey), crc); //And save it + } else { + std::cout << "Missing or invalid datatable key : " << std::filesystem::relative (newPath) << " couldn't be encrypted." << std::endl; + encPath = path.string(); + } + } + else std::cout << "Using cached file for " << std::filesystem::relative (json_path) << std::endl; //Otherwise use the already encrypted file. + return originalCreateFileAHook (encPath.c_str (), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, + dwFlagsAndAttributes, hTemplateFile); + } + } + + return originalCreateFileAHook (lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, + hTemplateFile); +} + +// HOOK (HANDLE, CreateFileWHook, PROC_ADDRESS ("kernel32.dll", "CreateFileW"), LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, +// LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { +// std::wstring_convert, wchar_t> converter; +// std::string strFileName = converter.to_bytes (lpFileName); + +// std::filesystem::path path (strFileName); +// if (!path.is_absolute ()) path = std::filesystem::absolute (path); + +// auto originalDataFolder = std::filesystem::current_path ().parent_path ().parent_path () / "Data" / "x64"; +// auto originalLayeredFsFolder = std::filesystem::current_path ().parent_path ().parent_path () / "Data_mods" / "x64"; + +// if (path.string ().find (originalDataFolder.string ()) != std::string::npos) { +// auto newPath = path.string (); +// newPath.replace (0, originalDataFolder.string ().length (), originalLayeredFsFolder.string ()); + +// if (std::filesystem::exists (newPath)) { +// std::wstring wNewPath = converter.from_bytes (newPath); +// std::wcout << L"Redirecting " << lpFileName << L" to " << wNewPath << std::endl; +// return originalCreateFileWHook (wNewPath.c_str (), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, +// dwFlagsAndAttributes, hTemplateFile); +// } +// } + +// return originalCreateFileWHook (lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, +// hTemplateFile); +// } + void GetGameVersion () { wchar_t w_path[MAX_PATH]; @@ -132,6 +392,13 @@ DllMain (HMODULE module, DWORD reason, LPVOID reserved) { autoIME = readConfigBool (keyboard, "auto_ime", autoIME); jpLayout = readConfigBool (keyboard, "jp_layout", jpLayout); } + + auto layeredFs = openConfigSection (config, "layeredfs"); + if (layeredFs) { + useLayeredFs = readConfigBool (layeredFs, "enabled", useLayeredFs); + datatableKey = readConfigString (layeredFs, "datatable_key", datatableKey); + fumenKey = readConfigString (layeredFs, "fumen_key", fumenKey); + } } if (version == "auto") { @@ -188,6 +455,13 @@ DllMain (HMODULE module, DWORD reason, LPVOID reserved) { INSTALL_HOOK (ws2_getaddrinfo); + if (useLayeredFs) { + std::wcout << "Using LayeredFs!" << std::endl; + register_cipher(&aes_desc); + INSTALL_HOOK (CreateFileAHook); + // INSTALL_HOOK (CreateFileWHook); + } + bnusio::Init (); switch (gameVersion) { diff --git a/src/patches/amauth.cpp b/src/patches/amauth.cpp index d726c5a..f130310 100644 --- a/src/patches/amauth.cpp +++ b/src/patches/amauth.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,7 +9,6 @@ #include #include #include -#include #include /* diff --git a/subprojects/libtomcrypt.wrap b/subprojects/libtomcrypt.wrap new file mode 100644 index 0000000..56b7605 --- /dev/null +++ b/subprojects/libtomcrypt.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = libtomcrypt-1.18.2 +source_url = https://github.com/libtom/libtomcrypt/releases/download/v1.18.2/crypt-1.18.2.tar.xz +source_filename = crypt-1.18.2.tar.xz +source_hash = 96ad4c3b8336050993c5bc2cf6c057484f2b0f9f763448151567fbab5e767b84 +patch_filename = libtomcrypt_1.18.2-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libtomcrypt_1.18.2-1/get_patch +patch_hash = 0ca889b864e5a23525774438439d9e385268ff7234d49029902375a6c5e18f14 +diff_files = libtomcrypt.patch + +[provide] +libtomcrypt = tomcrypt_dep + diff --git a/subprojects/packagefiles/libtomcrypt.patch b/subprojects/packagefiles/libtomcrypt.patch new file mode 100644 index 0000000..5395865 --- /dev/null +++ b/subprojects/packagefiles/libtomcrypt.patch @@ -0,0 +1,11 @@ +--- libtomcrypt-1.18.2/meson.build ++++ libtomcrypt-1.18.2/meson.build +@@ -14,7 +14,7 @@ extra_objects = [] + + subdir('src') + +-tomcryptlib = library('tomcrypt', all_sources, ++tomcryptlib = static_library('tomcrypt', all_sources, + objects : extra_objects, + include_directories : core_inc) + diff --git a/subprojects/packagefiles/zlib.patch b/subprojects/packagefiles/zlib.patch new file mode 100644 index 0000000..a10c909 --- /dev/null +++ b/subprojects/packagefiles/zlib.patch @@ -0,0 +1,33 @@ ++++ zlib/meson.build +@@ -0,0 +1,31 @@ ++project('zlib', 'c', version: '1.0.0') ++ ++zlib_inc = include_directories('.') ++zlib_sources = files( ++ 'adler32.c', ++ 'compress.c', ++ 'crc32.c', ++ 'deflate.c', ++ 'gzclose.c', ++ 'gzlib.c', ++ 'gzread.c', ++ 'gzwrite.c', ++ 'infback.c', ++ 'inffast.c', ++ 'inflate.c', ++ 'inftrees.c', ++ 'trees.c', ++ 'uncompr.c', ++ 'zutil.c' ++) ++ ++zlib_lib = static_library( ++ 'zlib', ++ zlib_sources, ++ include_directories: zlib_inc ++) ++ ++zlib_dep = declare_dependency( ++ include_directories: zlib_inc, ++ link_with: zlib_lib ++) diff --git a/subprojects/zlib.wrap b/subprojects/zlib.wrap new file mode 100755 index 0000000..109f743 --- /dev/null +++ b/subprojects/zlib.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/madler/zlib +revision = 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf +diff_files = zlib.patch \ No newline at end of file