From 8ad53ca70572dd7b08e99dac514e2db2cb3fdef6 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Sat, 1 Mar 2025 13:12:52 +0100 Subject: [PATCH] Encrypt downloaded apps with a console unique key --- .../configuration/configure_debug.cpp | 4 +- .../configuration/configure_system.cpp | 4 +- src/citra_qt/multiplayer/host_room.cpp | 9 +- src/citra_qt/multiplayer/lobby.cpp | 6 +- src/common/CMakeLists.txt | 2 +- src/common/file_util.cpp | 111 +++++++++- src/common/file_util.h | 86 ++++++-- src/core/cheats/gateway_cheat.cpp | 4 +- src/core/file_sys/ncch_container.cpp | 193 +++++++++++------- src/core/file_sys/ncch_container.h | 6 +- src/core/file_sys/romfs_reader.cpp | 8 +- src/core/file_sys/romfs_reader.h | 9 +- src/core/hle/service/am/am.cpp | 53 +++-- src/core/hle/service/am/am.h | 3 +- src/core/hw/unique_data.cpp | 32 +++ src/core/hw/unique_data.h | 11 +- src/core/loader/3dsx.cpp | 7 +- src/core/loader/ncch.cpp | 16 +- src/video_core/pica/regs_shader.h | 5 +- 19 files changed, 427 insertions(+), 142 deletions(-) diff --git a/src/citra_qt/configuration/configure_debug.cpp b/src/citra_qt/configuration/configure_debug.cpp index 8f4af8bc2..c1410002a 100644 --- a/src/citra_qt/configuration/configure_debug.cpp +++ b/src/citra_qt/configuration/configure_debug.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -126,7 +126,7 @@ void ConfigureDebug::SetConfiguration() { void ConfigureDebug::ApplyConfiguration() { Settings::values.use_gdbstub = ui->toggle_gdbstub->isChecked(); - Settings::values.gdbstub_port = ui->gdbport_spinbox->value(); + Settings::values.gdbstub_port = static_cast(ui->gdbport_spinbox->value()); UISettings::values.show_console = ui->toggle_console->isChecked(); Settings::values.log_filter = ui->log_filter_edit->text().toStdString(); Settings::values.log_regex_filter = ui->log_regex_filter_edit->text().toStdString(); diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index 3b532f595..f7e5b2ef8 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -412,7 +412,7 @@ void ConfigureSystem::ApplyConfiguration() { s32 new_birthmonth = ui->combo_birthmonth->currentIndex() + 1; s32 new_birthday = ui->combo_birthday->currentIndex() + 1; if (birthmonth != new_birthmonth || birthday != new_birthday) { - cfg->SetBirthday(new_birthmonth, new_birthday); + cfg->SetBirthday(static_cast(new_birthmonth), static_cast(new_birthday)); modified = true; } diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp index 3a8e31423..339e7155a 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/citra_qt/multiplayer/host_room.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -142,8 +142,8 @@ void HostRoomWindow::Host() { } if (auto room = Network::GetRoom().lock()) { bool created = room->Create(ui->room_name->text().toStdString(), - ui->room_description->toPlainText().toStdString(), "", port, - password, ui->max_player->value(), + ui->room_description->toPlainText().toStdString(), "", + static_cast(port), password, ui->max_player->value(), NetSettings::values.citra_username, game_name.toStdString(), game_id, CreateVerifyBackend(is_public), ban_list); if (!created) { @@ -193,7 +193,8 @@ void HostRoomWindow::Host() { } #endif member->Join(ui->username->text().toStdString(), Service::CFG::GetConsoleIdHash(system), - "127.0.0.1", port, 0, Network::NoPreferredMac, password, token); + "127.0.0.1", static_cast(port), 0, Network::NoPreferredMac, password, + token); // Store settings UISettings::values.room_nickname = ui->username->text(); diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/citra_qt/multiplayer/lobby.cpp index a512096e2..7a7f032c1 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/citra_qt/multiplayer/lobby.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -174,8 +174,8 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { } #endif if (auto room_member = Network::GetRoomMember().lock()) { - room_member->Join(nickname, Service::CFG::GetConsoleIdHash(system), ip.c_str(), port, 0, - Network::NoPreferredMac, password, token); + room_member->Join(nickname, Service::CFG::GetConsoleIdHash(system), ip.c_str(), + static_cast(port), 0, Network::NoPreferredMac, password, token); } }); watcher->setFuture(f); diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 85acf1c7a..1bf1c2182 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -187,7 +187,7 @@ endif() create_target_directory_groups(citra_common) target_link_libraries(citra_common PUBLIC fmt library-headers microprofile Boost::boost Boost::serialization Boost::iostreams) -target_link_libraries(citra_common PRIVATE zstd) +target_link_libraries(citra_common PRIVATE cryptopp zstd) if ("x86_64" IN_LIST ARCHITECTURE) target_link_libraries(citra_common PRIVATE xbyak) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index f6e1acfb9..68ef8e7f1 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -1,4 +1,4 @@ -// Copyright Citra Emulator Project / Lime3DS Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -13,7 +13,10 @@ #include #include #include +#include +#include #include +#include "common/archives.h" #include "common/assert.h" #include "common/common_funcs.h" #include "common/common_paths.h" @@ -1136,7 +1139,7 @@ u64 IOFile::GetSize() const { return 0; } -bool IOFile::Seek(s64 off, int origin) { +bool IOFile::SeekImpl(s64 off, int origin) { if (!IsOpen() || 0 != fseeko(m_file, off, origin)) m_good = false; @@ -1240,6 +1243,107 @@ bool IOFile::Resize(u64 size) { return m_good; } +struct CryptoIOFileImpl { + + std::vector key; + std::vector iv; + + CryptoPP::CTR_Mode::Decryption d; + CryptoPP::CTR_Mode::Encryption e; + + std::vector write_buffer; + + std::size_t ReadImpl(CryptoIOFile& f, void* data, std::size_t length, std::size_t data_size) { + std::size_t res = f.IOFile::ReadImpl(data, length, data_size); + if (res != std::numeric_limits::max() && res != 0) { + d.ProcessData(reinterpret_cast(data), + reinterpret_cast(data), length * data_size); + e.Seek(f.IOFile::Tell()); + } + return res; + } + + std::size_t ReadAtImpl(CryptoIOFile& f, void* data, std::size_t length, std::size_t data_size, + std::size_t offset) { + std::size_t res = f.IOFile::ReadAtImpl(data, length, data_size, offset); + if (res != std::numeric_limits::max() && res != 0) { + d.Seek(offset); + d.ProcessData(reinterpret_cast(data), + reinterpret_cast(data), length * data_size); + e.Seek(f.IOFile::Tell()); + } + return res; + } + + std::size_t WriteImpl(CryptoIOFile& f, const void* data, std::size_t length, + std::size_t data_size) { + if (write_buffer.size() < length * data_size) { + write_buffer.resize(length * data_size); + } + e.ProcessData(write_buffer.data(), reinterpret_cast(data), + length * data_size); + std::size_t res = f.IOFile::WriteImpl(write_buffer.data(), length, data_size); + if (res != std::numeric_limits::max() && res != 0) { + d.Seek(f.IOFile::Tell()); + } + return res; + } + + bool SeekImpl(CryptoIOFile& f, s64 off, int origin) { + bool res = f.IOFile::SeekImpl(off, origin); + if (res) { + u64 pos = f.IOFile::Tell(); + d.Seek(pos); + e.Seek(pos); + } + return res; + } +}; + +CryptoIOFile::CryptoIOFile() : IOFile() { + impl = std::make_unique(); +} + +CryptoIOFile::CryptoIOFile(const std::string& filename, const char openmode[], + const std::vector& aes_key, const std::vector& aes_iv, int flags) + : IOFile(filename, openmode, flags) { + impl = std::make_unique(); + impl->key = aes_key; + impl->iv = aes_iv; + impl->d.SetKeyWithIV(aes_key.data(), aes_key.size(), aes_iv.data()); + impl->e.SetKeyWithIV(aes_key.data(), aes_key.size(), aes_iv.data()); +} + +CryptoIOFile::~CryptoIOFile() {} + +std::size_t CryptoIOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) { + return impl->ReadImpl(*this, data, length, data_size); +} + +std::size_t CryptoIOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size, + std::size_t offset) { + return impl->ReadAtImpl(*this, data, length, data_size, offset); +} + +std::size_t CryptoIOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { + return impl->WriteImpl(*this, data, length, data_size); +} + +bool CryptoIOFile::SeekImpl(s64 off, int origin) { + return impl->SeekImpl(*this, off, origin); +} + +template +void CryptoIOFile::serialize(Archive& ar, const unsigned int) { + ar & impl->key; + ar & impl->iv; + if (Archive::is_loading::value) { + impl->e.SetKeyWithIV(impl->key.data(), impl->key.size(), impl->iv.data()); + impl->d.SetKeyWithIV(impl->key.data(), impl->key.size(), impl->iv.data()); + } + ar& boost::serialization::base_object(*this); +} + template using boost_iostreams = boost::iostreams::stream; @@ -1271,3 +1375,6 @@ void OpenFStream( fstream.open(file_descriptor_sink); } } // namespace FileUtil + +SERIALIZE_EXPORT_IMPL(FileUtil::IOFile) +SERIALIZE_EXPORT_IMPL(FileUtil::CryptoIOFile) diff --git a/src/common/file_util.h b/src/common/file_util.h index a1d5ef772..51ce3d7f2 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -1,4 +1,4 @@ -// Copyright Citra Emulator Project / Lime3DS Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -12,14 +12,18 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #include #include +#include #include #include "common/common_types.h" #ifdef _MSC_VER @@ -267,6 +271,8 @@ enum class DirectorySeparator { std::string_view path, DirectorySeparator directory_separator = DirectorySeparator::ForwardSlash); +struct CryptoIOFileImpl; + // simple wrapper for cstdlib file functions to // hopefully will make error checking easier // and make forgetting an fclose() harder @@ -279,7 +285,7 @@ public: // isn't considered "locked" while citra is open and people can open the log file and view it IOFile(const std::string& filename, const char openmode[], int flags = 0); - ~IOFile(); + virtual ~IOFile(); IOFile(IOFile&& other) noexcept; IOFile& operator=(IOFile&& other) noexcept; @@ -369,14 +375,10 @@ public: * @returns Count of T data successfully read. */ template - [[nodiscard]] size_t ReadSpan(std::span data) const { + [[nodiscard]] size_t ReadSpan(std::span data) { static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); - if (!IsOpen()) { - return 0; - } - - return std::fread(data.data(), sizeof(T), data.size(), m_file); + return ReadImpl(data.data(), data.size(), sizeof(T)); } /** @@ -395,14 +397,10 @@ public: * @returns Count of T data successfully written. */ template - [[nodiscard]] size_t WriteSpan(std::span data) const { + [[nodiscard]] size_t WriteSpan(std::span data) { static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); - if (!IsOpen()) { - return 0; - } - - return std::fwrite(data.data(), sizeof(T), data.size(), m_file); + return WriteImpl(data.data(), data.size(), sizeof(T)); } [[nodiscard]] bool IsOpen() const { @@ -426,7 +424,9 @@ public: return IsGood(); } - bool Seek(s64 off, int origin); + bool Seek(s64 off, int origin) { + return SeekImpl(off, origin); + } [[nodiscard]] u64 Tell() const; [[nodiscard]] u64 GetSize() const; bool Resize(u64 size); @@ -438,12 +438,24 @@ public: std::clearerr(m_file); } -private: - std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size); - std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset); - std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size); + virtual bool IsCrypto() { + return false; + } + const std::string& Filename() const { + return filename; + } + +protected: + friend struct CryptoIOFileImpl; + virtual std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size); + virtual std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, + std::size_t offset); + virtual std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size); + + virtual bool SeekImpl(s64 off, int origin); + +private: bool Open(); std::FILE* m_file = nullptr; @@ -472,6 +484,37 @@ private: friend class boost::serialization::access; }; +class CryptoIOFile : public IOFile { +public: + CryptoIOFile(); + + // flags is used for windows specific file open mode flags, which + // allows citra to open the logs in shared write mode, so that the file + // isn't considered "locked" while citra is open and people can open the log file and view it + CryptoIOFile(const std::string& filename, const char openmode[], const std::vector& aes_key, + const std::vector& aes_iv, int flags = 0); + + bool IsCrypto() override { + return true; + } + + ~CryptoIOFile() override; + +private: + std::unique_ptr impl; + + std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) override; + std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, + std::size_t offset) override; + std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size) override; + + bool SeekImpl(s64 off, int origin) override; + + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; + template void OpenFStream(T& fstream, const std::string& filename); } // namespace FileUtil @@ -485,3 +528,6 @@ void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmod fstream.open(filename, openmode); #endif } + +BOOST_CLASS_EXPORT_KEY(FileUtil::IOFile) +BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile) \ No newline at end of file diff --git a/src/core/cheats/gateway_cheat.cpp b/src/core/cheats/gateway_cheat.cpp index 4f9456699..b30fda542 100644 --- a/src/core/cheats/gateway_cheat.cpp +++ b/src/core/cheats/gateway_cheat.cpp @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -174,7 +174,7 @@ static inline void PatchOp(const GatewayCheat::CheatLine& line, State& state, Co u32 tmp = (first ? cheat_lines[state.current_line_nr].first : cheat_lines[state.current_line_nr].value) >> bit_offset; - system.Memory().Write8(addr, tmp); + system.Memory().Write8(addr, static_cast(tmp)); addr += 1; num_bytes -= 1; bit_offset += 8; diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 9e8066e95..a28ba8f38 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -16,6 +16,7 @@ #include "core/file_sys/patch.h" #include "core/file_sys/seed_db.h" #include "core/hw/aes/key.h" +#include "core/hw/unique_data.h" #include "core/loader/loader.h" namespace FileSys { @@ -112,7 +113,7 @@ static bool LZSS_Decompress(std::span compressed, std::span decomp NCCHContainer::NCCHContainer(const std::string& filepath, u32 ncch_offset, u32 partition) : ncch_offset(ncch_offset), partition(partition), filepath(filepath) { - file = FileUtil::IOFile(filepath, "rb"); + file = std::make_unique(filepath, "rb"); } Loader::ResultStatus NCCHContainer::OpenFile(const std::string& filepath_, u32 ncch_offset_, @@ -120,9 +121,9 @@ Loader::ResultStatus NCCHContainer::OpenFile(const std::string& filepath_, u32 n filepath = filepath_; ncch_offset = ncch_offset_; partition = partition_; - file = FileUtil::IOFile(filepath_, "rb"); + file = std::make_unique(filepath_, "rb"); - if (!file.IsOpen()) { + if (!file->IsOpen()) { LOG_WARNING(Service_FS, "Failed to open {}", filepath); return Loader::ResultStatus::Error; } @@ -135,33 +136,47 @@ Loader::ResultStatus NCCHContainer::LoadHeader() { if (has_header) { return Loader::ResultStatus::Success; } - if (!file.IsOpen()) { - return Loader::ResultStatus::Error; + + for (int i = 0; i < 2; i++) { + if (!file->IsOpen()) { + return Loader::ResultStatus::Error; + } + + // Reset read pointer in case this file has been read before. + file->Seek(ncch_offset, SEEK_SET); + + if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) { + return Loader::ResultStatus::Error; + } + + // Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)... + if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) { + NCSD_Header ncsd_header; + file->Seek(ncch_offset, SEEK_SET); + file->ReadBytes(&ncsd_header, sizeof(NCSD_Header)); + ASSERT(Loader::MakeMagic('N', 'C', 'S', 'D') == ncsd_header.magic); + ASSERT(partition < 8); + ncch_offset = ncsd_header.partitions[partition].offset * kBlockSize; + LOG_ERROR(Service_FS, "{}", ncch_offset); + file->Seek(ncch_offset, SEEK_SET); + file->ReadBytes(&ncch_header, sizeof(NCCH_Header)); + } + + // Verify we are loading the correct file type... + if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { + // We may be loading a crypto file, try again + if (i == 0) { + file.reset(); + file = HW::UniqueData::OpenUniqueCryptoFile( + filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); + } else { + return Loader::ResultStatus::ErrorInvalidFormat; + } + } } - // Reset read pointer in case this file has been read before. - file.Seek(ncch_offset, SEEK_SET); - - if (file.ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) { - return Loader::ResultStatus::Error; - } - - // Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)... - if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) { - NCSD_Header ncsd_header; - file.Seek(ncch_offset, SEEK_SET); - file.ReadBytes(&ncsd_header, sizeof(NCSD_Header)); - ASSERT(Loader::MakeMagic('N', 'C', 'S', 'D') == ncsd_header.magic); - ASSERT(partition < 8); - ncch_offset = ncsd_header.partitions[partition].offset * kBlockSize; - LOG_ERROR(Service_FS, "{}", ncch_offset); - file.Seek(ncch_offset, SEEK_SET); - file.ReadBytes(&ncch_header, sizeof(NCCH_Header)); - } - - // Verify we are loading the correct file type... - if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { - return Loader::ResultStatus::ErrorInvalidFormat; + if (file->IsCrypto()) { + LOG_DEBUG(Service_FS, "NCCH file has console unique crypto"); } has_header = true; @@ -174,30 +189,45 @@ Loader::ResultStatus NCCHContainer::Load() { int block_size = kBlockSize; - if (file.IsOpen()) { - size_t file_size = file.GetSize(); + if (file->IsOpen()) { + size_t file_size; - // Reset read pointer in case this file has been read before. - file.Seek(ncch_offset, SEEK_SET); + for (int i = 0; i < 2; i++) { + file_size = file->GetSize(); - if (file.ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) - return Loader::ResultStatus::Error; + // Reset read pointer in case this file has been read before. + file->Seek(ncch_offset, SEEK_SET); - // Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)... - if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) { - NCSD_Header ncsd_header; - file.Seek(ncch_offset, SEEK_SET); - file.ReadBytes(&ncsd_header, sizeof(NCSD_Header)); - ASSERT(Loader::MakeMagic('N', 'C', 'S', 'D') == ncsd_header.magic); - ASSERT(partition < 8); - ncch_offset = ncsd_header.partitions[partition].offset * kBlockSize; - file.Seek(ncch_offset, SEEK_SET); - file.ReadBytes(&ncch_header, sizeof(NCCH_Header)); + if (file->ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) + return Loader::ResultStatus::Error; + + // Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)... + if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) { + NCSD_Header ncsd_header; + file->Seek(ncch_offset, SEEK_SET); + file->ReadBytes(&ncsd_header, sizeof(NCSD_Header)); + ASSERT(Loader::MakeMagic('N', 'C', 'S', 'D') == ncsd_header.magic); + ASSERT(partition < 8); + ncch_offset = ncsd_header.partitions[partition].offset * kBlockSize; + file->Seek(ncch_offset, SEEK_SET); + file->ReadBytes(&ncch_header, sizeof(NCCH_Header)); + } + + // Verify we are loading the correct file type... + if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { + // We may be loading a crypto file, try again + if (i == 0) { + file = HW::UniqueData::OpenUniqueCryptoFile( + filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); + } else { + return Loader::ResultStatus::ErrorInvalidFormat; + } + } } - // Verify we are loading the correct file type... - if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) - return Loader::ResultStatus::ErrorInvalidFormat; + if (file->IsCrypto()) { + LOG_DEBUG(Service_FS, "NCCH file has console unique crypto"); + } has_header = true; @@ -215,12 +245,12 @@ Loader::ResultStatus NCCHContainer::Load() { // System archives and DLC don't have an extended header but have RomFS // Proto apps don't have an ext header size if (ncch_header.extended_header_size || is_proto) { - auto read_exheader = [this](FileUtil::IOFile& file) { + auto read_exheader = [this](FileUtil::IOFile* file) { const std::size_t size = sizeof(exheader_header); - return file && file.ReadBytes(&exheader_header, size) == size; + return file && file->ReadBytes(&exheader_header, size) == size; }; - if (!read_exheader(file)) { + if (!read_exheader(file.get())) { return Loader::ResultStatus::Error; } @@ -235,7 +265,7 @@ Loader::ResultStatus NCCHContainer::Load() { bool has_exheader_override = false; for (const auto& path : exheader_override_paths) { FileUtil::IOFile exheader_override_file{path, "rb"}; - if (read_exheader(exheader_override_file)) { + if (read_exheader(&exheader_override_file)) { has_exheader_override = true; break; } @@ -289,11 +319,17 @@ Loader::ResultStatus NCCHContainer::Load() { LOG_DEBUG(Service_FS, "ExeFS offset: 0x{:08X}", exefs_offset); LOG_DEBUG(Service_FS, "ExeFS size: 0x{:08X}", exefs_size); - file.Seek(exefs_offset + ncch_offset, SEEK_SET); - if (file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) + file->Seek(exefs_offset + ncch_offset, SEEK_SET); + if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) return Loader::ResultStatus::Error; - exefs_file = FileUtil::IOFile(filepath, "rb"); + if (file->IsCrypto()) { + exefs_file = HW::UniqueData::OpenUniqueCryptoFile( + filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); + } else { + exefs_file = std::make_unique(filepath, "rb"); + } + has_exefs = true; } @@ -322,15 +358,15 @@ Loader::ResultStatus NCCHContainer::LoadOverrides() { std::string exefs_override = filepath + ".exefs"; std::string exefsdir_override = filepath + ".exefsdir/"; if (FileUtil::Exists(exefs_override)) { - exefs_file = FileUtil::IOFile(exefs_override, "rb"); + exefs_file = std::make_unique(exefs_override, "rb"); - if (exefs_file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) == sizeof(ExeFs_Header)) { + if (exefs_file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) == sizeof(ExeFs_Header)) { LOG_DEBUG(Service_FS, "Loading ExeFS section from {}", exefs_override); exefs_offset = 0; is_tainted = true; has_exefs = true; } else { - exefs_file = FileUtil::IOFile(filepath, "rb"); + exefs_file = std::make_unique(filepath, "rb"); } } else if (FileUtil::Exists(exefsdir_override) && FileUtil::IsDirectory(exefsdir_override)) { is_tainted = true; @@ -385,9 +421,9 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect std::size_t logo_size = ncch_header.logo_region_size * block_size; buffer.resize(logo_size); - file.Seek(ncch_offset + logo_offset, SEEK_SET); + file->Seek(ncch_offset + logo_offset, SEEK_SET); - if (file.ReadBytes(buffer.data(), logo_size) != logo_size) { + if (file->ReadBytes(buffer.data(), logo_size) != logo_size) { LOG_ERROR(Service_FS, "Could not read NCCH logo"); return Loader::ResultStatus::Error; } @@ -398,7 +434,7 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect } // If we don't have any separate files, we'll need a full ExeFS - if (!exefs_file.IsOpen()) + if (!exefs_file->IsOpen()) return Loader::ResultStatus::Error; LOG_DEBUG(Service_FS, "{} sections:", kMaxSections); @@ -414,14 +450,14 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect s64 section_offset = is_proto ? section.offset : (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset); - exefs_file.Seek(section_offset, SEEK_SET); + exefs_file->Seek(section_offset, SEEK_SET); size_t section_size = is_proto ? Common::AlignUp(section.size, 0x10) : section.size; if (strcmp(section.name, ".code") == 0 && is_compressed) { // Section is compressed, read compressed .code section... std::vector temp_buffer(section_size); - if (exefs_file.ReadBytes(temp_buffer.data(), temp_buffer.size()) != + if (exefs_file->ReadBytes(temp_buffer.data(), temp_buffer.size()) != temp_buffer.size()) return Loader::ResultStatus::Error; @@ -433,7 +469,7 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect } else { // Section is uncompressed... buffer.resize(section_size); - if (exefs_file.ReadBytes(buffer.data(), section_size) != section_size) + if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size) return Loader::ResultStatus::Error; } @@ -552,7 +588,7 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf return Loader::ResultStatus::ErrorNotUsed; } - if (!file.IsOpen()) + if (!file->IsOpen()) return Loader::ResultStatus::Error; u32 romfs_offset = ncch_offset + (ncch_header.romfs_offset * block_size) + 0x1000; @@ -561,17 +597,22 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf LOG_DEBUG(Service_FS, "RomFS offset: 0x{:08X}", romfs_offset); LOG_DEBUG(Service_FS, "RomFS size: 0x{:08X}", romfs_size); - if (file.GetSize() < romfs_offset + romfs_size) + if (file->GetSize() < romfs_offset + romfs_size) return Loader::ResultStatus::Error; // We reopen the file, to allow its position to be independent from file's - FileUtil::IOFile romfs_file_inner(filepath, "rb"); - if (!romfs_file_inner.IsOpen()) + std::unique_ptr romfs_file_inner; + if (file->IsCrypto()) { + romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile( + filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); + } else { + romfs_file_inner = std::make_unique(filepath, "rb"); + } + + if (!romfs_file_inner->IsOpen()) return Loader::ResultStatus::Error; - std::shared_ptr direct_romfs; - - direct_romfs = + std::shared_ptr direct_romfs = std::make_shared(std::move(romfs_file_inner), romfs_offset, romfs_size); const auto path = @@ -590,6 +631,9 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf } Loader::ResultStatus NCCHContainer::DumpRomFS(const std::string& target_path) { + if (file->IsCrypto()) + return Loader::ResultStatus::ErrorEncrypted; + std::shared_ptr direct_romfs; Loader::ResultStatus result = ReadRomFS(direct_romfs, false); if (result != Loader::ResultStatus::Success) @@ -608,12 +652,13 @@ Loader::ResultStatus NCCHContainer::ReadOverrideRomFS(std::shared_ptr romfs_file_inner = + std::make_unique(split_filepath, "rb"); + if (romfs_file_inner->IsOpen()) { LOG_WARNING(Service_FS, "File {} overriding built-in RomFS; LayeredFS not enabled", split_filepath); romfs_file = std::make_shared(std::move(romfs_file_inner), 0, - romfs_file_inner.GetSize()); + romfs_file_inner->GetSize()); return Loader::ResultStatus::Success; } } diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index 17358bb71..214f91998 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -353,8 +353,8 @@ private: u32 partition = 0; std::string filepath; - FileUtil::IOFile file; - FileUtil::IOFile exefs_file; + std::unique_ptr file; + std::unique_ptr exefs_file; }; } // namespace FileSys diff --git a/src/core/file_sys/romfs_reader.cpp b/src/core/file_sys/romfs_reader.cpp index 6bd33521e..7f847efde 100644 --- a/src/core/file_sys/romfs_reader.cpp +++ b/src/core/file_sys/romfs_reader.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + #include #include #include @@ -24,7 +28,7 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length, // Skip cache if the read is too big if (segments.size() == 1 && segments[0].second > cache_line_size) { - length = file.ReadAtBytes(buffer, length, file_offset + offset); + length = file->ReadAtBytes(buffer, length, file_offset + offset); LOG_TRACE(Service_FS, "RomFS Cache SKIP: offset={}, length={}", offset, length); return length; } @@ -38,7 +42,7 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length, auto cache_entry = cache.request(page); if (!cache_entry.first) { // If not found, read from disk and cache the data - read_size = file.ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page); + read_size = file->ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page); LOG_TRACE(Service_FS, "RomFS Cache MISS: page={}, length={}, into={}", page, seg.second, (seg.first - page)); } else { diff --git a/src/core/file_sys/romfs_reader.h b/src/core/file_sys/romfs_reader.h index ccedd96e5..a427c77d0 100644 --- a/src/core/file_sys/romfs_reader.h +++ b/src/core/file_sys/romfs_reader.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + #pragma once #include @@ -41,7 +45,8 @@ private: */ class DirectRomFSReader : public RomFSReader { public: - DirectRomFSReader(FileUtil::IOFile&& file, std::size_t file_offset, std::size_t data_size) + DirectRomFSReader(std::unique_ptr&& file, std::size_t file_offset, + std::size_t data_size) : file(std::move(file)), file_offset(file_offset), data_size(data_size) {} ~DirectRomFSReader() override = default; @@ -57,7 +62,7 @@ public: bool CacheReady(std::size_t file_offset, std::size_t length) override; private: - FileUtil::IOFile file; + std::unique_ptr file; u64 file_offset; u64 data_size; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 7b38bbe28..29c7bc6c3 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -95,13 +95,24 @@ public: }; NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file) { - file = FileUtil::IOFile(out_file, "wb"); + // A console unique crypto file is used to store the decrypted NCCH file. This is done + // to prevent Azahar being used as a tool to download easy shareable decrypted contents + // from the eshop. + file = HW::UniqueData::OpenUniqueCryptoFile(out_file, "wb", + HW::UniqueData::UniqueCryptoFileID::NCCH); + if (!file->IsOpen()) { + is_error = true; + } } void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { if (is_error) return; + if (is_not_ncch) { + file->WriteBytes(buffer, length); + } + const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes) if (header_size != sizeof(NCCH_Header)) { @@ -114,7 +125,10 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { if (!header_parsed && header_size == sizeof(NCCH_Header)) { if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { - is_error = true; + // Most likely DS contents, store without additional operations + is_not_ncch = true; + file->WriteBytes(&ncch_header, sizeof(ncch_header)); + file->WriteBytes(buffer, length); return; } @@ -278,7 +292,7 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { u8 prev_crypto = ncch_header.no_crypto; ncch_header.no_crypto.Assign(1); - file.WriteBytes(&ncch_header, sizeof(ncch_header)); + file->WriteBytes(&ncch_header, sizeof(ncch_header)); written += sizeof(ncch_header); ncch_header.no_crypto.Assign(prev_crypto); } @@ -305,7 +319,7 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { if (reg == nullptr) { // This file has no encryption size_t to_write = length; - file.WriteBytes(buffer, to_write); + file->WriteBytes(buffer, to_write); written += to_write; buffer += to_write; length -= to_write; @@ -313,7 +327,7 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { if (written < reg->offset) { // Not inside a crypto region size_t to_write = std::min(length, reg->offset - written); - file.WriteBytes(buffer, to_write); + file->WriteBytes(buffer, to_write); written += to_write; buffer += to_write; length -= to_write; @@ -347,7 +361,7 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { d.Seek(offset); } d.ProcessData(temp.data(), buffer, to_write); - file.WriteBytes(temp.data(), to_write); + file->WriteBytes(temp.data(), to_write); if (reg->type == CryptoRegion::EXEFS_HDR) { if (exefs_header_written != sizeof(ExeFs_Header)) { @@ -376,7 +390,7 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { } } } else { - file.WriteBytes(buffer, to_write); + file->WriteBytes(buffer, to_write); } written += to_write; buffer += to_write; @@ -538,7 +552,6 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, // has been written since we might get a written buffer which contains multiple .app // contents or only part of a larger .app's contents. const u64 offset_max = offset + length; - bool success = true; for (std::size_t i = 0; i < content_written.size(); i++) { if (content_written[i] < container.GetContentSize(i)) { // The size, minimum unwritten offset, and maximum unwritten offset of this content @@ -573,8 +586,11 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, } file.Write(temp.data(), temp.size()); - if (file.IsError()) - success = false; + if (file.IsError()) { + // This can never happen in real HW + return Result(ErrCodes::InvalidImportState, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + } // Keep tabs on how much of this content ID has been written so new range_min // values can be calculated. @@ -584,7 +600,7 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, } } - return success ? length : 0; + return length; } ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush, @@ -705,13 +721,17 @@ ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 o } file.Write(temp.data(), temp.size()); - bool success = !file.IsError(); + if (file.IsError()) { + // This can never happen in real HW + return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent); + } content_written[content_index] += temp.size(); LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index, content_written[content_index]); - return success ? temp.size() : 0; + return temp.size(); } u64 CIAFile::GetSize() const { @@ -2313,8 +2333,6 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp, IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); rb.Push(ResultSuccess); - // TODO: Make this actually do something: - /* u32 count = 0; for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) { if ((include_installing && @@ -2325,9 +2343,8 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp, count++; } } - */ - rb.Push(static_cast(am->import_title_contexts.size())); + rb.Push(count); } void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp, @@ -3672,6 +3689,8 @@ void Module::Interface::Sign(Kernel::HLERequestContext& ctx) { template void Module::serialize(Archive& ar, const unsigned int) { ar & cia_installing; + ar & force_old_device_id; + ar & force_new_device_id; ar & am_title_list; ar & system_updater_mutex; } diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index ed1b16f2f..253044a57 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -121,8 +121,9 @@ public: private: friend class CIAFile; - FileUtil::IOFile file; + std::unique_ptr file; bool is_error = false; + bool is_not_ncch = false; bool decryption_authorized = false; std::size_t written = 0; diff --git a/src/core/hw/unique_data.cpp b/src/core/hw/unique_data.cpp index 8a9b5c8dc..cd1a4a2ce 100644 --- a/src/core/hw/unique_data.cpp +++ b/src/core/hw/unique_data.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include "common/common_paths.h" #include "common/logging/log.h" #include "core/file_sys/certificate.h" @@ -105,6 +106,7 @@ SecureDataLoadStatus LoadOTP() { const std::string filepath = GetOTPPath(); + HW::AES::InitKeys(); auto otp_keyiv = HW::AES::GetOTPKeyIV(); auto loader_status = otp.Load(filepath, otp_keyiv.first, otp_keyiv.second); @@ -226,4 +228,34 @@ void InvalidateSecureData() { movable.Invalidate(); } +std::unique_ptr OpenUniqueCryptoFile(const std::string& filename, + const char openmode[], UniqueCryptoFileID id, + int flags) { + LoadOTP(); + + if (!ct_cert.IsValid() || !otp.Valid()) { + return std::make_unique(); + } + + struct { + ECC::PublicKey pkey; + u32 device_id; + u32 id; + } hash_data; + hash_data.pkey = ct_cert.GetPublicKeyECC(); + hash_data.device_id = otp.GetDeviceID(); + hash_data.id = static_cast(id); + + CryptoPP::SHA256 hash; + u8 digest[CryptoPP::SHA256::DIGESTSIZE]; + hash.CalculateDigest(digest, reinterpret_cast(&hash_data), sizeof(hash_data)); + + std::vector key(0x10); + std::vector ctr(0x10); + memcpy(key.data(), digest, 0x10); + memcpy(ctr.data(), digest + 0x10, 12); + + return std::make_unique(filename, openmode, key, ctr, flags); +} + } // namespace HW::UniqueData diff --git a/src/core/hw/unique_data.h b/src/core/hw/unique_data.h index afb5fd3fc..b8cafe030 100644 --- a/src/core/hw/unique_data.h +++ b/src/core/hw/unique_data.h @@ -1,4 +1,4 @@ -// Copyright 2024 Azahar Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -8,6 +8,7 @@ #include #include #include "common/common_types.h" +#include "common/file_util.h" namespace FileSys { class Certificate; @@ -129,5 +130,13 @@ FileSys::Certificate& GetCTCert(); FileSys::OTP& GetOTP(); MovableSedFull& GetMovableSed(); +enum class UniqueCryptoFileID { + NCCH = 0, +}; + void InvalidateSecureData(); + +std::unique_ptr OpenUniqueCryptoFile(const std::string& filename, + const char openmode[], UniqueCryptoFileID id, + int flags = 0); } // namespace HW::UniqueData \ No newline at end of file diff --git a/src/core/loader/3dsx.cpp b/src/core/loader/3dsx.cpp index c839a414c..cbfd0e1f8 100644 --- a/src/core/loader/3dsx.cpp +++ b/src/core/loader/3dsx.cpp @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -314,8 +314,9 @@ ResultStatus AppLoader_THREEDSX::ReadRomFS(std::shared_ptr LOG_DEBUG(Loader, "RomFS size: {:#010X}", romfs_size); // We reopen the file, to allow its position to be independent from file's - FileUtil::IOFile romfs_file_inner(filepath, "rb"); - if (!romfs_file_inner.IsOpen()) + std::unique_ptr romfs_file_inner = + std::make_unique(filepath, "rb"); + if (!romfs_file_inner->IsOpen()) return ResultStatus::Error; romfs_file = std::make_shared(std::move(romfs_file_inner), diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index ca0a285df..a2ce7327e 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -22,6 +22,7 @@ #include "core/hle/service/cfg/cfg.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/fs/fs_user.h" +#include "core/hw/unique_data.h" #include "core/loader/ncch.h" #include "core/loader/smdh.h" #include "core/memory.h" @@ -45,6 +46,19 @@ FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile& file) { if (MakeMagic('N', 'C', 'C', 'H') == magic) return FileType::CXI; + std::unique_ptr file_crypto = HW::UniqueData::OpenUniqueCryptoFile( + file.Filename(), "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); + + file_crypto->Seek(0x100, SEEK_SET); + if (1 != file_crypto->ReadArray(&magic, 1)) + return FileType::Error; + + if (MakeMagic('N', 'C', 'S', 'D') == magic) + return FileType::CCI; + + if (MakeMagic('N', 'C', 'C', 'H') == magic) + return FileType::CXI; + return FileType::Error; } diff --git a/src/video_core/pica/regs_shader.h b/src/video_core/pica/regs_shader.h index 3e49e00a3..245f8e084 100644 --- a/src/video_core/pica/regs_shader.h +++ b/src/video_core/pica/regs_shader.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -23,7 +23,8 @@ struct ShaderRegs { Common::Vec4 GetIntUniform(u32 index) const { const auto& values = int_uniforms[index]; - return Common::MakeVec(values.x, values.y, values.z, values.w); + return Common::MakeVec(static_cast(values.x), static_cast(values.y), + static_cast(values.z), static_cast(values.w)); } INSERT_PADDING_WORDS(0x4);