Add checks for TMD and Ticket

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