diff --git a/CMakeLists.txt b/CMakeLists.txt index 74c2d429..4fde60ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ endif() set(SOURCE_FILES src/main.cpp src/emulator.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 @@ -119,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 @@ -130,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}) @@ -138,11 +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) +target_link_libraries(Alber PRIVATE dynarmic SDL2-static glad cryptopp) if(GPU_DEBUG_INFO) target_compile_definitions(Alber PRIVATE GPU_DEBUG_INFO=1) -endif() \ No newline at end of file +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..fe9d0466 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -6,6 +6,7 @@ #include #include "cpu.hpp" +#include "crypto/aes_engine.hpp" #include "io_file.hpp" #include "memory.hpp" #include "opengl.hpp" @@ -20,6 +21,7 @@ class Emulator { GPU gpu; Memory memory; Kernel kernel; + Crypto::AESEngine aesEngine; SDL_Window* window; SDL_GLContext glContext; 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/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..d41c6928 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -135,9 +135,18 @@ bool Emulator::loadROM(const std::filesystem::path& path) { // 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(); + 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); + SDL_free(appData); + + // 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(); @@ -154,7 +163,7 @@ bool Emulator::loadROM(const std::filesystem::path& path) { bool Emulator::loadNCSD(const std::filesystem::path& path) { romType = ROMType::NCSD; - std::optional opt = memory.loadNCSD(path); + std::optional opt = memory.loadNCSD(aesEngine, path); if (!opt.has_value()) { return false; 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());