am: Improve cia encrypted content detection (#1152)

This commit is contained in:
PabloMK7 2025-06-17 14:17:01 +02:00 committed by GitHub
parent eec1466b7b
commit 3c3dd2bd86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 126 additions and 59 deletions

View File

@ -448,13 +448,16 @@ ResultVal<std::size_t> CIAFile::Read(u64 offset, std::size_t length, u8* buffer)
return length; return length;
} }
Result CIAFile::WriteTicket() { CIAFile::InstallResult CIAFile::WriteTicket() {
InstallResult res{};
res.type = InstallResult::Type::TIK;
auto load_result = container.LoadTicket(data, container.GetTicketOffset()); auto load_result = container.LoadTicket(data, container.GetTicketOffset());
if (load_result != Loader::ResultStatus::Success) { if (load_result != Loader::ResultStatus::Success) {
LOG_ERROR(Service_AM, "Could not read ticket from CIA."); LOG_ERROR(Service_AM, "Could not read ticket from CIA.");
// TODO: Correct result code. // TODO: Correct result code.
return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, res.result = {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument,
ErrorLevel::Permanent}; ErrorLevel::Permanent};
return res;
} }
const auto& ticket = container.GetTicket(); const auto& ticket = container.GetTicket();
@ -466,23 +469,30 @@ Result CIAFile::WriteTicket() {
FileUtil::CreateFullPath(ticket_folder); FileUtil::CreateFullPath(ticket_folder);
// Save ticket // Save ticket
res.install_full_path = ticket_path;
if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) { if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) {
LOG_ERROR(Service_AM, "Failed to install ticket file from CIA."); LOG_ERROR(Service_AM, "Failed to install ticket file from CIA.");
// TODO: Correct result code. // TODO: Correct result code.
return FileSys::ResultFileNotFound; res.result = FileSys::ResultFileNotFound;
return res;
} }
install_state = CIAInstallState::TicketLoaded; install_state = CIAInstallState::TicketLoaded;
return ResultSuccess; res.result = ResultSuccess;
return res;
} }
Result CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t offset) { CIAFile::InstallResult CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data,
std::size_t offset) {
InstallResult res{};
res.type = InstallResult::Type::TMD;
auto load_result = container.LoadTitleMetadata(tmd_data, offset); auto load_result = container.LoadTitleMetadata(tmd_data, offset);
if (load_result != Loader::ResultStatus::Success) { if (load_result != Loader::ResultStatus::Success) {
LOG_ERROR(Service_AM, "Could not read title metadata."); LOG_ERROR(Service_AM, "Could not read title metadata.");
// TODO: Correct result code. // TODO: Correct result code.
return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, res.result = {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument,
ErrorLevel::Permanent}; ErrorLevel::Permanent};
return res;
} }
FileSys::TitleMetadata tmd = container.GetTitleMetadata(); FileSys::TitleMetadata tmd = container.GetTitleMetadata();
@ -503,13 +513,16 @@ Result CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t off
FileUtil::CreateFullPath(tmd_folder); FileUtil::CreateFullPath(tmd_folder);
// Save TMD so that we can start getting new .app paths // 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) { if (tmd.Save(tmd_path) != Loader::ResultStatus::Success) {
LOG_ERROR(Service_AM, "Failed to install title metadata file from CIA."); LOG_ERROR(Service_AM, "Failed to install title metadata file from CIA.");
// TODO: Correct result code. // TODO: Correct result code.
return FileSys::ResultFileNotFound; res.result = FileSys::ResultFileNotFound;
return res;
} }
return PrepareToImportContent(tmd); res.result = PrepareToImportContent(tmd);
return res;
} }
ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length, const u8* buffer) { ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length, const u8* buffer) {
@ -537,10 +550,18 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
// to get the content paths to write to. // to get the content paths to write to.
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata(); const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
if (i != current_content_index) { 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<u16>(i); current_content_index = static_cast<u16>(i);
current_content_file = current_content_file =
std::make_unique<NCCHCryptoFile>(content_file_paths[i], decryption_authorized); std::make_unique<NCCHCryptoFile>(content_file_paths[i], decryption_authorized);
current_content_file->decryption_authorized = 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; auto& file = *current_content_file;
@ -550,8 +571,11 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
if ((tmd.GetContentTypeByIndex(i) & FileSys::TMDContentTypeFlag::Encrypted) != 0) { if ((tmd.GetContentTypeByIndex(i) & FileSys::TMDContentTypeFlag::Encrypted) != 0) {
if (!decryption_authorized) { if (!decryption_authorized) {
LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation.");
return Result(ErrorDescription::NotAuthorized, ErrorModule::AM, current_content_install_result.result =
ErrorSummary::InvalidState, ErrorLevel::Permanent); 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()); decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
} }
@ -559,8 +583,11 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
file.Write(temp.data(), temp.size()); file.Write(temp.data(), temp.size());
if (file.IsError()) { if (file.IsError()) {
// This can never happen in real HW // This can never happen in real HW
return Result(ErrCodes::InvalidImportState, ErrorModule::AM, current_content_install_result.result =
ErrorSummary::InvalidState, ErrorLevel::Permanent); 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 // Keep tabs on how much of this content ID has been written so new range_min
@ -623,14 +650,16 @@ ResultVal<std::size_t> 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 // The end of our TMD is at the beginning of Content data, so ensure we have that much
// buffered before trying to parse. // buffered before trying to parse.
if (written >= container.GetContentOffset() && install_state != CIAInstallState::TMDLoaded) { if (written >= container.GetContentOffset() && install_state != CIAInstallState::TMDLoaded) {
auto result = WriteTicket(); InstallResult result = WriteTicket();
if (result.IsError()) { install_results.push_back(result);
return result; if (result.result.IsError()) {
return result.result;
} }
result = WriteTitleMetadata(data, container.GetTitleMetadataOffset()); result = WriteTitleMetadata(data, container.GetTitleMetadataOffset());
if (result.IsError()) { install_results.push_back(result);
return result; if (result.result.IsError()) {
return result.result;
} }
} }
@ -642,6 +671,7 @@ ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush
// From this point forward, data will no longer be buffered in data // From this point forward, data will no longer be buffered in data
auto result = WriteContentData(offset, length, buffer); auto result = WriteContentData(offset, length, buffer);
if (result.Failed()) { if (result.Failed()) {
current_content_install_result.type = InstallResult::Type::NONE;
return result; return result;
} }
@ -756,10 +786,19 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
tmd.GetContentSizeByIndex(content_index) - content_written[content_index]; tmd.GetContentSizeByIndex(content_index) - content_written[content_index];
if (content_index != current_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_index = content_index;
current_content_file = std::make_unique<NCCHCryptoFile>(content_file_paths[content_index], current_content_file = std::make_unique<NCCHCryptoFile>(content_file_paths[content_index],
decryption_authorized); decryption_authorized);
current_content_file->decryption_authorized = 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; auto& file = *current_content_file;
@ -768,8 +807,11 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
if ((tmd.GetContentTypeByIndex(content_index) & FileSys::TMDContentTypeFlag::Encrypted) != 0) { if ((tmd.GetContentTypeByIndex(content_index) & FileSys::TMDContentTypeFlag::Encrypted) != 0) {
if (!decryption_authorized) { if (!decryption_authorized) {
LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation.");
return Result(ErrorDescription::NotAuthorized, ErrorModule::AM, current_content_install_result.result =
ErrorSummary::InvalidState, ErrorLevel::Permanent); 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()); decryption_state->content[content_index].ProcessData(temp.data(), temp.data(), temp.size());
} }
@ -777,8 +819,11 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
file.Write(temp.data(), temp.size()); file.Write(temp.data(), temp.size());
if (file.IsError()) { if (file.IsError()) {
// This can never happen in real HW // This can never happen in real HW
return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, current_content_install_result.result =
ErrorLevel::Permanent); 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(); content_written[content_index] += temp.size();
@ -801,6 +846,12 @@ bool CIAFile::Close() {
return true; return true;
is_closed = 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 = bool complete =
from_cdn ? is_done from_cdn ? is_done
: (install_state >= CIAInstallState::TMDLoaded && : (install_state >= CIAInstallState::TMDLoaded &&
@ -824,8 +875,15 @@ bool CIAFile::Close() {
// Clean up older content data if we installed newer content on top // Clean up older content data if we installed newer content on top
std::string old_tmd_path = std::string old_tmd_path =
GetTitleMetadataPath(media_type, container.GetTitleMetadata().GetTitleID(), false); GetTitleMetadataPath(media_type, container.GetTitleMetadata().GetTitleID(), false);
std::string new_tmd_path = std::string new_tmd_path = old_tmd_path;
GetTitleMetadataPath(media_type, container.GetTitleMetadata().GetTitleID(), true);
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) { if (FileUtil::Exists(new_tmd_path) && old_tmd_path != new_tmd_path) {
FileSys::TitleMetadata old_tmd; FileSys::TitleMetadata old_tmd;
FileSys::TitleMetadata new_tmd; FileSys::TitleMetadata new_tmd;
@ -954,7 +1012,7 @@ bool TMDFile::Close() {
void TMDFile::Flush() const {} void TMDFile::Flush() const {}
Result TMDFile::Commit() { Result TMDFile::Commit() {
return importing_title->cia_file.WriteTitleMetadata(data, 0); return importing_title->cia_file.WriteTitleMetadata(data, 0).result;
} }
ContentFile::~ContentFile() { ContentFile::~ContentFile() {
@ -1041,39 +1099,29 @@ InstallStatus InstallCIA(const std::string& path,
} }
installFile.Close(); installFile.Close();
LOG_INFO(Service_AM, "Installed {} successfully.", path); InstallStatus install_res = InstallStatus::Success;
for (auto result : installFile.GetInstallResults()) {
const FileUtil::DirectoryEntryCallable callback = if (result.type != CIAFile::InstallResult::Type::APP || result.result.IsError()) {
[&callback](u64* num_entries_out, const std::string& directory, continue;
const std::string& virtual_name) -> bool { }
const std::string physical_name = directory + DIR_SEP + virtual_name;
const bool is_dir = FileUtil::IsDirectory(physical_name); std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(result.install_full_path);
if (!is_dir) { if (!loader) {
std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(physical_name); continue;
if (!loader) { }
return true;
} bool executable = false;
const auto res = loader->IsExecutable(executable);
bool executable = false; if (res == Loader::ResultStatus::ErrorEncrypted) {
const auto res = loader->IsExecutable(executable); LOG_ERROR(Service_AM, "CIA contains encrypted content: {}", path,
if (res == Loader::ResultStatus::ErrorEncrypted) { result.install_full_path);
return false; install_res = InstallStatus::ErrorEncrypted;
}
return true;
} else {
return FileUtil::ForeachDirectoryEntry(nullptr, physical_name, callback);
} }
};
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); LOG_ERROR(Service_AM, "CIA file {} is invalid!", path);

View File

@ -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. // A file handled returned for CIAs to be written into and subsequently installed.
class CIAFile final : public FileSys::FileBackend { class CIAFile final : public FileSys::FileBackend {
public: 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, explicit CIAFile(Core::System& system_, Service::FS::MediaType media_type,
bool from_cdn = false); bool from_cdn = false);
~CIAFile(); ~CIAFile();
ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override; ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
Result WriteTicket(); InstallResult WriteTicket();
Result WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t offset); InstallResult WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t offset);
ResultVal<std::size_t> WriteContentData(u64 offset, std::size_t length, const u8* buffer); ResultVal<std::size_t> WriteContentData(u64 offset, std::size_t length, const u8* buffer);
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override; const u8* buffer) override;
@ -202,6 +215,10 @@ public:
is_done = true; is_done = true;
} }
const std::vector<InstallResult>& GetInstallResults() const {
return install_results;
}
private: private:
friend void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx); friend void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx);
Core::System& system; Core::System& system;
@ -227,6 +244,8 @@ private:
std::vector<std::string> content_file_paths; std::vector<std::string> content_file_paths;
u16 current_content_index = -1; u16 current_content_index = -1;
std::unique_ptr<NCCHCryptoFile> current_content_file; std::unique_ptr<NCCHCryptoFile> current_content_file;
InstallResult current_content_install_result{};
std::vector<InstallResult> install_results;
Service::FS::MediaType media_type; Service::FS::MediaType media_type;
class DecryptionState; class DecryptionState;