diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index bb82d78..d13e79f 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(common STATIC + alignment.h assert.h bit_field.h common_funcs.h diff --git a/src/common/alignment.h b/src/common/alignment.h new file mode 100644 index 0000000..225770f --- /dev/null +++ b/src/common/alignment.h @@ -0,0 +1,22 @@ +// This file is under the public domain. + +#pragma once + +#include +#include + +namespace Common { + +template +constexpr T AlignUp(T value, std::size_t size) { + static_assert(std::is_unsigned_v, "T must be an unsigned value."); + return static_cast(value + (size - value % size) % size); +} + +template +constexpr T AlignDown(T value, std::size_t size) { + static_assert(std::is_unsigned_v, "T must be an unsigned value."); + return static_cast(value - value % size); +} + +} // namespace Common diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 52bca03..cb26f82 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -870,6 +870,7 @@ u64 IOFile::Tell() const { if (IsOpen()) return ftello(m_file); + LOG_ERROR(Core, "File is not open"); return -1; } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8620eb..a44e9e5 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -11,8 +11,15 @@ add_library(core STATIC key/arithmetic128.h key/key.cpp key/key.h + ncch/ncch_container.cpp + ncch/ncch_container.h + ncch/smdh.cpp + ncch/smdh.h + ncch/title_metadata.cpp + ncch/title_metadata.h quick_decryptor.cpp quick_decryptor.h + result_status.h ) target_link_libraries(core PRIVATE common cryptopp) diff --git a/src/core/decryptor.cpp b/src/core/decryptor.cpp index 6d38838..6bdcbcf 100644 --- a/src/core/decryptor.cpp +++ b/src/core/decryptor.cpp @@ -83,4 +83,66 @@ std::vector SDMCDecryptor::DecryptFile(const std::string& source) const { return data; } +SDMCFile::SDMCFile() {} + +SDMCFile::SDMCFile(std::string root_folder, const std::string& filename, const char openmode[], + int flags) { + if (root_folder.back() == '/' || root_folder.back() == '\\') { + // Remove '/' or '\' character at the end as we will add them back when combining path + root_folder.erase(root_folder.size() - 1); + } + + original_ctr = GetFileCTR(filename); + key = Key::GetNormalKey(Key::SDKey); + // aes.SetKeyWithIV(key.data(), key.size(), original_ctr.data()); + + Open(root_folder + filename, openmode, flags); +} + +SDMCFile::~SDMCFile() { + Close(); +} + +SDMCFile::SDMCFile(SDMCFile&& other) { + Swap(other); +} + +SDMCFile& SDMCFile::operator=(SDMCFile&& other) { + Swap(other); + return *this; +} + +void SDMCFile::Swap(SDMCFile& other) { + file.Swap(other.file); + std::swap(original_ctr, other.original_ctr); + std::swap(key, other.key); +} + +bool SDMCFile::Open(const std::string& filename, const char openmode[], int flags) { + return file.Open(filename, openmode, flags); +} + +bool SDMCFile::Close() { + return file.Close(); +} + +u64 SDMCFile::GetSize() const { + return file.GetSize(); +} + +bool SDMCFile::Seek(s64 off, int origin) { + return file.Seek(off, origin); +} + +u64 SDMCFile::Tell() const { + return file.Tell(); +} + +void SDMCFile::DecryptData(u8* data, std::size_t size) { + CryptoPP::CTR_Mode::Decryption aes; + aes.SetKeyWithIV(key.data(), key.size(), original_ctr.data()); + aes.Seek(Tell() - size); + aes.ProcessData(data, data, size); +} + } // namespace Core diff --git a/src/core/decryptor.h b/src/core/decryptor.h index 788639d..8423ac7 100644 --- a/src/core/decryptor.h +++ b/src/core/decryptor.h @@ -46,4 +46,67 @@ private: QuickDecryptor quick_decryptor; }; +/// Interface for reading an SDMC file like a normal IOFile. This is read-only. +class SDMCFile : public NonCopyable { +public: + SDMCFile(); + + SDMCFile(std::string root_folder, const std::string& filename, const char openmode[], + int flags = 0); + + ~SDMCFile(); + + SDMCFile(SDMCFile&& other); + SDMCFile& operator=(SDMCFile&& other); + + void Swap(SDMCFile& other); + + bool Open(const std::string& filename, const char openmode[], int flags = 0); + bool Close(); + + template + std::size_t ReadArray(T* data, std::size_t length) { + std::size_t items_read = file.ReadArray(data, length); + + if (IsGood()) { + LOG_CRITICAL(Core, "Decrypting data..."); + DecryptData(reinterpret_cast(data), sizeof(T) * length); + } + + return items_read; + } + + template + std::size_t ReadBytes(T* data, std::size_t length) { + static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); + return ReadArray(reinterpret_cast(data), length); + } + + bool IsOpen() const { + return file.IsOpen(); + } + + // m_good is set to false when a read, write or other function fails + bool IsGood() const { + return file.IsGood(); + } + explicit operator bool() const { + return IsGood(); + } + + bool Seek(s64 off, int origin); + u64 Tell() const; + u64 GetSize() const; + + void Clear(); + +private: + void DecryptData(u8* data, std::size_t size); + + FileUtil::IOFile file; + // CryptoPP::CTR_Mode::Decryption aes; + std::array original_ctr; + std::array key; +}; + } // namespace Core diff --git a/src/core/importer.cpp b/src/core/importer.cpp index ebaf66f..f63aca1 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -6,11 +6,15 @@ #include "common/assert.h" #include "common/common_paths.h" #include "common/file_util.h" +#include "common/string_util.h" #include "core/data_container.h" #include "core/decryptor.h" #include "core/importer.h" #include "core/inner_fat.h" #include "core/key/key.h" +#include "core/ncch/ncch_container.h" +#include "core/ncch/smdh.h" +#include "core/ncch/title_metadata.h" namespace Core { @@ -210,14 +214,73 @@ std::vector SDMCImporter::ListContent() const { // Regex for half Title IDs static const std::regex title_regex{"[0-9a-f]{8}"}; +std::string SDMCImporter::LoadTitleName(const std::string& path) const { + // Remove trailing '/' + const auto sdmc_path = config.sdmc_path.substr(0, config.sdmc_path.size() - 1); + + std::string title_metadata; + const bool ret = FileUtil::ForeachDirectoryEntry( + nullptr, sdmc_path + path, + [&title_metadata](u64* /*num_entries_out*/, const std::string& directory, + const std::string& virtual_name) { + if (FileUtil::IsDirectory(directory + virtual_name)) { + return true; + } + + if (virtual_name.substr(virtual_name.size() - 3) == "tmd" && + std::regex_match(virtual_name.substr(0, 8), title_regex)) { + + title_metadata = virtual_name; + return false; + } + + return true; + }); + + if (ret) { // TMD not found + return {}; + } + + if (!FileUtil::Exists(sdmc_path + path + title_metadata)) { + // Probably TMD is not directly inside, aborting. + return {}; + } + + TitleMetadata tmd; + tmd.Load(decryptor->DecryptFile(path + title_metadata)); + + const auto boot_content_path = fmt::format("{}{:08x}.app", path, tmd.GetBootContentID()); + + NCCHContainer ncch(config.sdmc_path, boot_content_path); + auto ret2 = ncch.Load(); + if (ret2 != ResultStatus::Success) { + LOG_CRITICAL(Core, "failed to load ncch: {}", ret2); + return {}; + } + + std::vector smdh_buffer; + if (ncch.LoadSectionExeFS("icon", smdh_buffer) != ResultStatus::Success) { + LOG_WARNING(Core, "Failed to load icon in ExeFS"); + return {}; + } + + if (smdh_buffer.size() != sizeof(SMDH)) { + LOG_ERROR(Core, "ExeFS icon section size is not correct"); + return {}; + } + + SMDH smdh; + std::memcpy(&smdh, smdh_buffer.data(), smdh_buffer.size()); + return Common::UTF16BufferToUTF8(smdh.GetShortTitle(SMDH::TitleLanguage::English)); +} + void SDMCImporter::ListTitle(std::vector& out) const { - const auto ProcessDirectory = [& decryptor = this->decryptor, &out, - &sdmc_path = config.sdmc_path](ContentType type, u64 high_id) { + const auto ProcessDirectory = [this, &out, &sdmc_path = config.sdmc_path](ContentType type, + u64 high_id) { FileUtil::ForeachDirectoryEntry( nullptr, fmt::format("{}title/{:08x}/", sdmc_path, high_id), - [&decryptor, type, high_id, &out](u64* /*num_entries_out*/, - const std::string& directory, - const std::string& virtual_name) { + [this, type, high_id, &out](u64* /*num_entries_out*/, const std::string& directory, + const std::string& virtual_name) { if (!FileUtil::IsDirectory(directory + virtual_name + "/")) { return true; } @@ -232,10 +295,14 @@ void SDMCImporter::ListTitle(std::vector& out) const { "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/" "{:08x}/{}/", FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), high_id, virtual_name); + if (FileUtil::Exists(directory + virtual_name + "/content/")) { + const auto content_path = + fmt::format("/title/{:08x}/{}/content/", high_id, virtual_name); out.push_back( {type, id, FileUtil::Exists(citra_path + "content/"), - FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/")}); + FileUtil::GetDirectoryTreeSize(directory + virtual_name + "/content/"), + LoadTitleName(content_path)}); } if (type != ContentType::Application) { diff --git a/src/core/importer.h b/src/core/importer.h index d83ea92..3c0c690 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -103,18 +103,28 @@ public: private: bool Init(); + bool ImportTitle(u64 id, const ProgressCallback& callback); bool ImportSavegame(u64 id, const ProgressCallback& callback); bool ImportExtdata(u64 id, const ProgressCallback& callback); bool ImportSysdata(u64 id, const ProgressCallback& callback); + void ListTitle(std::vector& out) const; void ListExtdata(std::vector& out) const; void ListSysdata(std::vector& out) const; + void DeleteTitle(u64 id) const; void DeleteSavegame(u64 id) const; void DeleteExtdata(u64 id) const; void DeleteSysdata(u64 id) const; + /** + * Loads the English short title name of a title. + * @param path Path of the 'content' folder relative to the SDMC root folder. + * Required to end with '/'. + */ + std::string LoadTitleName(const std::string& path) const; + bool is_good{}; Config config; std::unique_ptr decryptor; diff --git a/src/core/ncch/ncch_container.cpp b/src/core/ncch/ncch_container.cpp new file mode 100644 index 0000000..d00e679 --- /dev/null +++ b/src/core/ncch/ncch_container.cpp @@ -0,0 +1,307 @@ +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "common/common_types.h" +#include "common/logging/log.h" +#include "core/key/key.h" +#include "core/ncch/ncch_container.h" + +namespace Core { + +constexpr u32 MakeMagic(char a, char b, char c, char d) { + return a | b << 8 | c << 16 | d << 24; +} + +static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs +static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes) + +NCCHContainer::NCCHContainer(const std::string& root_folder, const std::string& filepath) + : root_folder(root_folder), filepath(filepath) { + file = SDMCFile(root_folder, filepath, "rb"); +} + +ResultStatus NCCHContainer::OpenFile(const std::string& root_folder, const std::string& filepath) { + this->root_folder = root_folder; + this->filepath = filepath; + file = SDMCFile(root_folder, filepath, "rb"); + + if (!file.IsOpen()) { + LOG_WARNING(Service_FS, "Failed to open {}", filepath); + return ResultStatus::Error; + } + + LOG_DEBUG(Service_FS, "Opened {}", filepath); + return ResultStatus::Success; +} + +ResultStatus NCCHContainer::Load() { + LOG_INFO(Service_FS, "Loading NCCH from file {}", filepath); + if (is_loaded) + return ResultStatus::Success; + + if (file.IsOpen()) { + // Reset read pointer in case this file has been read before. + file.Seek(0, SEEK_SET); + + if (file.ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) + return ResultStatus::Error; + + // Verify we are loading the correct file type... + if (MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) + return ResultStatus::ErrorInvalidFormat; + + has_header = true; + bool failed_to_decrypt = false; + if (!ncch_header.no_crypto) { + is_encrypted = true; + + // Find primary key + if (ncch_header.fixed_key) { + LOG_DEBUG(Service_FS, "Fixed-key crypto"); + primary_key.fill(0); + } else { + std::array key_y_primary; + + std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(), + key_y_primary.begin()); + + Key::SetKeyY(Key::KeySlotID::NCCHSecure1, key_y_primary); + if (!Key::IsNormalKeyAvailable(Key::KeySlotID::NCCHSecure1)) { + LOG_ERROR(Service_FS, "Secure1 KeyX missing"); + failed_to_decrypt = true; + } + primary_key = Key::GetNormalKey(Key::KeySlotID::NCCHSecure1); + } + + // Find CTR for each section + // Written with reference to + // https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52 + if (ncch_header.version == 0 || ncch_header.version == 2) { + LOG_DEBUG(Loader, "NCCH version 0/2"); + // In this version, CTR for each section is a magic number prefixed by partition ID + // (reverse order) + std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8, + exheader_ctr.begin()); + exefs_ctr = exheader_ctr; + exheader_ctr[8] = 1; + exefs_ctr[8] = 2; + } else if (ncch_header.version == 1) { + LOG_DEBUG(Loader, "NCCH version 1"); + // In this version, CTR for each section is the section offset prefixed by partition + // ID, as if the entire NCCH image is encrypted using a single CTR stream. + std::copy(ncch_header.partition_id, ncch_header.partition_id + 8, + exheader_ctr.begin()); + exefs_ctr = exheader_ctr; + auto u32ToBEArray = [](u32 value) -> std::array { + return std::array{ + static_cast(value >> 24), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF), + }; + }; + auto offset_exheader = u32ToBEArray(0x200); // exheader offset + auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize); + std::copy(offset_exheader.begin(), offset_exheader.end(), + exheader_ctr.begin() + 12); + std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12); + } else { + LOG_ERROR(Service_FS, "Unknown NCCH version {}", ncch_header.version); + failed_to_decrypt = true; + } + } else { + LOG_DEBUG(Service_FS, "No crypto"); + is_encrypted = false; + } + + // System archives and DLC don't have an extended header but have RomFS + if (ncch_header.extended_header_size) { + auto read_exheader = [this](SDMCFile& file) { + const std::size_t size = sizeof(exheader_header); + return file && file.ReadBytes(&exheader_header, size) == size; + }; + + if (!read_exheader(file)) { + return ResultStatus::Error; + } + + if (is_encrypted) { + // This ID check is masked to low 32-bit as a toleration to ill-formed ROM created + // by merging games and its updates. + if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) == + (ncch_header.program_id & 0xFFFFFFFF)) { + LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted " + "exheader. Force no crypto scheme."); + is_encrypted = false; + } else { + if (failed_to_decrypt) { + LOG_ERROR(Service_FS, "Failed to decrypt"); + return ResultStatus::ErrorEncrypted; + } + CryptoPP::byte* data = reinterpret_cast(&exheader_header); + CryptoPP::CTR_Mode::Decryption( + primary_key.data(), primary_key.size(), exheader_ctr.data()) + .ProcessData(data, data, sizeof(exheader_header)); + } + } + + u32 entry_point = exheader_header.codeset_info.text.address; + u32 code_size = exheader_header.codeset_info.text.code_size; + u32 stack_size = exheader_header.codeset_info.stack_size; + u32 bss_size = exheader_header.codeset_info.bss_size; + u32 core_version = exheader_header.arm11_system_local_caps.core_version; + u8 priority = exheader_header.arm11_system_local_caps.priority; + u8 resource_limit_category = + exheader_header.arm11_system_local_caps.resource_limit_category; + + LOG_DEBUG(Service_FS, "Name: {}", + exheader_header.codeset_info.name); + LOG_DEBUG(Service_FS, "Program ID: {:016X}", ncch_header.program_id); + LOG_DEBUG(Service_FS, "Entry point: 0x{:08X}", entry_point); + LOG_DEBUG(Service_FS, "Code size: 0x{:08X}", code_size); + LOG_DEBUG(Service_FS, "Stack size: 0x{:08X}", stack_size); + LOG_DEBUG(Service_FS, "Bss size: 0x{:08X}", bss_size); + LOG_DEBUG(Service_FS, "Core version: {}", core_version); + LOG_DEBUG(Service_FS, "Thread priority: 0x{:X}", priority); + LOG_DEBUG(Service_FS, "Resource limit category: {}", resource_limit_category); + LOG_DEBUG(Service_FS, "System Mode: {}", + static_cast(exheader_header.arm11_system_local_caps.system_mode)); + + has_exheader = true; + } + + // DLC can have an ExeFS and a RomFS but no extended header + if (ncch_header.exefs_size) { + exefs_offset = ncch_header.exefs_offset * kBlockSize; + u32 exefs_size = ncch_header.exefs_size * kBlockSize; + + LOG_DEBUG(Service_FS, "ExeFS offset: 0x{:08X}", exefs_offset); + LOG_DEBUG(Service_FS, "ExeFS size: 0x{:08X}", exefs_size); + file.Seek(exefs_offset, SEEK_SET); + if (file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) + return ResultStatus::Error; + + if (is_encrypted) { + CryptoPP::byte* data = reinterpret_cast(&exefs_header); + CryptoPP::CTR_Mode::Decryption(primary_key.data(), + primary_key.size(), exefs_ctr.data()) + .ProcessData(data, data, sizeof(exefs_header)); + } + + exefs_file = SDMCFile(root_folder, filepath, "rb"); + has_exefs = true; + } + } + + is_loaded = true; + return ResultStatus::Success; +} + +ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector& buffer) { + ResultStatus result = Load(); + if (result != ResultStatus::Success) + return result; + + if (!exefs_file.IsOpen()) + return ResultStatus::Error; + + LOG_DEBUG(Service_FS, "{} sections:", kMaxSections); + // Iterate through the ExeFs archive until we find a section with the specified name... + for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { + const auto& section = exefs_header.section[section_number]; + + if (strcmp(section.name, name) == 0) { + LOG_DEBUG(Service_FS, "{} - offset: 0x{:08X}, size: 0x{:08X}, name: {}", section_number, + section.offset, section.size, section.name); + + s64 section_offset = (section.offset + exefs_offset + sizeof(ExeFs_Header)); + exefs_file.Seek(section_offset, SEEK_SET); + + CryptoPP::CTR_Mode::Decryption dec(primary_key.data(), + primary_key.size(), exefs_ctr.data()); + dec.Seek(section.offset + sizeof(ExeFs_Header)); + + buffer.resize(section.size); + if (exefs_file.ReadBytes(&buffer[0], section.size) != section.size) + return ResultStatus::Error; + if (is_encrypted) { + dec.ProcessData(&buffer[0], &buffer[0], section.size); + } + + return ResultStatus::Success; + } + } + return ResultStatus::ErrorNotUsed; +} + +ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { + ResultStatus result = Load(); + if (result != ResultStatus::Success) + return result; + + if (!has_header) + return ResultStatus::ErrorNotUsed; + + program_id = ncch_header.program_id; + return ResultStatus::Success; +} + +ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) { + ResultStatus result = Load(); + if (result != ResultStatus::Success) + return result; + + if (!has_exheader) + return ResultStatus::ErrorNotUsed; + + if (exheader_header.arm11_system_local_caps.storage_info.other_attributes >> 1) { + // Using extended save data access + // There would be multiple possible extdata IDs in this case. The best we can do for now is + // guessing that the first one would be the main save. + const std::array extdata_ids{{ + exheader_header.arm11_system_local_caps.storage_info.extdata_id0.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id1.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id2.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id3.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id4.Value(), + exheader_header.arm11_system_local_caps.storage_info.extdata_id5.Value(), + }}; + for (u64 id : extdata_ids) { + if (id) { + // Found a non-zero ID, use it + extdata_id = id; + return ResultStatus::Success; + } + } + + return ResultStatus::ErrorNotUsed; + } + + extdata_id = exheader_header.arm11_system_local_caps.storage_info.ext_save_data_id; + return ResultStatus::Success; +} + +bool NCCHContainer::HasExeFS() { + ResultStatus result = Load(); + if (result != ResultStatus::Success) + return false; + + return has_exefs; +} + +bool NCCHContainer::HasExHeader() { + ResultStatus result = Load(); + if (result != ResultStatus::Success) + return false; + + return has_exheader; +} + +} // namespace Core diff --git a/src/core/ncch/ncch_container.h b/src/core/ncch/ncch_container.h new file mode 100644 index 0000000..41d70fb --- /dev/null +++ b/src/core/ncch/ncch_container.h @@ -0,0 +1,280 @@ +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/bit_field.h" +#include "common/common_types.h" +#include "common/file_util.h" +#include "common/swap.h" +#include "core/decryptor.h" +#include "core/result_status.h" + +namespace Core { + +//////////////////////////////////////////////////////////////////////////////////////////////////// +/// NCCH (Nintendo Content Container Header) header + +struct NCCH_Header { + u8 signature[0x100]; + u32_le magic; + u32_le content_size; + u8 partition_id[8]; + u16_le maker_code; + u16_le version; + u8 reserved_0[4]; + u64_le program_id; + u8 reserved_1[0x10]; + u8 logo_region_hash[0x20]; + u8 product_code[0x10]; + u8 extended_header_hash[0x20]; + u32_le extended_header_size; + u8 reserved_2[4]; + u8 reserved_flag[3]; + u8 secondary_key_slot; + u8 platform; + enum class ContentType : u8 { + Application = 0, + SystemUpdate = 1, + Manual = 2, + Child = 3, + Trial = 4, + }; + union { + BitField<0, 1, u8> is_data; + BitField<1, 1, u8> is_executable; + BitField<2, 3, ContentType> content_type; + }; + u8 content_unit_size; + union { + BitField<0, 1, u8> fixed_key; + BitField<1, 1, u8> no_romfs; + BitField<2, 1, u8> no_crypto; + BitField<5, 1, u8> seed_crypto; + }; + u32_le plain_region_offset; + u32_le plain_region_size; + u32_le logo_region_offset; + u32_le logo_region_size; + u32_le exefs_offset; + u32_le exefs_size; + u32_le exefs_hash_region_size; + u8 reserved_3[4]; + u32_le romfs_offset; + u32_le romfs_size; + u32_le romfs_hash_region_size; + u8 reserved_4[4]; + u8 exefs_super_block_hash[0x20]; + u8 romfs_super_block_hash[0x20]; +}; + +static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong"); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// ExeFS (executable file system) headers + +struct ExeFs_SectionHeader { + char name[8]; + u32 offset; + u32 size; +}; + +struct ExeFs_Header { + ExeFs_SectionHeader section[8]; + u8 reserved[0x80]; + u8 hashes[8][0x20]; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// ExHeader (executable file system header) headers + +struct ExHeader_SystemInfoFlags { + u8 reserved[5]; + u8 flag; + u8 remaster_version[2]; +}; + +struct ExHeader_CodeSegmentInfo { + u32 address; + u32 num_max_pages; + u32 code_size; +}; + +struct ExHeader_CodeSetInfo { + u8 name[8]; + ExHeader_SystemInfoFlags flags; + ExHeader_CodeSegmentInfo text; + u32 stack_size; + ExHeader_CodeSegmentInfo ro; + u8 reserved[4]; + ExHeader_CodeSegmentInfo data; + u32 bss_size; +}; + +struct ExHeader_DependencyList { + u8 program_id[0x30][8]; +}; + +struct ExHeader_SystemInfo { + u64 save_data_size; + u64_le jump_id; + u8 reserved_2[0x30]; +}; + +struct ExHeader_StorageInfo { + union { + u64_le ext_save_data_id; + // When using extended savedata access + // Prefer the ID specified in the most significant bits + BitField<40, 20, u64> extdata_id3; + BitField<20, 20, u64> extdata_id4; + BitField<0, 20, u64> extdata_id5; + }; + u8 system_save_data_id[8]; + union { + u64_le storage_accessible_unique_ids; + // When using extended savedata access + // Prefer the ID specified in the most significant bits + BitField<40, 20, u64> extdata_id0; + BitField<20, 20, u64> extdata_id1; + BitField<0, 20, u64> extdata_id2; + }; + u8 access_info[7]; + u8 other_attributes; +}; + +struct ExHeader_ARM11_SystemLocalCaps { + u64_le program_id; + u32_le core_version; + u8 reserved_flags[2]; + union { + u8 flags0; + BitField<0, 2, u8> ideal_processor; + BitField<2, 2, u8> affinity_mask; + BitField<4, 4, u8> system_mode; + }; + u8 priority; + u8 resource_limit_descriptor[0x10][2]; + ExHeader_StorageInfo storage_info; + u8 service_access_control[0x20][8]; + u8 ex_service_access_control[0x2][8]; + u8 reserved[0xf]; + u8 resource_limit_category; +}; + +struct ExHeader_ARM11_KernelCaps { + u32_le descriptors[28]; + u8 reserved[0x10]; +}; + +struct ExHeader_ARM9_AccessControl { + u8 descriptors[15]; + u8 descversion; +}; + +struct ExHeader_Header { + ExHeader_CodeSetInfo codeset_info; + ExHeader_DependencyList dependency_list; + ExHeader_SystemInfo system_info; + ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; + ExHeader_ARM11_KernelCaps arm11_kernel_caps; + ExHeader_ARM9_AccessControl arm9_access_control; + struct { + u8 signature[0x100]; + u8 ncch_public_key_modulus[0x100]; + ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; + ExHeader_ARM11_KernelCaps arm11_kernel_caps; + ExHeader_ARM9_AccessControl arm9_access_control; + } access_desc; +}; + +static_assert(sizeof(ExHeader_Header) == 0x800, "ExHeader structure size is wrong"); + +/** + * Helper which implements an interface to deal with NCCH containers which can + * contain ExeFS archives or RomFS archives for games or other applications. + * + * Note that this is heavily stripped down and can only read (primary-key + * encrypted non-code sections of) ExeFS and ExHeader by design. + */ +class NCCHContainer { +public: + /** + * Constructs the container. + * @param root_folder Path to SDMC folder + * @param filepath Path relative to SDMC folder, starting with / + */ + NCCHContainer(const std::string& root_folder, const std::string& filepath); + NCCHContainer() {} + + ResultStatus OpenFile(const std::string& root_folder, const std::string& filepath); + + /** + * Ensure ExeFS and exheader is loaded and ready for reading sections + * @return ResultStatus result of function + */ + ResultStatus Load(); + + /** + * Reads an application ExeFS section of an NCCH file (non-compressed, primary key only) + * @param name Name of section to read out of NCCH file + * @param buffer Vector to read data into + * @return ResultStatus result of function + */ + ResultStatus LoadSectionExeFS(const char* name, std::vector& buffer); + + /** + * Get the Program ID of the NCCH container + * @return ResultStatus result of function + */ + ResultStatus ReadProgramId(u64_le& program_id); + + /** + * Get the Extdata ID of the NCCH container + * @return ResultStatus result of function + */ + ResultStatus ReadExtdataId(u64& extdata_id); + + /** + * Checks whether the NCCH container contains an ExeFS + * @return bool check result + */ + bool HasExeFS(); + + /** + * Checks whether the NCCH container contains an ExHeader + * @return bool check result + */ + bool HasExHeader(); + + NCCH_Header ncch_header; + ExHeader_Header exheader_header; + ExeFs_Header exefs_header; + +private: + bool has_header = false; + bool has_exheader = false; + bool has_exefs = false; + + bool is_loaded = false; + + bool is_encrypted = false; + // for decrypting exheader, exefs header and icon/banner section + std::array primary_key{}; + std::array exheader_ctr{}; + std::array exefs_ctr{}; + + u32 exefs_offset = 0; + + std::string root_folder; + std::string filepath; + SDMCFile file; + SDMCFile exefs_file; +}; + +} // namespace Core diff --git a/src/core/ncch/smdh.cpp b/src/core/ncch/smdh.cpp new file mode 100644 index 0000000..54b4e7c --- /dev/null +++ b/src/core/ncch/smdh.cpp @@ -0,0 +1,110 @@ +// Copyright 2016 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/common_types.h" +#include "core/ncch/smdh.h" + +namespace Core { + +constexpr u32 MakeMagic(char a, char b, char c, char d) { + return a | b << 8 | c << 16 | d << 24; +} + +// 8x8 Z-Order coordinate from 2D coordinates +static constexpr u32 MortonInterleave(u32 x, u32 y) { + constexpr u32 xlut[] = {0x00, 0x01, 0x04, 0x05, 0x10, 0x11, 0x14, 0x15}; + constexpr u32 ylut[] = {0x00, 0x02, 0x08, 0x0a, 0x20, 0x22, 0x28, 0x2a}; + return xlut[x % 8] + ylut[y % 8]; +} + +/** + * Calculates the offset of the position of the pixel in Morton order + */ +static inline u32 GetMortonOffset(u32 x, u32 y, u32 bytes_per_pixel) { + // Images are split into 8x8 tiles. Each tile is composed of four 4x4 subtiles each + // of which is composed of four 2x2 subtiles each of which is composed of four texels. + // Each structure is embedded into the next-bigger one in a diagonal pattern, e.g. + // texels are laid out in a 2x2 subtile like this: + // 2 3 + // 0 1 + // + // The full 8x8 tile has the texels arranged like this: + // + // 42 43 46 47 58 59 62 63 + // 40 41 44 45 56 57 60 61 + // 34 35 38 39 50 51 54 55 + // 32 33 36 37 48 49 52 53 + // 10 11 14 15 26 27 30 31 + // 08 09 12 13 24 25 28 29 + // 02 03 06 07 18 19 22 23 + // 00 01 04 05 16 17 20 21 + // + // This pattern is what's called Z-order curve, or Morton order. + + const unsigned int block_height = 8; + const unsigned int coarse_x = x & ~7; + + u32 i = MortonInterleave(x, y); + + const unsigned int offset = coarse_x * block_height; + + return (i + offset) * bytes_per_pixel; +} + +bool IsValidSMDH(const std::vector& smdh_data) { + if (smdh_data.size() < sizeof(Core::SMDH)) + return false; + + u32 magic; + memcpy(&magic, smdh_data.data(), sizeof(u32)); + + return MakeMagic('S', 'M', 'D', 'H') == magic; +} + +std::vector SMDH::GetIcon(bool large) const { + u32 size; + const u8* icon_data; + + if (large) { + size = 48; + icon_data = large_icon.data(); + } else { + size = 24; + icon_data = small_icon.data(); + } + + std::vector icon(size * size); + for (u32 x = 0; x < size; ++x) { + for (u32 y = 0; y < size; ++y) { + u32 coarse_y = y & ~7; + const u8* pixel = icon_data + GetMortonOffset(x, y, 2) + coarse_y * size * 2; + icon[x + size * y] = (pixel[1] << 8) + pixel[0]; + } + } + return icon; +} + +std::array SMDH::GetShortTitle(Core::SMDH::TitleLanguage language) const { + return titles[static_cast(language)].short_title; +} + +std::vector SMDH::GetRegions() const { + if (region_lockout == 0x7fffffff) { + return std::vector{GameRegion::RegionFree}; + } + + constexpr u32 REGION_COUNT = 7; + std::vector result; + for (u32 region = 0; region < REGION_COUNT; ++region) { + if (region_lockout & (1 << region)) { + result.push_back(static_cast(region)); + } + } + + return result; +} + +} // namespace Core diff --git a/src/core/ncch/smdh.h b/src/core/ncch/smdh.h new file mode 100644 index 0000000..606428a --- /dev/null +++ b/src/core/ncch/smdh.h @@ -0,0 +1,94 @@ +// Copyright 2016 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/swap.h" + +namespace Core { + +/** + * Tests if data is a valid SMDH by its length and magic number. + * @param smdh_data data buffer to test + * @return bool test result + */ +bool IsValidSMDH(const std::vector& smdh_data); + +/// SMDH data structure that contains titles, icons etc. See https://www.3dbrew.org/wiki/SMDH +struct SMDH { + u32_le magic; + u16_le version; + INSERT_PADDING_BYTES(2); + + struct Title { + std::array short_title; + std::array long_title; + std::array publisher; + }; + std::array titles; + + std::array ratings; + u32_le region_lockout; + u32_le match_maker_id; + u64_le match_maker_bit_id; + u32_le flags; + u16_le eula_version; + INSERT_PADDING_BYTES(2); + float_le banner_animation_frame; + u32_le cec_id; + INSERT_PADDING_BYTES(8); + + std::array small_icon; + std::array large_icon; + + /// indicates the language used for each title entry + enum class TitleLanguage { + Japanese = 0, + English = 1, + French = 2, + German = 3, + Italian = 4, + Spanish = 5, + SimplifiedChinese = 6, + Korean = 7, + Dutch = 8, + Portuguese = 9, + Russian = 10, + TraditionalChinese = 11 + }; + + enum class GameRegion { + Japan = 0, + NorthAmerica = 1, + Europe = 2, + Australia = 3, + China = 4, + Korea = 5, + Taiwan = 6, + RegionFree = 7, + }; + + /** + * Gets game icon from SMDH + * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) + * @return vector of RGB565 data + */ + std::vector GetIcon(bool large) const; + + /** + * Gets the short game title from SMDH + * @param language title language + * @return UTF-16 array of the short title + */ + std::array GetShortTitle(Core::SMDH::TitleLanguage language) const; + + std::vector GetRegions() const; +}; +static_assert(sizeof(SMDH) == 0x36C0, "SMDH structure size is wrong"); + +} // namespace Core diff --git a/src/core/ncch/title_metadata.cpp b/src/core/ncch/title_metadata.cpp new file mode 100644 index 0000000..d0b4a14 --- /dev/null +++ b/src/core/ncch/title_metadata.cpp @@ -0,0 +1,143 @@ +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/alignment.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "core/ncch/title_metadata.h" + +namespace Core { + +ResultStatus TitleMetadata::Load(const std::vector file_data, std::size_t offset) { + std::size_t total_size = static_cast(file_data.size() - offset); + if (total_size < sizeof(u32_be)) + return ResultStatus::Error; + + memcpy(&signature_type, &file_data[offset], sizeof(u32_be)); + + // Signature lengths are variable, and the body follows the signature + u32 signature_size = GetSignatureSize(signature_type); + if (signature_size == 0) { + return ResultStatus::Error; + } + + // The TMD body start position is rounded to the nearest 0x40 after the signature + std::size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); + std::size_t body_end = body_start + sizeof(Body); + + if (total_size < body_end) + return ResultStatus::Error; + + // Read signature + TMD body, then load the amount of ContentChunks specified + tmd_signature.resize(signature_size); + memcpy(tmd_signature.data(), &file_data[offset + sizeof(u32_be)], signature_size); + memcpy(&tmd_body, &file_data[offset + body_start], sizeof(TitleMetadata::Body)); + + std::size_t expected_size = + body_start + sizeof(Body) + static_cast(tmd_body.content_count) * sizeof(ContentChunk); + if (total_size < expected_size) { + LOG_ERROR(Service_FS, "Malformed TMD, expected size 0x{:x}, got 0x{:x}!", expected_size, + total_size); + return ResultStatus::ErrorInvalidFormat; + } + + for (u16 i = 0; i < tmd_body.content_count; i++) { + ContentChunk chunk; + + memcpy(&chunk, &file_data[offset + body_end + (i * sizeof(ContentChunk))], + sizeof(ContentChunk)); + tmd_chunks.push_back(chunk); + } + + return ResultStatus::Success; +} + +u64 TitleMetadata::GetTitleID() const { + return tmd_body.title_id; +} + +u32 TitleMetadata::GetTitleType() const { + return tmd_body.title_type; +} + +u16 TitleMetadata::GetTitleVersion() const { + return tmd_body.title_version; +} + +u64 TitleMetadata::GetSystemVersion() const { + return tmd_body.system_version; +} + +size_t TitleMetadata::GetContentCount() const { + return tmd_chunks.size(); +} + +u32 TitleMetadata::GetBootContentID() const { + return tmd_chunks[TMDContentIndex::Main].id; +} + +u32 TitleMetadata::GetManualContentID() const { + return tmd_chunks[TMDContentIndex::Manual].id; +} + +u32 TitleMetadata::GetDLPContentID() const { + return tmd_chunks[TMDContentIndex::DLP].id; +} + +u32 TitleMetadata::GetContentIDByIndex(u16 index) const { + return tmd_chunks[index].id; +} + +u16 TitleMetadata::GetContentTypeByIndex(u16 index) const { + return tmd_chunks[index].type; +} + +u64 TitleMetadata::GetContentSizeByIndex(u16 index) const { + return tmd_chunks[index].size; +} + +std::array TitleMetadata::GetContentCTRByIndex(u16 index) const { + std::array ctr{}; + std::memcpy(ctr.data(), &tmd_chunks[index].index, sizeof(u16)); + return ctr; +} + +void TitleMetadata::Print() const { + LOG_DEBUG(Service_FS, "{} chunks", static_cast(tmd_body.content_count)); + + // Content info describes ranges of content chunks + LOG_DEBUG(Service_FS, "Content info:"); + for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) { + if (tmd_body.contentinfo[i].command_count == 0) + break; + + LOG_DEBUG(Service_FS, " Index {:04X}, Command Count {:04X}", + static_cast(tmd_body.contentinfo[i].index), + static_cast(tmd_body.contentinfo[i].command_count)); + } + + // For each content info, print their content chunk range + for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) { + u16 index = static_cast(tmd_body.contentinfo[i].index); + u16 count = static_cast(tmd_body.contentinfo[i].command_count); + + if (count == 0) + continue; + + LOG_DEBUG(Service_FS, "Content chunks for content info index {}:", i); + for (u16 j = index; j < index + count; j++) { + // Don't attempt to print content we don't have + if (j > tmd_body.content_count) + break; + + const ContentChunk& chunk = tmd_chunks[j]; + LOG_DEBUG(Service_FS, " ID {:08X}, Index {:04X}, Type {:04x}, Size {:016X}", + static_cast(chunk.id), static_cast(chunk.index), + static_cast(chunk.type), static_cast(chunk.size)); + } + } +} +} // namespace Core diff --git a/src/core/ncch/title_metadata.h b/src/core/ncch/title_metadata.h new file mode 100644 index 0000000..0419804 --- /dev/null +++ b/src/core/ncch/title_metadata.h @@ -0,0 +1,136 @@ +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/common_types.h" +#include "common/swap.h" +#include "core/result_status.h" + +namespace Core { + +enum TMDContentTypeFlag : u16 { + Encrypted = 1 << 0, + Disc = 1 << 2, + CFM = 1 << 3, + Optional = 1 << 14, + Shared = 1 << 15 +}; + +enum TMDContentIndex { Main = 0, Manual = 1, DLP = 2 }; + +enum TMDSignatureType : u32 { + Rsa4096Sha1 = 0x10000, + Rsa2048Sha1 = 0x10001, + EllipticSha1 = 0x10002, + Rsa4096Sha256 = 0x10003, + Rsa2048Sha256 = 0x10004, + EcdsaSha256 = 0x10005 +}; + +inline u32 GetSignatureSize(u32 signature_type) { + switch (signature_type) { + case Rsa4096Sha1: + case Rsa4096Sha256: + return 0x200; + + case Rsa2048Sha1: + case Rsa2048Sha256: + return 0x100; + + case EllipticSha1: + case EcdsaSha256: + return 0x3C; + } + + LOG_ERROR(Common_Filesystem, "Tried to read ticket with bad signature {}", signature_type); + return 0; +} + +/** + * Helper which implements an interface to read and write Title Metadata (TMD) files. + * If a file path is provided and the file exists, it can be parsed and used, otherwise + * it must be created. The TMD file can then be interpreted, modified and/or saved. + * + * This is a stripped down version of Citra's implementation which does not have writing + * and setting features. + */ +class TitleMetadata { +public: + struct ContentChunk { + u32_be id; + u16_be index; + u16_be type; + u64_be size; + std::array hash; + }; + + static_assert(sizeof(ContentChunk) == 0x30, "TMD ContentChunk structure size is wrong"); + + struct ContentInfo { + u16_be index; + u16_be command_count; + std::array hash; + }; + + static_assert(sizeof(ContentInfo) == 0x24, "TMD ContentInfo structure size is wrong"); + +#pragma pack(push, 1) + + struct Body { + std::array issuer; + u8 version; + u8 ca_crl_version; + u8 signer_crl_version; + u8 reserved; + u64_be system_version; + u64_be title_id; + u32_be title_type; + u16_be group_id; + u32_be savedata_size; + u32_be srl_private_savedata_size; + std::array reserved_2; + u8 srl_flag; + std::array reserved_3; + u32_be access_rights; + u16_be title_version; + u16_be content_count; + u16_be boot_content; + std::array reserved_4; + std::array contentinfo_hash; + std::array contentinfo; + }; + + static_assert(sizeof(Body) == 0x9C4, "TMD body structure size is wrong"); + +#pragma pack(pop) + + ResultStatus Load(const std::vector file_data, std::size_t offset = 0); + + u64 GetTitleID() const; + u32 GetTitleType() const; + u16 GetTitleVersion() const; + u64 GetSystemVersion() const; + std::size_t GetContentCount() const; + u32 GetBootContentID() const; + u32 GetManualContentID() const; + u32 GetDLPContentID() const; + u32 GetContentIDByIndex(u16 index) const; + u16 GetContentTypeByIndex(u16 index) const; + u64 GetContentSizeByIndex(u16 index) const; + std::array GetContentCTRByIndex(u16 index) const; + + void Print() const; + +private: + Body tmd_body; + u32_be signature_type; + std::vector tmd_signature; + std::vector tmd_chunks; +}; + +} // namespace Core diff --git a/src/core/result_status.h b/src/core/result_status.h new file mode 100644 index 0000000..b25ff9d --- /dev/null +++ b/src/core/result_status.h @@ -0,0 +1,19 @@ +// Copyright 2017 Citra Emulator Project / 2019 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +/// Result code for operations +enum class ResultStatus { + Success, + Error, + // Citra loader errors + ErrorInvalidFormat, + ErrorNotImplemented, + ErrorNotLoaded, + ErrorNotUsed, + ErrorAlreadyLoaded, + ErrorMemoryAllocationFailed, + ErrorEncrypted, +};