From 3c3dd2bd86cfddf79e0244077a86bcd06ea53012 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Tue, 17 Jun 2025 14:17:01 +0200 Subject: [PATCH] am: Improve cia encrypted content detection (#1152) --- src/core/hle/service/am/am.cpp | 162 +++++++++++++++++++++------------ src/core/hle/service/am/am.h | 23 ++++- 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index ffbd11c4a..8d9ec4bc5 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -448,13 +448,16 @@ ResultVal CIAFile::Read(u64 offset, std::size_t length, u8* buffer) return length; } -Result CIAFile::WriteTicket() { +CIAFile::InstallResult CIAFile::WriteTicket() { + InstallResult res{}; + res.type = InstallResult::Type::TIK; auto load_result = container.LoadTicket(data, container.GetTicketOffset()); if (load_result != Loader::ResultStatus::Success) { LOG_ERROR(Service_AM, "Could not read ticket from CIA."); // TODO: Correct result code. - return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, - ErrorLevel::Permanent}; + res.result = {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, + ErrorLevel::Permanent}; + return res; } const auto& ticket = container.GetTicket(); @@ -466,23 +469,30 @@ Result CIAFile::WriteTicket() { FileUtil::CreateFullPath(ticket_folder); // Save ticket + res.install_full_path = ticket_path; if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) { LOG_ERROR(Service_AM, "Failed to install ticket file from CIA."); // TODO: Correct result code. - return FileSys::ResultFileNotFound; + res.result = FileSys::ResultFileNotFound; + return res; } install_state = CIAInstallState::TicketLoaded; - return ResultSuccess; + res.result = ResultSuccess; + return res; } -Result CIAFile::WriteTitleMetadata(std::span tmd_data, std::size_t offset) { +CIAFile::InstallResult CIAFile::WriteTitleMetadata(std::span tmd_data, + std::size_t offset) { + InstallResult res{}; + res.type = InstallResult::Type::TMD; auto load_result = container.LoadTitleMetadata(tmd_data, offset); if (load_result != Loader::ResultStatus::Success) { LOG_ERROR(Service_AM, "Could not read title metadata."); // TODO: Correct result code. - return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, - ErrorLevel::Permanent}; + res.result = {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, + ErrorLevel::Permanent}; + return res; } FileSys::TitleMetadata tmd = container.GetTitleMetadata(); @@ -503,13 +513,16 @@ Result CIAFile::WriteTitleMetadata(std::span tmd_data, std::size_t off FileUtil::CreateFullPath(tmd_folder); // Save TMD so that we can start getting new .app paths + res.install_full_path = tmd_path; if (tmd.Save(tmd_path) != Loader::ResultStatus::Success) { LOG_ERROR(Service_AM, "Failed to install title metadata file from CIA."); // TODO: Correct result code. - return FileSys::ResultFileNotFound; + res.result = FileSys::ResultFileNotFound; + return res; } - return PrepareToImportContent(tmd); + res.result = PrepareToImportContent(tmd); + return res; } ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, const u8* buffer) { @@ -537,10 +550,18 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, // to get the content paths to write to. const FileSys::TitleMetadata& tmd = container.GetTitleMetadata(); if (i != current_content_index) { + // A previous content file was being installed, save it first + if (current_content_install_result.type == InstallResult::Type::APP) { + install_results.push_back(current_content_install_result); + } current_content_index = static_cast(i); current_content_file = std::make_unique(content_file_paths[i], decryption_authorized); current_content_file->decryption_authorized = decryption_authorized; + + current_content_install_result.type = InstallResult::Type::APP; + current_content_install_result.install_full_path = content_file_paths[i]; + current_content_install_result.result = ResultSuccess; } auto& file = *current_content_file; @@ -550,8 +571,11 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, if ((tmd.GetContentTypeByIndex(i) & FileSys::TMDContentTypeFlag::Encrypted) != 0) { if (!decryption_authorized) { LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); - return Result(ErrorDescription::NotAuthorized, ErrorModule::AM, - ErrorSummary::InvalidState, ErrorLevel::Permanent); + current_content_install_result.result = + Result(ErrorDescription::NotAuthorized, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + install_results.push_back(current_content_install_result); + return current_content_install_result.result; } decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size()); } @@ -559,8 +583,11 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, file.Write(temp.data(), temp.size()); if (file.IsError()) { // This can never happen in real HW - return Result(ErrCodes::InvalidImportState, ErrorModule::AM, - ErrorSummary::InvalidState, ErrorLevel::Permanent); + current_content_install_result.result = + Result(ErrCodes::InvalidImportState, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + install_results.push_back(current_content_install_result); + return current_content_install_result.result; } // Keep tabs on how much of this content ID has been written so new range_min @@ -623,14 +650,16 @@ ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush // The end of our TMD is at the beginning of Content data, so ensure we have that much // buffered before trying to parse. if (written >= container.GetContentOffset() && install_state != CIAInstallState::TMDLoaded) { - auto result = WriteTicket(); - if (result.IsError()) { - return result; + InstallResult result = WriteTicket(); + install_results.push_back(result); + if (result.result.IsError()) { + return result.result; } result = WriteTitleMetadata(data, container.GetTitleMetadataOffset()); - if (result.IsError()) { - return result; + install_results.push_back(result); + if (result.result.IsError()) { + return result.result; } } @@ -642,6 +671,7 @@ ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush // From this point forward, data will no longer be buffered in data auto result = WriteContentData(offset, length, buffer); if (result.Failed()) { + current_content_install_result.type = InstallResult::Type::NONE; return result; } @@ -756,10 +786,19 @@ ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 o tmd.GetContentSizeByIndex(content_index) - content_written[content_index]; if (content_index != current_content_index) { + // A previous content file was being installed, save it first + if (current_content_install_result.type == InstallResult::Type::APP) { + install_results.push_back(current_content_install_result); + } + current_content_index = content_index; current_content_file = std::make_unique(content_file_paths[content_index], decryption_authorized); current_content_file->decryption_authorized = decryption_authorized; + + current_content_install_result.type = InstallResult::Type::APP; + current_content_install_result.install_full_path = content_file_paths[content_index]; + current_content_install_result.result = ResultSuccess; } auto& file = *current_content_file; @@ -768,8 +807,11 @@ ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 o if ((tmd.GetContentTypeByIndex(content_index) & FileSys::TMDContentTypeFlag::Encrypted) != 0) { if (!decryption_authorized) { LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); - return Result(ErrorDescription::NotAuthorized, ErrorModule::AM, - ErrorSummary::InvalidState, ErrorLevel::Permanent); + current_content_install_result.result = + Result(ErrorDescription::NotAuthorized, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent); + install_results.push_back(current_content_install_result); + return current_content_install_result.result; } decryption_state->content[content_index].ProcessData(temp.data(), temp.data(), temp.size()); } @@ -777,8 +819,11 @@ ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 o file.Write(temp.data(), temp.size()); if (file.IsError()) { // This can never happen in real HW - return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, - ErrorLevel::Permanent); + current_content_install_result.result = + Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent); + install_results.push_back(current_content_install_result); + return current_content_install_result.result; } content_written[content_index] += temp.size(); @@ -801,6 +846,12 @@ bool CIAFile::Close() { return true; is_closed = true; + // Commit last pending install result + if (current_content_install_result.type != InstallResult::Type::NONE) { + install_results.push_back(current_content_install_result); + current_content_install_result.type = InstallResult::Type::NONE; + } + bool complete = from_cdn ? is_done : (install_state >= CIAInstallState::TMDLoaded && @@ -824,8 +875,15 @@ bool CIAFile::Close() { // Clean up older content data if we installed newer content on top std::string old_tmd_path = GetTitleMetadataPath(media_type, container.GetTitleMetadata().GetTitleID(), false); - std::string new_tmd_path = - GetTitleMetadataPath(media_type, container.GetTitleMetadata().GetTitleID(), true); + std::string new_tmd_path = old_tmd_path; + + for (auto result : install_results) { + if (result.type == InstallResult::Type::TMD && result.result.IsSuccess()) { + new_tmd_path = result.install_full_path; + break; + } + } + if (FileUtil::Exists(new_tmd_path) && old_tmd_path != new_tmd_path) { FileSys::TitleMetadata old_tmd; FileSys::TitleMetadata new_tmd; @@ -954,7 +1012,7 @@ bool TMDFile::Close() { void TMDFile::Flush() const {} Result TMDFile::Commit() { - return importing_title->cia_file.WriteTitleMetadata(data, 0); + return importing_title->cia_file.WriteTitleMetadata(data, 0).result; } ContentFile::~ContentFile() { @@ -1041,39 +1099,29 @@ InstallStatus InstallCIA(const std::string& path, } installFile.Close(); - LOG_INFO(Service_AM, "Installed {} successfully.", path); - - const FileUtil::DirectoryEntryCallable callback = - [&callback](u64* num_entries_out, const std::string& directory, - const std::string& virtual_name) -> bool { - const std::string physical_name = directory + DIR_SEP + virtual_name; - const bool is_dir = FileUtil::IsDirectory(physical_name); - if (!is_dir) { - std::unique_ptr loader = Loader::GetLoader(physical_name); - if (!loader) { - return true; - } - - bool executable = false; - const auto res = loader->IsExecutable(executable); - if (res == Loader::ResultStatus::ErrorEncrypted) { - return false; - } - return true; - } else { - return FileUtil::ForeachDirectoryEntry(nullptr, physical_name, callback); + InstallStatus install_res = InstallStatus::Success; + for (auto result : installFile.GetInstallResults()) { + if (result.type != CIAFile::InstallResult::Type::APP || result.result.IsError()) { + continue; + } + + std::unique_ptr loader = Loader::GetLoader(result.install_full_path); + if (!loader) { + continue; + } + + bool executable = false; + const auto res = loader->IsExecutable(executable); + if (res == Loader::ResultStatus::ErrorEncrypted) { + LOG_ERROR(Service_AM, "CIA contains encrypted content: {}", path, + result.install_full_path); + install_res = InstallStatus::ErrorEncrypted; } - }; - if (!FileUtil::ForeachDirectoryEntry( - nullptr, - GetTitlePath( - Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()), - container.GetTitleMetadata().GetTitleID()), - callback)) { - LOG_ERROR(Service_AM, "CIA {} contained encrypted files.", path); - return InstallStatus::ErrorEncrypted; } - return InstallStatus::Success; + if (install_res == InstallStatus::Success) { + LOG_INFO(Service_AM, "Installed {} successfully.", path); + } + return install_res; } LOG_ERROR(Service_AM, "CIA file {} is invalid!", path); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 9585f96e3..1798e596b 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -170,13 +170,26 @@ void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ct // A file handled returned for CIAs to be written into and subsequently installed. class CIAFile final : public FileSys::FileBackend { public: + class InstallResult { + public: + enum class Type { + NONE, + TIK, + TMD, + APP, + }; + Type type{Type::NONE}; + std::string install_full_path{}; + Result result{0}; + }; + explicit CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn = false); ~CIAFile(); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - Result WriteTicket(); - Result WriteTitleMetadata(std::span tmd_data, std::size_t offset); + InstallResult WriteTicket(); + InstallResult WriteTitleMetadata(std::span tmd_data, std::size_t offset); ResultVal WriteContentData(u64 offset, std::size_t length, const u8* buffer); ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; @@ -202,6 +215,10 @@ public: is_done = true; } + const std::vector& GetInstallResults() const { + return install_results; + } + private: friend void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx); Core::System& system; @@ -227,6 +244,8 @@ private: std::vector content_file_paths; u16 current_content_index = -1; std::unique_ptr current_content_file; + InstallResult current_content_install_result{}; + std::vector install_results; Service::FS::MediaType media_type; class DecryptionState;