diff --git a/src/common/common_paths.h b/src/common/common_paths.h index e788224..a4ee74a 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -41,3 +41,5 @@ #define AES_KEYS "aes_keys.txt" #define CERTS_DB "certs.db" #define TITLE_DB "title.db" +#define TICKET_DB "ticket.db" +#define ENC_TITLE_KEYS_BIN "encTitleKeys.bin" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9106e9d..df979b1 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -32,6 +32,8 @@ add_library(core STATIC savegame.h title_db.cpp title_db.h + title_keys_bin.cpp + title_keys_bin.h ) target_link_libraries(core PRIVATE common cryptopp) diff --git a/src/core/importer.cpp b/src/core/importer.cpp index 731a782..bed8bad 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -73,12 +73,7 @@ bool SDMCImporter::Init() { // Load NAND Title DB if (!config.nand_title_db_path.empty()) { - FileUtil::IOFile file(config.nand_title_db_path, "rb"); - DataContainer container(file.GetData()); - std::vector> data; - if (container.IsGood() && container.GetIVFCLevel4Data(data)) { - nand_title_db = std::make_unique(std::move(data[0])); - } + nand_title_db = std::make_unique(config.nand_title_db_path); } if (!nand_title_db || !nand_title_db->IsGood()) { LOG_WARNING(Core, "NAND title.db invalid"); @@ -680,7 +675,7 @@ bool SDMCImporter::BuildCIA(const ContentSpecifier& specifier, std::string desti } } - bool ret = cia_builder->Init(destination, tmd, config.certs_db_path, + bool ret = cia_builder->Init(destination, tmd, config, FileUtil::GetDirectoryTreeSize(physical_path), callback); if (!ret) { return false; @@ -1186,6 +1181,7 @@ std::vector LoadPresetConfig(std::string mount_point) { LOAD_DATA(bootrom_path, BOOTROM9); LOAD_DATA(certs_db_path, CERTS_DB); LOAD_DATA(nand_title_db_path, TITLE_DB); + LOAD_DATA(ticket_db_path, TICKET_DB); LOAD_DATA(safe_mode_firm_path, "firm/"); LOAD_DATA(seed_db_path, SEED_DB); LOAD_DATA(secret_sector_path, SECRET_SECTOR); @@ -1195,6 +1191,12 @@ std::vector LoadPresetConfig(std::string mount_point) { LOAD_DATA(nand_data_path, "data/"); #undef LOAD_DATA + // encTitleKeys.bin + if (FileUtil::Exists(mount_point + "gm9/support/" ENC_TITLE_KEYS_BIN)) { + config_template.enc_title_keys_bin_path = + mount_point + "gm9/support/" ENC_TITLE_KEYS_BIN; + } + // Load version if (FileUtil::Exists(mount_point + "threeSD/version.txt")) { std::ifstream stream; diff --git a/src/core/importer.h b/src/core/importer.h index 63ffe4c..e65449a 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -72,9 +72,12 @@ struct Config { // Necessary system files keys are loaded from. std::string movable_sed_path; ///< Path to movable.sed std::string bootrom_path; ///< Path to bootrom (boot9.bin) (Sysdata 0) + std::string certs_db_path; ///< Path to certs.db. Used while building CIA. - std::string certs_db_path; ///< Path to certs.db. Used while building CIA. - std::string nand_title_db_path; ///< Path to NAND title.db. Entirely optional. + // Optional, used while building CIA, but usually missing these files won't hinder CIA building. + std::string nand_title_db_path; ///< Path to NAND title.db. Entirely optional. + std::string ticket_db_path; ///< Path to ticket.db. Entirely optional. + std::string enc_title_keys_bin_path; ///< Path to encTitleKeys.bin. Entireley optional. // The following system files are optional for importing and are only copied so that Citra // will be able to decrypt imported encrypted ROMs. diff --git a/src/core/ncch/cia_builder.cpp b/src/core/ncch/cia_builder.cpp index b56656a..b8d5ca3 100644 --- a/src/core/ncch/cia_builder.cpp +++ b/src/core/ncch/cia_builder.cpp @@ -4,9 +4,12 @@ #include #include "common/alignment.h" +#include "core/importer.h" #include "core/ncch/cia_builder.h" #include "core/ncch/ticket.h" #include "core/ncch/title_metadata.h" +#include "core/title_db.h" +#include "core/title_keys_bin.h" namespace Core { @@ -43,9 +46,8 @@ private: CIABuilder::CIABuilder() = default; CIABuilder::~CIABuilder() = default; -bool CIABuilder::Init(const std::string& destination, TitleMetadata tmd_, - const std::string& certs_db_path, std::size_t total_size_, - const Common::ProgressCallback& callback_) { +bool CIABuilder::Init(const std::string& destination, TitleMetadata tmd_, const Config& config, + std::size_t total_size_, const Common::ProgressCallback& callback_) { header = {}; meta = {}; @@ -69,7 +71,7 @@ bool CIABuilder::Init(const std::string& destination, TitleMetadata tmd_, // Cert cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT); header.cert_size = CIA_CERT_SIZE; - if (!WriteCert(certs_db_path)) { + if (!WriteCert(config.certs_db_path)) { LOG_ERROR(Core, "Could not write cert to file {}", destination); file.reset(); return false; @@ -77,13 +79,7 @@ bool CIABuilder::Init(const std::string& destination, TitleMetadata tmd_, // Ticket ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT); - - const auto fake_ticket = BuildFakeTicket(tmd.GetTitleID()).GetData(); - header.tik_size = fake_ticket.size(); - - file->Seek(ticket_offset, SEEK_SET); - if (file->WriteBytes(fake_ticket.data(), fake_ticket.size()) != fake_ticket.size()) { - LOG_ERROR(Core, "Could not write ticket to file {}", destination); + if (!WriteTicket(config.ticket_db_path, config.enc_title_keys_bin_path)) { file.reset(); return false; } @@ -144,6 +140,42 @@ bool CIABuilder::WriteCert(const std::string& certs_db_path) { return true; } +bool CIABuilder::WriteTicket(const std::string& ticket_db_path, + const std::string& enc_title_keys_bin_path) { + const auto title_id = tmd.GetTitleID(); + Ticket ticket = BuildFakeTicket(title_id); + + // Fill in common_key_index and title_key from either ticket.db (installed tickets) + // or GM9 support files (encTitleKeys.bin) found on the SD card + if (TicketDB ticket_db(ticket_db_path); + ticket_db.IsGood() && ticket_db.tickets.count(title_id)) { // ticket.db + + const auto& legit_ticket = ticket_db.tickets.at(title_id); + ticket.body.common_key_index = legit_ticket.body.common_key_index; + ticket.body.title_key = legit_ticket.body.title_key; + + } else if (TitleKeysBin enc_title_keys(enc_title_keys_bin_path); + enc_title_keys.IsGood() && enc_title_keys.entries.count(title_id)) { // support files + + const auto& entry = enc_title_keys.entries.at(title_id); + ticket.body.common_key_index = entry.common_key_index; + ticket.body.title_key = entry.title_key; + } else { + LOG_WARNING(Core, "Could not find title key for {:016x}", title_id); + } + + const auto ticket_data = ticket.GetData(); + header.tik_size = ticket_data.size(); + + file->Seek(ticket_offset, SEEK_SET); + if (file->WriteBytes(ticket_data.data(), ticket_data.size()) != ticket_data.size()) { + LOG_ERROR(Core, "Could not write ticket"); + file.reset(); + return false; + } + return true; +} + bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) { file->Seek(written, SEEK_SET); // To enforce alignment file->SetHashEnabled(true); diff --git a/src/core/ncch/cia_builder.h b/src/core/ncch/cia_builder.h index 923ee97..8bd0c58 100644 --- a/src/core/ncch/cia_builder.h +++ b/src/core/ncch/cia_builder.h @@ -22,6 +22,7 @@ constexpr std::size_t CIA_HEADER_SIZE = 0x2020; constexpr std::size_t CIA_CERT_SIZE = 0xA00; constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0; +class Config; class HashedFile; class CIABuilder { @@ -33,7 +34,7 @@ public: * Initializes the building of the CIA. * @return true on success, false otherwise */ - bool Init(const std::string& destination, TitleMetadata tmd, const std::string& certs_db_path, + bool Init(const std::string& destination, TitleMetadata tmd, const Config& config, std::size_t total_size, const Common::ProgressCallback& callback); /** @@ -90,6 +91,7 @@ private: static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong"); bool WriteCert(const std::string& certs_db_path); + bool WriteTicket(const std::string& ticket_db_path, const std::string& enc_title_keys_bin_path); Header header{}; Metadata meta{}; diff --git a/src/core/ncch/ticket.cpp b/src/core/ncch/ticket.cpp index 872f764..fdd4788 100644 --- a/src/core/ncch/ticket.cpp +++ b/src/core/ncch/ticket.cpp @@ -69,6 +69,7 @@ Ticket BuildFakeTicket(u64 title_id) { Ticket ticket{}; ticket.signature_type = 0x10004; // RSA_2048 SHA256 + ticket.signature.resize(GetSignatureSize(ticket.signature_type)); std::memset(ticket.signature.data(), 0xFF, ticket.signature.size()); auto& body = ticket.body; @@ -80,18 +81,8 @@ Ticket BuildFakeTicket(u64 title_id) { body.common_key_index = 0x00; body.audit = 0x01; std::memcpy(body.content_index.data(), TicketContentIndex.data(), TicketContentIndex.size()); - // GodMode9 by default sets all remaining 0x80 bytes to 0xFF, but legit tickets only set 0x20 - std::memset(body.content_index.data() + TicketContentIndex.size(), 0xFF, 0x20); - return ticket; -} - -Ticket BuildStandardTicket(u64 title_id, Ticket legit_ticket) { - Ticket ticket = BuildFakeTicket(title_id); - - // Put in the title key from the legit ticket - ticket.body.title_key.swap(legit_ticket.body.title_key); - ticket.body.common_key_index = legit_ticket.body.common_key_index; - + // GodMode9 by default sets all remaining 0x80 bytes to 0xFF + std::memset(body.content_index.data() + TicketContentIndex.size(), 0xFF, 0x80); return ticket; } diff --git a/src/core/ncch/ticket.h b/src/core/ncch/ticket.h index 0ff4400..9921c52 100644 --- a/src/core/ncch/ticket.h +++ b/src/core/ncch/ticket.h @@ -51,6 +51,5 @@ public: }; Ticket BuildFakeTicket(u64 title_id); -Ticket BuildStandardTicket(u64 title_id, Ticket legit_ticket); } // namespace Core diff --git a/src/core/title_db.cpp b/src/core/title_db.cpp index b98127b..ca81741 100644 --- a/src/core/title_db.cpp +++ b/src/core/title_db.cpp @@ -2,7 +2,9 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "common/file_util.h" #include "common/logging/log.h" +#include "core/data_container.h" #include "core/title_db.h" namespace Core { @@ -11,6 +13,17 @@ TitleDB::TitleDB(std::vector data) { is_good = Init(std::move(data)); } +TitleDB::TitleDB(const std::string& path) { + FileUtil::IOFile file(path, "rb"); + DataContainer container(file.GetData()); + std::vector> data; + if (container.IsGood() && container.GetIVFCLevel4Data(data)) { + is_good = Init(std::move(data[0])); + } +} + +TitleDB::~TitleDB() = default; + bool TitleDB::IsGood() const { return is_good; } @@ -63,4 +76,67 @@ bool TitleDB::LoadTitleInfo(u32 index) { return true; } +TicketDB::TicketDB(std::vector data) { + is_good = Init(std::move(data)); +} + +TicketDB::TicketDB(const std::string& path) { + FileUtil::IOFile file(path, "rb"); + DataContainer container(file.GetData()); + std::vector> data; + if (container.IsGood() && container.GetIVFCLevel4Data(data)) { + is_good = Init(std::move(data[0])); + } +} + +TicketDB::~TicketDB() = default; + +bool TicketDB::IsGood() const { + return is_good; +} + +bool TicketDB::Init(std::vector data) { + if (!InnerFAT_TicketDB::Init({std::move(data)})) { + return false; + } + + u32 cur = directory_entry_table[1].first_file_index; + while (cur != 0) { + if (!LoadTicket(cur)) { + return false; + } + cur = file_entry_table[cur].next_sibling_index; + } + return true; +} + +bool TicketDB::CheckMagic() const { + if (header.pre_header.db_magic != MakeMagic('T', 'I', 'C', 'K')) { + LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); + return false; + } + + if (header.fat_header.magic != MakeMagic('B', 'D', 'R', 'I') || + header.fat_header.version != 0x30000) { + + LOG_ERROR(Core, "File is invalid, decryption errors may have happened."); + return false; + } + return true; +} + +bool TicketDB::LoadTicket(u32 index) { + std::vector data; + if (!GetFileData(data, index)) { + return false; + } + + Ticket ticket; + if (!ticket.Load(std::move(data), 8)) { // there is a 8-byte header + return false; + } + tickets.emplace(file_entry_table[index].title_id, ticket); + return true; +} + } // namespace Core diff --git a/src/core/title_db.h b/src/core/title_db.h index 924ae66..14cb627 100644 --- a/src/core/title_db.h +++ b/src/core/title_db.h @@ -11,6 +11,7 @@ #include "common/common_types.h" #include "common/swap.h" #include "core/inner_fat.hpp" +#include "core/ncch/ticket.h" namespace Core { @@ -69,6 +70,9 @@ using InnerFAT_TitleDB = InnerFAT data); + explicit TitleDB(const std::string& path); + ~TitleDB(); + bool IsGood() const; std::unordered_map titles; @@ -83,4 +87,32 @@ private: friend InnerFAT_TitleDB; }; +struct TicketDBPreheader { + u32_le db_magic; + INSERT_PADDING_BYTES(0x0C); +}; +static_assert(sizeof(TicketDBPreheader) == 0x10, "TicketDB pre-header has incorrect size"); + +class TicketDB; +using InnerFAT_TicketDB = InnerFAT; +class TicketDB final : public InnerFAT_TicketDB { +public: + explicit TicketDB(std::vector data); + explicit TicketDB(const std::string& path); + ~TicketDB(); + + bool IsGood() const; + + std::unordered_map tickets; + +private: + bool Init(std::vector data); + bool CheckMagic() const; + bool LoadTicket(u32 index); + + bool is_good = false; + friend InnerFAT_TicketDB; +}; + } // namespace Core diff --git a/src/core/title_keys_bin.cpp b/src/core/title_keys_bin.cpp new file mode 100644 index 0000000..f0dd02c --- /dev/null +++ b/src/core/title_keys_bin.cpp @@ -0,0 +1,49 @@ +// Copyright 2021 threeSD Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/file_util.h" +#include "core/title_keys_bin.h" + +namespace Core { + +TitleKeysBin::TitleKeysBin(const std::string& path) { + is_good = Load(path); +} + +TitleKeysBin::~TitleKeysBin() = default; + +bool TitleKeysBin::IsGood() const { + return is_good; +} + +bool TitleKeysBin::Load(const std::string& path) { + FileUtil::IOFile file(path, "rb"); + if (!file) { + LOG_ERROR(Core, "Could not open file {}", path); + return false; + } + + TitleKeysBinHeader header; + if (file.ReadBytes(&header, sizeof(header)) != sizeof(header)) { + LOG_ERROR(Core, "Could not read header from {}", path); + return false; + } + + for (std::size_t i = 0; i < header.num_entries; ++i) { + TitleKeysBinEntry entry; + if (file.ReadBytes(&entry, sizeof(entry)) != sizeof(entry)) { + LOG_ERROR(Core, "Could not read entry {} from {}", i, path); + return false; + } + entries.emplace(entry.title_id, entry); + } + + if (file.Tell() != file.GetSize()) { + LOG_ERROR(Core, "File {} has redundant data, may be corrupted"); + return false; + } + return true; +} + +} // namespace Core diff --git a/src/core/title_keys_bin.h b/src/core/title_keys_bin.h new file mode 100644 index 0000000..f1b0110 --- /dev/null +++ b/src/core/title_keys_bin.h @@ -0,0 +1,44 @@ +// Copyright 2021 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 { + +struct TitleKeysBinHeader { + u32_le num_entries; + INSERT_PADDING_BYTES(12); +}; +static_assert(sizeof(TitleKeysBinHeader) == 16); + +struct TitleKeysBinEntry { + u32_be common_key_index; + INSERT_PADDING_BYTES(4); + u64_be title_id; + std::array title_key; +}; +static_assert(sizeof(TitleKeysBinEntry) == 32); + +// GM9 support files encTitleKeys.bin and decTitleKeys.bin. +class TitleKeysBin { +public: + explicit TitleKeysBin(const std::string& path); + ~TitleKeysBin(); + + bool IsGood() const; + + std::unordered_map entries; + +private: + bool Load(const std::string& path); + bool is_good = false; +}; + +} // namespace Core