diff --git a/src/core/importer.cpp b/src/core/importer.cpp index e8ba239..e52528c 100644 --- a/src/core/importer.cpp +++ b/src/core/importer.cpp @@ -649,8 +649,9 @@ void SDMCImporter::AbortDumpCXI() { dump_cxi_ncch->AbortDecryptToFile(); } -bool SDMCImporter::BuildCIA(const ContentSpecifier& specifier, std::string destination, - const Common::ProgressCallback& callback, bool auto_filename) { +bool SDMCImporter::BuildCIA(CIABuildType type, const ContentSpecifier& specifier, + std::string destination, const Common::ProgressCallback& callback, + bool auto_filename) { if (config.certs_db_path.empty()) { LOG_ERROR(Core, "Missing certs.db"); @@ -693,7 +694,7 @@ bool SDMCImporter::BuildCIA(const ContentSpecifier& specifier, std::string desti } } - const bool ret = cia_builder->Init(CIABuildType::Standard, destination, tmd, config, + const bool ret = cia_builder->Init(type, destination, tmd, config, FileUtil::GetDirectoryTreeSize(physical_path), callback); if (!ret) { FileUtil::Delete(destination); diff --git a/src/core/importer.h b/src/core/importer.h index ea9ed32..ff5bd25 100644 --- a/src/core/importer.h +++ b/src/core/importer.h @@ -10,6 +10,7 @@ #include #include "common/common_types.h" #include "common/progress_callback.h" +#include "core/ncch/cia_common.h" namespace Core { @@ -143,7 +144,7 @@ public: * Blocks, but can be aborted on another thread. * @return true on success, false otherwise */ - bool BuildCIA(const ContentSpecifier& specifier, std::string destination, + bool BuildCIA(CIABuildType type, const ContentSpecifier& specifier, std::string destination, const Common::ProgressCallback& callback, bool auto_filename = false); /** diff --git a/src/core/ncch/cia_builder.cpp b/src/core/ncch/cia_builder.cpp index f3d4743..d28f4a5 100644 --- a/src/core/ncch/cia_builder.cpp +++ b/src/core/ncch/cia_builder.cpp @@ -37,12 +37,14 @@ public: } bool VerifyHash(u8* out) { - sha.Verify(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); - sha.Update(reinterpret_cast(data), length_written); + if (hash_enabled) { + sha.Update(reinterpret_cast(data), length_written); + } return length_written; } @@ -80,6 +82,7 @@ bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleM // Check for legit TMD if (!tmd.VerifyHashes() || !tmd.ValidateSignature()) { LOG_ERROR(Core, "TMD is not legit"); + file.reset(); return false; } } @@ -188,7 +191,7 @@ static Key::AESKey GetTitleKey(const Ticket& ticket) { Key::AESKey ctr{}; std::memcpy(ctr.data(), &ticket.body.title_id, 8); - CryptoPP::CTR_Mode::Decryption aes; + CryptoPP::CBC_Mode::Decryption aes; aes.SetKeyWithIV(ticket_key.data(), ticket_key.size(), ctr.data()); Key::AESKey title_key = ticket.body.title_key; @@ -222,44 +225,104 @@ bool CIABuilder::WriteTicket(const std::string& ticket_db_path, 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) { - file->Seek(written, SEEK_SET); // To enforce alignment - file->SetHashEnabled(true); - - { - std::lock_guard lock{abort_ncch_mutex}; - abort_ncch = &ncch; - } - - const auto ret = ncch.DecryptToFile(file, [this](std::size_t current, std::size_t total) { - callback(written + current, total_size); - }); - - { - std::lock_guard lock{abort_ncch_mutex}; - abort_ncch = nullptr; - } - - if (ret != ResultStatus::Success) { - file.reset(); + if (ncch.Load() != ResultStatus::Success) { return false; } - written = Common::AlignUp(file->Tell(), CIA_ALIGNMENT); - header.content_size = written - content_offset; + file->Seek(written, SEEK_SET); // To enforce alignment auto& tmd_chunk = tmd.GetContentChunkByID(content_id); - header.SetContentPresent(tmd_chunk.index); + const auto progress_callback = [this](std::size_t current, std::size_t total) { + callback(written + current, total_size); + }; + 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, progress_callback); + { + std::lock_guard lock{abort_ncch_mutex}; + abort_ncch = nullptr; + } - if (type == CIABuildType::Standard) { // Fix hash + if (ret != ResultStatus::Success) { + file.reset(); + return false; + } file->GetHash(tmd_chunk.hash.data()); - } else { // Verify hash - if (!file->VerifyHash(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 QuickDecryptor. + // 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, + progress_callback)) { + + file.reset(); + 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); + file.reset(); return false; } } - file->SetHashEnabled(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) { @@ -322,9 +385,13 @@ bool CIABuilder::Finalize() { } void CIABuilder::Abort() { - std::lock_guard lock{abort_ncch_mutex}; - if (abort_ncch) { - abort_ncch->AbortDecryptToFile(); + 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(); } } diff --git a/src/core/ncch/cia_builder.h b/src/core/ncch/cia_builder.h index 87397b6..cba2612 100644 --- a/src/core/ncch/cia_builder.h +++ b/src/core/ncch/cia_builder.h @@ -11,6 +11,7 @@ #include "common/progress_callback.h" #include "common/swap.h" #include "core/key/key.h" +#include "core/ncch/cia_common.h" #include "core/ncch/ncch_container.h" #include "core/ncch/title_metadata.h" #include "core/quick_decryptor.h" @@ -26,12 +27,6 @@ constexpr std::size_t CIA_METADATA_SIZE = 0x3AC0; struct Config; class HashedFile; -enum class CIABuildType { - Standard, /// Decrypted CIA with generalized ticket - PirateLegit, /// Uses legit TMD and encryption, but with generalized ticket - Legit, /// Fully legit, with personal ticket containing console ID and eshop account -}; - class CIABuilder { public: explicit CIABuilder(); @@ -122,6 +117,8 @@ private: // The NCCH to abort on std::mutex abort_ncch_mutex; NCCHContainer* abort_ncch{}; + + QuickDecryptor decryptor; }; } // namespace Core diff --git a/src/core/ncch/cia_common.h b/src/core/ncch/cia_common.h index 36d7ec5..ab51ae8 100644 --- a/src/core/ncch/cia_common.h +++ b/src/core/ncch/cia_common.h @@ -16,4 +16,10 @@ constexpr std::array CIACertNames{{ "Root-CA00000003-CP0000000b", }}; +enum class CIABuildType { + Standard, /// Decrypted CIA with generalized ticket + PirateLegit, /// Uses legit TMD and encryption, but with generalized ticket + Legit, /// Fully legit, with personal ticket containing console ID and eshop account +}; + } // namespace Core diff --git a/src/core/ncch/ncch_container.h b/src/core/ncch/ncch_container.h index ab8fa2b..371bc08 100644 --- a/src/core/ncch/ncch_container.h +++ b/src/core/ncch/ncch_container.h @@ -318,6 +318,8 @@ private: // Used for DecryptToFile QuickDecryptor decryptor; std::atomic_bool aborted{false}; + + friend class CIABuilder; }; /** diff --git a/src/frontend/import_dialog.cpp b/src/frontend/import_dialog.cpp index b7f2031..26a5d11 100644 --- a/src/frontend/import_dialog.cpp +++ b/src/frontend/import_dialog.cpp @@ -798,7 +798,8 @@ void ImportDialog::StartBuildingCIASingle(const Core::ContentSpecifier& specifie auto* job = new SimpleJob( this, [this, specifier, path](const Common::ProgressCallback& callback) { - return importer.BuildCIA(specifier, path.toStdString(), callback); + return importer.BuildCIA(Core::CIABuildType::Standard, specifier, path.toStdString(), + callback); }, [this] { importer.AbortBuildCIA(); }); RunSimpleJob(job); @@ -854,7 +855,8 @@ void ImportDialog::StartBatchBuildingCIA() { this, importer, std::move(to_import), [path](Core::SDMCImporter& importer, const Core::ContentSpecifier& specifier, const Common::ProgressCallback& callback) { - return importer.BuildCIA(specifier, path.toStdString(), callback, true); + return importer.BuildCIA(Core::CIABuildType::Standard, specifier, path.toStdString(), + callback, true); }, &Core::SDMCImporter::AbortBuildCIA); RunMultiJob(job, total_count, total_size);