diff --git a/CMakeLists.txt b/CMakeLists.txt index ff02efb7..0bc21f49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,8 @@ endif() project(Alber) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +option(GPU_DEBUG_INFO "Enable additional GPU debugging info" OFF) + include_directories(${PROJECT_SOURCE_DIR}/include/) include_directories(${PROJECT_SOURCE_DIR}/include/kernel) include_directories (${FMT_INCLUDE_DIR}) @@ -70,6 +72,7 @@ endif() set(SOURCE_FILES src/main.cpp src/emulator.cpp src/io_file.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp src/core/memory.cpp ) +set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp src/core/kernel/memory_management.cpp src/core/kernel/ports.cpp src/core/kernel/events.cpp src/core/kernel/threads.cpp @@ -117,6 +120,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/opengl.hpp inc include/system_models.hpp include/services/dlp_srvr.hpp include/result/result.hpp include/result/result_common.hpp include/result/result_fs.hpp include/result/result_fnd.hpp include/result/result_gsp.hpp include/result/result_kernel.hpp include/result/result_os.hpp + include/crypto/aes_engine.hpp include/metaprogramming.hpp ) set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp @@ -128,6 +132,7 @@ set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp source_group("Header Files\\Core" FILES ${HEADER_FILES}) source_group("Source Files\\Core" FILES ${SOURCE_FILES}) +source_group("Source Files\\Core\\Crypto" FILES ${CRYPTO_SOURCE_FILES}) source_group("Source Files\\Core\\Filesystem" FILES ${FS_SOURCE_FILES}) source_group("Source Files\\Core\\Kernel" FILES ${KERNEL_SOURCE_FILES}) source_group("Source Files\\Core\\Loader" FILES ${LOADER_SOURCE_FILES}) @@ -136,6 +141,11 @@ source_group("Source Files\\Core\\PICA" FILES ${PICA_SOURCE_FILES}) source_group("Source Files\\Core\\OpenGL Renderer" FILES ${RENDERER_GL_SOURCE_FILES}) source_group("Source Files\\Third Party" FILES ${THIRD_PARTY_SOURCE_FILES}) -add_executable(Alber ${SOURCE_FILES} ${FS_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} +add_executable(Alber ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${PICA_SOURCE_FILES} ${RENDERER_GL_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${HEADER_FILES}) -target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad) \ No newline at end of file +target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad cryptopp) + + +if(GPU_DEBUG_INFO) + target_compile_definitions(Alber PRIVATE GPU_DEBUG_INFO=1) +endif() diff --git a/include/crypto/aes_engine.hpp b/include/crypto/aes_engine.hpp new file mode 100644 index 00000000..96ec900e --- /dev/null +++ b/include/crypto/aes_engine.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "helpers.hpp" + +namespace Crypto { + constexpr std::size_t AesKeySize = 0x10; + using AESKey = std::array; + + template + static std::array rolArray(const std::array& value, std::size_t bits) { + const auto bitWidth = N * CHAR_BIT; + + bits %= bitWidth; + + const auto byteShift = bits / CHAR_BIT; + const auto bitShift = bits % CHAR_BIT; + + std::array result; + + for (std::size_t i = 0; i < N; i++) { + result[i] = ((value[(i + byteShift) % N] << bitShift) | (value[(i + byteShift + 1) % N] >> (CHAR_BIT - bitShift))) & UINT8_MAX; + } + + return result; + } + + template + static std::array addArray(const std::array& a, const std::array& b) { + std::array result; + std::size_t sum = 0; + std::size_t carry = 0; + + for (std::int64_t i = N - 1; i >= 0; i--) { + sum = a[i] + b[i] + carry; + carry = sum >> CHAR_BIT; + result[i] = static_cast(sum & UINT8_MAX); + } + + return result; + } + + template + static std::array xorArray(const std::array& a, const std::array& b) { + std::array result; + + for (std::size_t i = 0; i < N; i++) { + result[i] = a[i] ^ b[i]; + } + + return result; + } + + static std::optional createKeyFromHex(const std::string& hex) { + if (hex.size() < 32) { + return {}; + } + + AESKey rawKey; + for (std::size_t i = 0; i < rawKey.size(); i++) { + rawKey[i] = static_cast(std::stoi(hex.substr(i * 2, 2), 0, 16)); + } + + return rawKey; + } + + struct AESKeySlot { + std::optional keyX = std::nullopt; + std::optional keyY = std::nullopt; + std::optional normalKey = std::nullopt; + }; + + enum KeySlotId : std::size_t { + NCCHKey0 = 0x2C, + NCCHKey1 = 0x25, + NCCHKey2 = 0x18, + NCCHKey3 = 0x1B, + }; + + class AESEngine { + private: + constexpr static std::size_t AesKeySlotCount = 0x40; + + std::optional m_generator = std::nullopt; + std::array m_slots; + bool keysLoaded = false; + + constexpr void updateNormalKey(std::size_t slotId) { + if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) { + auto& keySlot = m_slots.at(slotId); + AESKey keyX = keySlot.keyX.value(); + AESKey keyY = keySlot.keyY.value(); + + keySlot.normalKey = rolArray(addArray(xorArray(rolArray(keyX, 2), keyY), m_generator.value()), 87); + } + } + + public: + AESEngine() {} + void loadKeys(const std::filesystem::path& path); + bool haveKeys() { return keysLoaded; } + + constexpr bool hasKeyX(std::size_t slotId) { + if (slotId >= AesKeySlotCount) { + return false; + } + + return m_slots.at(slotId).keyX.has_value(); + } + + constexpr AESKey getKeyX(std::size_t slotId) { + return m_slots.at(slotId).keyX.value_or(AESKey{}); + } + + constexpr void setKeyX(std::size_t slotId, const AESKey &key) { + if (slotId < AesKeySlotCount) { + m_slots.at(slotId).keyX = key; + updateNormalKey(slotId); + } + } + + constexpr bool hasKeyY(std::size_t slotId) { + if (slotId >= AesKeySlotCount) { + return false; + } + + return m_slots.at(slotId).keyY.has_value(); + } + + constexpr AESKey getKeyY(std::size_t slotId) { + return m_slots.at(slotId).keyY.value_or(AESKey{}); + } + + constexpr void setKeyY(std::size_t slotId, const AESKey &key) { + if (slotId < AesKeySlotCount) { + m_slots.at(slotId).keyY = key; + updateNormalKey(slotId); + } + } + + constexpr bool hasNormalKey(std::size_t slotId) { + if (slotId >= AesKeySlotCount) { + return false; + } + + return m_slots.at(slotId).normalKey.has_value(); + } + + constexpr AESKey getNormalKey(std::size_t slotId) { + return m_slots.at(slotId).normalKey.value_or(AESKey{}); + } + + constexpr void setNormalKey(std::size_t slotId, const AESKey &key) { + if (slotId < AesKeySlotCount) { + m_slots.at(slotId).normalKey = key; + } + } + }; +} \ No newline at end of file diff --git a/include/emulator.hpp b/include/emulator.hpp index 46aa0ade..59a7a293 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -1,78 +1,101 @@ #pragma once -#include -#include #include #include +#include +#include + +#include "PICA/gpu.hpp" #include "cpu.hpp" +#include "crypto/aes_engine.hpp" #include "io_file.hpp" #include "memory.hpp" #include "opengl.hpp" -#include "PICA/gpu.hpp" -enum class ROMType { - None, ELF, NCSD -}; +enum class ROMType { None, ELF, NCSD }; class Emulator { - CPU cpu; - GPU gpu; - Memory memory; - Kernel kernel; + CPU cpu; + GPU gpu; + Memory memory; + Kernel kernel; + Crypto::AESEngine aesEngine; - SDL_Window* window; - SDL_GLContext glContext; + SDL_Window* window; + SDL_GLContext glContext; + SDL_GameController* gameController; + int gameControllerID; - static constexpr u32 width = 400; - static constexpr u32 height = 240 * 2; // * 2 because 2 screens - ROMType romType = ROMType::None; - bool running = true; + // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard + // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state + // And so the user can still use the keyboard to control the analog + bool keyboardAnalogX = false; + bool keyboardAnalogY = false; - // Keep the handle for the ROM here to reload when necessary and to prevent deleting it - // This is currently only used for ELFs, NCSDs use the IOFile API instead - std::ifstream loadedELF; - NCSD loadedNCSD; + static constexpr u32 width = 400; + static constexpr u32 height = 240 * 2; // * 2 because 2 screens + ROMType romType = ROMType::None; + bool running = true; -public: - Emulator() : kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory), memory(cpu.getTicksRef()) { - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { - Helpers::panic("Failed to initialize SDL2"); - } + // Keep the handle for the ROM here to reload when necessary and to prevent deleting it + // This is currently only used for ELFs, NCSDs use the IOFile API instead + std::ifstream loadedELF; + NCSD loadedNCSD; - // Request OpenGL 4.1 Core (Max available on MacOS) - // MacOS gets mad if we don't explicitly demand a core profile - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); - window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL); + public: + Emulator() : kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory), memory(cpu.getTicksRef()) { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { + Helpers::panic("Failed to initialize SDL2"); + } - if (window == nullptr) { - Helpers::panic("Window creation failed: %s", SDL_GetError()); - } + // Make SDL use consistent positional button mapping + SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); + if (SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) { + Helpers::warn("Failed to initialize SDL2 GameController: %s", SDL_GetError()); + } - glContext = SDL_GL_CreateContext(window); + // Request OpenGL 4.1 Core (Max available on MacOS) + // MacOS gets mad if we don't explicitly demand a core profile + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); + window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL); - if (glContext == nullptr) { - Helpers::panic("OpenGL context creation failed: %s", SDL_GetError()); - } + if (window == nullptr) { + Helpers::panic("Window creation failed: %s", SDL_GetError()); + } - if(!gladLoadGL(reinterpret_cast(SDL_GL_GetProcAddress))) { - Helpers::panic("OpenGL init failed: %s", SDL_GetError()); - } + glContext = SDL_GL_CreateContext(window); + if (glContext == nullptr) { + Helpers::panic("OpenGL context creation failed: %s", SDL_GetError()); + } - reset(); - } + if (!gladLoadGL(reinterpret_cast(SDL_GL_GetProcAddress))) { + Helpers::panic("OpenGL init failed: %s", SDL_GetError()); + } - void step(); - void render(); - void reset(); - void run(); - void runFrame(); + if (SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + gameController = SDL_GameControllerOpen(0); - bool loadROM(const std::filesystem::path& path); - bool loadNCSD(const std::filesystem::path& path); - bool loadELF(const std::filesystem::path& path); - bool loadELF(std::ifstream& file); - void initGraphicsContext() { gpu.initGraphicsContext(); } -}; \ No newline at end of file + if (gameController != nullptr) { + SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); + gameControllerID = SDL_JoystickInstanceID(stick); + } + } + + reset(); + } + + void step(); + void render(); + void reset(); + void run(); + void runFrame(); + + bool loadROM(const std::filesystem::path& path); + bool loadNCSD(const std::filesystem::path& path); + bool loadELF(const std::filesystem::path& path); + bool loadELF(std::ifstream& file); + void initGraphicsContext() { gpu.initGraphicsContext(); } +}; diff --git a/include/helpers.hpp b/include/helpers.hpp index 853e487b..53c57c7c 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -2,11 +2,10 @@ #include #include #include -#include #include #include -#include -#include +#include +#include #include #include "termcolor.hpp" @@ -51,21 +50,6 @@ namespace Helpers { va_end(args); } - static std::vector loadROM(std::string directory) { - std::ifstream file(directory, std::ios::binary); - if (file.fail()) panic("Couldn't read %s", directory.c_str()); - - std::vector ROM; - - file.unsetf(std::ios::skipws); - ROM.insert(ROM.begin(), std::istream_iterator(file), std::istream_iterator()); - - file.close(); - - printf("%s loaded successfully\n", directory.c_str()); - return ROM; - } - static constexpr bool buildingInDebugMode() { #ifdef NDEBUG return false; @@ -120,39 +104,6 @@ namespace Helpers { return (value >> offset) & ones(); } - /// Check if a bit "bit" of value is set - static constexpr bool isBitSet(u32 value, int bit) { return (value >> bit) & 1; } - - /// rotate number right - template - static constexpr T rotr(T value, int bits) { - constexpr auto bitWidth = sizeof(T) * 8; - bits &= bitWidth - 1; - return (value >> bits) | (value << (bitWidth - bits)); - } - - // rotate number left - template - static constexpr T rotl(T value, int bits) { - constexpr auto bitWidth = sizeof(T) * 8; - bits &= bitWidth - 1; - return (value << bits) | (value >> (bitWidth - bits)); - } - - /// Used to make the compiler evaluate beeg loops at compile time for the tablegen - template - static constexpr void static_for_impl(Func&& f, std::integer_sequence) { - (f(std::integral_constant{}), ...); - } - - template - static constexpr void static_for(Func&& f) { - static_for_impl(std::forward(f), std::make_integer_sequence{}); - } - - // For values < 0x99 - static constexpr inline u8 incBCDByte(u8 value) { return ((value & 0xf) == 0x9) ? value + 7 : value + 1; } - #ifdef HELPERS_APPLE_CLANG template constexpr To bit_cast(const From& from) noexcept { @@ -164,6 +115,19 @@ namespace Helpers { return std::bit_cast(from); } #endif + + static std::vector split(const std::string& s, const char c) { + std::istringstream tmp(s); + std::vector result(1); + + while (std::getline(tmp, *result.rbegin(), c)) { + result.emplace_back(); + } + + // Remove temporary slot + result.pop_back(); + return result; + } }; // namespace Helpers // UDLs for memory size values @@ -171,12 +135,3 @@ constexpr size_t operator""_KB(unsigned long long int x) { return 1024ULL * x; } constexpr size_t operator""_MB(unsigned long long int x) { return 1024_KB * x; } constexpr size_t operator""_GB(unsigned long long int x) { return 1024_MB * x; } -// useful macros -// likely/unlikely -#ifdef __GNUC__ -#define likely(x) __builtin_expect((x), 1) -#define unlikely(x) __builtin_expect((x), 0) -#else -#define likely(x) (x) -#define unlikely(x) (x) -#endif diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 2d05602d..95856e8c 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -1,14 +1,22 @@ #pragma once #include +#include #include #include "io_file.hpp" #include "helpers.hpp" +#include "crypto/aes_engine.hpp" struct NCCH { + struct EncryptionInfo { + Crypto::AESKey normalKey; + Crypto::AESKey initialCounter; + }; + struct FSInfo { // Info on the ExeFS/RomFS u64 offset = 0; u64 size = 0; u64 hashRegionSize = 0; + std::optional encryptionInfo; }; // Descriptions for .text, .data and .rodata sections @@ -34,6 +42,8 @@ struct NCCH { bool mountRomFS = false; bool encrypted = false; bool fixedCryptoKey = false; + bool seedCrypto = false; + u8 secondaryKeySlot = 0; static constexpr u64 mediaUnit = 0x200; u64 size = 0; // Size of NCCH converted to bytes @@ -41,6 +51,7 @@ struct NCCH { u32 bssSize = 0; u32 exheaderSize = 0; + FSInfo exheaderInfo; FSInfo exeFS; FSInfo romFS; CodeSetInfo text, data, rodata; @@ -50,10 +61,9 @@ struct NCCH { // Contains of the cart's save data std::vector saveData; - // Header: 0x200 + 0x800 byte NCCH header + exheadr // Returns true on success, false on failure // Partition index/offset/size must have been set before this - bool loadFromHeader(u8* header, IOFile& file); + bool loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info); bool hasExtendedHeader() { return exheaderSize != 0; } bool hasExeFS() { return exeFS.size != 0; } @@ -61,7 +71,8 @@ struct NCCH { bool hasCode() { return codeFile.size() != 0; } bool hasSaveData() { return saveData.size() != 0; } -private: - std::array primaryKey = {}; // For exheader, ExeFS header and icons - std::array secondaryKey = {}; // For RomFS and some files in ExeFS + std::pair getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY); + std::pair getSecondaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY); + + std::pair readFromFile(IOFile& file, const FSInfo &info, u8 *dst, std::size_t offset, std::size_t size); }; \ No newline at end of file diff --git a/include/memory.hpp b/include/memory.hpp index 33b18ca5..3d68866e 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -5,6 +5,7 @@ #include #include #include +#include "crypto/aes_engine.hpp" #include "helpers.hpp" #include "handles.hpp" #include "loader/ncsd.hpp" @@ -146,7 +147,7 @@ public: void* getReadPointer(u32 address); void* getWritePointer(u32 address); std::optional loadELF(std::ifstream& file); - std::optional loadNCSD(const std::filesystem::path& path); + std::optional loadNCSD(Crypto::AESEngine &aesEngine, const std::filesystem::path& path); u8 read8(u32 vaddr); u16 read16(u32 vaddr); diff --git a/include/metaprogramming.hpp b/include/metaprogramming.hpp new file mode 100644 index 00000000..e43decef --- /dev/null +++ b/include/metaprogramming.hpp @@ -0,0 +1,16 @@ +#pragma once +#include +#include + +namespace Helpers { + /// Used to make the compiler evaluate beeg loops at compile time for things like generating compile-time tables + template + static constexpr void static_for_impl(Func&& f, std::integer_sequence) { + (f(std::integral_constant{}), ...); + } + + template + static constexpr void static_for(Func&& f) { + static_for_impl(std::forward(f), std::make_integer_sequence{}); + } +} \ No newline at end of file diff --git a/include/opengl.hpp b/include/opengl.hpp index 16ed8221..67c45689 100644 --- a/include/opengl.hpp +++ b/include/opengl.hpp @@ -20,6 +20,7 @@ #pragma once #include #include +#include #include #include #include @@ -43,6 +44,19 @@ #include #endif +#if defined(GPU_DEBUG_INFO) +#define OPENGL_DEBUG_INFO +#endif + +#ifdef _MSC_VER +#include +#define OPENGL_PRINTF_FORMAT _Printf_format_string_ +#define OPENGL_PRINTF_FORMAT_ATTR(format_arg_index, dots_arg_index) +#else +#define OPENGL_PRINTF_FORMAT +#define OPENGL_PRINTF_FORMAT_ATTR(format_arg_index, dots_arg_index) __attribute__((__format__(__printf__, format_arg_index, dots_arg_index))) +#endif + // Uncomment the following define if you want GL objects to automatically free themselves when their lifetime ends // #define OPENGL_DESTRUCTORS @@ -53,7 +67,50 @@ namespace OpenGL { template constexpr std::false_type AlwaysFalse{}; - struct VertexArray { + OPENGL_PRINTF_FORMAT_ATTR(3, 4) + static void setObjectLabel(GLenum identifier, GLuint name, OPENGL_PRINTF_FORMAT const char* format, ...) { +#if defined(OPENGL_DEBUG_INFO) + GLchar label[256] = {}; + va_list args; + va_start(args, format); + const GLsizei length = vsnprintf(label, std::size(label), format, args); + va_end(args); + glObjectLabel(identifier, name, length, label); +#endif + } + + class DebugScope { +#if defined(OPENGL_DEBUG_INFO) + inline static GLuint scopeDepth = 0; + const GLuint m_scope_depth; +#endif + + public: + OPENGL_PRINTF_FORMAT_ATTR(2, 3) + DebugScope(OPENGL_PRINTF_FORMAT const char* format, ...) +#if defined(OPENGL_DEBUG_INFO) + : m_scope_depth(scopeDepth++) { + GLchar message[256] = {}; + va_list args; + va_start(args, format); + const GLsizei length = vsnprintf(message, std::size(message), format, args); + va_end(args); + glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, m_scope_depth, length, message); + } +#else + { + } +#endif + + ~DebugScope() { +#if defined(OPENGL_DEBUG_INFO) + glPopDebugGroup(); + scopeDepth--; +#endif + } + }; + + struct VertexArray { GLuint m_handle = 0; void create() { diff --git a/include/services/hid.hpp b/include/services/hid.hpp index ea36bb0d..beadf7f1 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -1,6 +1,7 @@ #pragma once #include #include + #include "helpers.hpp" #include "kernel_types.hpp" #include "logger.hpp" @@ -22,12 +23,12 @@ namespace HID::Keys { X = 1 << 10, Y = 1 << 11, - GPIO0Inv = 1 << 12, // Inverted value of GPIO bit 0 - GPIO14Inv = 1 << 13, // Inverted value of GPIO bit 14 + GPIO0Inv = 1 << 12, // Inverted value of GPIO bit 0 + GPIO14Inv = 1 << 13, // Inverted value of GPIO bit 14 - CirclePadRight = 1 << 28, // X >= 41 - CirclePadLeft = 1 << 29, // X <= -41 - CirclePadUp = 1 << 30, // Y >= 41 + CirclePadRight = 1 << 28, // X >= 41 + CirclePadLeft = 1 << 29, // X <= -41 + CirclePadUp = 1 << 30, // Y >= 41 CirclePadDown = 1u << 31 // Y <= -41 }; } @@ -39,18 +40,18 @@ class HIDService { Handle handle = KernelHandles::HID; Memory& mem; Kernel& kernel; - u8* sharedMem = nullptr; // Pointer to HID shared memory + u8* sharedMem = nullptr; // Pointer to HID shared memory uint nextPadIndex; uint nextTouchscreenIndex; uint nextAccelerometerIndex; uint nextGyroIndex; - u32 newButtons; // The button state currently being edited - u32 oldButtons; // The previous pad state + u32 newButtons; // The button state currently being edited + u32 oldButtons; // The previous pad state - s16 circlePadX, circlePadY; // Circlepad state - s16 touchScreenX, touchScreenY; // Touchscreen state + s16 circlePadX, circlePadY; // Circlepad state + s16 touchScreenX, touchScreenY; // Touchscreen state bool accelerometerEnabled; bool eventsInitialized; @@ -79,7 +80,7 @@ class HIDService { *(T*)&sharedMem[offset] = value; } -public: + public: HIDService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); @@ -87,15 +88,18 @@ public: void pressKey(u32 mask) { newButtons |= mask; } void releaseKey(u32 mask) { newButtons &= ~mask; } + s16 getCirclepadX() { return circlePadX; } + s16 getCirclepadY() { return circlePadY; } + void setCirclepadX(s16 x) { circlePadX = x; // Turn bits 28 and 29 off in the new button state, which indicate whether the circlepad is steering left or right // Then, set them according to the new value of x newButtons &= ~0x3000'0000; - if (x >= 41) // Pressing right + if (x >= 41) // Pressing right newButtons |= 1 << 28; - else if (x <= -41) // Pressing left + else if (x <= -41) // Pressing left newButtons |= 1 << 29; } @@ -105,9 +109,9 @@ public: // Turn bits 30 and 31 off in the new button state, which indicate whether the circlepad is steering up or down // Then, set them according to the new value of y newButtons &= ~0xC000'0000; - if (y >= 41) // Pressing up + if (y >= 41) // Pressing up newButtons |= 1 << 30; - else if (y <= -41) // Pressing down + else if (y <= -41) // Pressing down newButtons |= 1 << 31; } @@ -115,7 +119,7 @@ public: void setSharedMem(u8* ptr) { sharedMem = ptr; - if (ptr != nullptr) { // Zero-fill shared memory in case the process tries to read stale service data or vice versa + if (ptr != nullptr) { // Zero-fill shared memory in case the process tries to read stale service data or vice versa std::memset(ptr, 0, 0x2b0); } } @@ -129,4 +133,4 @@ public: void releaseTouchScreen() { touchScreenPressed = false; } -}; \ No newline at end of file +}; diff --git a/include/services/service_manager.hpp b/include/services/service_manager.hpp index e09fd455..ee2677ad 100644 --- a/include/services/service_manager.hpp +++ b/include/services/service_manager.hpp @@ -90,9 +90,11 @@ class ServiceManager { // Input function wrappers void pressKey(u32 key) { hid.pressKey(key); } void releaseKey(u32 key) { hid.releaseKey(key); } - void setCirclepadX(u16 x) { hid.setCirclepadX(x); } - void setCirclepadY(u16 y) { hid.setCirclepadY(y); } + s16 getCirclepadX() { return hid.getCirclepadX(); } + s16 getCirclepadY() { return hid.getCirclepadY(); } + void setCirclepadX(s16 x) { hid.setCirclepadX(x); } + void setCirclepadY(s16 y) { hid.setCirclepadY(y); } void updateInputs(u64 currentTimestamp) { hid.updateInputs(currentTimestamp); } void setTouchScreenPress(u16 x, u16 y) { hid.setTouchScreenPress(x, y); } void releaseTouchScreen() { hid.releaseTouchScreen(); } -}; \ No newline at end of file +}; diff --git a/src/core/crypto/aes_engine.cpp b/src/core/crypto/aes_engine.cpp new file mode 100644 index 00000000..f4bf3494 --- /dev/null +++ b/src/core/crypto/aes_engine.cpp @@ -0,0 +1,83 @@ +#include +#include + +#include "crypto/aes_engine.hpp" +#include "helpers.hpp" + +namespace Crypto { + void AESEngine::loadKeys(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::in); + + if (file.fail()) { + Helpers::warn("Keys: Couldn't read key file: %s", path.c_str()); + return; + } + + while (!file.eof()) { + std::string line; + std::getline(file, line); + + // Skip obvious invalid lines + if (line.empty() || line.starts_with("#")) { + continue; + } + + const auto parts = Helpers::split(line, '='); + if (parts.size() != 2) { + Helpers::warn("Keys: Failed to parse %s", line.c_str()); + continue; + } + + const std::string& name = parts[0]; + const std::string& rawKeyHex = parts[1]; + + std::size_t slotId; + char keyType; + + bool is_generator = name == "generator"; + if (!is_generator && std::sscanf(name.c_str(), "slot0x%zXKey%c", &slotId, &keyType) != 2) { + Helpers::warn("Keys: Ignoring unknown key %s", name.c_str()); + continue; + } + + auto key = createKeyFromHex(rawKeyHex); + + if (!key.has_value()) { + Helpers::warn("Keys: Failed to parse raw key %s", rawKeyHex.c_str()); + continue; + } + + if (is_generator) { + m_generator = key; + continue; + } + + if (slotId >= AesKeySlotCount) { + Helpers::warn("Keys: Invalid key slot id %u", slotId); + continue; + } + + switch (keyType) { + case 'X': + setKeyX(slotId, key.value()); + break; + case 'Y': + setKeyY(slotId, key.value()); + break; + case 'N': + setNormalKey(slotId, key.value()); + break; + default: + Helpers::warn("Keys: Invalid key type %c", keyType); + break; + } + } + + // As the generator key can be set at any time, force update all normal keys. + for (std::size_t i = 0; i < AesKeySlotCount; i++) { + updateNormalKey(i); + } + + keysLoaded = true; + } +}; \ No newline at end of file diff --git a/src/core/fs/archive_ncch.cpp b/src/core/fs/archive_ncch.cpp index 0c635c21..3a3330b8 100644 --- a/src/core/fs/archive_ncch.cpp +++ b/src/core/fs/archive_ncch.cpp @@ -133,6 +133,8 @@ std::optional NCCHArchive::readFile(FileSession* file, u64 offset, u32 size auto cxi = mem.getCXI(); IOFile& ioFile = mem.CXIFile; + NCCH::FSInfo fsInfo; + // Seek to file offset depending on if we're reading from RomFS, ExeFS, etc switch (type) { case PathType::RomFS: { @@ -142,9 +144,8 @@ std::optional NCCHArchive::readFile(FileSession* file, u64 offset, u32 size Helpers::panic("Tried to read from NCCH with too big of an offset"); } - if (!ioFile.seek(cxi->fileOffset + romFSOffset + offset + 0x1000)) { - Helpers::panic("Failed to seek while reading from RomFS"); - } + fsInfo = cxi->romFS; + offset += 0x1000; break; } @@ -153,7 +154,7 @@ std::optional NCCHArchive::readFile(FileSession* file, u64 offset, u32 size } std::unique_ptr data(new u8[size]); - auto [success, bytesRead] = ioFile.readBytes(&data[0], size); + auto [success, bytesRead] = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size); if (!success) { Helpers::panic("Failed to read from NCCH archive"); diff --git a/src/core/fs/archive_self_ncch.cpp b/src/core/fs/archive_self_ncch.cpp index 2146c578..fa141b03 100644 --- a/src/core/fs/archive_self_ncch.cpp +++ b/src/core/fs/archive_self_ncch.cpp @@ -71,6 +71,8 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 auto cxi = mem.getCXI(); IOFile& ioFile = mem.CXIFile; + NCCH::FSInfo fsInfo; + // Seek to file offset depending on if we're reading from RomFS, ExeFS, etc switch (type) { case PathType::RomFS: { @@ -80,9 +82,8 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); } - if (!ioFile.seek(cxi->fileOffset + romFSOffset + offset + 0x1000)) { - Helpers::panic("Failed to seek while reading from RomFS"); - } + fsInfo = cxi->romFS; + offset += 0x1000; break; } @@ -93,9 +94,7 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); } - if (!ioFile.seek(cxi->fileOffset + exeFSOffset + offset)) { // TODO: Not sure if this needs the + 0x1000 - Helpers::panic("Failed to seek while reading from ExeFS"); - } + fsInfo = cxi->exeFS; break; } @@ -104,7 +103,7 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 } std::unique_ptr data(new u8[size]); - auto [success, bytesRead] = ioFile.readBytes(&data[0], size); + auto [success, bytesRead] = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size); if (!success) { Helpers::panic("Failed to read from SelfNCCH archive"); diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index c3dd8d2e..0f29ddb5 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -1,142 +1,311 @@ +#include +#include #include #include #include "loader/lz77.hpp" #include "loader/ncch.hpp" #include "memory.hpp" -bool NCCH::loadFromHeader(u8* header, IOFile& file) { - if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'C' || header[0x103] != 'H') { - printf("Invalid header on NCCH\n"); +#include + +bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) { + // 0x200 bytes for the NCCH header + constexpr u64 headerSize = 0x200; + u8 header[headerSize]; + + auto [success, bytes] = readFromFile(file, info, header, 0, headerSize); + if (!success || bytes != headerSize) { + printf("Failed to read NCCH header\n"); return false; } + + if (header[0x100] != 'N' || header[0x101] != 'C' || header[0x102] != 'C' || header[0x103] != 'H') { + printf("Invalid header on NCCH\n"); + return false; + } - codeFile.clear(); - saveData.clear(); + codeFile.clear(); + saveData.clear(); - size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break - exheaderSize = *(u32*)&header[0x180]; + size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break + exheaderSize = *(u32*)&header[0x180]; - const u64 programID = *(u64*)&header[0x118]; + const u64 programID = *(u64*)&header[0x118]; - // Read NCCH flags - isNew3DS = header[0x188 + 4] == 2; - fixedCryptoKey = (header[0x188 + 7] & 0x1) == 0x1; - mountRomFS = (header[0x188 + 7] & 0x2) != 0x2; - encrypted = (header[0x188 + 7] & 0x4) != 0x4; - - // Read ExeFS and RomFS info - exeFS.offset = u64(*(u32*)&header[0x1A0]) * mediaUnit; - exeFS.size = u64(*(u32*)&header[0x1A4]) * mediaUnit; - exeFS.hashRegionSize = u64(*(u32*)&header[0x1A8]) * mediaUnit; + // Read NCCH flags + secondaryKeySlot = header[0x188 + 3]; + isNew3DS = header[0x188 + 4] == 2; + fixedCryptoKey = (header[0x188 + 7] & 0x1) == 0x1; + mountRomFS = (header[0x188 + 7] & 0x2) != 0x2; + encrypted = (header[0x188 + 7] & 0x4) != 0x4; + seedCrypto = (header[0x188 + 7] & 0x20) == 0x20; - romFS.offset = u64(*(u32*)&header[0x1B0]) * mediaUnit; - romFS.size = u64(*(u32*)&header[0x1B4]) * mediaUnit; - romFS.hashRegionSize = u64(*(u32*)&header[0x1B8]) * mediaUnit; + // Read exheader, ExeFS and RomFS info + exheaderInfo.offset = info.offset + 0x200; + exheaderInfo.size = exheaderSize; + exheaderInfo.hashRegionSize = 0; - if (fixedCryptoKey) { - Helpers::panic("Fixed crypto keys for NCCH"); - } + exeFS.offset = info.offset + u64(*(u32*)&header[0x1A0]) * mediaUnit; + exeFS.size = u64(*(u32*)&header[0x1A4]) * mediaUnit; + exeFS.hashRegionSize = u64(*(u32*)&header[0x1A8]) * mediaUnit; - if (exheaderSize != 0) { - const u8* exheader = &header[0x200]; // Extended NCCH header - const u64 jumpID = *(u64*)&exheader[0x1C0 + 0x8]; + romFS.offset = info.offset + u64(*(u32*)&header[0x1B0]) * mediaUnit; + romFS.size = u64(*(u32*)&header[0x1B4]) * mediaUnit; + romFS.hashRegionSize = u64(*(u32*)&header[0x1B8]) * mediaUnit; - // It seems like some decryption tools will decrypt the file, without actually setting the NoCrypto flag in the NCCH header - // This is a nice and easy hack to see if a file is pretending to be encrypted, taken from 3DMoo and Citra - if (u32(programID) == u32(jumpID) && encrypted) { - printf("NCSD is supposedly ecrypted but not actually encrypted\n"); - encrypted = false; - } else if (encrypted) { - Helpers::panic("Encrypted NCSD file"); - } + if (encrypted) { + if (!aesEngine.haveKeys()) { + Helpers::panic( + "Loaded an encrypted ROM but AES keys don't seem to have been provided correctly! Navigate to the emulator's\n" + "app data folder and make sure you have a sysdata directory with a file called aes_keys.txt which contains your keys!" + ); + return false; + } - const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes - saveData.resize(saveDataSize, 0xff); + Crypto::AESKey primaryKeyY; + Crypto::AESKey secondaryKeyY; + std::memcpy(primaryKeyY.data(), header, primaryKeyY.size()); - compressCode = (exheader[0xD] & 1) != 0; - stackSize = *(u32*)&exheader[0x1C]; - bssSize = *(u32*)&exheader[0x3C]; + if (!seedCrypto) { + secondaryKeyY = primaryKeyY; + } else { + Helpers::panic("Seed crypto is not supported"); + return false; + } - text.extract(&exheader[0x10]); - rodata.extract(&exheader[0x20]); - data.extract(&exheader[0x30]); - } + auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY); - printf("Stack size: %08X\nBSS size: %08X\n", stackSize, bssSize); + if (!primaryResult.first) { + Helpers::panic("getPrimaryKey failed!"); + return false; + } - // Read ExeFS - if (hasExeFS()) { - u64 exeFSOffset = fileOffset + exeFS.offset; // Offset of ExeFS in the file = exeFS offset + ncch offset - printf("ExeFS offset: %08llX, size: %08llX (Offset in file = %08llX)\n", exeFS.offset, exeFS.size, exeFSOffset); - constexpr size_t exeFSHeaderSize = 0x200; + Crypto::AESKey primaryKey = primaryResult.second; - u8 exeFSHeader[exeFSHeaderSize]; + auto secondaryResult = getSecondaryKey(aesEngine, secondaryKeyY); - file.seek(exeFSOffset); - auto [success, bytes] = file.readBytes(exeFSHeader, exeFSHeaderSize); - if (!success || bytes != exeFSHeaderSize) { - printf("Failed to parse ExeFS header\n"); - return false; - } + if (!secondaryResult.first) { + Helpers::panic("getSecondaryKey failed!"); + return false; + } - // ExeFS format allows up to 10 files - for (int i = 0; i < 10; i++) { - u8* fileInfo = &exeFSHeader[i * 16]; + Crypto::AESKey secondaryKey = secondaryResult.second; - char name[9]; - std::memcpy(name, fileInfo, 8); // Get file name as a string - name[8] = '\0'; // Add null terminator to it just in case there's none + EncryptionInfo encryptionInfoTmp; + encryptionInfoTmp.normalKey = primaryKey; + encryptionInfoTmp.initialCounter.fill(0); - u32 fileOffset = *(u32*)&fileInfo[0x8]; - u32 fileSize = *(u32*)&fileInfo[0xC]; + for (std::size_t i = 1; i <= sizeof(std::uint64_t) - 1; i++) { + encryptionInfoTmp.initialCounter[i] = header[0x108 + sizeof(std::uint64_t) - 1 - i]; + } + encryptionInfoTmp.initialCounter[8] = 1; + exheaderInfo.encryptionInfo = encryptionInfoTmp; - if (fileSize != 0) { - printf("File %d. Name: %s, Size: %08X, Offset: %08X\n", i, name, fileSize, fileOffset); - } + encryptionInfoTmp.initialCounter[8] = 2; + exeFS.encryptionInfo = encryptionInfoTmp; - if (std::strcmp(name, ".code") == 0) { - if (hasCode()) { - Helpers::panic("Second code file in a single NCCH partition. What should this do?\n"); - } + encryptionInfoTmp.normalKey = secondaryKey; + encryptionInfoTmp.initialCounter[8] = 3; + romFS.encryptionInfo = encryptionInfoTmp; + } - if (compressCode) { - std::vector tmp; - tmp.resize(fileSize); + if (exheaderSize != 0) { + std::unique_ptr exheader(new u8[exheaderSize]); - // A file offset of 0 means our file is located right after the ExeFS header - // So in the ROM, files are located at (file offset + exeFS offset + exeFS header size) - file.seek(exeFSOffset + exeFSHeaderSize + fileOffset); - file.readBytes(tmp.data(), fileSize); - - // Decompress .code file from the tmp vector to the "code" vector - if (!CartLZ77::decompress(codeFile, tmp)) { - printf("Failed to decompress .code file\n"); - return false; - } - } else { - codeFile.resize(fileSize); - file.seek(exeFSOffset + exeFSHeaderSize + fileOffset); - file.readBytes(codeFile.data(), fileSize); - } - } - } - } + auto [success, bytes] = readFromFile(file, info, &exheader[0], 0x200, exheaderSize); + if (!success || bytes != exheaderSize) { + printf("Failed to read Extended NCCH header\n"); + return false; + } - if (hasRomFS()) { - printf("RomFS offset: %08llX, size: %08llX\n", romFS.offset, romFS.size); - } + const u64 jumpID = *(u64*)&exheader[0x1C0 + 0x8]; - if (stackSize != 0 && stackSize != VirtualAddrs::DefaultStackSize) { - Helpers::warn("Requested stack size is %08X bytes. Temporarily emulated as 0x4000 until adjustable sizes are added\n", stackSize); - } + // It seems like some decryption tools will decrypt the file, without actually setting the NoCrypto flag in the NCCH header + // This is a nice and easy hack to see if a file is pretending to be encrypted, taken from 3DMoo and Citra + if (u32(programID) == u32(jumpID) && encrypted) { + printf("NCSD is supposedly ecrypted but not actually encrypted\n"); + encrypted = false; + } + // If it's truly encrypted, we need to read section again. + if (encrypted) { + auto [success, bytes] = readFromFile(file, exheaderInfo, &exheader[0], 0, exheaderSize); + if (!success || bytes != exheaderSize) { + printf("Failed to read Extended NCCH header\n"); + return false; + } + } - if (encrypted) { - if (hasExeFS()) - Helpers::panic("Encrypted NCCH partition with ExeFS"); - else - printf("Encrypted NCCH partition. Hopefully not required because it doesn't have an ExeFS. Skipped\n"); - } + const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes + saveData.resize(saveDataSize, 0xff); - initialized = true; - return true; + compressCode = (exheader[0xD] & 1) != 0; + stackSize = *(u32*)&exheader[0x1C]; + bssSize = *(u32*)&exheader[0x3C]; + + text.extract(&exheader[0x10]); + rodata.extract(&exheader[0x20]); + data.extract(&exheader[0x30]); + } + + printf("Stack size: %08X\nBSS size: %08X\n", stackSize, bssSize); + + // Read ExeFS + if (hasExeFS()) { + u64 exeFSOffset = fileOffset + exeFS.offset; // Offset of ExeFS in the file = exeFS offset + ncch offset + printf("ExeFS offset: %08llX, size: %08llX (Offset in file = %08llX)\n", exeFS.offset, exeFS.size, exeFSOffset); + constexpr size_t exeFSHeaderSize = 0x200; + + u8 exeFSHeader[exeFSHeaderSize]; + + auto [success, bytes] = readFromFile(file, exeFS, exeFSHeader, 0, exeFSHeaderSize); + if (!success || bytes != exeFSHeaderSize) { + printf("Failed to parse ExeFS header\n"); + return false; + } + + // ExeFS format allows up to 10 files + for (int i = 0; i < 10; i++) { + u8* fileInfo = &exeFSHeader[i * 16]; + + char name[9]; + std::memcpy(name, fileInfo, 8); // Get file name as a string + name[8] = '\0'; // Add null terminator to it just in case there's none + + u32 fileOffset = *(u32*)&fileInfo[0x8]; + u32 fileSize = *(u32*)&fileInfo[0xC]; + + if (fileSize != 0) { + printf("File %d. Name: %s, Size: %08X, Offset: %08X\n", i, name, fileSize, fileOffset); + } + + if (std::strcmp(name, ".code") == 0) { + if (hasCode()) { + Helpers::panic("Second code file in a single NCCH partition. What should this do?\n"); + } + + if (compressCode) { + std::vector tmp; + tmp.resize(fileSize); + + // A file offset of 0 means our file is located right after the ExeFS header + // So in the ROM, files are located at (file offset + exeFS offset + exeFS header size) + readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize); + + // Decompress .code file from the tmp vector to the "code" vector + if (!CartLZ77::decompress(codeFile, tmp)) { + printf("Failed to decompress .code file\n"); + return false; + } + } else { + codeFile.resize(fileSize); + readFromFile(file, exeFS, codeFile.data(), fileOffset + exeFSHeaderSize, fileSize); + } + } + } + } + + if (hasRomFS()) { + printf("RomFS offset: %08llX, size: %08llX\n", romFS.offset, romFS.size); + } + + if (stackSize != 0 && stackSize != VirtualAddrs::DefaultStackSize) { + Helpers::warn("Requested stack size is %08X bytes. Temporarily emulated as 0x4000 until adjustable sizes are added\n", stackSize); + } + + initialized = true; + return true; +} + +std::pair NCCH::getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY) { + Crypto::AESKey result; + + if (encrypted) { + if (fixedCryptoKey) { + return {true, result}; + } + + aesEngine.setKeyY(Crypto::KeySlotId::NCCHKey0, keyY); + + if (!aesEngine.hasNormalKey(Crypto::KeySlotId::NCCHKey0)) { + return {false, result}; + } + + result = aesEngine.getNormalKey(Crypto::KeySlotId::NCCHKey0); + } + + return {true, result}; +} + +std::pair NCCH::getSecondaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY) { + Crypto::AESKey result; + + if (encrypted) { + + if (fixedCryptoKey) { + return {true, result}; + } + + Crypto::KeySlotId keySlotId; + + switch (secondaryKeySlot) { + case 0: + keySlotId = Crypto::KeySlotId::NCCHKey0; + break; + case 1: + keySlotId = Crypto::KeySlotId::NCCHKey1; + break; + case 10: + keySlotId = Crypto::KeySlotId::NCCHKey2; + break; + case 11: + keySlotId = Crypto::KeySlotId::NCCHKey3; + break; + default: + return {false, result}; + } + + if (!aesEngine.hasKeyX(keySlotId)) { + return {false, result}; + } + + aesEngine.setKeyY(keySlotId, keyY); + + if (!aesEngine.hasNormalKey(keySlotId)) { + return {false, result}; + } + + result = aesEngine.getNormalKey(keySlotId); + } + + return {true, result}; +} + +std::pair NCCH::readFromFile(IOFile& file, const FSInfo &info, u8 *dst, std::size_t offset, std::size_t size) { + if (size == 0) { + return { true, 0 }; + } + + std::size_t readMaxSize = std::min(size, static_cast(info.size) - offset); + + file.seek(info.offset + offset); + auto [success, bytes] = file.readBytes(dst, readMaxSize); + + if (!success) { + return { success, bytes}; + } + + if (success && info.encryptionInfo.has_value()) { + auto& encryptionInfo = info.encryptionInfo.value(); + + CryptoPP::CTR_Mode::Decryption d(encryptionInfo.normalKey.data(), encryptionInfo.normalKey.size(), encryptionInfo.initialCounter.data()); + + if (offset > 0) { + d.Seek(offset); + } + + CryptoPP::byte* data = reinterpret_cast(dst); + d.ProcessData(data, data, bytes); + } + + return { success, bytes}; } \ No newline at end of file diff --git a/src/core/loader/ncsd.cpp b/src/core/loader/ncsd.cpp index bf93f490..1166b3fe 100644 --- a/src/core/loader/ncsd.cpp +++ b/src/core/loader/ncsd.cpp @@ -3,7 +3,7 @@ #include "loader/ncsd.hpp" #include "memory.hpp" -std::optional Memory::loadNCSD(const std::filesystem::path& path) { +std::optional Memory::loadNCSD(Crypto::AESEngine &aesEngine, const std::filesystem::path& path) { NCSD ncsd; if (!ncsd.file.open(path, "rb")) return std::nullopt; @@ -51,18 +51,12 @@ std::optional Memory::loadNCSD(const std::filesystem::path& path) { ncch.fileOffset = partition.offset; if (partition.length != 0) { // Initialize the NCCH of each partition - ncsd.file.seek(partition.offset); + NCCH::FSInfo ncchFsInfo; - // 0x200 bytes for the NCCH header and another 0x800 for the exheader - constexpr u64 headerSize = 0x200 + 0x800; - u8 ncchHeader[headerSize]; - std::tie(success, bytes) = ncsd.file.readBytes(ncchHeader, headerSize); - if (!success || bytes != headerSize) { - printf("Failed to read NCCH header\n"); - return std::nullopt; - } + ncchFsInfo.offset = partition.offset; + ncchFsInfo.size = partition.length; - if (!ncch.loadFromHeader(ncchHeader, ncsd.file)) { + if (!ncch.loadFromHeader(aesEngine, ncsd.file, ncchFsInfo)) { printf("Invalid NCCH partition\n"); return std::nullopt; } diff --git a/src/emulator.cpp b/src/emulator.cpp index 8141a94a..3a862231 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,194 +1,286 @@ #include "emulator.hpp" void Emulator::reset() { - cpu.reset(); - gpu.reset(); - memory.reset(); - // Kernel must be reset last because it depends on CPU/Memory state - kernel.reset(); + cpu.reset(); + gpu.reset(); + memory.reset(); + // Kernel must be reset last because it depends on CPU/Memory state + kernel.reset(); - // Reloading r13 and r15 needs to happen after everything has been reset - // Otherwise resetting the kernel or cpu might nuke them - cpu.setReg(13, VirtualAddrs::StackTop); // Set initial SP - if (romType == ROMType::ELF) { // Reload ELF if we're using one - loadELF(loadedELF); - } + // Reloading r13 and r15 needs to happen after everything has been reset + // Otherwise resetting the kernel or cpu might nuke them + cpu.setReg(13, VirtualAddrs::StackTop); // Set initial SP + if (romType == ROMType::ELF) { // Reload ELF if we're using one + loadELF(loadedELF); + } } void Emulator::step() {} - -void Emulator::render() { - -} +void Emulator::render() {} void Emulator::run() { - while (running) { - gpu.getGraphicsContext(); // Give the GPU a rendering context - runFrame(); // Run 1 frame of instructions - gpu.display(); // Display graphics + while (running) { + gpu.getGraphicsContext(); // Give the GPU a rendering context + runFrame(); // Run 1 frame of instructions + gpu.display(); // Display graphics - ServiceManager& srv = kernel.getServiceManager(); + ServiceManager& srv = kernel.getServiceManager(); - // Send VBlank interrupts - srv.sendGPUInterrupt(GPUInterrupt::VBlank0); - srv.sendGPUInterrupt(GPUInterrupt::VBlank1); + // Send VBlank interrupts + srv.sendGPUInterrupt(GPUInterrupt::VBlank0); + srv.sendGPUInterrupt(GPUInterrupt::VBlank1); - SDL_Event event; - while (SDL_PollEvent(&event)) { - namespace Keys = HID::Keys; + SDL_Event event; + while (SDL_PollEvent(&event)) { + namespace Keys = HID::Keys; - switch (event.type) { - case SDL_QUIT: - printf("Bye :(\n"); - running = false; - return; - case SDL_KEYDOWN: - switch (event.key.keysym.sym) { - case SDLK_l: srv.pressKey(Keys::A); break; - case SDLK_k: srv.pressKey(Keys::B); break; - case SDLK_o: srv.pressKey(Keys::X); break; - case SDLK_i: srv.pressKey(Keys::Y); break; + switch (event.type) { + case SDL_QUIT: + printf("Bye :(\n"); + running = false; + return; - case SDLK_q: srv.pressKey(Keys::L); break; - case SDLK_p: srv.pressKey(Keys::R); break; + case SDL_KEYDOWN: + switch (event.key.keysym.sym) { + case SDLK_l: srv.pressKey(Keys::A); break; + case SDLK_k: srv.pressKey(Keys::B); break; + case SDLK_o: srv.pressKey(Keys::X); break; + case SDLK_i: srv.pressKey(Keys::Y); break; - case SDLK_RIGHT: srv.pressKey(Keys::Right); break; - case SDLK_LEFT: srv.pressKey(Keys::Left); break; - case SDLK_UP: srv.pressKey(Keys::Up); break; - case SDLK_DOWN: srv.pressKey(Keys::Down); break; + case SDLK_q: srv.pressKey(Keys::L); break; + case SDLK_p: srv.pressKey(Keys::R); break; - case SDLK_w: srv.setCirclepadY(0x9C); break; - case SDLK_a: srv.setCirclepadX(-0x9C); break; - case SDLK_s: srv.setCirclepadY(-0x9C); break; - case SDLK_d: srv.setCirclepadX(0x9C); break; + case SDLK_RIGHT: srv.pressKey(Keys::Right); break; + case SDLK_LEFT: srv.pressKey(Keys::Left); break; + case SDLK_UP: srv.pressKey(Keys::Up); break; + case SDLK_DOWN: srv.pressKey(Keys::Down); break; - case SDLK_RETURN: srv.pressKey(Keys::Start); break; - case SDLK_BACKSPACE: srv.pressKey(Keys::Select); break; - } - break; - case SDL_KEYUP: - switch (event.key.keysym.sym) { - case SDLK_l: srv.releaseKey(Keys::A); break; - case SDLK_k: srv.releaseKey(Keys::B); break; - case SDLK_o: srv.releaseKey(Keys::X); break; - case SDLK_i: srv.releaseKey(Keys::Y); break; + case SDLK_w: + srv.setCirclepadY(0x9C); + keyboardAnalogY = true; + break; - case SDLK_q: srv.releaseKey(Keys::L); break; - case SDLK_p: srv.releaseKey(Keys::R); break; + case SDLK_a: + srv.setCirclepadX(-0x9C); + keyboardAnalogX = true; + break; - case SDLK_RIGHT: srv.releaseKey(Keys::Right); break; - case SDLK_LEFT: srv.releaseKey(Keys::Left); break; - case SDLK_UP: srv.releaseKey(Keys::Up); break; - case SDLK_DOWN: srv.releaseKey(Keys::Down); break; + case SDLK_s: + srv.setCirclepadY(-0x9C); + keyboardAnalogY = true; + break; - // Err this is probably not ideal - case SDLK_w: srv.setCirclepadY(0); break; - case SDLK_a: srv.setCirclepadX(0); break; - case SDLK_s: srv.setCirclepadY(0); break; - case SDLK_d: srv.setCirclepadX(0); break; + case SDLK_d: + srv.setCirclepadX(0x9C); + keyboardAnalogX = true; + break; - case SDLK_RETURN: srv.releaseKey(Keys::Start); break; - case SDLK_BACKSPACE: srv.releaseKey(Keys::Select); break; - } - break; + case SDLK_RETURN: srv.pressKey(Keys::Start); break; + case SDLK_BACKSPACE: srv.pressKey(Keys::Select); break; + } + break; - case SDL_MOUSEBUTTONDOWN: { - if (event.button.button == SDL_BUTTON_LEFT) { - const s32 x = event.button.x; - const s32 y = event.button.y; + case SDL_KEYUP: + switch (event.key.keysym.sym) { + case SDLK_l: srv.releaseKey(Keys::A); break; + case SDLK_k: srv.releaseKey(Keys::B); break; + case SDLK_o: srv.releaseKey(Keys::X); break; + case SDLK_i: srv.releaseKey(Keys::Y); break; - // Check if touch falls in the touch screen area - if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { - // Convert to 3DS coordinates - u16 x_converted = static_cast(x) - 40; - u16 y_converted = static_cast(y) - 240; + case SDLK_q: srv.releaseKey(Keys::L); break; + case SDLK_p: srv.releaseKey(Keys::R); break; - srv.setTouchScreenPress(x_converted, y_converted); - } - else { - srv.releaseTouchScreen(); - } - } - break; - } + case SDLK_RIGHT: srv.releaseKey(Keys::Right); break; + case SDLK_LEFT: srv.releaseKey(Keys::Left); break; + case SDLK_UP: srv.releaseKey(Keys::Up); break; + case SDLK_DOWN: srv.releaseKey(Keys::Down); break; - case SDL_MOUSEBUTTONUP: - if (event.button.button == SDL_BUTTON_LEFT) { - srv.releaseTouchScreen(); - } - break; - } - } + // Err this is probably not ideal + case SDLK_w: + case SDLK_s: + srv.setCirclepadY(0); + keyboardAnalogY = false; + break; - // Update inputs in the HID module - srv.updateInputs(cpu.getTicks()); - SDL_GL_SwapWindow(window); - } + case SDLK_a: + case SDLK_d: + srv.setCirclepadX(0); + keyboardAnalogX = false; + break; + + case SDLK_RETURN: srv.releaseKey(Keys::Start); break; + case SDLK_BACKSPACE: srv.releaseKey(Keys::Select); break; + } + break; + + case SDL_MOUSEBUTTONDOWN: { + if (event.button.button == SDL_BUTTON_LEFT) { + const s32 x = event.button.x; + const s32 y = event.button.y; + + // Check if touch falls in the touch screen area + if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { + // Convert to 3DS coordinates + u16 x_converted = static_cast(x) - 40; + u16 y_converted = static_cast(y) - 240; + + srv.setTouchScreenPress(x_converted, y_converted); + } else { + srv.releaseTouchScreen(); + } + } + break; + } + + case SDL_MOUSEBUTTONUP: + if (event.button.button == SDL_BUTTON_LEFT) { + srv.releaseTouchScreen(); + } + break; + + case SDL_CONTROLLERDEVICEADDED: + if (gameController == nullptr) { + gameController = SDL_GameControllerOpen(event.cdevice.which); + gameControllerID = event.cdevice.which; + } + break; + + case SDL_CONTROLLERDEVICEREMOVED: + if (event.cdevice.which == gameControllerID) { + SDL_GameControllerClose(gameController); + gameController = nullptr; + gameControllerID = 0; + } + break; + + case SDL_CONTROLLERBUTTONUP: + case SDL_CONTROLLERBUTTONDOWN: { + u32 key = 0; + + switch (event.cbutton.button) { + case SDL_CONTROLLER_BUTTON_A: key = Keys::B; break; + case SDL_CONTROLLER_BUTTON_B: key = Keys::A; break; + case SDL_CONTROLLER_BUTTON_X: key = Keys::Y; break; + case SDL_CONTROLLER_BUTTON_Y: key = Keys::X; break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: key = Keys::L; break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: key = Keys::R; break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: key = Keys::Left; break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: key = Keys::Right; break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: key = Keys::Up; break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: key = Keys::Down; break; + case SDL_CONTROLLER_BUTTON_BACK: key = Keys::Select; break; + case SDL_CONTROLLER_BUTTON_START: key = Keys::Start; break; + } + + if (key != 0) { + if (event.cbutton.state == SDL_PRESSED) { + srv.pressKey(key); + } else { + srv.releaseKey(key); + } + } + } + } + } + + if (gameController != nullptr) { + const s16 stickX = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTX); + const s16 stickY = SDL_GameControllerGetAxis(gameController, SDL_CONTROLLER_AXIS_LEFTY); + constexpr s16 deadzone = 3276; + constexpr s16 maxValue = 0x9C; + constexpr s16 div = 0x8000 / maxValue; + + // Avoid overriding the keyboard's circlepad input + if (abs(stickX) < deadzone && !keyboardAnalogX) { + srv.setCirclepadX(0); + } else { + srv.setCirclepadX(stickX / div); + } + + if (abs(stickY) < deadzone && !keyboardAnalogY) { + srv.setCirclepadY(0); + } else { + srv.setCirclepadY(-(stickY / div)); + } + } + + // Update inputs in the HID module + srv.updateInputs(cpu.getTicks()); + SDL_GL_SwapWindow(window); + } } -void Emulator::runFrame() { - cpu.runFrame(); -} +void Emulator::runFrame() { cpu.runFrame(); } bool Emulator::loadROM(const std::filesystem::path& path) { - // Get path for saving files (AppData on Windows, /home/user/.local/share/ApplcationName on Linux, etc) - // Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in - // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart - char* appData = SDL_GetPrefPath(nullptr, "Alber"); - const std::filesystem::path dataPath = std::filesystem::path(appData) / path.filename().stem(); - IOFile::setAppDataDir(dataPath); - SDL_free(appData); + // Get path for saving files (AppData on Windows, /home/user/.local/share/ApplcationName on Linux, etc) + // Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in + // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart + char* appData = SDL_GetPrefPath(nullptr, "Alber"); + const std::filesystem::path appDataPath = std::filesystem::path(appData); + const std::filesystem::path dataPath = appDataPath / path.filename().stem(); + const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt"; + IOFile::setAppDataDir(dataPath); + SDL_free(appData); - kernel.initializeFS(); - auto extension = path.extension(); - - if (extension == ".elf" || extension == ".axf") - return loadELF(path); - else if (extension == ".3ds") - return loadNCSD(path); - else { - printf("Unknown file type\n"); - return false; - } + // Open the text file containing our AES keys if it exists. We use the std::filesystem::exists overload that takes an error code param to + // avoid the call throwing exceptions + std::error_code ec; + if (std::filesystem::exists(aesKeysPath, ec) && !ec) { + aesEngine.loadKeys(aesKeysPath); + } + + kernel.initializeFS(); + auto extension = path.extension(); + + if (extension == ".elf" || extension == ".axf") + return loadELF(path); + else if (extension == ".3ds") + return loadNCSD(path); + else { + printf("Unknown file type\n"); + return false; + } } bool Emulator::loadNCSD(const std::filesystem::path& path) { - romType = ROMType::NCSD; - std::optional opt = memory.loadNCSD(path); + romType = ROMType::NCSD; + std::optional opt = memory.loadNCSD(aesEngine, path); - if (!opt.has_value()) { - return false; - } + if (!opt.has_value()) { + return false; + } - loadedNCSD = opt.value(); - cpu.setReg(15, loadedNCSD.entrypoint); + loadedNCSD = opt.value(); + cpu.setReg(15, loadedNCSD.entrypoint); - if (loadedNCSD.entrypoint & 1) { - Helpers::panic("Misaligned NCSD entrypoint; should this start the CPU in Thumb mode?"); - } + if (loadedNCSD.entrypoint & 1) { + Helpers::panic("Misaligned NCSD entrypoint; should this start the CPU in Thumb mode?"); + } - return true; + return true; } bool Emulator::loadELF(const std::filesystem::path& path) { - loadedELF.open(path, std::ios_base::binary); // Open ROM in binary mode - romType = ROMType::ELF; + loadedELF.open(path, std::ios_base::binary); // Open ROM in binary mode + romType = ROMType::ELF; - return loadELF(loadedELF); + return loadELF(loadedELF); } bool Emulator::loadELF(std::ifstream& file) { - // Rewind ifstream - loadedELF.clear(); - loadedELF.seekg(0); + // Rewind ifstream + loadedELF.clear(); + loadedELF.seekg(0); - std::optional entrypoint = memory.loadELF(loadedELF); - if (!entrypoint.has_value()) - return false; + std::optional entrypoint = memory.loadELF(loadedELF); + if (!entrypoint.has_value()) { + return false; + } - cpu.setReg(15, entrypoint.value()); // Set initial PC - if (entrypoint.value() & 1) { - Helpers::panic("Misaligned ELF entrypoint. TODO: Check if ELFs can boot in thumb mode"); - } - return true; -} \ No newline at end of file + cpu.setReg(15, entrypoint.value()); // Set initial PC + if (entrypoint.value() & 1) { + Helpers::panic("Misaligned ELF entrypoint. TODO: Check if ELFs can boot in thumb mode"); + } + return true; +} diff --git a/src/main.cpp b/src/main.cpp index 39f6403c..d78dc407 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ int main (int argc, char *argv[]) { emu.initGraphicsContext(); - auto romPath = std::filesystem::current_path() / (argc > 1 ? argv[1] : "Metroid Prime - Federation Force (Europe) (En,Fr,De,Es,It).3ds"); + auto romPath = std::filesystem::current_path() / (argc > 1 ? argv[1] : "OoT Demo Encrypted.3ds"); if (!emu.loadROM(romPath)) { // For some reason just .c_str() doesn't show the proper path Helpers::panic("Failed to load ROM file: %s", romPath.string().c_str());