diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index c6de61f46..dbc3766ff 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -241,20 +241,28 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent) connect(ui->button_secure_info, &QPushButton::clicked, this, [this] { ui->button_secure_info->setEnabled(false); - const QString file_path_qtstr = - QFileDialog::getOpenFileName(this, tr("Select SecureInfo_A"), QString(), - tr("SecureInfo_A (SecureInfo_A);;All Files (*.*)")); + const QString file_path_qtstr = QFileDialog::getOpenFileName( + this, tr("Select SecureInfo_A/B"), QString(), + tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)")); ui->button_secure_info->setEnabled(true); InstallSecureData(file_path_qtstr.toStdString(), cfg->GetSecureInfoAPath()); }); connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] { ui->button_friend_code_seed->setEnabled(false); - const QString file_path_qtstr = QFileDialog::getOpenFileName( - this, tr("Select LocalFriendCodeSeed_B"), QString(), - tr("LocalFriendCodeSeed_B (LocalFriendCodeSeed_B);;All Files (*.*)")); + const QString file_path_qtstr = + QFileDialog::getOpenFileName(this, tr("Select LocalFriendCodeSeed_A/B"), QString(), + tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A " + "LocalFriendCodeSeed_B);;All Files (*.*)")); ui->button_friend_code_seed->setEnabled(true); InstallSecureData(file_path_qtstr.toStdString(), cfg->GetLocalFriendCodeSeedBPath()); }); + connect(ui->button_ct_cert, &QPushButton::clicked, this, [this] { + ui->button_ct_cert->setEnabled(false); + const QString file_path_qtstr = QFileDialog::getOpenFileName( + this, tr("Select CTCert"), QString(), tr("CTCert.bin (*.bin);;All Files (*.*)")); + ui->button_ct_cert->setEnabled(true); + InstallCTCert(file_path_qtstr.toStdString()); + }); for (u8 i = 0; i < country_names.size(); i++) { if (std::strcmp(country_names.at(i), "") != 0) { @@ -320,6 +328,7 @@ void ConfigureSystem::SetConfiguration() { ui->edit_init_ticks_value->setText( QString::number(Settings::values.init_ticks_override.GetValue())); + am = Service::AM::GetModule(system); cfg = Service::CFG::GetModule(system); ReadSystemSettings(); @@ -559,6 +568,19 @@ void ConfigureSystem::InstallSecureData(const std::string& from_path, const std: RefreshSecureDataStatus(); } +void ConfigureSystem::InstallCTCert(const std::string& from_path) { + std::string from = + FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault); + std::string to = + FileUtil::SanitizePath(am->GetCTCertPath(), FileUtil::DirectorySeparator::PlatformDefault); + if (from.empty() || from == to) { + return; + } + FileUtil::Copy(from, to); + am->InvalidateCTCertData(); + RefreshSecureDataStatus(); +} + void ConfigureSystem::RefreshSecureDataStatus() { auto status_to_str = [](Service::CFG::SecureDataLoadStatus status) { switch (status) { @@ -579,6 +601,10 @@ void ConfigureSystem::RefreshSecureDataStatus() { tr((std::string("Status: ") + status_to_str(cfg->LoadSecureInfoAFile())).c_str())); ui->label_friend_code_seed_status->setText( tr((std::string("Status: ") + status_to_str(cfg->LoadLocalFriendCodeSeedBFile())).c_str())); + ui->label_ct_cert_status->setText( + tr((std::string("Status: ") + + status_to_str(static_cast(am->LoadCTCertFile()))) + .c_str())); } void ConfigureSystem::RetranslateUI() { diff --git a/src/citra_qt/configuration/configure_system.h b/src/citra_qt/configuration/configure_system.h index 68819bf98..772a9f995 100644 --- a/src/citra_qt/configuration/configure_system.h +++ b/src/citra_qt/configuration/configure_system.h @@ -20,6 +20,12 @@ namespace Core { class System; } +namespace Service { +namespace AM { +class Module; +} // namespace AM +} // namespace Service + namespace Service { namespace CFG { class Module; @@ -47,6 +53,7 @@ private: void RefreshConsoleID(); void InstallSecureData(const std::string& from_path, const std::string& to_path); + void InstallCTCert(const std::string& from_path); void RefreshSecureDataStatus(); void SetupPerGameUI(); @@ -60,6 +67,7 @@ private: ConfigurationShared::CheckState lle_applets; bool enabled = false; + std::shared_ptr am; std::shared_ptr cfg; std::u16string username; int birthmonth = 0; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index a741e3488..fac2408fb 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -80,6 +80,35 @@ struct TicketInfo { static_assert(sizeof(TicketInfo) == 0x18, "Ticket info structure size is wrong"); +bool CTCert::IsValid() const { + constexpr std::string_view expected_issuer_prod = "Nintendo CA - G3_NintendoCTR2prod"; + constexpr std::string_view expected_issuer_dev = "Nintendo CA - G3_NintendoCTR2dev"; + constexpr u32 expected_signature_type = 0x010005; + + return signature_type == expected_signature_type && + (std::string(issuer.data()) == expected_issuer_prod || + std::string(issuer.data()) == expected_issuer_dev); + + return false; +} + +u32 CTCert::GetDeviceID() const { + constexpr std::string_view key_id_prefix = "CT"; + + const std::string key_id_str(key_id.data()); + if (key_id_str.starts_with(key_id_prefix)) { + const std::string device_id = + key_id_str.substr(key_id_prefix.size(), key_id_str.find('-') - key_id_prefix.size()); + char* end_ptr; + const u32 device_id_value = std::strtoul(device_id.c_str(), &end_ptr, 16); + if (*end_ptr == '\0') { + return device_id_value; + } + } + // Error + return 0; +} + class CIAFile::DecryptionState { public: std::vector::Decryption> content; @@ -1213,6 +1242,21 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) { ticket_list_count, ticket_index); } +void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u32 deviceID = am->ct_cert.IsValid() ? am->ct_cert.GetDeviceID() : 0; + + if (deviceID == 0) { + LOG_ERROR(Service_AM, "Invalid or missing CTCert"); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(RESULT_SUCCESS); + rb.Push(0); + rb.Push(deviceID); +} + void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto media_type = rp.Pop(); @@ -1802,13 +1846,76 @@ void Module::serialize(Archive& ar, const unsigned int) { } SERIALIZE_IMPL(Module) -Module::Module(Core::System& system) : system(system) { +void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + [[maybe_unused]] u32 size = rp.Pop(); + auto buffer = rp.PopMappedBuffer(); + + if (!am->ct_cert.IsValid()) { + LOG_ERROR(Service_AM, "Invalid or missing CTCert"); + } + + buffer.Write(&am->ct_cert, 0, std::min(sizeof(CTCert), buffer.GetSize())); + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + rb.Push(RESULT_SUCCESS); + rb.Push(0); + rb.PushMappedBuffer(buffer); +} + +std::string Module::GetCTCertPath() { + return FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + "CTCert.bin"; +} + +void Module::InvalidateCTCertData() { + ct_cert = CTCert(); +} + +CTCertLoadStatus Module::LoadCTCertFile() { + if (ct_cert.IsValid()) { + return CTCertLoadStatus::Loaded; + } + std::string file_path = GetCTCertPath(); + if (!FileUtil::Exists(file_path)) { + return CTCertLoadStatus::NotFound; + } + FileUtil::IOFile file(file_path, "rb"); + if (!file.IsOpen()) { + return CTCertLoadStatus::IOError; + } + if (file.GetSize() != sizeof(CTCert)) { + return CTCertLoadStatus::Invalid; + } + if (file.ReadBytes(&ct_cert, sizeof(CTCert)) != sizeof(CTCert)) { + return CTCertLoadStatus::IOError; + } + if (!ct_cert.IsValid()) { + ct_cert = CTCert(); + return CTCertLoadStatus::Invalid; + } + return CTCertLoadStatus::Loaded; +} + +Module::Module() { + LoadCTCertFile(); +} + +Module::Module(Core::System& system) : kernel(&system.Kernel()) { ScanForAllTitles(); + LoadCTCertFile(); system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex"); } +Module::Module(Kernel::KernelSystem& kernel) : kernel(&kernel) {} + Module::~Module() = default; +std::shared_ptr GetModule(Core::System& system) { + auto am = system.ServiceManager().GetService("am:u"); + if (!am) + return nullptr; + return am->GetModule(); +} + void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); auto am = std::make_shared(system); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 09fe9dc87..105375c8b 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -68,6 +68,31 @@ enum class InstallStatus : u32 { ErrorEncrypted, }; +enum class CTCertLoadStatus { + Loaded, + NotFound, + Invalid, + IOError, +}; + +struct CTCert { + u32_be signature_type{}; + std::array signature_r{}; + std::array signature_s{}; + INSERT_PADDING_BYTES(0x40){}; + std::array issuer{}; + u32_be key_type{}; + std::array key_id{}; + u32_be expiration_time{}; + std::array public_key_x{}; + std::array public_key_y{}; + INSERT_PADDING_BYTES(0x3C){}; + + bool IsValid() const; + u32 GetDeviceID() const; +}; +static_assert(sizeof(CTCert) == 0x180, "Invalid CTCert size."); + // Title ID valid length constexpr std::size_t TITLE_ID_VALID_LENGTH = 16; @@ -208,6 +233,7 @@ Result UninstallProgram(const FS::MediaType media_type, const u64 title_id); class Module final { public: + Module(); explicit Module(Core::System& system); ~Module(); @@ -216,6 +242,10 @@ public: Interface(std::shared_ptr am, const char* name, u32 max_session); ~Interface(); + std::shared_ptr GetModule() const { + return am; + } + protected: /** * AM::GetNumPrograms service function @@ -415,6 +445,16 @@ public: */ void GetTicketList(Kernel::HLERequestContext& ctx); + /** + * AM::GetDeviceID service function + * Inputs: + * Outputs: + * 1 : Result, 0 on success, otherwise error code + * 2 : Unknown + * 3 : DeviceID + */ + void GetDeviceID(Kernel::HLERequestContext& ctx); + /** * AM::NeedsCleanup service function * Inputs: @@ -702,10 +742,37 @@ public: */ void EndImportTicket(Kernel::HLERequestContext& ctx); + /** + * AM::GetDeviceCert service function + * Inputs: + * Outputs: + * 1 : Result, 0 on success, otherwise error code + * 2 : Unknown + * 3-4 : Device cert + */ + void GetDeviceCert(Kernel::HLERequestContext& ctx); + protected: std::shared_ptr am; }; + /** + * Gets the CTCert.bin path in the host filesystem + * @returns std::string CTCert.bin path in the host filesystem + */ + std::string GetCTCertPath(); + + /** + * Invalidates the CTCert data so that it is loaded again. + */ + void InvalidateCTCertData(); + + /** + * Loads the CTCert.bin file from the filesystem. + * @returns CTCertLoadStatus indicating the file load status. + */ + CTCertLoadStatus LoadCTCertFile(); + private: /** * Scans the for titles in a storage medium for listing. @@ -722,12 +789,15 @@ private: bool cia_installing = false; std::array, 3> am_title_list; std::shared_ptr system_updater_mutex; + CTCert ct_cert{}; template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; }; +std::shared_ptr GetModule(Core::System& system); + void InstallInterfaces(Core::System& system); } // namespace Service::AM diff --git a/src/core/hle/service/am/am_net.cpp b/src/core/hle/service/am/am_net.cpp index fed0ab912..eb9470752 100644 --- a/src/core/hle/service/am/am_net.cpp +++ b/src/core/hle/service/am/am_net.cpp @@ -19,7 +19,7 @@ AM_NET::AM_NET(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0007, &AM_NET::DeleteTicket, "DeleteTicket"}, {0x0008, &AM_NET::GetNumTickets, "GetNumTickets"}, {0x0009, &AM_NET::GetTicketList, "GetTicketList"}, - {0x000A, nullptr, "GetDeviceID"}, + {0x000A, &AM_NET::GetDeviceID, "GetDeviceID"}, {0x000B, nullptr, "GetNumImportTitleContexts"}, {0x000C, nullptr, "GetImportTitleContextList"}, {0x000D, nullptr, "GetImportTitleContexts"}, @@ -103,7 +103,7 @@ AM_NET::AM_NET(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0815, nullptr, "GetCurrentImportContentContexts"}, {0x0816, nullptr, "Sign"}, {0x0817, nullptr, "Verify"}, - {0x0818, nullptr, "GetDeviceCert"}, + {0x0818, &AM_NET::GetDeviceCert, "GetDeviceCert"}, {0x0819, nullptr, "ImportCertificates"}, {0x081A, nullptr, "ImportCertificate"}, {0x081B, nullptr, "CommitImportTitlesAndUpdateFirmwareAuto"}, diff --git a/src/core/hle/service/am/am_sys.cpp b/src/core/hle/service/am/am_sys.cpp index 48bc39cbb..a4e39e6d7 100644 --- a/src/core/hle/service/am/am_sys.cpp +++ b/src/core/hle/service/am/am_sys.cpp @@ -19,7 +19,7 @@ AM_SYS::AM_SYS(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0007, &AM_SYS::DeleteTicket, "DeleteTicket"}, {0x0008, &AM_SYS::GetNumTickets, "GetNumTickets"}, {0x0009, &AM_SYS::GetTicketList, "GetTicketList"}, - {0x000A, nullptr, "GetDeviceID"}, + {0x000A, &AM_SYS::GetDeviceID, "GetDeviceID"}, {0x000B, nullptr, "GetNumImportTitleContexts"}, {0x000C, nullptr, "GetImportTitleContextList"}, {0x000D, nullptr, "GetImportTitleContexts"}, diff --git a/src/core/hle/service/am/am_u.cpp b/src/core/hle/service/am/am_u.cpp index b1d8450a5..75cc46e51 100644 --- a/src/core/hle/service/am/am_u.cpp +++ b/src/core/hle/service/am/am_u.cpp @@ -19,7 +19,7 @@ AM_U::AM_U(std::shared_ptr am) : Module::Interface(std::move(am), "am:u" {0x0007, &AM_U::DeleteTicket, "DeleteTicket"}, {0x0008, &AM_U::GetNumTickets, "GetNumTickets"}, {0x0009, &AM_U::GetTicketList, "GetTicketList"}, - {0x000A, nullptr, "GetDeviceID"}, + {0x000A, &AM_U::GetDeviceCert, "GetDeviceID"}, {0x000B, nullptr, "GetNumImportTitleContexts"}, {0x000C, nullptr, "GetImportTitleContextList"}, {0x000D, nullptr, "GetImportTitleContexts"},