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;
}
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<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);
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<const u8> 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<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.
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<u16>(i);
current_content_file =
std::make_unique<NCCHCryptoFile>(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<std::size_t> 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<std::size_t> 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<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
// 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<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
auto result = WriteContentData(offset, length, buffer);
if (result.Failed()) {
current_content_install_result.type = InstallResult::Type::NONE;
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];
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<NCCHCryptoFile>(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<std::size_t> 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<std::size_t> 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::AppLoader> 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::AppLoader> 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);

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.
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<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
Result WriteTicket();
Result WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t offset);
InstallResult WriteTicket();
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> 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<InstallResult>& 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<std::string> content_file_paths;
u16 current_content_index = -1;
std::unique_ptr<NCCHCryptoFile> current_content_file;
InstallResult current_content_install_result{};
std::vector<InstallResult> install_results;
Service::FS::MediaType media_type;
class DecryptionState;