mirror of
https://github.com/Dark98/threeSD.git
synced 2026-07-06 08:39:06 +00:00
Add checks for TMD and Ticket
This commit is contained in:
+395
-402
@@ -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
@@ -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
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user