diff --git a/src/core/cia_builder.cpp b/src/core/cia_builder.cpp index 28f4c14..3640535 100644 --- a/src/core/cia_builder.cpp +++ b/src/core/cia_builder.cpp @@ -1,402 +1,395 @@ -// Copyright 2020 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include "common/alignment.h" -#include "core/cia_builder.h" -#include "core/db/title_db.h" -#include "core/db/title_keys_bin.h" -#include "core/file_sys/certificate.h" -#include "core/file_sys/cia_common.h" -#include "core/file_sys/ticket.h" -#include "core/file_sys/title_metadata.h" -#include "core/importer.h" - -namespace Core { - -constexpr std::size_t CIA_ALIGNMENT = 0x40; - -class HashedFile : public FileUtil::IOFile { -public: - explicit HashedFile(const std::string& filename, const char openmode[], int flags = 0) - : FileUtil::IOFile(filename, openmode, flags) {} - ~HashedFile() override = default; - - void SetHashEnabled(bool enabled) { - hash_enabled = enabled; - if (enabled) { // Restart when hash is newly restarted - sha.Restart(); - } - } - - void GetHash(u8* out) { - sha.Final(out); - } - - bool VerifyHash(u8* out) { - return sha.Verify(out); - } - - std::size_t Write(const char* data, std::size_t length) override { - const std::size_t length_written = FileUtil::IOFile::Write(data, length); - if (hash_enabled) { - sha.Update(reinterpret_cast(data), length_written); - } - return length_written; - } - -private: - CryptoPP::SHA256 sha; - bool hash_enabled{}; -}; - -CIABuilder::CIABuilder(const Config& config) { - if (!config.ticket_db_path.empty()) { - ticket_db = std::make_unique(config.ticket_db_path); - } - if (!ticket_db || !ticket_db->IsGood()) { - LOG_WARNING(Core, "ticket.db not present or is invalid"); - ticket_db.reset(); - } - - if (!config.enc_title_keys_bin_path.empty()) { - enc_title_keys_bin = std::make_unique(); - if (!LoadTitleKeysBin(*enc_title_keys_bin, config.enc_title_keys_bin_path)) { - LOG_WARNING(Core, "encTitleKeys.bin invalid"); - enc_title_keys_bin.reset(); - } - } -} - -CIABuilder::~CIABuilder() = default; - -bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleMetadata tmd_, - std::size_t total_size_, const Common::ProgressCallback& callback_) { - - type = type_; - header = {}; - meta = {}; - - if (!FileUtil::CreateFullPath(destination)) { - LOG_ERROR(Core, "Could not create {}", destination); - return false; - } - file = std::make_shared(destination, "wb"); - if (!*file) { - LOG_ERROR(Core, "Could not open file {}", destination); - return false; - } - - tmd = std::move(tmd_); - if (type == CIABuildType::Standard) { - // Remove encrypted flag from TMD chunks - for (auto& chunk : tmd.tmd_chunks) { - chunk.type &= ~0x01; - } - } - if (type == CIABuildType::Legit || type == CIABuildType::PirateLegit) { - // Check for legit TMD - if (!tmd.VerifyHashes() || !tmd.ValidateSignature()) { - LOG_ERROR(Core, "TMD is not legit"); - return false; - } - } - - header.header_size = sizeof(header); - // Header will be written in Finalize - - // Cert - cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT); - header.cert_size = CIA_CERT_SIZE; - if (!WriteCert()) { - LOG_ERROR(Core, "Could not write cert to file {}", destination); - return false; - } - - // Ticket - ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT); - if (!WriteTicket()) { - return false; - } - - // TMD will be written in Finalize (we need to set content hash, etc) - tmd_offset = Common::AlignUp(ticket_offset + header.tik_size, CIA_ALIGNMENT); - header.tmd_size = tmd.GetSize(); - - content_offset = Common::AlignUp(tmd_offset + header.tmd_size, CIA_ALIGNMENT); - header.content_size = 0; - - // Meta will be written in Finalize - header.meta_size = 0; - - // Initialize variables - written = content_offset; - total_size = total_size_; - - callback = callback_; - wrapper.total_size = total_size; - wrapper.SetCurrent(written); - - callback(written, total_size); - return true; -} - -void CIABuilder::Cleanup() { - file.reset(); -} - -bool CIABuilder::WriteCert() { - if (!Certs::IsLoaded()) { - return false; - } - - file->Seek(cert_offset, SEEK_SET); - for (const auto& cert : CIACertNames) { - if (!Certs::Get(cert).Save(*file)) { - LOG_ERROR(Core, "Failed to write cert {}", cert); - return false; - } - } - return true; -} - -bool CIABuilder::FindLegitTicket(Ticket& ticket, u64 title_id) const { - if (ticket_db && ticket_db->tickets.count(title_id)) { - ticket = ticket_db->tickets.at(title_id); - if (!ticket.ValidateSignature()) { - LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id); - return false; - } - return true; - } - - LOG_ERROR(Core, "Ticket for {:016x} does not exist in ticket.db", title_id); - return false; -} - -Ticket CIABuilder::BuildStandardTicket(u64 title_id) const { - 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 (ticket_db && 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 (enc_title_keys_bin && enc_title_keys_bin->count(title_id)) { // support files - const auto& entry = enc_title_keys_bin->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); - } - return ticket; -} - -static Key::AESKey GetTitleKey(const Ticket& ticket) { - Key::SelectCommonKeyIndex(ticket.body.common_key_index); - if (!Key::IsNormalKeyAvailable(Key::TicketCommonKey)) { - LOG_ERROR(Core, "Ticket common key is not available"); - return {}; - } - - const auto ticket_key = Key::GetNormalKey(Key::TicketCommonKey); - Key::AESKey ctr{}; - std::memcpy(ctr.data(), &ticket.body.title_id, 8); - - CryptoPP::CBC_Mode::Decryption aes; - aes.SetKeyWithIV(ticket_key.data(), ticket_key.size(), ctr.data()); - - Key::AESKey title_key = ticket.body.title_key; - aes.ProcessData(title_key.data(), title_key.data(), title_key.size()); - return title_key; -} - -bool CIABuilder::WriteTicket() { - const auto title_id = tmd.GetTitleID(); - - Ticket ticket; - if (type == CIABuildType::Legit) { - if (!FindLegitTicket(ticket, title_id)) { - return false; - } - } else { - ticket = BuildStandardTicket(title_id); - } - title_key = GetTitleKey(ticket); - - header.tik_size = ticket.GetSize(); - - file->Seek(ticket_offset, SEEK_SET); - if (!ticket.Save(*file)) { - LOG_ERROR(Core, "Could not write ticket"); - return false; - } - return true; -} - -class CIAEncryptAndHash final : public CryptoFunc { -public: - explicit CIAEncryptAndHash(const Key::AESKey& key, const Key::AESKey& iv) { - aes.SetKeyWithIV(key.data(), key.size(), iv.data()); - } - - ~CIAEncryptAndHash() override = default; - - void ProcessData(u8* data, std::size_t size) override { - sha.Update(data, size); - aes.ProcessData(data, data, size); - } - - bool VerifyHash(const u8* hash) { - return sha.Verify(hash); - } - -private: - CryptoPP::CBC_Mode::Encryption aes; - CryptoPP::SHA256 sha; -}; - -bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) { - if (!ncch.Load()) { - return false; - } - - file->Seek(written, SEEK_SET); // To enforce alignment - wrapper.SetCurrent(written); - - auto& tmd_chunk = tmd.GetContentChunkByID(content_id); - - if (type == CIABuildType::Standard) { - // Decrypt the NCCH. We created a HashedFile to transparently calculate the hash as there - // is no easy way to get decrypted NCCH content otherwise. - file->SetHashEnabled(true); - { - std::lock_guard lock{abort_ncch_mutex}; - abort_ncch = &ncch; - } - const auto ret = ncch.DecryptToFile(file, wrapper.Wrap(callback)); - { - std::lock_guard lock{abort_ncch_mutex}; - abort_ncch = nullptr; - } - - if (!ret) { - return false; - } - file->GetHash(tmd_chunk.hash.data()); - file->SetHashEnabled(false); - } else { - ncch.file->Seek(0, SEEK_SET); - - // Calculate IV - Key::AESKey iv{}; - std::memcpy(iv.data(), &tmd_chunk.index, sizeof(tmd_chunk.index)); - - const bool is_encrypted = static_cast(tmd_chunk.type) & 0x01; - - // For encrypted content, the hashes are calculated before CIA/CDN encryption. - // So we have to add hash calculation to the CryptoFunc of the FileDecryptor. - // For unencrypted content, we can just use HashedFile's hashing. - std::shared_ptr crypto; - if (is_encrypted) { - crypto = std::make_shared(title_key, iv); - } else { // crypto left to be null - file->SetHashEnabled(true); - } - decryptor.SetCrypto(crypto); - if (!decryptor.CryptAndWriteFile(ncch.file, ncch.file->GetSize(), file, - wrapper.Wrap(callback))) { - - return false; - } - - // Verify the hash - bool verified{}; - if (is_encrypted) { - verified = crypto->VerifyHash(tmd_chunk.hash.data()); - } else { - verified = file->VerifyHash(tmd_chunk.hash.data()); - file->SetHashEnabled(false); - } - if (!verified) { - LOG_ERROR(Core, "Hash dismatch for content {}", content_id); - return false; - } - } - - written = Common::AlignUp(file->Tell(), CIA_ALIGNMENT); - - header.content_size = written - content_offset; - header.SetContentPresent(tmd_chunk.index); - - // DLCs do not have a meta - if (tmd_chunk.index != TMDContentIndex::Main || (tmd.GetTitleID() >> 32) == 0x0004008c) { - return true; - } - - // Load meta if the content is main - static_assert(sizeof(ncch.exheader_header.dependency_list) == sizeof(meta.dependencies), - "Dependency list should be of the same size in NCCH and CIA"); - std::memcpy(meta.dependencies.data(), &ncch.exheader_header.dependency_list, - sizeof(meta.dependencies)); - - // Note: GodMode9 has this hardcoded to 2. - meta.core_version = ncch.exheader_header.arm11_system_local_caps.core_version; - - std::vector smdh_buffer; - if (!ncch.LoadSectionExeFS("icon", smdh_buffer)) { - LOG_WARNING(Core, "Failed to load icon in ExeFS"); - return true; - } - std::memcpy(meta.icon_data.data(), smdh_buffer.data(), - std::min(meta.icon_data.size(), smdh_buffer.size())); - header.meta_size = sizeof(meta); - return true; -} - -bool CIABuilder::Finalize() { - // Write header - file->Seek(0, SEEK_SET); - if (file->WriteBytes(&header, sizeof(header)) != sizeof(header)) { - LOG_ERROR(Core, "Failed to write header"); - return false; - } - - // Write TMD - if (type == CIABuildType::Standard) { - tmd.FixHashes(); - } - file->Seek(tmd_offset, SEEK_SET); - if (!tmd.Save(*file)) { - return false; - } - - // Write meta - if (header.meta_size) { - file->Seek(written, SEEK_SET); - if (file->WriteBytes(&meta, sizeof(meta)) != sizeof(meta)) { - LOG_ERROR(Core, "Failed to write meta"); - return false; - } - } - - callback(total_size, total_size); - return true; -} - -void CIABuilder::Abort() { - if (type == CIABuildType::Standard) { // Abort NCCH decryption - std::lock_guard lock{abort_ncch_mutex}; - if (abort_ncch) { - abort_ncch->AbortDecryptToFile(); - } - } else { // Abort the decryptor - decryptor.Abort(); - } -} - -} // namespace Core +// Copyright 2020 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/alignment.h" +#include "core/cia_builder.h" +#include "core/db/title_db.h" +#include "core/db/title_keys_bin.h" +#include "core/file_sys/certificate.h" +#include "core/file_sys/cia_common.h" +#include "core/file_sys/ticket.h" +#include "core/file_sys/title_metadata.h" +#include "core/importer.h" + +namespace Core { + +constexpr std::size_t CIA_ALIGNMENT = 0x40; + +class HashedFile : public FileUtil::IOFile { +public: + explicit HashedFile(const std::string& filename, const char openmode[], int flags = 0) + : FileUtil::IOFile(filename, openmode, flags) {} + ~HashedFile() override = default; + + void SetHashEnabled(bool enabled) { + hash_enabled = enabled; + if (enabled) { // Restart when hash is newly restarted + sha.Restart(); + } + } + + void GetHash(u8* out) { + sha.Final(out); + } + + bool VerifyHash(u8* out) { + return sha.Verify(out); + } + + std::size_t Write(const char* data, std::size_t length) override { + const std::size_t length_written = FileUtil::IOFile::Write(data, length); + if (hash_enabled) { + sha.Update(reinterpret_cast(data), length_written); + } + return length_written; + } + +private: + CryptoPP::SHA256 sha; + bool hash_enabled{}; +}; + +CIABuilder::CIABuilder(const Config& config, std::shared_ptr ticket_db_) + : ticket_db(std::move(ticket_db_)) { + if (!config.enc_title_keys_bin_path.empty()) { + enc_title_keys_bin = std::make_unique(); + if (!LoadTitleKeysBin(*enc_title_keys_bin, config.enc_title_keys_bin_path)) { + LOG_WARNING(Core, "encTitleKeys.bin invalid"); + enc_title_keys_bin.reset(); + } + } +} + +CIABuilder::~CIABuilder() = default; + +bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleMetadata tmd_, + std::size_t total_size_, const Common::ProgressCallback& callback_) { + + type = type_; + header = {}; + meta = {}; + + if (!FileUtil::CreateFullPath(destination)) { + LOG_ERROR(Core, "Could not create {}", destination); + return false; + } + file = std::make_shared(destination, "wb"); + if (!*file) { + LOG_ERROR(Core, "Could not open file {}", destination); + return false; + } + + tmd = std::move(tmd_); + if (type == CIABuildType::Standard) { + // Remove encrypted flag from TMD chunks + for (auto& chunk : tmd.tmd_chunks) { + chunk.type &= ~0x01; + } + } + if (type == CIABuildType::Legit || type == CIABuildType::PirateLegit) { + // Check for legit TMD + if (!tmd.VerifyHashes() || !tmd.ValidateSignature()) { + LOG_ERROR(Core, "TMD is not legit"); + return false; + } + } + + header.header_size = sizeof(header); + // Header will be written in Finalize + + // Cert + cert_offset = Common::AlignUp(header.header_size, CIA_ALIGNMENT); + header.cert_size = CIA_CERT_SIZE; + if (!WriteCert()) { + LOG_ERROR(Core, "Could not write cert to file {}", destination); + return false; + } + + // Ticket + ticket_offset = Common::AlignUp(cert_offset + header.cert_size, CIA_ALIGNMENT); + if (!WriteTicket()) { + return false; + } + + // TMD will be written in Finalize (we need to set content hash, etc) + tmd_offset = Common::AlignUp(ticket_offset + header.tik_size, CIA_ALIGNMENT); + header.tmd_size = tmd.GetSize(); + + content_offset = Common::AlignUp(tmd_offset + header.tmd_size, CIA_ALIGNMENT); + header.content_size = 0; + + // Meta will be written in Finalize + header.meta_size = 0; + + // Initialize variables + written = content_offset; + total_size = total_size_; + + callback = callback_; + wrapper.total_size = total_size; + wrapper.SetCurrent(written); + + callback(written, total_size); + return true; +} + +void CIABuilder::Cleanup() { + file.reset(); +} + +bool CIABuilder::WriteCert() { + if (!Certs::IsLoaded()) { + return false; + } + + file->Seek(cert_offset, SEEK_SET); + for (const auto& cert : CIACertNames) { + if (!Certs::Get(cert).Save(*file)) { + LOG_ERROR(Core, "Failed to write cert {}", cert); + return false; + } + } + return true; +} + +bool CIABuilder::FindLegitTicket(Ticket& ticket, u64 title_id) const { + if (ticket_db && ticket_db->tickets.count(title_id)) { + ticket = ticket_db->tickets.at(title_id); + if (!ticket.ValidateSignature()) { + LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id); + return false; + } + return true; + } + + LOG_ERROR(Core, "Ticket for {:016x} does not exist in ticket.db", title_id); + return false; +} + +Ticket CIABuilder::BuildStandardTicket(u64 title_id) const { + 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 (ticket_db && 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 (enc_title_keys_bin && enc_title_keys_bin->count(title_id)) { // support files + const auto& entry = enc_title_keys_bin->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); + } + return ticket; +} + +static Key::AESKey GetTitleKey(const Ticket& ticket) { + Key::SelectCommonKeyIndex(ticket.body.common_key_index); + if (!Key::IsNormalKeyAvailable(Key::TicketCommonKey)) { + LOG_ERROR(Core, "Ticket common key is not available"); + return {}; + } + + const auto ticket_key = Key::GetNormalKey(Key::TicketCommonKey); + Key::AESKey ctr{}; + std::memcpy(ctr.data(), &ticket.body.title_id, 8); + + CryptoPP::CBC_Mode::Decryption aes; + aes.SetKeyWithIV(ticket_key.data(), ticket_key.size(), ctr.data()); + + Key::AESKey title_key = ticket.body.title_key; + aes.ProcessData(title_key.data(), title_key.data(), title_key.size()); + return title_key; +} + +bool CIABuilder::WriteTicket() { + const auto title_id = tmd.GetTitleID(); + + Ticket ticket; + if (type == CIABuildType::Legit) { + if (!FindLegitTicket(ticket, title_id)) { + return false; + } + } else { + ticket = BuildStandardTicket(title_id); + } + title_key = GetTitleKey(ticket); + + header.tik_size = ticket.GetSize(); + + file->Seek(ticket_offset, SEEK_SET); + if (!ticket.Save(*file)) { + LOG_ERROR(Core, "Could not write ticket"); + return false; + } + return true; +} + +class CIAEncryptAndHash final : public CryptoFunc { +public: + explicit CIAEncryptAndHash(const Key::AESKey& key, const Key::AESKey& iv) { + aes.SetKeyWithIV(key.data(), key.size(), iv.data()); + } + + ~CIAEncryptAndHash() override = default; + + void ProcessData(u8* data, std::size_t size) override { + sha.Update(data, size); + aes.ProcessData(data, data, size); + } + + bool VerifyHash(const u8* hash) { + return sha.Verify(hash); + } + +private: + CryptoPP::CBC_Mode::Encryption aes; + CryptoPP::SHA256 sha; +}; + +bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) { + if (!ncch.Load()) { + return false; + } + + file->Seek(written, SEEK_SET); // To enforce alignment + wrapper.SetCurrent(written); + + auto& tmd_chunk = tmd.GetContentChunkByID(content_id); + + if (type == CIABuildType::Standard) { + // Decrypt the NCCH. We created a HashedFile to transparently calculate the hash as there + // is no easy way to get decrypted NCCH content otherwise. + file->SetHashEnabled(true); + { + std::lock_guard lock{abort_ncch_mutex}; + abort_ncch = &ncch; + } + const auto ret = ncch.DecryptToFile(file, wrapper.Wrap(callback)); + { + std::lock_guard lock{abort_ncch_mutex}; + abort_ncch = nullptr; + } + + if (!ret) { + return false; + } + file->GetHash(tmd_chunk.hash.data()); + file->SetHashEnabled(false); + } else { + ncch.file->Seek(0, SEEK_SET); + + // Calculate IV + Key::AESKey iv{}; + std::memcpy(iv.data(), &tmd_chunk.index, sizeof(tmd_chunk.index)); + + const bool is_encrypted = static_cast(tmd_chunk.type) & 0x01; + + // For encrypted content, the hashes are calculated before CIA/CDN encryption. + // So we have to add hash calculation to the CryptoFunc of the FileDecryptor. + // For unencrypted content, we can just use HashedFile's hashing. + std::shared_ptr crypto; + if (is_encrypted) { + crypto = std::make_shared(title_key, iv); + } else { // crypto left to be null + file->SetHashEnabled(true); + } + decryptor.SetCrypto(crypto); + if (!decryptor.CryptAndWriteFile(ncch.file, ncch.file->GetSize(), file, + wrapper.Wrap(callback))) { + + return false; + } + + // Verify the hash + bool verified{}; + if (is_encrypted) { + verified = crypto->VerifyHash(tmd_chunk.hash.data()); + } else { + verified = file->VerifyHash(tmd_chunk.hash.data()); + file->SetHashEnabled(false); + } + if (!verified) { + LOG_ERROR(Core, "Hash dismatch for content {}", content_id); + return false; + } + } + + written = Common::AlignUp(file->Tell(), CIA_ALIGNMENT); + + header.content_size = written - content_offset; + header.SetContentPresent(tmd_chunk.index); + + // DLCs do not have a meta + if (tmd_chunk.index != TMDContentIndex::Main || (tmd.GetTitleID() >> 32) == 0x0004008c) { + return true; + } + + // Load meta if the content is main + static_assert(sizeof(ncch.exheader_header.dependency_list) == sizeof(meta.dependencies), + "Dependency list should be of the same size in NCCH and CIA"); + std::memcpy(meta.dependencies.data(), &ncch.exheader_header.dependency_list, + sizeof(meta.dependencies)); + + // Note: GodMode9 has this hardcoded to 2. + meta.core_version = ncch.exheader_header.arm11_system_local_caps.core_version; + + std::vector smdh_buffer; + if (!ncch.LoadSectionExeFS("icon", smdh_buffer)) { + LOG_WARNING(Core, "Failed to load icon in ExeFS"); + return true; + } + std::memcpy(meta.icon_data.data(), smdh_buffer.data(), + std::min(meta.icon_data.size(), smdh_buffer.size())); + header.meta_size = sizeof(meta); + return true; +} + +bool CIABuilder::Finalize() { + // Write header + file->Seek(0, SEEK_SET); + if (file->WriteBytes(&header, sizeof(header)) != sizeof(header)) { + LOG_ERROR(Core, "Failed to write header"); + return false; + } + + // Write TMD + if (type == CIABuildType::Standard) { + tmd.FixHashes(); + } + file->Seek(tmd_offset, SEEK_SET); + if (!tmd.Save(*file)) { + return false; + } + + // Write meta + if (header.meta_size) { + file->Seek(written, SEEK_SET); + if (file->WriteBytes(&meta, sizeof(meta)) != sizeof(meta)) { + LOG_ERROR(Core, "Failed to write meta"); + return false; + } + } + + callback(total_size, total_size); + return true; +} + +void CIABuilder::Abort() { + if (type == CIABuildType::Standard) { // Abort NCCH decryption + std::lock_guard lock{abort_ncch_mutex}; + if (abort_ncch) { + abort_ncch->AbortDecryptToFile(); + } + } else { // Abort the decryptor + decryptor.Abort(); + } +} + +} // namespace Core diff --git a/src/core/cia_builder.h b/src/core/cia_builder.h index 99db383..9e32f76 100644 --- a/src/core/cia_builder.h +++ b/src/core/cia_builder.h @@ -1,137 +1,137 @@ -// Copyright 2017 Citra Emulator Project / 2020 Pengfei Zhu -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include "common/file_util.h" -#include "common/progress_callback.h" -#include "common/swap.h" -#include "core/file_decryptor.h" -#include "core/file_sys/cia_common.h" -#include "core/file_sys/ncch_container.h" -#include "core/file_sys/title_metadata.h" -#include "core/key/key.h" - -namespace Core { - -constexpr std::size_t CIA_CONTENT_MAX_COUNT = 0x10000; -constexpr std::size_t CIA_CONTENT_BITS_SIZE = (CIA_CONTENT_MAX_COUNT / 8); -constexpr std::size_t CIA_HEADER_SIZE = 0x2020; -constexpr std::size_t CIA_CERT_SIZE = 0xA00; -constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0; - -struct Config; -class EncTitleKeysBin; -class HashedFile; -class Ticket; -class TicketDB; - -class CIABuilder { -public: - explicit CIABuilder(const Config& config); - ~CIABuilder(); - - /** - * Initializes the building of the CIA. - * @return true on success, false otherwise - */ - bool Init(CIABuildType type, const std::string& destination, TitleMetadata tmd, - std::size_t total_size, const Common::ProgressCallback& callback); - - void Cleanup(); - - /** - * Adds an NCCH content to the CIA. - * @return true on success, false otherwise - */ - bool AddContent(u16 content_id, NCCHContainer& ncch); - - /** - * Finalizes this CIA and write remaining data. - * @return true on success, false otherwise - */ - bool Finalize(); - - /** - * Aborts the current work. In fact, only usable during AddContent. - */ - void Abort(); - -private: - struct Header { - u32_le header_size; - u16_le type; - u16_le version; - u32_le cert_size; - u32_le tik_size; - u32_le tmd_size; - u32_le meta_size; - u64_le content_size; - std::array content_present; - - bool IsContentPresent(u16 index) const { - // The content_present is a bit array which defines which content in the TMD - // is included in the CIA, so check the bit for this index and add if set. - // The bits in the content index are arranged w/ index 0 as the MSB, 7 as the LSB, etc. - return (content_present[index >> 3] & (0x80 >> (index & 7))); - } - - void SetContentPresent(u16 index) { - content_present[index >> 3] |= (0x80 >> (index & 7)); - } - }; - - static_assert(sizeof(Header) == CIA_HEADER_SIZE, "CIA Header structure size is wrong"); - - struct Metadata { - std::array dependencies; - std::array reserved; - u32_le core_version; - std::array reserved_2; - std::array icon_data; - }; - - static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong"); - - bool WriteCert(); - - bool FindLegitTicket(Ticket& ticket, u64 title_id) const; - Ticket BuildStandardTicket(u64 title_id) const; - bool WriteTicket(); - - // Persistent state - std::unique_ptr ticket_db; - std::unique_ptr enc_title_keys_bin; - - // State of a single task - CIABuildType type; - - Header header{}; - Metadata meta{}; - - TitleMetadata tmd; - Key::AESKey title_key{}; - - std::size_t cert_offset{}; - std::size_t ticket_offset{}; - std::size_t tmd_offset{}; - std::size_t content_offset{}; - - std::shared_ptr file; - std::size_t written{}; // size written (with alignment) - std::size_t total_size{}; - Common::ProgressCallback callback; - Common::ProgressCallbackWrapper wrapper; - - // The NCCH to abort on - std::mutex abort_ncch_mutex; - NCCHContainer* abort_ncch{}; - - FileDecryptor decryptor; -}; - -} // namespace Core +// Copyright 2017 Citra Emulator Project / 2020 Pengfei Zhu +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/file_util.h" +#include "common/progress_callback.h" +#include "common/swap.h" +#include "core/file_decryptor.h" +#include "core/file_sys/cia_common.h" +#include "core/file_sys/ncch_container.h" +#include "core/file_sys/title_metadata.h" +#include "core/key/key.h" + +namespace Core { + +constexpr std::size_t CIA_CONTENT_MAX_COUNT = 0x10000; +constexpr std::size_t CIA_CONTENT_BITS_SIZE = (CIA_CONTENT_MAX_COUNT / 8); +constexpr std::size_t CIA_HEADER_SIZE = 0x2020; +constexpr std::size_t CIA_CERT_SIZE = 0xA00; +constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0; + +struct Config; +class EncTitleKeysBin; +class HashedFile; +class Ticket; +class TicketDB; + +class CIABuilder { +public: + explicit CIABuilder(const Config& config, std::shared_ptr ticket_db); + ~CIABuilder(); + + /** + * Initializes the building of the CIA. + * @return true on success, false otherwise + */ + bool Init(CIABuildType type, const std::string& destination, TitleMetadata tmd, + std::size_t total_size, const Common::ProgressCallback& callback); + + void Cleanup(); + + /** + * Adds an NCCH content to the CIA. + * @return true on success, false otherwise + */ + bool AddContent(u16 content_id, NCCHContainer& ncch); + + /** + * Finalizes this CIA and write remaining data. + * @return true on success, false otherwise + */ + bool Finalize(); + + /** + * Aborts the current work. In fact, only usable during AddContent. + */ + void Abort(); + +private: + struct Header { + u32_le header_size; + u16_le type; + u16_le version; + u32_le cert_size; + u32_le tik_size; + u32_le tmd_size; + u32_le meta_size; + u64_le content_size; + std::array content_present; + + bool IsContentPresent(u16 index) const { + // The content_present is a bit array which defines which content in the TMD + // is included in the CIA, so check the bit for this index and add if set. + // The bits in the content index are arranged w/ index 0 as the MSB, 7 as the LSB, etc. + return (content_present[index >> 3] & (0x80 >> (index & 7))); + } + + void SetContentPresent(u16 index) { + content_present[index >> 3] |= (0x80 >> (index & 7)); + } + }; + + static_assert(sizeof(Header) == CIA_HEADER_SIZE, "CIA Header structure size is wrong"); + + struct Metadata { + std::array dependencies; + std::array reserved; + u32_le core_version; + std::array reserved_2; + std::array icon_data; + }; + + static_assert(sizeof(Metadata) == CIA_METADATA_SIZE, "CIA Metadata structure size is wrong"); + + bool WriteCert(); + + bool FindLegitTicket(Ticket& ticket, u64 title_id) const; + Ticket BuildStandardTicket(u64 title_id) const; + bool WriteTicket(); + + // Persistent state + const std::shared_ptr ticket_db; + std::unique_ptr enc_title_keys_bin; + + // State of a single task + CIABuildType type; + + Header header{}; + Metadata meta{}; + + TitleMetadata tmd; + Key::AESKey title_key{}; + + std::size_t cert_offset{}; + std::size_t ticket_offset{}; + std::size_t tmd_offset{}; + std::size_t content_offset{}; + + std::shared_ptr file; + std::size_t written{}; // size written (with alignment) + std::size_t total_size{}; + Common::ProgressCallback callback; + Common::ProgressCallbackWrapper wrapper; + + // The NCCH to abort on + std::mutex abort_ncch_mutex; + NCCHContainer* abort_ncch{}; + + FileDecryptor decryptor; +}; + +} // namespace Core diff --git a/src/core/importer.cpp b/src/core/importer.cpp index 55e765a..303a268 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -65,9 +65,18 @@ bool SDMCImporter::Init() { Certs::Load(config.certs_db_path); } + // Load Ticket DB + if (!config.ticket_db_path.empty()) { + ticket_db = std::make_shared(config.ticket_db_path); + } + if (!ticket_db || !ticket_db->IsGood()) { + LOG_WARNING(Core, "ticket.db not present or is invalid"); + ticket_db.reset(); + } + // Create children sdmc_decryptor = std::make_unique(config.sdmc_path); - cia_builder = std::make_unique(config); + cia_builder = std::make_unique(config, ticket_db); // Load SDMC Title DB { diff --git a/src/core/importer.h b/src/core/importer.h index 15a1b4d..d26faea 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -178,6 +178,14 @@ public: bool LoadTMD(ContentType type, u64 id, TitleMetadata& out) const; bool LoadTMD(const ContentSpecifier& specifier, TitleMetadata& out) const; + std::shared_ptr& GetTicketDB() { + return ticket_db; + } + + const std::shared_ptr& GetTicketDB() const { + return ticket_db; + } + private: bool Init(); @@ -216,6 +224,7 @@ private: // Used for CIA building std::unique_ptr cia_builder; + std::shared_ptr ticket_db; // The NCCH used to dump CXIs. std::unique_ptr dump_cxi_ncch; diff --git a/src/frontend/title_info_dialog.cpp b/src/frontend/title_info_dialog.cpp index c8c518d..3b9d444 100644 --- a/src/frontend/title_info_dialog.cpp +++ b/src/frontend/title_info_dialog.cpp @@ -7,6 +7,7 @@ #include #include #include "common/string_util.h" +#include "core/db/title_db.h" #include "core/file_sys/ncch_container.h" #include "core/file_sys/title_metadata.h" #include "core/importer.h" @@ -23,16 +24,15 @@ TitleInfoDialog::TitleInfoDialog(QWidget* parent, const Core::Config& config, const double scale = qApp->desktop()->logicalDpiX() / 96.0; resize(static_cast(width() * scale), static_cast(height() * scale)); - InitializeInfo(config, importer, specifier); - InitializeLanguageComboBox(); + LoadInfo(config, importer, specifier); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &TitleInfoDialog::accept); } TitleInfoDialog::~TitleInfoDialog() = default; -void TitleInfoDialog::InitializeInfo(const Core::Config& config, Core::SDMCImporter& importer, - const Core::ContentSpecifier& specifier) { +void TitleInfoDialog::LoadInfo(const Core::Config& config, Core::SDMCImporter& importer, + const Core::ContentSpecifier& specifier) { Core::TitleMetadata tmd; if (!importer.LoadTMD(specifier, tmd)) { QMessageBox::warning(this, tr("threeSD"), tr("Could not load title information.")); @@ -80,6 +80,27 @@ void TitleInfoDialog::InitializeInfo(const Core::Config& config, Core::SDMCImpor } ui->encryptionLineEdit->setText(encryption_text); + // Checks + const bool tmd_legit = tmd.ValidateSignature() && tmd.VerifyHashes(); + if (tmd_legit) { + ui->tmdCheckLabel->setText(tr("Legit")); + } else { + ui->tmdCheckLabel->setText(tr("Illegit")); + } + + if (const auto& ticket_db = importer.GetTicketDB(); + ticket_db && ticket_db->tickets.count(specifier.id)) { + + const bool ticket_legit = ticket_db->tickets.at(specifier.id).ValidateSignature(); + if (ticket_legit) { + ui->ticketCheckLabel->setText(tr("Legit")); + } else { + ui->ticketCheckLabel->setText(tr("Illegit")); + } + } else { + ui->ticketCheckLabel->setText(tr("Missing")); + } + // Load SMDH std::vector smdh_buffer; if (!ncch.LoadSectionExeFS("icon", smdh_buffer) || smdh_buffer.size() != sizeof(Core::SMDH) || @@ -98,6 +119,8 @@ void TitleInfoDialog::InitializeInfo(const Core::Config& config, Core::SDMCImpor ui->iconSmallLabel->setPixmap( QPixmap::fromImage(QImage(reinterpret_cast(smdh.GetIcon(false).data()), 24, 24, QImage::Format::Format_RGB16))); + // Load names + InitializeLanguageComboBox(); } void TitleInfoDialog::InitializeLanguageComboBox() { diff --git a/src/frontend/title_info_dialog.h b/src/frontend/title_info_dialog.h index 2468d59..d5da1d5 100644 --- a/src/frontend/title_info_dialog.h +++ b/src/frontend/title_info_dialog.h @@ -10,6 +10,7 @@ namespace Core { class Config; class ContentSpecifier; class SDMCImporter; +class TitleMetadata; } // namespace Core namespace Ui { @@ -25,8 +26,8 @@ public: ~TitleInfoDialog(); private: - void InitializeInfo(const Core::Config& config, Core::SDMCImporter& importer, - const Core::ContentSpecifier& specifier); + void LoadInfo(const Core::Config& config, Core::SDMCImporter& importer, + const Core::ContentSpecifier& specifier); void InitializeLanguageComboBox(); void UpdateNames(); diff --git a/src/frontend/title_info_dialog.ui b/src/frontend/title_info_dialog.ui index 4f97d84..3ba34cc 100644 --- a/src/frontend/title_info_dialog.ui +++ b/src/frontend/title_info_dialog.ui @@ -28,7 +28,11 @@ - + + + true + + @@ -58,7 +62,11 @@ - + + + true + + @@ -68,7 +76,11 @@ - + + + true + + @@ -87,7 +99,11 @@ - + + + true + + @@ -100,7 +116,11 @@ - + + + true + + @@ -110,7 +130,11 @@ - + + + true + +