From dc1ebb63cbbecfc72ebaac26ed79ec775035cd97 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Thu, 27 Feb 2025 14:13:17 +0100 Subject: [PATCH] Major revamps to match game loading decisions. - Allow downloading titles from eshop and system settings - Remove encrypted game support --- src/citra_qt/citra_qt.cpp | 99 +- .../configuration/configure_system.cpp | 100 +- src/citra_qt/configuration/configure_system.h | 1 - .../configuration/configure_system.ui | 48 +- src/citra_qt/game_list.cpp | 20 +- src/citra_qt/game_list_p.h | 21 +- src/citra_qt/game_list_worker.cpp | 3 +- src/citra_qt/main.ui | 4 +- src/common/common_paths.h | 2 +- src/common/hacks/hack_list.cpp | 12 + src/common/hacks/hack_list.h | 1 + src/common/logging/filter.cpp | 2 + src/common/logging/types.h | 2 + src/core/CMakeLists.txt | 13 +- src/core/file_sys/certificate.cpp | 164 ++ src/core/file_sys/certificate.h | 92 + src/core/file_sys/cia_container.cpp | 5 + src/core/file_sys/cia_container.h | 1 + src/core/file_sys/ncch_container.cpp | 250 +-- src/core/file_sys/ncch_container.h | 9 +- src/core/file_sys/otp.cpp | 56 + src/core/file_sys/otp.h | 89 + src/core/file_sys/romfs_reader.cpp | 10 - src/core/file_sys/romfs_reader.h | 17 +- src/core/file_sys/seed_db.cpp | 25 + src/core/file_sys/seed_db.h | 2 + .../file_sys/{cia_common.h => signature.h} | 11 +- src/core/file_sys/ticket.cpp | 111 +- src/core/file_sys/ticket.h | 20 +- src/core/file_sys/title_metadata.cpp | 2 +- src/core/hle/applets/erreula.cpp | 13 +- src/core/hle/applets/erreula.h | 29 +- src/core/hle/kernel/ipc.cpp | 12 +- src/core/hle/kernel/svc.cpp | 2 +- src/core/hle/service/ac/ac.cpp | 26 +- src/core/hle/service/ac/ac.h | 21 +- src/core/hle/service/ac/ac_i.cpp | 2 +- src/core/hle/service/ac/ac_u.cpp | 2 +- src/core/hle/service/act/act_errors.cpp | 4 +- src/core/hle/service/act/act_errors.h | 22 +- src/core/hle/service/am/am.cpp | 1933 ++++++++++++++--- src/core/hle/service/am/am.h | 324 ++- src/core/hle/service/am/am_net.cpp | 63 +- src/core/hle/service/am/am_sys.cpp | 22 +- src/core/hle/service/cfg/cfg.cpp | 102 +- src/core/hle/service/cfg/cfg.h | 58 +- src/core/hle/service/fs/fs_user.cpp | 110 +- src/core/hle/service/fs/fs_user.h | 32 +- src/core/hle/service/http/ctr-common-1-cert.h | 85 + src/core/hle/service/http/ctr-common-1-key.h | 83 + src/core/hle/service/http/http_c.cpp | 259 ++- src/core/hle/service/http/http_c.h | 8 + src/core/hw/aes/key.cpp | 168 +- src/core/hw/aes/key.h | 11 + src/core/hw/default_keys.h | 466 ++++ src/core/hw/ecc.cpp | 212 ++ src/core/hw/ecc.h | 70 + src/core/hw/rsa/rsa.cpp | 242 ++- src/core/hw/rsa/rsa.h | 40 +- src/core/hw/unique_data.cpp | 228 ++ src/core/hw/unique_data.h | 133 ++ src/core/loader/loader.h | 1 + 62 files changed, 4860 insertions(+), 1115 deletions(-) create mode 100644 src/core/file_sys/certificate.cpp create mode 100644 src/core/file_sys/certificate.h create mode 100644 src/core/file_sys/otp.cpp create mode 100644 src/core/file_sys/otp.h rename src/core/file_sys/{cia_common.h => signature.h} (74%) create mode 100644 src/core/hle/service/http/ctr-common-1-cert.h create mode 100644 src/core/hle/service/http/ctr-common-1-key.h create mode 100644 src/core/hw/default_keys.h create mode 100644 src/core/hw/ecc.cpp create mode 100644 src/core/hw/ecc.h create mode 100644 src/core/hw/unique_data.cpp create mode 100644 src/core/hw/unique_data.h diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 02a8f416c..2c310c8fe 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -507,8 +507,8 @@ void GMainWindow::InitializeWidgets() { emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " "indicate emulation is running faster or slower than a 3DS.")); game_fps_label = new QLabel(); - game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " - "This will vary from game to game and scene to scene.")); + game_fps_label->setToolTip(tr("How many frames per second the app is currently displaying. " + "This will vary from app to app and scene to scene.")); emu_frametime_label = new QLabel(); emu_frametime_label->setToolTip( tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " @@ -756,7 +756,7 @@ void GMainWindow::InitializeHotkeys() { link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); link_action_shortcut(ui->action_View_Lobby, - QStringLiteral("Multiplayer Browse Public Game Lobby")); + QStringLiteral("Multiplayer Browse Public Application Lobby")); link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); link_action_shortcut(ui->action_Connect_To_Room, QStringLiteral("Multiplayer Direct Connect to Room")); @@ -779,7 +779,7 @@ void GMainWindow::InitializeHotkeys() { ToggleFullscreen(); } }); - connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + connect_shortcut(QStringLiteral("Toggle Per-Application Speed"), [&] { Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); UpdateStatusBar(); }); @@ -1164,7 +1164,7 @@ void GMainWindow::OnUpdateFound(bool found, bool error) { } if (emulation_running && !explicit_update_check) { - LOG_INFO(Frontend, "Update found, deferring as game is running"); + LOG_INFO(Frontend, "Update found, deferring as application is running"); defer_update_prompt = true; return; } @@ -1223,7 +1223,7 @@ static std::optional HoldWakeLockLinux(u32 window_id = 0) { //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the //: computer from sleeping options.insert(QString::fromLatin1("reason"), - QCoreApplication::translate("GMainWindow", "Azahar is running a game")); + QCoreApplication::translate("GMainWindow", "Azahar is running an application")); // 0x4: Suspend lock; 0x8: Idle lock QDBusReply reply = xdp.call(QString::fromLatin1("Inhibit"), @@ -1295,8 +1295,8 @@ bool GMainWindow::LoadROM(const QString& filename) { case Core::System::ResultStatus::ErrorGetLoader: LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); QMessageBox::critical( - this, tr("Invalid ROM Format"), - tr("Your ROM format is not supported.
Please follow the guides to redump your " + this, tr("Invalid App Format"), + tr("Your app format is not supported.
Please follow the guides to redump your " "game " @@ -1308,10 +1308,10 @@ bool GMainWindow::LoadROM(const QString& filename) { break; case Core::System::ResultStatus::ErrorSystemMode: - LOG_CRITICAL(Frontend, "Failed to load ROM!"); + LOG_CRITICAL(Frontend, "Failed to load App!"); QMessageBox::critical( - this, tr("ROM Corrupted"), - tr("Your ROM is corrupted.
Please follow the guides to redump your " + this, tr("App Corrupted"), + tr("Your app is corrupted.
Please follow the guides to redump your " "
game " @@ -1323,23 +1323,17 @@ bool GMainWindow::LoadROM(const QString& filename) { break; case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { - QMessageBox::critical( - this, tr("ROM Encrypted"), - tr("Your ROM is encrypted.
Please follow the guides to redump your " - "
game " - "cartridges or " - "installed " - "titles.")); + QMessageBox::critical(this, tr("App Encrypted"), + tr("Your app is encrypted.
" + "" + "Please check our blog for more info.")); break; } case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: QMessageBox::critical( - this, tr("Invalid ROM Format"), - tr("Your ROM format is not supported.
Please follow the guides to redump your " + this, tr("Invalid App Format"), + tr("Your app format is not supported.
Please follow the guides to redump your " "game " @@ -1351,8 +1345,8 @@ bool GMainWindow::LoadROM(const QString& filename) { break; case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: - QMessageBox::critical(this, tr("Unsupported ROM"), - tr("GBA Virtual Console ROMs are not supported by Azahar.")); + QMessageBox::critical(this, tr("Unsupported App"), + tr("GBA Virtual Console is not supported by Azahar.")); break; case Core::System::ResultStatus::ErrorArticDisconnected: @@ -1365,7 +1359,7 @@ bool GMainWindow::LoadROM(const QString& filename) { break; default: QMessageBox::critical( - this, tr("Error while loading ROM!"), + this, tr("Error while loading App!"), tr("An unknown error occurred. Please see the log for more details.")); break; } @@ -1430,7 +1424,7 @@ void GMainWindow::BootGame(const QString& filename) { const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id); - LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + LOG_INFO(Frontend, "Loading per application config file for title {}", config_file_name); QtConfig per_game_config(config_file_name, QtConfig::ConfigType::PerGameConfig); } @@ -1932,9 +1926,9 @@ bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int message, switch (message) { case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_PROMPT: buttons = QMessageBox::Yes | QMessageBox::No; - result = - QMessageBox::information(parent, tr("Create Shortcut"), - tr("Do you want to launch the game in fullscreen?"), buttons); + result = QMessageBox::information( + parent, tr("Create Shortcut"), + tr("Do you want to launch the application in fullscreen?"), buttons); return result == QMessageBox::Yes; case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: QMessageBox::information(parent, tr("Create Shortcut"), @@ -2147,7 +2141,7 @@ void GMainWindow::OnGameListAddDirectory() { UISettings::values.game_dirs.append(game_dir); game_list->PopulateAsync(UISettings::values.game_dirs); } else { - LOG_WARNING(Frontend, "Selected directory is already in the game list"); + LOG_WARNING(Frontend, "Selected directory is already in the application list"); } } @@ -2164,7 +2158,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { u64 title_id{}; if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { QMessageBox::information(this, tr("Properties"), - tr("The game properties could not be loaded.")); + tr("The application properties could not be loaded.")); return; } @@ -2260,10 +2254,11 @@ void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); break; case Service::AM::InstallStatus::ErrorEncrypted: - QMessageBox::critical(this, tr("Encrypted File"), - tr("%1 must be decrypted " - "before being used with Azahar. A real 3DS is required.") - .arg(filename)); + QMessageBox::critical(this, tr("CIA Encrypted"), + tr("Your CIA file is encrypted.
" + "
" + "Please check our blog for more info.")); break; case Service::AM::InstallStatus::ErrorFileNotFound: QMessageBox::critical(this, tr("Unable to find File"), @@ -2625,9 +2620,10 @@ void GMainWindow::OnLoadState() { ASSERT(action); if (UISettings::values.save_state_warning) { - QMessageBox::warning(this, tr("Savestates"), - tr("Warning: Savestates are NOT a replacement for in-game saves, " - "and are not meant to be reliable.\n\nUse at your own risk!")); + QMessageBox::warning( + this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-application saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); UISettings::values.save_state_warning = false; config->Save(); } @@ -2709,7 +2705,7 @@ void GMainWindow::OnLoadAmiibo() { if (!nfc->IsSearchingForAmiibos()) { QMessageBox::warning(this, tr("Error opening amiibo data file"), - tr("Game is not looking for amiibos.")); + tr("Application is not looking for amiibos.")); return; } @@ -2859,11 +2855,11 @@ void GMainWindow::OnCaptureScreenshot() { const bool was_running = emu_thread->IsRunning(); - if (was_running || - (QMessageBox::question( - this, tr("Game will unpause"), - tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running || (QMessageBox::question(this, tr("Application will unpause"), + tr("The application will be unpaused, and the next " + "frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::Yes)) { if (was_running) { OnPauseGame(); } @@ -3136,7 +3132,7 @@ void GMainWindow::UpdateStatusBar() { .arg(results.emulation_speed * 100.0, 0, 'f', 0) .arg(Settings::values.frame_limit.GetValue())); } - game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + game_fps_label->setText(tr("App: %1 FPS").arg(results.game_fps, 0, 'f', 0)); emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); if (show_artic_label) { @@ -3340,7 +3336,8 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det if (can_continue) { message_box.addButton(tr("Continue"), QMessageBox::RejectRole); } - QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + QPushButton* abort_button = + message_box.addButton(tr("Quit Application"), QMessageBox::AcceptRole); if (result != Core::System::ResultStatus::ShutdownRequested) message_box.exec(); @@ -3470,7 +3467,7 @@ bool GMainWindow::ConfirmChangeGame() { } auto answer = QMessageBox::question( - this, tr("Azahar"), tr("The game is still running. Would you like to stop emulation?"), + this, tr("Azahar"), tr("The application is still running. Would you like to stop emulation?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); return answer != QMessageBox::No; } @@ -3675,8 +3672,8 @@ void GMainWindow::RetranslateStatusBar() { emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " "indicate emulation is running faster or slower than a 3DS.")); - game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " - "This will vary from game to game and scene to scene.")); + game_fps_label->setToolTip(tr("How many frames per second the app is currently displaying. " + "This will vary from app to app and scene to scene.")); emu_frametime_label->setToolTip( tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index da15db77f..14727ccf5 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -17,6 +17,7 @@ #include "core/hle/service/cfg/cfg.h" #include "core/hle/service/ptm/ptm.h" #include "core/hw/aes/key.h" +#include "core/hw/unique_data.h" #include "core/system_titles.h" #include "ui_configure_system.h" @@ -245,7 +246,7 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent) 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()); + InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetSecureInfoAPath()); }); connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] { ui->button_friend_code_seed->setEnabled(false); @@ -254,14 +255,23 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent) tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A " "LocalFriendCodeSeed_B);;All Files (*.*)")); ui->button_friend_code_seed->setEnabled(true); - InstallSecureData(file_path_qtstr.toStdString(), cfg->GetLocalFriendCodeSeedBPath()); + InstallSecureData(file_path_qtstr.toStdString(), + HW::UniqueData::GetLocalFriendCodeSeedBPath()); }); - connect(ui->button_ct_cert, &QPushButton::clicked, this, [this] { - ui->button_ct_cert->setEnabled(false); + connect(ui->button_otp, &QPushButton::clicked, this, [this] { + ui->button_otp->setEnabled(false); + const QString file_path_qtstr = + QFileDialog::getOpenFileName(this, tr("Select encrypted OTP file"), QString(), + tr("Binary file (*.bin);;All Files (*.*)")); + ui->button_otp->setEnabled(true); + InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetOTPPath()); + }); + connect(ui->button_movable, &QPushButton::clicked, this, [this] { + ui->button_movable->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()); + this, tr("Select movable.sed"), QString(), tr("Sed file (*.sed);;All Files (*.*)")); + ui->button_movable->setEnabled(true); + InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetMovablePath()); }); for (u8 i = 0; i < country_names.size(); i++) { @@ -562,50 +572,39 @@ void ConfigureSystem::InstallSecureData(const std::string& from_path, const std: if (from.empty() || from == to) { return; } - FileUtil::CreateFullPath(to_path); - FileUtil::Copy(from, to); - cfg->InvalidateSecureData(); - RefreshSecureDataStatus(); -} - -void ConfigureSystem::InstallCTCert(const std::string& from_path) { - std::string from = - FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault); - std::string to = FileUtil::SanitizePath(Service::AM::Module::GetCTCertPath(), - FileUtil::DirectorySeparator::PlatformDefault); - if (from.empty() || from == to) { - return; - } + FileUtil::CreateFullPath(to); FileUtil::Copy(from, to); + HW::UniqueData::InvalidateSecureData(); RefreshSecureDataStatus(); } void ConfigureSystem::RefreshSecureDataStatus() { - auto status_to_str = [](Service::CFG::SecureDataLoadStatus status) { + auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) { switch (status) { - case Service::CFG::SecureDataLoadStatus::Loaded: + case HW::UniqueData::SecureDataLoadStatus::Loaded: return "Loaded"; - case Service::CFG::SecureDataLoadStatus::NotFound: + case HW::UniqueData::SecureDataLoadStatus::InvalidSignature: + return "Loaded (Invalid Signature)"; + case HW::UniqueData::SecureDataLoadStatus::NotFound: return "Not Found"; - case Service::CFG::SecureDataLoadStatus::Invalid: + case HW::UniqueData::SecureDataLoadStatus::Invalid: return "Invalid"; - case Service::CFG::SecureDataLoadStatus::IOError: + case HW::UniqueData::SecureDataLoadStatus::IOError: return "IO Error"; default: return ""; } }; - Service::AM::CTCert ct_cert; - ui->label_secure_info_status->setText( - tr((std::string("Status: ") + status_to_str(cfg->LoadSecureInfoAFile())).c_str())); + tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadSecureInfoA())).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( - Service::AM::Module::LoadCTCertFile(ct_cert)))) + tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadLocalFriendCodeSeedB())) .c_str())); + ui->label_otp_status->setText( + tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str())); + ui->label_movable_status->setText( + tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadMovable())).c_str())); } void ConfigureSystem::RetranslateUI() { @@ -669,40 +668,7 @@ void ConfigureSystem::SetupPerGameUI() { void ConfigureSystem::DownloadFromNUS() { ui->button_start_download->setEnabled(false); - const auto mode = - static_cast(1 << ui->combo_download_set->currentIndex()); - const auto region = static_cast(ui->combo_download_region->currentIndex()); - const std::vector titles = Core::GetSystemTitleIds(mode, region); - - QProgressDialog progress(tr("Downloading files..."), tr("Cancel"), 0, - static_cast(titles.size()), this); - progress.setWindowModality(Qt::WindowModal); - - QFutureWatcher future_watcher; - QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, - &QProgressDialog::reset); - QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, - &QFutureWatcher::cancel); - QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, - &QProgressDialog::setValue); - - auto failed = false; - const auto download_title = [&future_watcher, &failed](const u64& title_id) { - if (Service::AM::InstallFromNus(title_id) != Service::AM::InstallStatus::Success) { - failed = true; - future_watcher.cancel(); - } - }; - - future_watcher.setFuture(QtConcurrent::map(titles, download_title)); - progress.exec(); - future_watcher.waitForFinished(); - - if (failed) { - QMessageBox::critical(this, tr("Azahar"), tr("Downloading system files failed.")); - } else if (!future_watcher.isCanceled()) { - QMessageBox::information(this, tr("Azahar"), tr("Successfully downloaded system files.")); - } + QMessageBox::critical(this, tr("Azahar"), tr("Downloading from NUS has been deprecated.")); ui->button_start_download->setEnabled(true); } diff --git a/src/citra_qt/configuration/configure_system.h b/src/citra_qt/configuration/configure_system.h index 9db21ba1c..3041b20a0 100644 --- a/src/citra_qt/configuration/configure_system.h +++ b/src/citra_qt/configuration/configure_system.h @@ -53,7 +53,6 @@ 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(); diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui index b116797ff..e02127ceb 100644 --- a/src/citra_qt/configuration/configure_system.ui +++ b/src/citra_qt/configuration/configure_system.ui @@ -629,24 +629,60 @@ - + - CTCert + OTP - - + + - + - + + + + 0 + 0 + + + + Qt::RightToLeft + + + Choose + + + + + + + + + + movable.sed + + + + + + + + + + + + + + + 0 diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index ef0b2dfea..1dbb3d7e1 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -596,7 +596,7 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr QMenu* uninstall_menu = context_menu.addMenu(tr("Uninstall")); QAction* uninstall_all = uninstall_menu->addAction(tr("Everything")); uninstall_menu->addSeparator(); - QAction* uninstall_game = uninstall_menu->addAction(tr("Game")); + QAction* uninstall_game = uninstall_menu->addAction(tr("Application")); QAction* uninstall_update = uninstall_menu->addAction(tr("Update")); QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); @@ -736,7 +736,7 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr QMessageBox::StandardButton answer = QMessageBox::question( this, tr("Azahar"), tr("Are you sure you want to completely uninstall '%1'?\n\nThis will " - "delete the game if installed, as well as any installed updates or DLC.") + "delete the application if installed, as well as any installed updates or DLC.") .arg(name), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer == QMessageBox::Yes) { @@ -805,7 +805,7 @@ void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { UISettings::values.game_dirs[selected.data(GameListDir::GameDirRole).toInt()]; QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); - QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); + QAction* delete_dir = context_menu.addAction(tr("Remove Application Directory")); deep_scan->setCheckable(true); deep_scan->setChecked(game_dir.deep_scan); @@ -885,18 +885,18 @@ void GameList::LoadCompatibilityList() { QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")}; if (!compat_list.open(QFile::ReadOnly | QFile::Text)) { - LOG_ERROR(Frontend, "Unable to open game compatibility list"); + LOG_ERROR(Frontend, "Unable to open application compatibility list"); return; } if (compat_list.size() == 0) { - LOG_WARNING(Frontend, "Game compatibility list is empty"); + LOG_WARNING(Frontend, "Application compatibility list is empty"); return; } const QByteArray content = compat_list.readAll(); if (content.isEmpty()) { - LOG_ERROR(Frontend, "Unable to completely read game compatibility list"); + LOG_ERROR(Frontend, "Unable to completely read application compatibility list"); return; } @@ -1004,12 +1004,12 @@ void GameList::LoadInterfaceLayout() { } const QStringList GameList::supported_file_extensions = { - QStringLiteral("3ds"), QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"), - QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")}; + QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"), + QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")}; void GameList::RefreshGameDirectory() { if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { - LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); + LOG_INFO(Frontend, "Change detected in the applications directory. Reloading game list."); PopulateAsync(UISettings::values.game_dirs); } } @@ -1094,7 +1094,7 @@ GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} layout->setAlignment(Qt::AlignCenter); image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); - text->setText(tr("Double-click to add a new folder to the game list")); + text->setText(tr("Double-click to add a new folder to the application list")); QFont font = text->font(); font.setPointSize(20); text->setFont(font); diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index c41e43df4..09dc5ab4e 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -163,7 +163,7 @@ public: GameListItemPath() = default; GameListItemPath(const QString& game_path, std::span smdh_data, u64 program_id, - u64 extdata_id, Service::FS::MediaType media_type) { + u64 extdata_id, Service::FS::MediaType media_type, bool is_encrypted) { setData(type(), TypeRole); setData(game_path, FullPathRole); setData(qulonglong(program_id), ProgramIdRole); @@ -184,6 +184,9 @@ public: if (UISettings::values.game_list_icon_size.GetValue() != UISettings::GameListIconSize::NoIcon) setData(GetDefaultIcon(large), Qt::DecorationRole); + if (is_encrypted) { + setData(QObject::tr("Unsupported encrypted application"), TitleRole); + } return; } @@ -262,13 +265,13 @@ public: }; // clang-format off static const std::map status_data = { - {QStringLiteral("0"), {QStringLiteral("#5c93ed"), QT_TR_NOOP("Perfect"), QT_TR_NOOP("Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without\nany workarounds needed.")}}, - {QStringLiteral("1"), {QStringLiteral("#47d35c"), QT_TR_NOOP("Great"), QT_TR_NOOP("Game functions with minor graphical or audio glitches and is playable from start to finish. May require some\nworkarounds.")}}, - {QStringLiteral("2"), {QStringLiteral("#94b242"), QT_TR_NOOP("Okay"), QT_TR_NOOP("Game functions with major graphical or audio glitches, but game is playable from start to finish with\nworkarounds.")}}, - {QStringLiteral("3"), {QStringLiteral("#f2d624"), QT_TR_NOOP("Bad"), QT_TR_NOOP("Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches\neven with workarounds.")}}, - {QStringLiteral("4"), {QStringLiteral("#ff0000"), QT_TR_NOOP("Intro/Menu"), QT_TR_NOOP("Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start\nScreen.")}}, - {QStringLiteral("5"), {QStringLiteral("#828282"), QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The game crashes when attempting to startup.")}}, - {QStringLiteral("99"), {QStringLiteral("#000000"), QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}}}; + {QStringLiteral("0"), {QStringLiteral("#5c93ed"), QT_TR_NOOP("Perfect"), QT_TR_NOOP("App functions flawless with no audio or graphical glitches, all tested functionality works as intended without\nany workarounds needed.")}}, + {QStringLiteral("1"), {QStringLiteral("#47d35c"), QT_TR_NOOP("Great"), QT_TR_NOOP("App functions with minor graphical or audio glitches and is playable from start to finish. May require some\nworkarounds.")}}, + {QStringLiteral("2"), {QStringLiteral("#94b242"), QT_TR_NOOP("Okay"), QT_TR_NOOP("App functions with major graphical or audio glitches, but app is playable from start to finish with\nworkarounds.")}}, + {QStringLiteral("3"), {QStringLiteral("#f2d624"), QT_TR_NOOP("Bad"), QT_TR_NOOP("App functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches\neven with workarounds.")}}, + {QStringLiteral("4"), {QStringLiteral("#ff0000"), QT_TR_NOOP("Intro/Menu"), QT_TR_NOOP("App is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start\nScreen.")}}, + {QStringLiteral("5"), {QStringLiteral("#828282"), QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The app crashes when attempting to startup.")}}, + {QStringLiteral("99"), {QStringLiteral("#000000"), QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The app has not yet been tested.")}}}; // clang-format on auto iterator = status_data.find(compatibility); @@ -445,7 +448,7 @@ public: int icon_size = IconSizes.at(UISettings::values.game_list_icon_size.GetValue()); setData(QIcon::fromTheme(QStringLiteral("plus")).pixmap(icon_size), Qt::DecorationRole); - setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole); + setData(QObject::tr("Add New Application Directory"), Qt::DisplayRole); } int type() const override { diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index 65e6e7719..96c8baa70 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -108,7 +108,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign emit EntryReady( { new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, - extdata_id, media_type), + extdata_id, media_type, + res == Loader::ResultStatus::ErrorEncrypted), new GameListItemCompat(compatibility), new GameListItemRegion(smdh), new GameListItem( diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index 6f4f3bf1e..0abac57f6 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -446,7 +446,7 @@ true - Browse Public Game Lobby + Browse Public Application Lobby @@ -688,7 +688,7 @@ false - Configure Current Game... + Configure Current Application... QAction::NoRole diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 8876ed5cb..393c7a475 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -65,6 +65,6 @@ // Sys files #define SHARED_FONT "shared_font.bin" -#define AES_KEYS "aes_keys.txt" +#define KEYS_FILE "keys.txt" #define BOOTROM9 "boot9.bin" #define SECRET_SECTOR "sector0x96.bin" diff --git a/src/common/hacks/hack_list.cpp b/src/common/hacks/hack_list.cpp index bf1ce6c52..be043ed47 100644 --- a/src/common/hacks/hack_list.cpp +++ b/src/common/hacks/hack_list.cpp @@ -58,5 +58,17 @@ HackManager hack_manager = { }, }}, + {HackType::DECRYPTION_AUTHORIZED, + HackEntry{ + .mode = HackAllowMode::ALLOW, + .affected_title_ids = + { + // NIM + 0x0004013000002C02, // Normal + 0x0004013000002C03, // Safe mode + 0x0004013020002C03, // New 3DS safe mode + }, + }}, + }}; } \ No newline at end of file diff --git a/src/common/hacks/hack_list.h b/src/common/hacks/hack_list.h index d8ea037fd..55986a617 100644 --- a/src/common/hacks/hack_list.h +++ b/src/common/hacks/hack_list.h @@ -11,6 +11,7 @@ namespace Common::Hacks { enum class HackType : int { RIGHT_EYE_DISABLE, ACCURATE_MULTIPLICATION, + DECRYPTION_AUTHORIZED, }; class UserHackData {}; diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 85f271d2c..fbeb6d69f 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -121,6 +121,8 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(HW, LCD) \ SUB(HW, GPU) \ SUB(HW, AES) \ + SUB(HW, RSA) \ + SUB(HW, ECC) \ CLS(Frontend) \ CLS(Render) \ SUB(Render, Software) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index c73759ce2..ea912ee57 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -88,6 +88,8 @@ enum class Class : u8 { HW_LCD, ///< LCD register emulation HW_GPU, ///< GPU control emulation HW_AES, ///< AES engine emulation + HW_RSA, ///< RSA engine emulation + HW_ECC, ///< ECC engine emulation Frontend, ///< Emulator UI Render, ///< Emulator video output and hardware acceleration Render_Software, ///< Software renderer backend diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 331bc9cfb..f069ff33d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -64,7 +64,8 @@ add_library(citra_core STATIC file_sys/archive_systemsavedata.h file_sys/artic_cache.cpp file_sys/artic_cache.h - file_sys/cia_common.h + file_sys/certificate.cpp + file_sys/certificate.h file_sys/cia_container.cpp file_sys/cia_container.h file_sys/directory_backend.h @@ -80,6 +81,8 @@ add_library(citra_core STATIC file_sys/layered_fs.h file_sys/ncch_container.cpp file_sys/ncch_container.h + file_sys/otp.cpp + file_sys/otp.h file_sys/patch.cpp file_sys/patch.h file_sys/path_parser.cpp @@ -97,6 +100,7 @@ add_library(citra_core STATIC file_sys/secure_value_backend.h file_sys/seed_db.cpp file_sys/seed_db.h + file_sys/signature.h file_sys/ticket.cpp file_sys/ticket.h file_sys/title_metadata.cpp @@ -449,8 +453,13 @@ add_library(citra_core STATIC hw/aes/ccm.h hw/aes/key.cpp hw/aes/key.h + hw/ecc.cpp + hw/ecc.h + hw/default_keys.h hw/rsa/rsa.cpp hw/rsa/rsa.h + hw/unique_data.cpp + hw/unique_data.h hw/y2r.cpp hw/y2r.h loader/3dsx.cpp @@ -482,7 +491,7 @@ add_library(citra_core STATIC tracer/citrace.h tracer/recorder.cpp tracer/recorder.h - ) +) create_target_directory_groups(citra_core) diff --git a/src/core/file_sys/certificate.cpp b/src/core/file_sys/certificate.cpp new file mode 100644 index 000000000..43e302376 --- /dev/null +++ b/src/core/file_sys/certificate.cpp @@ -0,0 +1,164 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "common/alignment.h" +#include "common/logging/log.h" +#include "core/file_sys/certificate.h" +#include "core/file_sys/signature.h" +#include "cryptopp/eccrypto.h" +#include "cryptopp/osrng.h" + +namespace FileSys { +void Certificate::BuildECC(Certificate& parent, const std::array issuer, + const std::array name, u32 expiration) { + + certificate_signature_type = SignatureType::EcdsaSha256; + certificate_body.key_type = PublicKeyType::ECC; + + certificate_body.issuer = issuer; + certificate_body.name = name; + certificate_body.expiration = expiration; + + auto [private_key, public_key] = HW::ECC::GenerateKeyPair(); + + SetPrivateKeyECC(private_key); + SetPublicKeyECC(public_key); + SetSignatureECC(parent.Sign(SerializeBody())); + + if (!VerifyMyself(parent.GetPublicKeyECC())) { + LOG_ERROR(HW, "Failed to verify newly generated certificate"); + } +} + +void Certificate::BuildECC(const std::array issuer, const std::array name, + u32 expiration, HW::ECC::PrivateKey private_key, + HW::ECC::Signature signature) { + + certificate_signature_type = SignatureType::EcdsaSha256; + certificate_body.key_type = PublicKeyType::ECC; + + certificate_body.issuer = issuer; + certificate_body.name = name; + certificate_body.expiration = expiration; + + SetPrivateKeyECC(private_key); + SetPublicKeyECC(HW::ECC::MakePublicKey(private_key)); + SetSignatureECC(signature); +} + +bool Certificate::VerifyMyself(const HW::ECC::PublicKey& parent_public) { + if (certificate_signature_type == SignatureType::EcdsaSha256) { + return HW::ECC::Verify(SerializeBody(), HW::ECC::CreateECCSignature(certificate_signature), + parent_public); + } else { + UNIMPLEMENTED(); + return false; + } +} + +bool Certificate::Verify(std::span data, HW::ECC::Signature signature) { + + if (certificate_body.key_type == PublicKeyType::ECC) { + return HW::ECC::Verify(data, signature, GetPublicKeyECC()); + } else { + UNIMPLEMENTED(); + } + return false; +} + +HW::ECC::Signature Certificate::Sign(std::span data) { + if (certificate_body.key_type == PublicKeyType::ECC) { + return HW::ECC::Sign(data, certificate_private_key_ecc); + } else { + UNIMPLEMENTED(); + } + return HW::ECC::Signature(); +} + +std::vector Certificate::ECDHAgree(const HW::ECC::PublicKey& others_public_key) { + if (certificate_body.key_type != PublicKeyType::ECC) { + LOG_ERROR(HW, "Tried to agree with a non ECC certificate"); + return {}; + } + return HW::ECC::Agree(certificate_private_key_ecc, others_public_key); +} + +std::vector Certificate::SerializeSignature() const { + std::vector ret; + ret.resize(Common::AlignUp(certificate_signature.size() + sizeof(u32) + 1, 0x40)); + memcpy(ret.data(), &certificate_signature_type, sizeof(u32_be)); + memcpy(ret.data() + sizeof(u32_be), certificate_signature.data(), certificate_signature.size()); + return ret; +} + +std::vector Certificate::SerializeBody() const { + std::vector ret; + ret.resize(Common::AlignUp(sizeof(Body) + certificate_public_key.size() + 1, 0x40)); + memcpy(ret.data(), &certificate_body, sizeof(certificate_body)); + memcpy(ret.data() + sizeof(certificate_body), certificate_public_key.data(), + certificate_public_key.size()); + return ret; +} + +std::vector Certificate::Serialize() const { + if (!IsValid()) { + return {}; + } + + auto signature = SerializeSignature(); + auto body = SerializeBody(); + signature.insert(signature.end(), body.begin(), body.end()); + + return signature; +} + +void Certificate::SetPrivateKeyECC(const HW::ECC::PrivateKey& private_key) { + if (certificate_body.key_type != PublicKeyType::ECC) { + LOG_ERROR(HW, "Certificate is not ECC"); + return; + } + certificate_private_key_ecc = private_key; +} + +const HW::ECC::PrivateKey& Certificate::GetPrivateKeyECC() { + if (certificate_body.key_type != PublicKeyType::ECC) { + LOG_ERROR(HW, "Certificate is not ECC"); + } + return certificate_private_key_ecc; +} + +void Certificate::SetPublicKeyECC(const HW::ECC::PublicKey& public_key) { + if (certificate_body.key_type != PublicKeyType::ECC) { + LOG_ERROR(HW, "Certificate is not ECC"); + return; + } + certificate_public_key.resize(public_key.xy.size()); + memcpy(certificate_public_key.data(), public_key.xy.data(), public_key.xy.size()); +} + +HW::ECC::PublicKey Certificate::GetPublicKeyECC() { + if (certificate_body.key_type != PublicKeyType::ECC) { + LOG_ERROR(HW, "Certificate is not ECC"); + return HW::ECC::PublicKey(); + } + return HW::ECC::CreateECCPublicKey(certificate_public_key); +} +void Certificate::SetSignatureECC(const HW::ECC::Signature& signature) { + if (certificate_signature_type != SignatureType::EcdsaSha256) { + LOG_ERROR(HW, "Signature is not ECC"); + return; + } + certificate_signature.resize(signature.rs.size()); + memcpy(certificate_signature.data(), signature.rs.data(), signature.rs.size()); +} + +HW::ECC::Signature Certificate::GetSignatureECC() { + if (certificate_signature_type != SignatureType::EcdsaSha256) { + LOG_ERROR(HW, "Signature is not ECC"); + return HW::ECC::Signature(); + } + return HW::ECC::CreateECCSignature(certificate_signature); +} +} // namespace FileSys diff --git a/src/core/file_sys/certificate.h b/src/core/file_sys/certificate.h new file mode 100644 index 000000000..e674849c9 --- /dev/null +++ b/src/core/file_sys/certificate.h @@ -0,0 +1,92 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/swap.h" +#include "core/hw/ecc.h" + +namespace Loader { +enum class ResultStatus; +} + +namespace FileSys { + +class Certificate { +public: +#pragma pack(push, 1) + struct Body { + std::array issuer; + u32_be key_type; + std::array name; + u32_be expiration; + }; + + struct PublicKeyType { + enum : u32 { + RSA_4096 = 0, + RSA_2048 = 1, + ECC = 2, + }; + }; + +#pragma pack(pop) + void BuildECC(Certificate& parent, const std::array issuer, + const std::array name, u32 expiration); + + void BuildECC(const std::array issuer, const std::array name, + u32 expiration, HW::ECC::PrivateKey private_key, HW::ECC::Signature signature); + + bool VerifyMyself(const HW::ECC::PublicKey& parent_public); + bool Verify(std::span data, HW::ECC::Signature signature); + + HW::ECC::Signature Sign(std::span data); + + std::vector ECDHAgree(const HW::ECC::PublicKey& others_public_key); + + std::vector SerializeSignature() const; + std::vector SerializeBody() const; + std::vector Serialize() const; + + const std::array GetIssuer() const { + return certificate_body.issuer; + } + + const std::array GetName() const { + return certificate_body.name; + } + + void SetPrivateKeyECC(const HW::ECC::PrivateKey& private_key); + const HW::ECC::PrivateKey& GetPrivateKeyECC(); + + void SetPublicKeyECC(const HW::ECC::PublicKey& public_key); + HW::ECC::PublicKey GetPublicKeyECC(); + + void SetSignatureECC(const HW::ECC::Signature& signature); + HW::ECC::Signature GetSignatureECC(); + + const bool IsValid() const { + return certificate_signature_type != 0u; + } + + void Invalidate() { + certificate_signature_type = 0u; + } + +private: + Body certificate_body; + u32_be certificate_signature_type; + std::vector certificate_signature; + std::vector certificate_public_key; + + HW::ECC::PrivateKey certificate_private_key_ecc; +}; +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/cia_container.cpp b/src/core/file_sys/cia_container.cpp index b2092bcab..3297b96b0 100644 --- a/src/core/file_sys/cia_container.cpp +++ b/src/core/file_sys/cia_container.cpp @@ -147,6 +147,11 @@ Loader::ResultStatus CIAContainer::LoadTicket(std::span ticket_data, s return cia_ticket.Load(ticket_data, offset); } +Loader::ResultStatus CIAContainer::LoadTicket(const Ticket& ticket) { + cia_ticket = ticket; + return Loader::ResultStatus::Success; +} + Loader::ResultStatus CIAContainer::LoadTitleMetadata(std::span tmd_data, std::size_t offset) { return cia_tmd.Load(tmd_data, offset); diff --git a/src/core/file_sys/cia_container.h b/src/core/file_sys/cia_container.h index 4b284fc85..d7fa46dad 100644 --- a/src/core/file_sys/cia_container.h +++ b/src/core/file_sys/cia_container.h @@ -44,6 +44,7 @@ public: // Load parts of CIAs (for CIAs streamed in) Loader::ResultStatus LoadHeader(std::span header_data, std::size_t offset = 0); Loader::ResultStatus LoadTicket(std::span ticket_data, std::size_t offset = 0); + Loader::ResultStatus LoadTicket(const Ticket& ticket); Loader::ResultStatus LoadTitleMetadata(std::span tmd_data, std::size_t offset = 0); Loader::ResultStatus LoadMetadata(std::span meta_data, std::size_t offset = 0); diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 2ebd31f2e..9e8066e95 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -172,7 +172,11 @@ Loader::ResultStatus NCCHContainer::Load() { if (is_loaded) return Loader::ResultStatus::Success; + int block_size = kBlockSize; + if (file.IsOpen()) { + size_t file_size = file.GetSize(); + // Reset read pointer in case this file has been read before. file.Seek(ncch_offset, SEEK_SET); @@ -196,136 +200,21 @@ Loader::ResultStatus NCCHContainer::Load() { return Loader::ResultStatus::ErrorInvalidFormat; has_header = true; - bool failed_to_decrypt = false; + + if (ncch_header.content_size == file_size) { + // The NCCH is a proto version, which does not use media size units + is_proto = true; + block_size = 1; + } + if (!ncch_header.no_crypto) { - is_encrypted = true; - - // Find primary and secondary keys - if (ncch_header.fixed_key) { - LOG_DEBUG(Service_FS, "Fixed-key crypto"); - primary_key.fill(0); - secondary_key.fill(0); - } else { - using namespace HW::AES; - InitKeys(); - std::array key_y_primary, key_y_secondary; - - std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(), - key_y_primary.begin()); - - if (!ncch_header.seed_crypto) { - key_y_secondary = key_y_primary; - } else { - auto opt{FileSys::GetSeed(ncch_header.program_id)}; - if (!opt.has_value()) { - LOG_ERROR(Service_FS, "Seed for program {:016X} not found", - ncch_header.program_id); - failed_to_decrypt = true; - } else { - auto seed{*opt}; - std::array input; - std::memcpy(input.data(), key_y_primary.data(), key_y_primary.size()); - std::memcpy(input.data() + key_y_primary.size(), seed.data(), seed.size()); - CryptoPP::SHA256 sha; - std::array hash; - sha.CalculateDigest(hash.data(), input.data(), input.size()); - std::memcpy(key_y_secondary.data(), hash.data(), key_y_secondary.size()); - } - } - - SetKeyY(KeySlotID::NCCHSecure1, key_y_primary); - if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure1)) { - LOG_ERROR(Service_FS, "Secure1 KeyX missing"); - failed_to_decrypt = true; - } - primary_key = GetNormalKey(KeySlotID::NCCHSecure1); - - switch (ncch_header.secondary_key_slot) { - case 0: - LOG_DEBUG(Service_FS, "Secure1 crypto"); - SetKeyY(KeySlotID::NCCHSecure1, key_y_secondary); - if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure1)) { - LOG_ERROR(Service_FS, "Secure1 KeyX missing"); - failed_to_decrypt = true; - } - secondary_key = GetNormalKey(KeySlotID::NCCHSecure1); - break; - case 1: - LOG_DEBUG(Service_FS, "Secure2 crypto"); - SetKeyY(KeySlotID::NCCHSecure2, key_y_secondary); - if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure2)) { - LOG_ERROR(Service_FS, "Secure2 KeyX missing"); - failed_to_decrypt = true; - } - secondary_key = GetNormalKey(KeySlotID::NCCHSecure2); - break; - case 10: - LOG_DEBUG(Service_FS, "Secure3 crypto"); - SetKeyY(KeySlotID::NCCHSecure3, key_y_secondary); - if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure3)) { - LOG_ERROR(Service_FS, "Secure3 KeyX missing"); - failed_to_decrypt = true; - } - secondary_key = GetNormalKey(KeySlotID::NCCHSecure3); - break; - case 11: - LOG_DEBUG(Service_FS, "Secure4 crypto"); - SetKeyY(KeySlotID::NCCHSecure4, key_y_secondary); - if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure4)) { - LOG_ERROR(Service_FS, "Secure4 KeyX missing"); - failed_to_decrypt = true; - } - secondary_key = GetNormalKey(KeySlotID::NCCHSecure4); - break; - } - } - - // Find CTR for each section - // Written with reference to - // https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52 - if (ncch_header.version == 0 || ncch_header.version == 2) { - LOG_DEBUG(Loader, "NCCH version 0/2"); - // In this version, CTR for each section is a magic number prefixed by partition ID - // (reverse order) - std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8, - exheader_ctr.begin()); - exefs_ctr = romfs_ctr = exheader_ctr; - exheader_ctr[8] = 1; - exefs_ctr[8] = 2; - romfs_ctr[8] = 3; - } else if (ncch_header.version == 1) { - LOG_DEBUG(Loader, "NCCH version 1"); - // In this version, CTR for each section is the section offset prefixed by partition - // ID, as if the entire NCCH image is encrypted using a single CTR stream. - std::copy(ncch_header.partition_id, ncch_header.partition_id + 8, - exheader_ctr.begin()); - exefs_ctr = romfs_ctr = exheader_ctr; - auto u32ToBEArray = [](u32 value) -> std::array { - return std::array{ - static_cast(value >> 24), - static_cast((value >> 16) & 0xFF), - static_cast((value >> 8) & 0xFF), - static_cast(value & 0xFF), - }; - }; - auto offset_exheader = u32ToBEArray(0x200); // exheader offset - auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize); - auto offset_romfs = u32ToBEArray(ncch_header.romfs_offset * kBlockSize); - std::copy(offset_exheader.begin(), offset_exheader.end(), - exheader_ctr.begin() + 12); - std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12); - std::copy(offset_romfs.begin(), offset_romfs.end(), romfs_ctr.begin() + 12); - } else { - LOG_ERROR(Service_FS, "Unknown NCCH version {}", ncch_header.version); - failed_to_decrypt = true; - } - } else { - LOG_DEBUG(Service_FS, "No crypto"); - is_encrypted = false; + // Encrypted NCCH are not supported + return Loader::ResultStatus::ErrorEncrypted; } // System archives and DLC don't have an extended header but have RomFS - if (ncch_header.extended_header_size) { + // Proto apps don't have an ext header size + if (ncch_header.extended_header_size || is_proto) { auto read_exheader = [this](FileUtil::IOFile& file) { const std::size_t size = sizeof(exheader_header); return file && file.ReadBytes(&exheader_header, size) == size; @@ -335,26 +224,6 @@ Loader::ResultStatus NCCHContainer::Load() { return Loader::ResultStatus::Error; } - if (is_encrypted) { - // This ID check is masked to low 32-bit as a toleration to ill-formed ROM created - // by merging games and its updates. - if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) == - (ncch_header.program_id & 0xFFFFFFFF)) { - LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted " - "exheader. Force no crypto scheme."); - is_encrypted = false; - } else { - if (failed_to_decrypt) { - LOG_ERROR(Service_FS, "Failed to decrypt"); - return Loader::ResultStatus::ErrorEncrypted; - } - CryptoPP::byte* data = reinterpret_cast(&exheader_header); - CryptoPP::CTR_Mode::Decryption( - primary_key.data(), primary_key.size(), exheader_ctr.data()) - .ProcessData(data, data, sizeof(exheader_header)); - } - } - const auto mods_path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), GetModId(ncch_header.program_id)); @@ -380,6 +249,11 @@ Loader::ResultStatus NCCHContainer::Load() { is_tainted = true; } + if (is_proto) { + exheader_header.arm11_system_local_caps.priority = 0x30; + exheader_header.arm11_system_local_caps.resource_limit_category = 0; + } + is_compressed = (exheader_header.codeset_info.flags.flag & 1) == 1; u32 entry_point = exheader_header.codeset_info.text.address; u32 code_size = exheader_header.codeset_info.text.code_size; @@ -409,8 +283,8 @@ Loader::ResultStatus NCCHContainer::Load() { // DLC can have an ExeFS and a RomFS but no extended header if (ncch_header.exefs_size) { - exefs_offset = ncch_header.exefs_offset * kBlockSize; - u32 exefs_size = ncch_header.exefs_size * kBlockSize; + exefs_offset = ncch_header.exefs_offset * block_size; + u32 exefs_size = ncch_header.exefs_size * block_size; LOG_DEBUG(Service_FS, "ExeFS offset: 0x{:08X}", exefs_offset); LOG_DEBUG(Service_FS, "ExeFS size: 0x{:08X}", exefs_size); @@ -419,13 +293,6 @@ Loader::ResultStatus NCCHContainer::Load() { if (file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) return Loader::ResultStatus::Error; - if (is_encrypted) { - CryptoPP::byte* data = reinterpret_cast(&exefs_header); - CryptoPP::CTR_Mode::Decryption(primary_key.data(), - primary_key.size(), exefs_ctr.data()) - .ProcessData(data, data, sizeof(exefs_header)); - } - exefs_file = FileUtil::IOFile(filepath, "rb"); has_exefs = true; } @@ -482,6 +349,29 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect if (result != Loader::ResultStatus::Success) return result; + int block_size = is_proto ? 1 : kBlockSize; + + // Proto has a different exefs format + if (std::strcmp(name, ".code") == 0 && is_proto) { + std::vector ro; + std::vector rw; + auto res = LoadSectionExeFS(".text", buffer); + if (res != Loader::ResultStatus::Success) { + return res; + } + res = LoadSectionExeFS(".ro", ro); + if (res != Loader::ResultStatus::Success) { + return res; + } + res = LoadSectionExeFS(".rw", rw); + if (res != Loader::ResultStatus::Success) { + return res; + } + buffer.insert(buffer.end(), ro.begin(), ro.end()); + buffer.insert(buffer.end(), rw.begin(), rw.end()); + return res; + } + // Check if we have files that can drop-in and replace result = LoadOverrideExeFSSection(name, buffer); if (result == Loader::ResultStatus::Success || !has_exefs) @@ -491,8 +381,8 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect // instead of the ExeFS. if (std::strcmp(name, "logo") == 0) { if (ncch_header.logo_region_offset && ncch_header.logo_region_size) { - std::size_t logo_offset = ncch_header.logo_region_offset * kBlockSize; - std::size_t logo_size = ncch_header.logo_region_size * kBlockSize; + std::size_t logo_offset = ncch_header.logo_region_offset * block_size; + std::size_t logo_size = ncch_header.logo_region_size * block_size; buffer.resize(logo_size); file.Seek(ncch_offset + logo_offset, SEEK_SET); @@ -522,31 +412,19 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect section.offset, section.size, section.name); s64 section_offset = - (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset); + is_proto ? section.offset + : (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset); exefs_file.Seek(section_offset, SEEK_SET); - std::array key; - if (strcmp(section.name, "icon") == 0 || strcmp(section.name, "banner") == 0) { - key = primary_key; - } else { - key = secondary_key; - } - - CryptoPP::CTR_Mode::Decryption dec(key.data(), key.size(), - exefs_ctr.data()); - dec.Seek(section.offset + sizeof(ExeFs_Header)); + 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); + std::vector temp_buffer(section_size); if (exefs_file.ReadBytes(temp_buffer.data(), temp_buffer.size()) != temp_buffer.size()) return Loader::ResultStatus::Error; - if (is_encrypted) { - dec.ProcessData(&temp_buffer[0], &temp_buffer[0], section.size); - } - // Decompress .code section... buffer.resize(LZSS_GetDecompressedSize(temp_buffer)); if (!LZSS_Decompress(temp_buffer, buffer)) { @@ -554,12 +432,9 @@ 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) + buffer.resize(section_size); + if (exefs_file.ReadBytes(buffer.data(), section_size) != section_size) return Loader::ResultStatus::Error; - if (is_encrypted) { - dec.ProcessData(buffer.data(), buffer.data(), section.size); - } } return Loader::ResultStatus::Success; @@ -667,6 +542,8 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf if (result != Loader::ResultStatus::Success) return result; + int block_size = is_proto ? 1 : kBlockSize; + if (ReadOverrideRomFS(romfs_file) == Loader::ResultStatus::Success) return Loader::ResultStatus::Success; @@ -678,8 +555,8 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf if (!file.IsOpen()) return Loader::ResultStatus::Error; - u32 romfs_offset = ncch_offset + (ncch_header.romfs_offset * kBlockSize) + 0x1000; - u32 romfs_size = (ncch_header.romfs_size * kBlockSize) - 0x1000; + u32 romfs_offset = ncch_offset + (ncch_header.romfs_offset * block_size) + 0x1000; + u32 romfs_size = (ncch_header.romfs_size * block_size) - 0x1000; LOG_DEBUG(Service_FS, "RomFS offset: 0x{:08X}", romfs_offset); LOG_DEBUG(Service_FS, "RomFS size: 0x{:08X}", romfs_size); @@ -693,19 +570,14 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf return Loader::ResultStatus::Error; std::shared_ptr direct_romfs; - if (is_encrypted) { - direct_romfs = - std::make_shared(std::move(romfs_file_inner), romfs_offset, - romfs_size, secondary_key, romfs_ctr, 0x1000); - } else { - direct_romfs = std::make_shared(std::move(romfs_file_inner), - romfs_offset, romfs_size); - } + + direct_romfs = + std::make_shared(std::move(romfs_file_inner), romfs_offset, romfs_size); const auto path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), GetModId(ncch_header.program_id)); - if (use_layered_fs && + if (!is_proto && use_layered_fs && (FileUtil::Exists(path + "romfs/") || FileUtil::Exists(path + "romfs_ext/"))) { romfs_file = std::make_shared(std::move(direct_romfs), path + "romfs/", diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index ea1b66b48..17358bb71 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -343,18 +343,11 @@ private: bool has_exefs = false; bool has_romfs = false; + bool is_proto = false; bool is_tainted = false; // Are there parts of this container being overridden? bool is_loaded = false; bool is_compressed = false; - bool is_encrypted = false; - // for decrypting exheader, exefs header and icon/banner section - std::array primary_key{}; - std::array secondary_key{}; // for decrypting romfs and .code section - std::array exheader_ctr{}; - std::array exefs_ctr{}; - std::array romfs_ctr{}; - u32 ncch_offset = 0; // Offset to NCCH header, can be 0 for NCCHs or non-zero for CIAs/NCSDs u32 exefs_offset = 0; u32 partition = 0; diff --git a/src/core/file_sys/otp.cpp b/src/core/file_sys/otp.cpp new file mode 100644 index 000000000..ab01db403 --- /dev/null +++ b/src/core/file_sys/otp.cpp @@ -0,0 +1,56 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/file_util.h" +#include "common/logging/log.h" +#include "core/file_sys/otp.h" +#include "core/loader/loader.h" + +namespace FileSys { +Loader::ResultStatus OTP::Load(const std::string& file_path, std::span key, + std::span iv) { + FileUtil::IOFile file(file_path, "rb"); + if (!file.IsOpen()) + return Loader::ResultStatus::ErrorNotFound; + + if (file.GetSize() != sizeof(OTPBin)) { + LOG_ERROR(HW_AES, "Invalid OTP size"); + return Loader::ResultStatus::Error; + } + + OTPBin temp_otp; + if (file.ReadBytes(&temp_otp, sizeof(OTPBin)) != sizeof(OTPBin)) { + return Loader::ResultStatus::Error; + } + + // OTP is probably encrypted, decrypt it. + if (temp_otp.body.magic != otp_magic) { + CryptoPP::CBC_Mode::Decryption d; + d.SetKeyWithIV(key.data(), key.size(), iv.data()); + d.ProcessData(reinterpret_cast(&temp_otp), reinterpret_cast(&temp_otp), + sizeof(temp_otp)); + + if (temp_otp.body.magic != otp_magic) { + LOG_ERROR(HW_AES, "OTP failed to decrypt (or uses dev keys)"); + return Loader::ResultStatus::Error; + } + } + + // Verify OTP hash + CryptoPP::SHA256 hash; + std::array digest; + hash.CalculateDigest(digest.data(), reinterpret_cast(&temp_otp.body), + sizeof(temp_otp.body)); + if (temp_otp.hash != digest) { + LOG_ERROR(HW_AES, "OTP is corrupted"); + return Loader::ResultStatus::Error; + } + + otp = temp_otp; + return Loader::ResultStatus::Success; +} +} // namespace FileSys diff --git a/src/core/file_sys/otp.h b/src/core/file_sys/otp.h new file mode 100644 index 000000000..cb4dac261 --- /dev/null +++ b/src/core/file_sys/otp.h @@ -0,0 +1,89 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/swap.h" + +namespace Loader { +enum class ResultStatus; +} + +namespace FileSys { +class OTP { +public: + static constexpr u32 otp_magic = 0xDEADB00F; // Very dead, such boof +#pragma pack(push, 1) + struct Body { + u32 magic; + u32 device_id; + std::array fallback_movable_keyY; + u8 otp_version; + u8 system_type; + std::array manufacture_date; + struct { + u32 expiry_date; + std::array priv_key; + std::array signature; + } ctcert; + INSERT_PADDING_BYTES(0x10); + std::array random_key_seed_bytes; + }; + struct OTPBin { + Body body; + std::array hash; + }; +#pragma pack(pop) + static_assert(sizeof(OTPBin) == 0x100, "Invalid OTP size"); + + Loader::ResultStatus Load(const std::string& file_path, std::span key, + std::span iv); + + u8 GetSystemType() const { + return otp.body.system_type; + } + + bool IsDev() const { + return GetSystemType() != 0; + } + + u32 GetDeviceID() const { + return otp.body.device_id; + } + + u32 GetCTCertExpiration() const { + if (otp.body.otp_version < 5) { + return *reinterpret_cast(&otp.body.ctcert.expiry_date); + } else { + return otp.body.ctcert.expiry_date; + } + } + + const std::array GetCTCertPrivateKey() const { + return otp.body.ctcert.priv_key; + } + + const std::array GetCTCertSignature() const { + return otp.body.ctcert.signature; + } + + bool Valid() const { + return otp.body.magic == otp_magic; + } + + void Invalidate() { + otp.body.magic = 0; + } + +private: + OTPBin otp = {0}; +}; +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/romfs_reader.cpp b/src/core/file_sys/romfs_reader.cpp index 2cff2825b..6bd33521e 100644 --- a/src/core/file_sys/romfs_reader.cpp +++ b/src/core/file_sys/romfs_reader.cpp @@ -25,11 +25,6 @@ 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); - if (is_encrypted) { - CryptoPP::CTR_Mode::Decryption d(key.data(), key.size(), ctr.data()); - d.Seek(crypto_offset + offset); - d.ProcessData(buffer, buffer, length); - } LOG_TRACE(Service_FS, "RomFS Cache SKIP: offset={}, length={}", offset, length); return length; } @@ -44,11 +39,6 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length, 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); - if (is_encrypted && read_size) { - CryptoPP::CTR_Mode::Decryption d(key.data(), key.size(), ctr.data()); - d.Seek(crypto_offset + page); - d.ProcessData(cache_entry.second.data(), cache_entry.second.data(), read_size); - } 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 3ca6aaa70..ccedd96e5 100644 --- a/src/core/file_sys/romfs_reader.h +++ b/src/core/file_sys/romfs_reader.h @@ -42,14 +42,7 @@ private: class DirectRomFSReader : public RomFSReader { public: DirectRomFSReader(FileUtil::IOFile&& file, std::size_t file_offset, std::size_t data_size) - : is_encrypted(false), file(std::move(file)), file_offset(file_offset), - data_size(data_size) {} - - DirectRomFSReader(FileUtil::IOFile&& file, std::size_t file_offset, std::size_t data_size, - const std::array& key, const std::array& ctr, - std::size_t crypto_offset) - : is_encrypted(true), file(std::move(file)), key(key), ctr(ctr), file_offset(file_offset), - crypto_offset(crypto_offset), data_size(data_size) {} + : file(std::move(file)), file_offset(file_offset), data_size(data_size) {} ~DirectRomFSReader() override = default; @@ -64,12 +57,8 @@ public: bool CacheReady(std::size_t file_offset, std::size_t length) override; private: - bool is_encrypted; FileUtil::IOFile file; - std::array key; - std::array ctr; u64 file_offset; - u64 crypto_offset; u64 data_size; // Total cache size: 128KB @@ -92,12 +81,8 @@ private: template void serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); - ar & is_encrypted; ar & file; - ar & key; - ar & ctr; ar & file_offset; - ar & crypto_offset; ar & data_size; } friend class boost::serialization::access; diff --git a/src/core/file_sys/seed_db.cpp b/src/core/file_sys/seed_db.cpp index 1e29a203b..4f99cc9dc 100644 --- a/src/core/file_sys/seed_db.cpp +++ b/src/core/file_sys/seed_db.cpp @@ -102,6 +102,16 @@ void SeedDB::Add(const Seed& seed) { seeds.push_back(seed); } +bool SeedDB::Delete(u64 title_id) { + auto it = std::find_if(seeds.begin(), seeds.end(), + [title_id](const auto& seed) { return seed.title_id == title_id; }); + if (it == seeds.end()) { + return false; + } + seeds.erase(it); + return true; +} + std::size_t SeedDB::GetCount() const { return seeds.size(); } @@ -138,6 +148,21 @@ std::optional GetSeed(u64 title_id) { return std::nullopt; } +bool DeleteSeed(u64 title_id) { + SeedDB db; + if (!db.Load()) { + LOG_ERROR(Service_FS, "Failed to load seed database"); + return false; + } + bool found = db.Delete(title_id); + if (found) { + if (!db.Save()) { + LOG_ERROR(Service_FS, "Failed to save seed database"); + } + } + return found; +} + u32 GetSeedCount() { SeedDB db; if (!db.Load()) { diff --git a/src/core/file_sys/seed_db.h b/src/core/file_sys/seed_db.h index 6f4714092..9a702d930 100644 --- a/src/core/file_sys/seed_db.h +++ b/src/core/file_sys/seed_db.h @@ -28,6 +28,7 @@ struct SeedDB { bool Load(); bool Save(); void Add(const Seed& seed); + bool Delete(u64 title_id); std::size_t GetCount() const; auto FindSeedByTitleID(u64 title_id) const; @@ -35,6 +36,7 @@ struct SeedDB { bool AddSeed(const Seed& seed); std::optional GetSeed(u64 title_id); +bool DeleteSeed(u64 title_id); u32 GetSeedCount(); } // namespace FileSys diff --git a/src/core/file_sys/cia_common.h b/src/core/file_sys/signature.h similarity index 74% rename from src/core/file_sys/cia_common.h rename to src/core/file_sys/signature.h index 6e02ac91b..66bd55ea4 100644 --- a/src/core/file_sys/cia_common.h +++ b/src/core/file_sys/signature.h @@ -1,15 +1,15 @@ -// Copyright 2018 Citra Emulator Project +// Copyright 2024 Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once -#include "common/assert.h" #include "common/common_types.h" +#include "common/logging/log.h" namespace FileSys { -enum TMDSignatureType : u32 { +enum SignatureType : u32 { Rsa4096Sha1 = 0x10000, Rsa2048Sha1 = 0x10001, EllipticSha1 = 0x10002, @@ -33,8 +33,7 @@ inline u32 GetSignatureSize(u32 signature_type) { return 0x3C; } - LOG_ERROR(Common_Filesystem, "Tried to read ticket with bad signature {}", signature_type); + LOG_ERROR(Common_Filesystem, "Bad signature {}", signature_type); return 0; } - -} // namespace FileSys +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/ticket.cpp b/src/core/file_sys/ticket.cpp index 3537a8cab..8cbd1831d 100644 --- a/src/core/file_sys/ticket.cpp +++ b/src/core/file_sys/ticket.cpp @@ -5,16 +5,66 @@ #include #include #include +#include #include "common/alignment.h" -#include "core/file_sys/cia_common.h" +#include "core/file_sys/certificate.h" +#include "core/file_sys/otp.h" +#include "core/file_sys/signature.h" #include "core/file_sys/ticket.h" +#include "core/hle/service/am/am.h" #include "core/hw/aes/key.h" +#include "core/hw/ecc.h" +#include "core/hw/unique_data.h" #include "core/loader/loader.h" namespace FileSys { +Loader::ResultStatus Ticket::DoTitlekeyFixup() { + + if (ticket_body.console_id == 0u) { + // Common ticket, no need to fix up + return Loader::ResultStatus::Success; + } + + auto& otp = HW::UniqueData::GetOTP(); + auto& ct_cert = HW::UniqueData::GetCTCert(); + if (!otp.Valid() || !ct_cert.IsValid()) { + LOG_ERROR(HW_AES, "Tried to fixup a ticket without a valid OTP/CTCert"); + return Loader::ResultStatus::Error; + } + + if (ticket_body.console_id != otp.GetDeviceID()) { + // Ticket does not correspond to this console, cannot fixup + LOG_ERROR(HW_AES, "Tried to fixup a ticket that does not correspond to this console"); + return Loader::ResultStatus::Error; + } + + HW::ECC::PublicKey ticket_ecc_public = HW::ECC::CreateECCPublicKey(ticket_body.ecc_public_key); + + auto agreement = ct_cert.ECDHAgree(ticket_ecc_public); + if (agreement.empty()) { + LOG_ERROR(HW_AES, "Failed to perform ECDH agreement"); + return Loader::ResultStatus::Error; + } + + CryptoPP::SHA1 hash; + u8 digest[CryptoPP::SHA1::DIGESTSIZE]; + hash.CalculateDigest(digest, agreement.data(), agreement.size()); + + std::vector key(0x10); + memcpy(key.data(), digest, key.size()); + + std::vector iv(0x10); + *reinterpret_cast(iv.data()) = ticket_body.ticket_id; + + CryptoPP::CBC_Mode::Decryption{key.data(), key.size(), iv.data()}.ProcessData( + ticket_body.title_key.data(), ticket_body.title_key.data(), ticket_body.title_key.size()); + + return Loader::ResultStatus::Success; +} Loader::ResultStatus Ticket::Load(std::span file_data, std::size_t offset) { std::size_t total_size = static_cast(file_data.size() - offset); + serialized_size = total_size; if (total_size < sizeof(u32)) return Loader::ResultStatus::Error; @@ -38,6 +88,65 @@ Loader::ResultStatus Ticket::Load(std::span file_data, std::size_t off std::memcpy(ticket_signature.data(), &file_data[offset + sizeof(u32)], signature_size); std::memcpy(&ticket_body, &file_data[offset + body_start], sizeof(Body)); + std::size_t content_index_start = body_end; + if (total_size < content_index_start + (2 * sizeof(u32))) + return Loader::ResultStatus::Error; + + // Read content index size from the second u32 into it. Actual format is undocumented. + const size_t content_index_size = *reinterpret_cast( + &file_data[offset + content_index_start + 1 * sizeof(u32)]); + const size_t content_index_end = content_index_start + content_index_size; + + if (total_size < content_index_end) + return Loader::ResultStatus::Error; + content_index.resize(content_index_size); + std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size); + + return Loader::ResultStatus::Success; +} + +Loader::ResultStatus Ticket::Load(u64 title_id, u64 ticket_id) { + FileUtil::IOFile f(Service::AM::GetTicketPath(title_id, ticket_id), "rb"); + if (!f.IsOpen()) { + return Loader::ResultStatus::Error; + } + + std::vector ticket_data(f.GetSize()); + f.ReadBytes(ticket_data.data(), ticket_data.size()); + + return Load(ticket_data, 0); +} + +std::vector Ticket::Serialize() const { + std::vector ret(sizeof(u32_be)); + + *reinterpret_cast(ret.data()) = signature_type; + + ret.insert(ret.end(), ticket_signature.begin(), ticket_signature.end()); + + u32 padding = 0x40 - (ret.size() % 0x40); + ret.insert(ret.end(), padding, 0); + + std::span body_span{reinterpret_cast(&ticket_body), + reinterpret_cast(&ticket_body) + sizeof(ticket_body)}; + ret.insert(ret.end(), body_span.begin(), body_span.end()); + + ret.insert(ret.end(), content_index.begin(), content_index.end()); + + return ret; +} + +Loader::ResultStatus Ticket::Save(const std::string& file_path) const { + FileUtil::IOFile file(file_path, "wb"); + if (!file.IsOpen()) + return Loader::ResultStatus::Error; + + auto serialized = Serialize(); + + if (file.WriteBytes(serialized.data(), serialized.size()) != serialized.size()) { + return Loader::ResultStatus::Error; + } + return Loader::ResultStatus::Success; } diff --git a/src/core/file_sys/ticket.h b/src/core/file_sys/ticket.h index aec608cf5..ac93ced3e 100644 --- a/src/core/file_sys/ticket.h +++ b/src/core/file_sys/ticket.h @@ -43,20 +43,36 @@ public: u8 audit; INSERT_PADDING_BYTES(0x42); std::array limits; - std::array content_index; }; - static_assert(sizeof(Body) == 0x210, "Ticket body structure size is wrong"); + static_assert(sizeof(Body) == 0x164, "Ticket body structure size is wrong"); #pragma pack(pop) + Loader::ResultStatus DoTitlekeyFixup(); Loader::ResultStatus Load(std::span file_data, std::size_t offset = 0); + Loader::ResultStatus Load(u64 title_id, u64 ticket_id); + std::vector Serialize() const; + Loader::ResultStatus Save(const std::string& file_path) const; + std::optional> GetTitleKey() const; u64 GetTitleID() const { return ticket_body.title_id; } + u64 GetTicketID() const { + return ticket_body.ticket_id; + } + u16 GetVersion() const { + return ticket_body.ticket_title_version; + } + size_t GetSerializedSize() { + return serialized_size; + } private: Body ticket_body; u32_be signature_type; std::vector ticket_signature; + std::vector content_index; + + size_t serialized_size = 0; }; } // namespace FileSys diff --git a/src/core/file_sys/title_metadata.cpp b/src/core/file_sys/title_metadata.cpp index f9f05ec8b..794a14398 100644 --- a/src/core/file_sys/title_metadata.cpp +++ b/src/core/file_sys/title_metadata.cpp @@ -6,7 +6,7 @@ #include "common/alignment.h" #include "common/file_util.h" #include "common/logging/log.h" -#include "core/file_sys/cia_common.h" +#include "core/file_sys/signature.h" #include "core/file_sys/title_metadata.h" #include "core/loader/loader.h" diff --git a/src/core/hle/applets/erreula.cpp b/src/core/hle/applets/erreula.cpp index 5d0fc6e35..0454cdb57 100644 --- a/src/core/hle/applets/erreula.cpp +++ b/src/core/hle/applets/erreula.cpp @@ -44,11 +44,12 @@ Result ErrEula::ReceiveParameterImpl(const Service::APT::MessageParameter& param } Result ErrEula::Start(const Service::APT::MessageParameter& parameter) { - startup_param = parameter.buffer; + memcpy(¶m, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param))); - // TODO(Subv): Set the expected fields in the response buffer before resending it to the - // application. - // TODO(Subv): Reverse the parameter format for the ErrEula applet + // Do something here, like showing error codes, or prompting for EULA agreement. + if (param.type == DisplayType::Agree) { + param.result = 1; + } // Let the application know that we're closing. Finalize(); @@ -56,8 +57,8 @@ Result ErrEula::Start(const Service::APT::MessageParameter& parameter) { } Result ErrEula::Finalize() { - std::vector buffer(startup_param.size()); - std::fill(buffer.begin(), buffer.end(), 0); + std::vector buffer(sizeof(param)); + memcpy(buffer.data(), ¶m, buffer.size()); CloseApplet(nullptr, buffer); return ResultSuccess; } diff --git a/src/core/hle/applets/erreula.h b/src/core/hle/applets/erreula.h index ba5dbb6e0..49f41bb88 100644 --- a/src/core/hle/applets/erreula.h +++ b/src/core/hle/applets/erreula.h @@ -6,11 +6,38 @@ #include "core/hle/applets/applet.h" #include "core/hle/kernel/shared_memory.h" +#include "core/hle/result.h" namespace HLE::Applets { class ErrEula final : public Applet { public: + enum class DisplayType : u8 { ErrorCode = 0, Error = 1, Agree = 5 }; + struct ErrEulaParam { + static constexpr u32 MESSAGE_SIZE = 1900; + + // Input data + DisplayType type; + INSERT_PADDING_BYTES(0x3); // Unknown + s32 error_code; + INSERT_PADDING_BYTES(0x2); // Unknown + u16 language; + + char16_t message[MESSAGE_SIZE]; + + bool allow_home_button; + INSERT_PADDING_BYTES(0x1); // Unknown + bool launch_system_settings; + INSERT_PADDING_BYTES(0x89); // Unknown + + // Output data + u32 result; + u8 agreed_eula_minor; + u8 agreed_eula_major; + INSERT_PADDING_BYTES(0xA); // Unknown + }; + static_assert(sizeof(ErrEulaParam) == 0xF80, "Invalid ErrEulaParam size"); + explicit ErrEula(Core::System& system, Service::APT::AppletId id, Service::APT::AppletId parent, bool preload, std::weak_ptr manager) : Applet(system, id, parent, preload, std::move(manager)) {} @@ -27,7 +54,7 @@ private: std::shared_ptr framebuffer_memory; /// Parameter received by the applet on start. - std::vector startup_param; + ErrEulaParam param{}; }; } // namespace HLE::Applets diff --git a/src/core/hle/kernel/ipc.cpp b/src/core/hle/kernel/ipc.cpp index 2dc17ea7a..17b5b20f0 100644 --- a/src/core/hle/kernel/ipc.cpp +++ b/src/core/hle/kernel/ipc.cpp @@ -205,12 +205,12 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size); // Map the guard pages and mapped pages at once. - target_address = - dst_process->vm_manager - .MapBackingMemoryToBase(Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, - buffer, static_cast(buffer->GetSize()), - Kernel::MemoryState::Shared) - .Unwrap(); + auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase( + Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer, + static_cast(buffer->GetSize()), Kernel::MemoryState::Shared); + + ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address"); + target_address = target_address_result.Unwrap(); // Change the permissions and state of the guard pages. const VAddr low_guard_address = target_address; diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index dd9df5fea..ba0917d86 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -2362,7 +2362,7 @@ void SVC::CallSVC(u32 immediate) { if (info->func) { (this->*(info->func))(); } else { - LOG_ERROR(Kernel_SVC, "unimplemented SVC function {}(..)", info->name); + LOG_ERROR(Kernel_SVC, "unimplemented SVC function {:02X} {}(..)", info->id, info->name); } } } diff --git a/src/core/hle/service/ac/ac.cpp b/src/core/hle/service/ac/ac.cpp index 5d6eea454..7495ce711 100644 --- a/src/core/hle/service/ac/ac.cpp +++ b/src/core/hle/service/ac/ac.cpp @@ -13,6 +13,7 @@ #include "core/hle/kernel/event.h" #include "core/hle/kernel/handle_table.h" #include "core/hle/kernel/resource_limit.h" +#include "core/hle/kernel/shared_page.h" #include "core/hle/result.h" #include "core/hle/service/ac/ac.h" #include "core/hle/service/ac/ac_i.h" @@ -95,21 +96,20 @@ void Module::Interface::GetCloseResult(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_AC, "(STUBBED) called"); } -void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) { +void Module::Interface::GetStatus(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - bool can_reach_internet = false; - - std::shared_ptr socu_module = SOC::GetService(ac->system); - if (socu_module) { - can_reach_internet = socu_module->GetDefaultInterfaceInfo().has_value(); - } IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); - rb.Push(static_cast(can_reach_internet ? (Settings::values.is_new_3ds - ? WifiStatus::STATUS_CONNECTED_N3DS - : WifiStatus::STATUS_CONNECTED_O3DS) - : WifiStatus::STATUS_DISCONNECTED)); + rb.Push(static_cast(Status::STATUS_INTERNET)); +} + +void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(static_cast(WifiStatus::STATUS_CONNECTED_SLOT1)); } void Module::Interface::GetInfraPriority(Kernel::HLERequestContext& ctx) { @@ -176,8 +176,8 @@ void Module::Interface::IsConnected(Kernel::HLERequestContext& ctx) { rb.Push(ResultSuccess); rb.Push(ac->ac_connected); - LOG_WARNING(Service_AC, "(STUBBED) called unk=0x{:08X} descriptor=0x{:08X} param=0x{:08X}", unk, - unk_descriptor, unk_param); + LOG_DEBUG(Service_AC, "(STUBBED) called unk=0x{:08X} descriptor=0x{:08X} param=0x{:08X}", unk, + unk_descriptor, unk_param); } void Module::Interface::SetClientVersion(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/ac/ac.h b/src/core/hle/service/ac/ac.h index 2b0d2e5ce..db2d7ec5b 100644 --- a/src/core/hle/service/ac/ac.h +++ b/src/core/hle/service/ac/ac.h @@ -82,7 +82,15 @@ public: * AC::GetWifiStatus service function * Outputs: * 1 : Result of function, 0 on success, otherwise error code - * 2 : Output connection type, 0 = none, 1 = Old3DS Internet, 2 = New3DS Internet. + * 2 : Status + */ + void GetStatus(Kernel::HLERequestContext& ctx); + + /** + * AC::GetWifiStatus service function + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + * 2 : WifiStatus */ void GetWifiStatus(Kernel::HLERequestContext& ctx); @@ -153,10 +161,17 @@ public: }; protected: + enum class Status { + STATUS_DISCONNECTED = 0, + STATUS_ENABLED = 1, + STATUS_CONNECTED = 2, + STATUS_INTERNET = 3, + }; enum class WifiStatus { STATUS_DISCONNECTED = 0, - STATUS_CONNECTED_O3DS = 1, - STATUS_CONNECTED_N3DS = 2, + STATUS_CONNECTED_SLOT1 = (1 << 0), + STATUS_CONNECTED_SLOT2 = (1 << 1), + STATUS_CONNECTED_SLOT3 = (1 << 2), }; struct ACConfig { diff --git a/src/core/hle/service/ac/ac_i.cpp b/src/core/hle/service/ac/ac_i.cpp index f7766b324..09b72f6fc 100644 --- a/src/core/hle/service/ac/ac_i.cpp +++ b/src/core/hle/service/ac/ac_i.cpp @@ -17,7 +17,7 @@ AC_I::AC_I(std::shared_ptr ac) : Module::Interface(std::move(ac), "ac:i" {0x0008, &AC_I::CloseAsync, "CloseAsync"}, {0x0009, &AC_I::GetCloseResult, "GetCloseResult"}, {0x000A, nullptr, "GetLastErrorCode"}, - {0x000C, nullptr, "GetStatus"}, + {0x000C, &AC_I::GetStatus, "GetStatus"}, {0x000D, &AC_I::GetWifiStatus, "GetWifiStatus"}, {0x000E, nullptr, "GetCurrentAPInfo"}, {0x0010, nullptr, "GetCurrentNZoneInfo"}, diff --git a/src/core/hle/service/ac/ac_u.cpp b/src/core/hle/service/ac/ac_u.cpp index 12506962c..1682b7166 100644 --- a/src/core/hle/service/ac/ac_u.cpp +++ b/src/core/hle/service/ac/ac_u.cpp @@ -17,7 +17,7 @@ AC_U::AC_U(std::shared_ptr ac) : Module::Interface(std::move(ac), "ac:u" {0x0008, &AC_U::CloseAsync, "CloseAsync"}, {0x0009, &AC_U::GetCloseResult, "GetCloseResult"}, {0x000A, nullptr, "GetLastErrorCode"}, - {0x000C, nullptr, "GetStatus"}, + {0x000C, &AC_U::GetStatus, "GetStatus"}, {0x000D, &AC_U::GetWifiStatus, "GetWifiStatus"}, {0x000E, nullptr, "GetCurrentAPInfo"}, {0x0010, nullptr, "GetCurrentNZoneInfo"}, diff --git a/src/core/hle/service/act/act_errors.cpp b/src/core/hle/service/act/act_errors.cpp index cab975d00..1f9b86561 100644 --- a/src/core/hle/service/act/act_errors.cpp +++ b/src/core/hle/service/act/act_errors.cpp @@ -25,8 +25,8 @@ u32 GetACTErrorCode(Result result) { case ErrDescriptions::AlreadyInitialized: error_code = ErrCodes::AlreadyInitialized; break; - case ErrDescriptions::ErrDesc103: - error_code = ErrCodes::ErrCode225103; + case ErrDescriptions::AcStatusDisconnected: + error_code = ErrCodes::AcStatusDisconnected; break; case ErrDescriptions::ErrDesc104: error_code = ErrCodes::ErrCode225104; diff --git a/src/core/hle/service/act/act_errors.h b/src/core/hle/service/act/act_errors.h index 8a65eaeda..ee6efabf1 100644 --- a/src/core/hle/service/act/act_errors.h +++ b/src/core/hle/service/act/act_errors.h @@ -17,7 +17,7 @@ enum { LibraryError = 100, NotInitialized = 101, AlreadyInitialized = 102, - ErrDesc103 = 103, + AcStatusDisconnected = 103, ErrDesc104 = 104, Busy = 111, ErrDesc112 = 112, @@ -313,16 +313,16 @@ enum { MailAddressNotConfirmed = 220001, // 022-0001 // Library errors - LibraryError = 220500, // 022-0500 - NotInitialized = 220501, // 022-0501 - AlreadyInitialized = 220502, // 022-0502 - ErrCode225103 = 225103, // 022-5103 - ErrCode225104 = 225104, // 022-5104 - Busy = 220511, // 022-0511 - ErrCode225112 = 225112, // 022-5112 - NotImplemented = 220591, // 022-0591 - Deprecated = 220592, // 022-0592 - DevelopmentOnly = 220593, // 022-0593 + LibraryError = 220500, // 022-0500 + NotInitialized = 220501, // 022-0501 + AlreadyInitialized = 220502, // 022-0502 + AcStatusDisconnected = 225103, // 022-5103 + ErrCode225104 = 225104, // 022-5104 + Busy = 220511, // 022-0511 + ErrCode225112 = 225112, // 022-5112 + NotImplemented = 220591, // 022-0591 + Deprecated = 220592, // 022-0592 + DevelopmentOnly = 220593, // 022-0593 InvalidArgument = 220600, // 022-0600 InvalidPointer = 220601, // 022-0601 diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 77f2b396d..2e6c68799 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -8,19 +8,25 @@ #include #include #include +#include #include "common/alignment.h" #include "common/archives.h" #include "common/common_paths.h" #include "common/file_util.h" +#include "common/hacks/hack_manager.h" #include "common/logging/log.h" #include "common/string_util.h" #include "core/core.h" +#include "core/file_sys/certificate.h" #include "core/file_sys/errors.h" #include "core/file_sys/ncch_container.h" +#include "core/file_sys/otp.h" +#include "core/file_sys/seed_db.h" #include "core/file_sys/title_metadata.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/client_session.h" #include "core/hle/kernel/errors.h" +#include "core/hle/kernel/process.h" #include "core/hle/kernel/server_session.h" #include "core/hle/kernel/session.h" #include "core/hle/service/am/am.h" @@ -30,6 +36,9 @@ #include "core/hle/service/am/am_u.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/fs/fs_user.h" +#include "core/hw/aes/key.h" +#include "core/hw/rsa/rsa.h" +#include "core/hw/unique_data.h" #include "core/loader/loader.h" #include "core/loader/smdh.h" #include "core/nus_download.h" @@ -80,43 +89,332 @@ 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; }; -CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type) - : system(system_), media_type(media_type), - decryption_state(std::make_unique()) {} +NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file) { + file = FileUtil::IOFile(out_file, "wb"); +} + +void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { + if (is_error) + return; + + const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes) + + if (header_size != sizeof(NCCH_Header)) { + std::size_t to_copy = std::min(length, sizeof(NCCH_Header) - header_size); + memcpy(reinterpret_cast(&ncch_header) + header_size, buffer, to_copy); + header_size += to_copy; + buffer += to_copy; + length -= to_copy; + } + + if (!header_parsed && header_size == sizeof(NCCH_Header)) { + if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { + is_error = true; + return; + } + + if (!ncch_header.no_crypto) { + if (!decryption_authorized) { + LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); + is_error = true; + return; + } + is_encrypted = true; + + // Find primary and secondary keys + if (ncch_header.fixed_key) { + LOG_DEBUG(Service_AM, "Fixed-key crypto"); + primary_key.fill(0); + secondary_key.fill(0); + } else { + using namespace HW::AES; + InitKeys(); + std::array key_y_primary, key_y_secondary; + + std::copy(ncch_header.signature, ncch_header.signature + key_y_primary.size(), + key_y_primary.begin()); + + if (!ncch_header.seed_crypto) { + key_y_secondary = key_y_primary; + } else { + auto opt{FileSys::GetSeed(ncch_header.program_id)}; + if (!opt.has_value()) { + LOG_ERROR(Service_AM, "Seed for program {:016X} not found", + ncch_header.program_id); + is_error = true; + } else { + auto seed{*opt}; + std::array input; + std::memcpy(input.data(), key_y_primary.data(), key_y_primary.size()); + std::memcpy(input.data() + key_y_primary.size(), seed.data(), seed.size()); + CryptoPP::SHA256 sha; + std::array hash; + sha.CalculateDigest(hash.data(), input.data(), input.size()); + std::memcpy(key_y_secondary.data(), hash.data(), key_y_secondary.size()); + } + } + + SetKeyY(KeySlotID::NCCHSecure1, key_y_primary); + if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure1)) { + LOG_ERROR(Service_AM, "Secure1 KeyX missing"); + is_error = true; + } + primary_key = GetNormalKey(KeySlotID::NCCHSecure1); + + switch (ncch_header.secondary_key_slot) { + case 0: + LOG_DEBUG(Service_AM, "Secure1 crypto"); + SetKeyY(KeySlotID::NCCHSecure1, key_y_secondary); + if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure1)) { + LOG_ERROR(Service_AM, "Secure1 KeyX missing"); + is_error = true; + } + secondary_key = GetNormalKey(KeySlotID::NCCHSecure1); + break; + case 1: + LOG_DEBUG(Service_AM, "Secure2 crypto"); + SetKeyY(KeySlotID::NCCHSecure2, key_y_secondary); + if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure2)) { + LOG_ERROR(Service_AM, "Secure2 KeyX missing"); + is_error = true; + } + secondary_key = GetNormalKey(KeySlotID::NCCHSecure2); + break; + case 10: + LOG_DEBUG(Service_AM, "Secure3 crypto"); + SetKeyY(KeySlotID::NCCHSecure3, key_y_secondary); + if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure3)) { + LOG_ERROR(Service_AM, "Secure3 KeyX missing"); + is_error = true; + } + secondary_key = GetNormalKey(KeySlotID::NCCHSecure3); + break; + case 11: + LOG_DEBUG(Service_AM, "Secure4 crypto"); + SetKeyY(KeySlotID::NCCHSecure4, key_y_secondary); + if (!IsNormalKeyAvailable(KeySlotID::NCCHSecure4)) { + LOG_ERROR(Service_AM, "Secure4 KeyX missing"); + is_error = true; + } + secondary_key = GetNormalKey(KeySlotID::NCCHSecure4); + break; + } + } + + // Find CTR for each section + // Written with reference to + // https://github.com/d0k3/GodMode9/blob/99af6a73be48fa7872649aaa7456136da0df7938/arm9/source/game/ncch.c#L34-L52 + if (ncch_header.version == 0 || ncch_header.version == 2) { + LOG_DEBUG(Service_AM, "NCCH version 0/2"); + // In this version, CTR for each section is a magic number prefixed by partition ID + // (reverse order) + std::reverse_copy(ncch_header.partition_id, ncch_header.partition_id + 8, + exheader_ctr.begin()); + exefs_ctr = romfs_ctr = exheader_ctr; + exheader_ctr[8] = 1; + exefs_ctr[8] = 2; + romfs_ctr[8] = 3; + } else if (ncch_header.version == 1) { + LOG_DEBUG(Service_AM, "NCCH version 1"); + // In this version, CTR for each section is the section offset prefixed by partition + // ID, as if the entire NCCH image is encrypted using a single CTR stream. + std::copy(ncch_header.partition_id, ncch_header.partition_id + 8, + exheader_ctr.begin()); + exefs_ctr = romfs_ctr = exheader_ctr; + auto u32ToBEArray = [](u32 value) -> std::array { + return std::array{ + static_cast(value >> 24), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF), + }; + }; + auto offset_exheader = u32ToBEArray(0x200); // exheader offset + auto offset_exefs = u32ToBEArray(ncch_header.exefs_offset * kBlockSize); + auto offset_romfs = u32ToBEArray(ncch_header.romfs_offset * kBlockSize); + std::copy(offset_exheader.begin(), offset_exheader.end(), + exheader_ctr.begin() + 12); + std::copy(offset_exefs.begin(), offset_exefs.end(), exefs_ctr.begin() + 12); + std::copy(offset_romfs.begin(), offset_romfs.end(), romfs_ctr.begin() + 12); + } else { + LOG_ERROR(Service_AM, "Unknown NCCH version {}", ncch_header.version); + is_error = true; + } + } else { + LOG_DEBUG(Service_AM, "No crypto"); + is_encrypted = false; + } + header_parsed = true; + + if (is_error) { + return; + } + + if (is_encrypted) { + if (ncch_header.extended_header_size) { + regions.push_back(CryptoRegion{.type = CryptoRegion::EXHEADER, + .offset = sizeof(NCCH_Header), + .size = sizeof(ExHeader_Header), + .seek_from = sizeof(NCCH_Header)}); + } + if (ncch_header.exefs_size) { + regions.push_back(CryptoRegion{.type = CryptoRegion::EXEFS_HDR, + .offset = ncch_header.exefs_offset * kBlockSize, + .size = sizeof(ExeFs_Header), + .seek_from = ncch_header.exefs_offset * kBlockSize}); + } + if (ncch_header.romfs_size) { + regions.push_back(CryptoRegion{.type = CryptoRegion::ROMFS, + .offset = ncch_header.romfs_offset * kBlockSize, + .size = ncch_header.romfs_size * kBlockSize, + .seek_from = ncch_header.romfs_offset * kBlockSize}); + } + } + + u8 prev_crypto = ncch_header.no_crypto; + ncch_header.no_crypto.Assign(1); + file.WriteBytes(&ncch_header, sizeof(ncch_header)); + written += sizeof(ncch_header); + ncch_header.no_crypto.Assign(prev_crypto); + } + + while (length) { + auto find_closest_region = [this](size_t offset) -> CryptoRegion* { + CryptoRegion* closest = nullptr; + for (auto& reg : regions) { + if (offset >= reg.offset && offset < reg.offset + reg.size) { + return ® + } + if (offset < reg.offset) { + size_t dist = reg.offset - offset; + if ((closest && closest->offset - offset > dist) || !closest) { + closest = ® + } + } + } + // Return the closest one + return closest; + }; + + CryptoRegion* reg = find_closest_region(written); + if (reg == nullptr) { + // This file has no encryption + size_t to_write = length; + file.WriteBytes(buffer, to_write); + written += to_write; + buffer += to_write; + length -= to_write; + } else { + if (written < reg->offset) { + // Not inside a crypto region + size_t to_write = std::min(length, reg->offset - written); + file.WriteBytes(buffer, to_write); + written += to_write; + buffer += to_write; + length -= to_write; + } else { + size_t to_write = std::min(length, (reg->offset + reg->size) - written); + if (is_encrypted) { + std::vector temp(to_write); + + std::array* key; + std::array* ctr; + + if (reg->type == CryptoRegion::EXHEADER) { + key = &primary_key; + ctr = &exheader_ctr; + } else if (reg->type == CryptoRegion::EXEFS_HDR || + reg->type == CryptoRegion::EXEFS_PRI) { + key = &primary_key; + ctr = &exefs_ctr; + } else if (reg->type == CryptoRegion::EXEFS_SEC) { + key = &secondary_key; + ctr = &exefs_ctr; + } else if (reg->type == CryptoRegion::ROMFS) { + key = &secondary_key; + ctr = &romfs_ctr; + } + + CryptoPP::CTR_Mode::Decryption d(key->data(), key->size(), + ctr->data()); + size_t offset = written - reg->seek_from; + if (offset != 0) { + d.Seek(offset); + } + d.ProcessData(temp.data(), buffer, to_write); + file.WriteBytes(temp.data(), to_write); + + if (reg->type == CryptoRegion::EXEFS_HDR) { + if (exefs_header_written != sizeof(ExeFs_Header)) { + memcpy(reinterpret_cast(&exefs_header) + exefs_header_written, + temp.data(), to_write); + exefs_header_written += to_write; + } + if (!exefs_header_processed && + exefs_header_written == sizeof(ExeFs_Header)) { + for (int i = 0; i < 8; i++) { + if (exefs_header.section[i].size != 0) { + bool is_primary = + strcmp(exefs_header.section[i].name, "icon") == 0 || + strcmp(exefs_header.section[i].name, "banner") == 0; + regions.push_back(CryptoRegion{ + .type = is_primary ? CryptoRegion::EXEFS_PRI + : CryptoRegion::EXEFS_SEC, + .offset = reg->offset + sizeof(ExeFs_Header) + + exefs_header.section[i].offset, + .size = + Common::AlignUp(exefs_header.section[i].size, 0x200), + .seek_from = reg->offset}); + } + } + exefs_header_processed = true; + } + } + } else { + file.WriteBytes(buffer, to_write); + } + written += to_write; + buffer += to_write; + length -= to_write; + } + } + } +} + +void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx) { + u64 caller_tid = ctx.ClientThread()->owner_process.lock()->codeset->program_id; + if (Common::Hacks::hack_manager.GetHackAllowMode( + Common::Hacks::HackType::DECRYPTION_AUTHORIZED, caller_tid, + Common::Hacks::HackAllowMode::DISALLOW) == Common::Hacks::HackAllowMode::ALLOW) { + LOG_INFO(Service_AM, "Authorized encrypted CIA installation."); + cia_file->decryption_authorized = true; + } +} + +CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn_) + : system(system_), from_cdn(from_cdn_), decryption_authorized(false), media_type(media_type), + decryption_state(std::make_unique()) { + // If data is being installing from CDN, provide a fake header to the container so that + // it's not uninitialized. + if (from_cdn) { + FileSys::CIAContainer::Header fake_header{ + .header_size = sizeof(FileSys::CIAContainer::Header), + .type = 0, + .version = 0, + .cert_size = 0, + .tik_size = 0, + .tmd_size = 0, + .meta_size = 0, + }; + container.LoadHeader({reinterpret_cast(&fake_header), sizeof(fake_header)}); + install_state = CIAInstallState::HeaderLoaded; + } +} CIAFile::~CIAFile() { Close(); @@ -136,16 +434,29 @@ Result CIAFile::WriteTicket() { ErrorLevel::Permanent}; } - // TODO: Write out .tik files to nand? + const auto& ticket = container.GetTicket(); + const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID()); + + // Create ticket folder if it does not exist + std::string ticket_folder; + Common::SplitPath(ticket_path, &ticket_folder, nullptr, nullptr); + FileUtil::CreateFullPath(ticket_folder); + + // Save ticket + 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; + } install_state = CIAInstallState::TicketLoaded; return ResultSuccess; } -Result CIAFile::WriteTitleMetadata() { - auto load_result = container.LoadTitleMetadata(data, container.GetTitleMetadataOffset()); +Result CIAFile::WriteTitleMetadata(std::span tmd_data, std::size_t offset) { + auto load_result = container.LoadTitleMetadata(tmd_data, offset); if (load_result != Loader::ResultStatus::Success) { - LOG_ERROR(Service_AM, "Could not read title metadata from CIA."); + LOG_ERROR(Service_AM, "Could not read title metadata."); // TODO: Correct result code. return {ErrCodes::InvalidCIAHeader, ErrorModule::AM, ErrorSummary::InvalidArgument, ErrorLevel::Permanent}; @@ -185,29 +496,32 @@ Result CIAFile::WriteTitleMetadata() { auto content_count = container.GetTitleMetadata().GetContentCount(); content_written.resize(content_count); - content_files.clear(); + current_content_file.reset(); + current_content_index = -1; + content_file_paths.clear(); for (std::size_t i = 0; i < content_count; i++) { auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update); - auto& file = content_files.emplace_back(path, "wb"); - if (!file.IsOpen()) { - LOG_ERROR(Service_AM, "Could not open output file '{}' for content {}.", path, i); - // TODO: Correct error code. - return FileSys::ResultFileNotFound; - } + content_file_paths.emplace_back(path); } if (container.GetTitleMetadata().HasEncryptedContent()) { - if (auto title_key = container.GetTicket().GetTitleKey()) { - decryption_state->content.resize(content_count); - for (std::size_t i = 0; i < content_count; ++i) { - auto ctr = tmd.GetContentCTRByIndex(i); - decryption_state->content[i].SetKeyWithIV(title_key->data(), title_key->size(), - ctr.data()); - } + if (!decryption_authorized) { + LOG_ERROR(Service_AM, "Blocked unauthorized encrypted CIA installation."); + return {ErrorDescription::NotAuthorized, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent}; } else { - LOG_ERROR(Service_AM, "Could not read title key from ticket for encrypted CIA."); - // TODO: Correct error code. - return FileSys::ResultFileNotFound; + if (auto title_key = container.GetTicket().GetTitleKey()) { + decryption_state->content.resize(content_count); + for (std::size_t i = 0; i < content_count; ++i) { + auto ctr = tmd.GetContentCTRByIndex(i); + decryption_state->content[i].SetKeyWithIV(title_key->data(), title_key->size(), + ctr.data()); + } + } else { + LOG_ERROR(Service_AM, "Could not read title key from ticket for encrypted CIA."); + // TODO: Correct error code. + return FileSys::ResultFileNotFound; + } } } else { LOG_INFO(Service_AM, @@ -224,6 +538,7 @@ 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 @@ -242,8 +557,13 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, // Since the incoming TMD has already been written, we can use GetTitleContentPath // to get the content paths to write to. - FileSys::TitleMetadata tmd = container.GetTitleMetadata(); - auto& file = content_files[i]; + const FileSys::TitleMetadata& tmd = container.GetTitleMetadata(); + if (i != current_content_index) { + current_content_index = static_cast(i); + current_content_file = std::make_unique(content_file_paths[i]); + current_content_file->decryption_authorized = decryption_authorized; + } + auto& file = *current_content_file; std::vector temp(buffer + (range_min - offset), buffer + (range_min - offset) + available_to_write); @@ -252,17 +572,19 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size()); } - file.WriteBytes(temp.data(), temp.size()); + file.Write(temp.data(), temp.size()); + if (file.IsError()) + success = false; // Keep tabs on how much of this content ID has been written so new range_min // values can be calculated. content_written[i] += available_to_write; - LOG_DEBUG(Service_AM, "Wrote {:x} to content {}, total {:x}", available_to_write, i, + LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", available_to_write, i, content_written[i]); } } - return length; + return success ? length : 0; } ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush, @@ -319,7 +641,7 @@ ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush return result; } - result = WriteTitleMetadata(); + result = WriteTitleMetadata(data, container.GetTitleMetadataOffset()); if (result.IsError()) { return result; } @@ -339,6 +661,59 @@ ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush return length; } +Result CIAFile::ProvideTicket(const FileSys::Ticket& ticket) { + // There is no need to write the ticket to nand, as that will + ASSERT_MSG(from_cdn, "This method should only be used when installing from CDN"); + + auto load_result = container.LoadTicket(ticket); + 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}; + } + + install_state = CIAInstallState::TicketLoaded; + return ResultSuccess; +} + +const FileSys::TitleMetadata& CIAFile::GetTMD() { + return container.GetTitleMetadata(); +} + +ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 offset, + std::size_t length, const u8* buffer) { + + ASSERT_MSG(from_cdn, "This method should only be used when installing from CDN"); + + const FileSys::TitleMetadata& tmd = container.GetTitleMetadata(); + + u64 remaining_to_write = + tmd.GetContentSizeByIndex(content_index) - content_written[content_index]; + + if (content_index != current_content_index) { + current_content_index = content_index; + current_content_file = std::make_unique(content_file_paths[content_index]); + current_content_file->decryption_authorized = decryption_authorized; + } + auto& file = *current_content_file; + + std::vector temp(buffer, buffer + std::min(static_cast(length), remaining_to_write)); + + if ((tmd.GetContentTypeByIndex(content_index) & FileSys::TMDContentTypeFlag::Encrypted) != 0) { + decryption_state->content[content_index].ProcessData(temp.data(), temp.data(), temp.size()); + } + + file.Write(temp.data(), temp.size()); + bool success = !file.IsError(); + + 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; +} + u64 CIAFile::GetSize() const { return written; } @@ -348,18 +723,25 @@ bool CIAFile::SetSize(u64 size) const { } bool CIAFile::Close() { + if (is_closed) + return true; + is_closed = true; + bool complete = - install_state >= CIAInstallState::TMDLoaded && - content_written.size() == container.GetTitleMetadata().GetContentCount() && - std::all_of(content_written.begin(), content_written.end(), - [this, i = 0](auto& bytes_written) mutable { - return bytes_written >= container.GetContentSize(static_cast(i++)); - }); + from_cdn ? is_done + : (install_state >= CIAInstallState::TMDLoaded && + content_written.size() == container.GetTitleMetadata().GetContentCount() && + std::all_of(content_written.begin(), content_written.end(), + [this, i = 0](auto& bytes_written) mutable { + return bytes_written >= + container.GetContentSize(static_cast(i++)); + })); // Install aborted if (!complete) { LOG_ERROR(Service_AM, "CIAFile closed prematurely, aborting install..."); - FileUtil::DeleteDir(GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID())); + FileUtil::DeleteDirRecursively( + GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID())); return true; } @@ -435,17 +817,112 @@ bool TicketFile::SetSize(u64 size) const { } bool TicketFile::Close() { - FileSys::Ticket ticket; - if (ticket.Load(data, 0) == Loader::ResultStatus::Success) { - LOG_WARNING(Service_AM, "Discarding ticket for {:#016X}.", ticket.GetTitleID()); - } else { - LOG_ERROR(Service_AM, "Invalid ticket provided to TicketFile."); - } return true; } void TicketFile::Flush() const {} +Result TicketFile::Commit() { + FileSys::Ticket ticket; + if (ticket.Load(data, 0) == Loader::ResultStatus::Success) { + if (ticket.DoTitlekeyFixup() != Loader::ResultStatus::Success) { + LOG_ERROR(Service_AM, "Failed to do ticket title key fixup"); + return ResultUnknown; + } + + title_id = ticket.GetTitleID(); + ticket_id = ticket.GetTicketID(); + const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID()); + + // Create ticket folder if it does not exist + std::string ticket_folder; + Common::SplitPath(ticket_path, &ticket_folder, nullptr, nullptr); + FileUtil::CreateFullPath(ticket_folder); + + // Save ticket + if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) { + LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile."); + return ResultUnknown; + } + return ResultSuccess; + } else { + LOG_ERROR(Service_AM, "Invalid ticket provided to TicketFile."); + return ResultUnknown; + } +} + +TMDFile::~TMDFile() { + Close(); +} + +ResultVal TMDFile::Read(u64 offset, std::size_t length, u8* buffer) const { + UNIMPLEMENTED(); + return length; +} + +ResultVal TMDFile::Write(u64 offset, std::size_t length, bool flush, + bool update_timestamp, const u8* buffer) { + written += length; + data.resize(written); + std::memcpy(data.data() + offset, buffer, length); + return length; +} + +u64 TMDFile::GetSize() const { + return written; +} + +bool TMDFile::SetSize(u64 size) const { + return false; +} + +bool TMDFile::Close() { + return true; +} + +void TMDFile::Flush() const {} + +Result TMDFile::Commit() { + return importing_title->cia_file.WriteTitleMetadata(data, 0); +} + +ContentFile::~ContentFile() { + Close(); +} + +ResultVal ContentFile::Read(u64 offset, std::size_t length, u8* buffer) const { + UNIMPLEMENTED(); + return length; +} + +ResultVal ContentFile::Write(u64 offset, std::size_t length, bool flush, + bool update_timestamp, const u8* buffer) { + auto res = importing_title->cia_file.WriteContentDataIndexed(index, offset, length, buffer); + if (res.Succeeded()) { + import_context.current_size += static_cast(res.Unwrap()); + } + return res; +} + +u64 ContentFile::GetSize() const { + return written; +} + +bool ContentFile::SetSize(u64 size) const { + return false; +} + +bool ContentFile::Close() { + return false; +} + +void ContentFile::Flush() const {} + +void ContentFile::Cancel(FS::MediaType media_type, u64 title_id) { + auto path = GetTitleContentPath(media_type, title_id, index, true); + FileUtil::Delete(path); +} + InstallStatus InstallCIA(const std::string& path, std::function&& update_callback) { LOG_INFO(Service_AM, "Installing {}...", path); @@ -461,10 +938,8 @@ InstallStatus InstallCIA(const std::string& path, Core::System::GetInstance(), Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID())); - bool title_key_available = container.GetTicket().GetTitleKey().has_value(); - if (!title_key_available && container.GetTitleMetadata().HasEncryptedContent()) { - LOG_ERROR(Service_AM, "File {} is encrypted and no title key is available! Aborting...", - path); + if (container.GetTitleMetadata().HasEncryptedContent()) { + LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path); return InstallStatus::ErrorEncrypted; } @@ -474,7 +949,8 @@ InstallStatus InstallCIA(const std::string& path, return InstallStatus::ErrorFailedToOpenFile; } - std::array buffer; + std::vector buffer; + buffer.resize(0x10000); auto file_size = file.GetSize(); std::size_t total_bytes_read = 0; while (total_bytes_read != file_size) { @@ -533,96 +1009,6 @@ InstallStatus InstallCIA(const std::string& path, return InstallStatus::ErrorInvalid; } -InstallStatus InstallFromNus(u64 title_id, int version) { - LOG_DEBUG(Service_AM, "Downloading {:X}", title_id); - - CIAFile install_file{Core::System::GetInstance(), GetTitleMediaType(title_id)}; - - std::string path = fmt::format("/ccs/download/{:016X}/tmd", title_id); - if (version != -1) { - path += fmt::format(".{}", version); - } - auto tmd_response = Core::NUS::Download(path); - if (!tmd_response) { - LOG_ERROR(Service_AM, "Failed to download tmd for {:016X}", title_id); - return InstallStatus::ErrorFileNotFound; - } - FileSys::TitleMetadata tmd; - tmd.Load(*tmd_response); - - path = fmt::format("/ccs/download/{:016X}/cetk", title_id); - auto cetk_response = Core::NUS::Download(path); - if (!cetk_response) { - LOG_ERROR(Service_AM, "Failed to download cetk for {:016X}", title_id); - return InstallStatus::ErrorFileNotFound; - } - - std::vector content; - const auto content_count = tmd.GetContentCount(); - for (std::size_t i = 0; i < content_count; ++i) { - const std::string filename = fmt::format("{:08x}", tmd.GetContentIDByIndex(i)); - path = fmt::format("/ccs/download/{:016X}/{}", title_id, filename); - const auto temp_response = Core::NUS::Download(path); - if (!temp_response) { - LOG_ERROR(Service_AM, "Failed to download content for {:016X}", title_id); - return InstallStatus::ErrorFileNotFound; - } - content.insert(content.end(), temp_response->begin(), temp_response->end()); - } - - FileSys::CIAContainer::Header fake_header{ - .header_size = sizeof(FileSys::CIAContainer::Header), - .type = 0, - .version = 0, - .cert_size = 0, - .tik_size = static_cast(cetk_response->size()), - .tmd_size = static_cast(tmd_response->size()), - .meta_size = 0, - }; - for (u16 i = 0; i < content_count; ++i) { - fake_header.SetContentPresent(i); - } - std::vector header_data(sizeof(fake_header)); - std::memcpy(header_data.data(), &fake_header, sizeof(fake_header)); - - std::size_t current_offset = 0; - const auto write_to_cia_file_aligned = [&install_file, ¤t_offset](std::vector& data) { - const u64 offset = - Common::AlignUp(current_offset + data.size(), FileSys::CIA_SECTION_ALIGNMENT); - data.resize(offset - current_offset, 0); - const auto result = - install_file.Write(current_offset, data.size(), true, false, data.data()); - if (result.Failed()) { - LOG_ERROR(Service_AM, "CIA file installation aborted with error code {:08x}", - result.Code().raw); - return InstallStatus::ErrorAborted; - } - current_offset += data.size(); - return InstallStatus::Success; - }; - - auto result = write_to_cia_file_aligned(header_data); - if (result != InstallStatus::Success) { - return result; - } - - result = write_to_cia_file_aligned(*cetk_response); - if (result != InstallStatus::Success) { - return result; - } - - result = write_to_cia_file_aligned(*tmd_response); - if (result != InstallStatus::Success) { - return result; - } - - result = write_to_cia_file_aligned(content); - if (result != InstallStatus::Success) { - return result; - } - return InstallStatus::Success; -} - u64 GetTitleUpdateId(u64 title_id) { // Real services seem to just discard and replace the whole high word. return (title_id & 0xFFFFFFFF) | (static_cast(TID_HIGH_UPDATE) << 32); @@ -642,6 +1028,14 @@ Service::FS::MediaType GetTitleMediaType(u64 titleId) { return Service::FS::MediaType::SDMC; } +std::string GetTicketDirectory() { + return fmt::format("{}/dbs/ticket.db/", FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)); +} + +std::string GetTicketPath(u64 title_id, u64 ticket_id) { + return GetTicketDirectory() + fmt::format("{:016X}.{:016X}.tik", title_id, ticket_id); +} + std::string GetTitleMetadataPath(Service::FS::MediaType media_type, u64 tid, bool update) { std::string content_path = GetTitlePath(media_type, tid) + "content/"; @@ -760,6 +1154,31 @@ std::string GetMediaTitlePath(Service::FS::MediaType media_type) { return ""; } +void Module::ScanForTickets() { + am_ticket_list.clear(); + + std::string ticket_path = GetTicketDirectory(); + + FileUtil::FSTEntry entries; + FileUtil::ScanDirectoryTree(ticket_path, entries, 0); + for (const FileUtil::FSTEntry& ticket : entries.children) { + if (ticket.virtualName.ends_with(".tik")) { + std::string file_name = ticket.virtualName.substr(0, ticket.virtualName.size() - 4); + auto pos = file_name.find('.'); + if (pos != file_name.npos) { + std::string title_id_str = file_name.substr(0, pos); + std::string ticket_id_str = file_name.substr(pos + 1); + try { + u64 title_id = std::stoull(title_id_str, nullptr, 16); + u64 ticket_id = std::stoull(ticket_id_str, nullptr, 16); + am_ticket_list.insert(std::make_pair(title_id, ticket_id)); + } catch (...) { + } + } + } + } +} + void Module::ScanForTitles(Service::FS::MediaType media_type) { am_title_list[static_cast(media_type)].clear(); @@ -793,6 +1212,7 @@ void Module::ScanForTitles(Service::FS::MediaType media_type) { } void Module::ScanForAllTitles() { + ScanForTickets(); ScanForTitles(Service::FS::MediaType::NAND); ScanForTitles(Service::FS::MediaType::SDMC); } @@ -806,6 +1226,8 @@ void Module::Interface::GetNumPrograms(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u8 media_type = rp.Pop(); + LOG_DEBUG(Service_AM, "media_type={}", media_type); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -866,6 +1288,9 @@ void Module::Interface::FindDLCContentInfos(Kernel::HLERequestContext& ctx) { u64 title_id = rp.Pop(); u32 content_count = rp.Pop(); auto& content_requested_in = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -989,6 +1414,8 @@ void Module::Interface::ListDLCContentInfos(Kernel::HLERequestContext& ctx) { u64 title_id = rp.Pop(); u32 start_index = rp.Pop(); + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -1115,6 +1542,8 @@ void Module::Interface::GetProgramList(Kernel::HLERequestContext& ctx) { u32 count = rp.Pop(); u8 media_type = rp.Pop(); + LOG_DEBUG(Service_AM, "media_type={}", media_type); + if (artic_client.get()) { struct AsyncData { u32 count; @@ -1230,6 +1659,8 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool auto media_type = static_cast(rp.Pop()); u32 title_count = rp.Pop(); + LOG_DEBUG(Service_AM, "media_type={}", media_type); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -1312,7 +1743,24 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool std::vector title_id_list(title_count); title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); - Result result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); + // eShop checks if the current importing title already exists during installation. + // Normally, since the program wouldn't be commited, getting the title info returns not + // found. However, since GetTitleInfoFromList does not care if the program was commited and + // only checks for the tmd, it will detect the title and return information while it + // shouldn't. To prevent this, we check if the title ID corresponds to the currently + // importing title and return not found if so. + Result result = ResultSuccess; + if (am->importing_title) { + for (auto tid : title_id_list) { + if (tid == am->importing_title->title_id) { + result = Result(ErrorDescription::NotFound, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + } + } + } + + if (result.IsSuccess()) + result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); IPC::RequestBuilder rb = rp.MakeBuilder(1, ignore_platform ? 0 : 4); rb.Push(result); @@ -1335,6 +1783,9 @@ void Module::Interface::DeleteUserProgram(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); auto media_type = rp.PopEnum(); u64 title_id = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); u16 category = static_cast((title_id >> 32) & 0xFFFF); u8 variation = static_cast(title_id & 0xFF); @@ -1365,6 +1816,8 @@ void Module::Interface::GetProductCode(Kernel::HLERequestContext& ctx) { u64 title_id = rp.Pop(); std::string path = GetTitleContentPath(media_type, title_id); + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + if (!FileUtil::Exists(path)) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, @@ -1391,6 +1844,8 @@ void Module::Interface::GetDLCTitleInfos(Kernel::HLERequestContext& ctx) { auto media_type = static_cast(rp.Pop()); u32 title_count = rp.Pop(); + LOG_DEBUG(Service_AM, "media_type={}", media_type); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -1494,6 +1949,8 @@ void Module::Interface::GetPatchTitleInfos(Kernel::HLERequestContext& ctx) { auto media_type = static_cast(rp.Pop()); u32 title_count = rp.Pop(); + LOG_DEBUG(Service_AM, "media_type={}", media_type); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -1596,6 +2053,7 @@ void Module::Interface::ListDataTitleTicketInfos(Kernel::HLERequestContext& ctx) u32 ticket_count = rp.Pop(); u64 title_id = rp.Pop(); u32 start_index = rp.Pop(); + if (artic_client.get()) { struct AsyncData { u64 title_id; @@ -1682,6 +2140,9 @@ void Module::Interface::GetDLCContentInfoCount(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); auto media_type = static_cast(rp.Pop()); u64 title_id = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + if (artic_client.get()) { struct AsyncData { u8 media_type; @@ -1752,8 +2213,8 @@ void Module::Interface::GetDLCContentInfoCount(Kernel::HLERequestContext& ctx) { rb.Push(static_cast(tmd.GetContentCount())); } else { rb.Push(1); // Number of content infos plus one - LOG_WARNING(Service_AM, "(STUBBED) called media_type={}, title_id=0x{:016x}", - media_type, title_id); + LOG_WARNING(Service_AM, "missing TMD media_type={}, title_id=0x{:016x}", media_type, + title_id); } } } @@ -1762,23 +2223,36 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u64 title_id = rp.Pop(); + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto range = am->am_ticket_list.equal_range(title_id); + if (range.first == range.second) { + rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success, + ErrorLevel::Success)); + return; + } + auto it = range.first; + for (; it != range.second; it++) { + auto path = GetTicketPath(title_id, it->second); + FileUtil::Delete(path); + } + + am->ScanForTickets(); + rb.Push(ResultSuccess); - LOG_WARNING(Service_AM, "(STUBBED) called title_id=0x{:016x}", title_id); } void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - u32 ticket_count = 0; - for (const auto& title_list : am->am_title_list) { - ticket_count += static_cast(title_list.size()); - } + LOG_DEBUG(Service_AM, ""); + + u32 ticket_count = static_cast(am->am_ticket_list.size()); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); rb.Push(ticket_count); - LOG_WARNING(Service_AM, "(STUBBED) called ticket_count=0x{:08x}", ticket_count); } void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) { @@ -1787,30 +2261,42 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) { u32 ticket_index = rp.Pop(); auto& ticket_tids_out = rp.PopMappedBuffer(); + LOG_DEBUG(Service_AM, "ticket_list_count={}, ticket_index={}", ticket_list_count, ticket_index); + u32 tickets_written = 0; - for (const auto& title_list : am->am_title_list) { - const auto tickets_to_write = - std::min(static_cast(title_list.size()), ticket_list_count - tickets_written); - ticket_tids_out.Write(title_list.data(), tickets_written * sizeof(u64), - tickets_to_write * sizeof(u64)); - tickets_written += tickets_to_write; + auto it = am->am_ticket_list.begin(); + std::advance(it, std::min(static_cast(ticket_index), am->am_ticket_list.size())); + + for (; it != am->am_ticket_list.end() && tickets_written < ticket_list_count; + it++, tickets_written++) { + ticket_tids_out.Write(&it->first, tickets_written * sizeof(u64), sizeof(u64)); } IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); rb.Push(ResultSuccess); rb.Push(tickets_written); rb.PushMappedBuffer(ticket_tids_out); - LOG_WARNING(Service_AM, "(STUBBED) ticket_list_count=0x{:08x}, ticket_index=0x{:08x}", - 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; + LOG_DEBUG(Service_AM, ""); - if (deviceID == 0) { - LOG_ERROR(Service_AM, "Invalid or missing CTCert"); + const auto& otp = HW::UniqueData::GetOTP(); + if (!otp.Valid()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::NotFound, + ErrorLevel::Permanent)); + return; + } + + u32 deviceID = otp.GetDeviceID(); + if (am->force_new_device_id) { + deviceID |= 0x800000000; + } + if (am->force_old_device_id) { + deviceID &= ~0x80000000; } IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); @@ -1819,6 +2305,219 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) { rb.Push(deviceID); } +void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp, + FS::MediaType media_type, + bool include_installing, + bool include_finalizing) { + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(ResultSuccess); + + u32 count = 0; + for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) { + if (include_installing && + (it->second.state == ImportTitleContextState::WAITING_FOR_IMPORT || + it->second.state == ImportTitleContextState::RESUMABLE) || + include_finalizing && it->second.state == ImportTitleContextState::WAITING_FOR_COMMIT) { + count++; + } + } + + rb.Push(static_cast(am->import_title_contexts.size())); +} + +void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp, + FS::MediaType media_type, u32 list_count, + bool include_installing, + bool include_finalizing) { + + auto out_buf = rp.PopMappedBuffer(); + u32 written = 0; + + for (auto& key_value : am->import_content_contexts) { + if (include_installing && + (key_value.second.state == ImportTitleContextState::WAITING_FOR_IMPORT || + key_value.second.state == ImportTitleContextState::RESUMABLE) || + include_finalizing && + key_value.second.state == ImportTitleContextState::WAITING_FOR_COMMIT) { + + out_buf.Write(&key_value.first, written * sizeof(u64), sizeof(u64)); + written++; + if (written >= list_count) + break; + } + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(ResultSuccess); + rb.Push(written); +} + +void Module::Interface::GetNumImportTitleContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const FS::MediaType media_type = static_cast(rp.Pop()); + + GetNumImportTitleContextsImpl(rp, media_type, true, true); + + LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type); +} + +void Module::Interface::GetImportTitleContextList(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u32 list_count = rp.Pop(); + [[maybe_unused]] const FS::MediaType media_type = + static_cast(rp.Pop()); + + GetImportTitleContextListImpl(rp, media_type, list_count, true, true); + + LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type); +} + +void Module::Interface::GetImportTitleContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u32 list_count = rp.Pop(); + [[maybe_unused]] const FS::MediaType media_type = + static_cast(rp.Pop()); + + auto in_buf = rp.PopMappedBuffer(); + auto out_buf = rp.PopMappedBuffer(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + LOG_WARNING(Service_AM, "(STUBBED) media_type={} list_count={}", media_type, list_count); + + u32 written = 0; + for (u32 i = 0; i < list_count; i++) { + u64 title_id; + in_buf.Read(&title_id, 0, sizeof(title_id)); + + LOG_WARNING(Service_AM, "title_id={:016X}", title_id); + + auto it = am->import_title_contexts.find(title_id); + if (it == am->import_title_contexts.end()) { + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } else { + out_buf.Write(&it->second, written * sizeof(ImportTitleContext), + sizeof(ImportTitleContext)); + written++; + } + } + + rb.Push(ResultSuccess); +} + +void Module::Interface::DeleteImportTitleContext(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const FS::MediaType media_type = static_cast(rp.Pop()); + const u64 title_id = rp.Pop(); + + LOG_WARNING(Service_AM, "(STUBBED) media_type={} title_id={:016X}", media_type, title_id); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto range = am->import_title_contexts.equal_range(title_id); + if (range.first == range.second) { + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } + + am->import_title_contexts.erase(title_id); + rb.Push(ResultSuccess); +} + +void Module::Interface::GetNumImportContentContextsImpl(IPC::RequestParser& rp, u64 title_id, + FS::MediaType media_type) { + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + auto range = am->import_content_contexts.equal_range(title_id); + rb.Push(static_cast(std::distance(range.first, range.second))); +} + +void Module::Interface::GetImportContentContextListImpl(IPC::RequestParser& rp, u32 list_count, + u64 title_id, FS::MediaType media_type) { + auto out_buf = rp.PopMappedBuffer(); + + auto range = am->import_content_contexts.equal_range(title_id); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + + u32 written = 0; + for (auto it = range.first; it != range.second && written < list_count; it++, written++) { + out_buf.Write(&it->second.index, written * sizeof(u16), sizeof(u16)); + } + + rb.Push(written); +} + +void Module::Interface::GetImportContentContextsImpl(IPC::RequestParser& rp, u32 list_count, + u64 title_id, FS::MediaType media_type) { + auto in_buf = rp.PopMappedBuffer(); + auto out_buf = rp.PopMappedBuffer(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto range = am->import_content_contexts.equal_range(title_id); + + for (u32 i = 0; i < list_count; i++) { + u16 index; + in_buf.Read(&index, i * sizeof(u16), sizeof(u16)); + + LOG_WARNING(Service_AM, "index={}", index); + + auto it = range.first; + for (; it != range.second; it++) + if (it->second.index == index) + break; + + if (it == range.second) { + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } + + out_buf.Write(&it->second, i * sizeof(ImportContentContext), sizeof(ImportContentContext)); + } + + rb.Push(ResultSuccess); +} + +void Module::Interface::GetNumImportContentContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const FS::MediaType media_type = static_cast(rp.Pop()); + const u64 title_id = rp.Pop(); + + GetNumImportContentContextsImpl(rp, title_id, media_type); + + LOG_WARNING(Service_AM, "(STUBBED) media_type={} title_id={:016X}", media_type, title_id); +} + +void Module::Interface::GetImportContentContextList(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 list_count = rp.Pop(); + const FS::MediaType media_type = static_cast(rp.Pop()); + const u64 title_id = rp.Pop(); + + GetImportContentContextListImpl(rp, list_count, title_id, media_type); + + LOG_WARNING(Service_AM, "(STUBBED) media_type={} title_id={:016X}", media_type, title_id); +} + +void Module::Interface::GetImportContentContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 list_count = rp.Pop(); + const FS::MediaType media_type = static_cast(rp.Pop()); + const u64 title_id = rp.Pop(); + + LOG_WARNING(Service_AM, "(STUBBED) media_type={} title_id={:016X}", media_type, title_id); + + GetImportContentContextsImpl(rp, list_count, title_id, media_type); +} + void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto media_type = rp.Pop(); @@ -1853,39 +2552,60 @@ void Module::Interface::QueryAvailableTitleDatabase(Kernel::HLERequestContext& c void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - [[maybe_unused]] u32 ticket_count = rp.Pop(); - [[maybe_unused]] auto& buffer = rp.PopMappedBuffer(); + u32 ticket_count = rp.Pop(); + auto& out_buffer = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_AM, "(STUBBED) called, ticket_count={}", ticket_count); + + u32 written = 0; + for (auto it = am->am_ticket_list.begin(); + it != am->am_ticket_list.end() && written < ticket_count; it++) { + u64 title_id = it->first; + u32 tid_high = static_cast(title_id << 32); + if ((tid_high & 0x00048010) == 0x00040010 || (tid_high & 0x00048001) == 0x00048001) + continue; + + FileSys::Ticket ticket; + if (ticket.Load(title_id, it->second) != Loader::ResultStatus::Success) + continue; + + TicketInfo info = {}; + info.title_id = ticket.GetTitleID(); + info.ticket_id = ticket.GetTicketID(); + info.version = ticket.GetVersion(); + info.size = static_cast(ticket.GetSerializedSize()); + + out_buffer.Write(&info, written * sizeof(TicketInfo), sizeof(TicketInfo)); + written++; + } IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); // No error - rb.Push(0); - - LOG_WARNING(Service_AM, "(STUBBED) called, ticket_count={}", ticket_count); + rb.Push(written); } void Module::Interface::GetNumImportTitleContextsFiltered(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - u8 media_type = rp.Pop(); - u8 filter = rp.Pop(); - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); // No error - rb.Push(0); + const FS::MediaType media_type = static_cast(rp.Pop()); + const u8 filter = rp.Pop(); + + GetNumImportTitleContextsImpl(rp, media_type, (filter & (1 << 0)) != 0, + (filter & (1 << 1)) != 0); LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter); } void Module::Interface::GetImportTitleContextListFiltered(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - [[maybe_unused]] const u32 count = rp.Pop(); - const u8 media_type = rp.Pop(); - const u8 filter = rp.Pop(); - auto& buffer = rp.PopMappedBuffer(); - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(ResultSuccess); // No error - rb.Push(0); - rb.PushMappedBuffer(buffer); + const u32 list_count = rp.Pop(); + [[maybe_unused]] const FS::MediaType media_type = + static_cast(rp.Pop()); + const u8 filter = rp.Pop(); + + GetImportTitleContextListImpl(rp, media_type, list_count, (filter & (1 << 0)) != 0, + (filter & (1 << 1)) != 0); LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter); } @@ -1929,8 +2649,8 @@ void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) { if (am->cia_installing) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM, - ErrorSummary::InvalidState, ErrorLevel::Permanent)); + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); return; } @@ -1954,8 +2674,8 @@ void Module::Interface::BeginImportProgramTemporarily(Kernel::HLERequestContext& if (am->cia_installing) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM, - ErrorSummary::InvalidState, ErrorLevel::Permanent)); + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); return; } @@ -1980,6 +2700,8 @@ void Module::Interface::EndImportProgram(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); [[maybe_unused]] const auto cia = rp.PopObject(); + LOG_DEBUG(Service_AM, ""); + am->ScanForAllTitles(); am->cia_installing = false; @@ -1998,6 +2720,8 @@ void Module::Interface::EndImportProgramWithoutCommit(Kernel::HLERequestContext& am->cia_installing = false; IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); + + LOG_WARNING(Service_AM, "(STUBBED)"); } void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) { @@ -2014,6 +2738,8 @@ void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); rb.PushMappedBuffer(buffer); + + LOG_WARNING(Service_AM, "(STUBBED)"); } /// Wraps all File operations to allow adding an offset to them. @@ -2048,7 +2774,7 @@ private: std::size_t file_size; }; -ResultVal> GetFileFromSession( +ResultVal> GetCiaFileFromSession( std::shared_ptr file_session) { // Step up the chain from ClientSession->ServerSession and then // cast to File. For AM on 3DS, invalid handles actually hang the system. @@ -2087,12 +2813,58 @@ ResultVal> GetFileFromSession( return Kernel::ResultNotImplemented; } +template +ResultVal GetFileBackendFromSession(std::shared_ptr file_session) { + // Step up the chain from ClientSession->ServerSession and then + // cast to file backend. For AM on 3DS, invalid handles actually hang the system. + + if (file_session->parent == nullptr) { + LOG_WARNING(Service_AM, "Invalid file handle!"); + return Kernel::ResultInvalidHandle; + } + + std::shared_ptr server = + Kernel::SharedFrom(file_session->parent->server); + if (server == nullptr) { + LOG_WARNING(Service_AM, "File handle ServerSession disconnected!"); + return Kernel::ResultSessionClosed; + } + + if (server->hle_handler != nullptr) { + auto file = std::dynamic_pointer_cast(server->hle_handler); + + // TODO(shinyquagsire23): This requires RTTI, use service calls directly instead? + if (file != nullptr) { + // Grab the session file offset in case we were given a subfile opened with + // File::OpenSubFile + auto backing_file = dynamic_cast(file->backend.get()); + + if (!backing_file) { + LOG_ERROR(Service_AM, "Failed to cast to file backend!"); + return Kernel::ResultInvalidHandle; + } + + return backing_file; + } + + LOG_ERROR(Service_AM, "Failed to cast handle to FSFile!"); + return Kernel::ResultInvalidHandle; + } + + // Probably the best bet if someone is LLEing the fs service is to just have them LLE AM + // while they're at it, so not implemented. + LOG_ERROR(Service_AM, "Given file handle does not have an HLE handler!"); + return Kernel::ResultNotImplemented; +} + void Module::Interface::GetProgramInfoFromCia(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); [[maybe_unused]] const auto media_type = static_cast(rp.Pop()); auto cia = rp.PopObject(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(file_res.Code()); @@ -2129,7 +2901,9 @@ void Module::Interface::GetSystemMenuDataFromCia(Kernel::HLERequestContext& ctx) auto cia = rp.PopObject(); auto& output_buffer = rp.PopMappedBuffer(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(file_res.Code()); @@ -2172,7 +2946,9 @@ void Module::Interface::GetDependencyListFromCia(Kernel::HLERequestContext& ctx) IPC::RequestParser rp(ctx); auto cia = rp.PopObject(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(file_res.Code()); @@ -2199,7 +2975,9 @@ void Module::Interface::GetTransferSizeFromCia(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); auto cia = rp.PopObject(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(file_res.Code()); @@ -2223,7 +3001,9 @@ void Module::Interface::GetCoreVersionFromCia(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); auto cia = rp.PopObject(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(file_res.Code()); @@ -2248,7 +3028,9 @@ void Module::Interface::GetRequiredSizeFromCia(Kernel::HLERequestContext& ctx) { [[maybe_unused]] const auto media_type = static_cast(rp.Pop()); auto cia = rp.PopObject(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(file_res.Code()); @@ -2303,6 +3085,8 @@ void Module::Interface::DeleteProgram(Kernel::HLERequestContext& ctx) { void Module::Interface::GetSystemUpdaterMutex(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + LOG_DEBUG(Service_AM, ""); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); rb.PushCopyObjects(am->system_updater_mutex); @@ -2312,7 +3096,9 @@ void Module::Interface::GetMetaSizeFromCia(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); auto cia = rp.PopObject(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(file_res.Code()); @@ -2340,7 +3126,9 @@ void Module::Interface::GetMetaDataFromCia(Kernel::HLERequestContext& ctx) { auto cia = rp.PopObject(); auto& output_buffer = rp.PopMappedBuffer(); - auto file_res = GetFileFromSession(cia); + LOG_DEBUG(Service_AM, ""); + + auto file_res = GetCiaFileFromSession(cia); if (!file_res.Succeeded()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(file_res.Code()); @@ -2379,6 +3167,8 @@ void Module::Interface::GetMetaDataFromCia(Kernel::HLERequestContext& ctx) { void Module::Interface::BeginImportTicket(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + LOG_DEBUG(Service_AM, ""); + // Create our TicketFile handle for the app to write to auto file = std::make_shared( am->system.Kernel(), std::make_unique(), FileSys::Path{}); @@ -2386,18 +3176,493 @@ void Module::Interface::BeginImportTicket(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); // No error rb.PushCopyObjects(file->Connect()); - - LOG_WARNING(Service_AM, "(STUBBED) called"); } void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - [[maybe_unused]] const auto ticket = rp.PopObject(); + const auto ticket = rp.PopObject(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto ticket_file = GetFileBackendFromSession(ticket); + if (ticket_file.Succeeded()) { + rb.Push(ticket_file.Unwrap()->Commit()); + am->ScanForTickets(); + } else { + rb.Push(ticket_file.Code()); + } + + LOG_DEBUG(Service_AM, "title_id={:016X} ticket_id={:016X}", ticket_file.Unwrap()->GetTitleID(), + ticket_file.Unwrap()->GetTicketID()); +} + +void Module::Interface::BeginImportTitle(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const auto media_type = static_cast(rp.Pop()); + const u64 title_id = rp.Pop(); + [[maybe_unused]] const u8 database = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X} media_type={:016X}", title_id, media_type); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + am->importing_title = + std::make_shared(Core::System::GetInstance(), title_id, media_type); + + auto entries = am->am_ticket_list.find(title_id); + if (entries == am->am_ticket_list.end()) { + // Ticket is not installed + rb.Push(ResultUnknown); + return; + } + FileSys::Ticket ticket; + if (ticket.Load(title_id, (*entries).second) != Loader::ResultStatus::Success) { + // Ticket failed to load + rb.Push(ResultUnknown); + return; + } + + Result res = am->importing_title->cia_file.ProvideTicket(ticket); + if (res.IsError()) { + // Failed to load ticket + rb.Push(res); + return; + } + + AuthorizeCIAFileDecryption(&am->importing_title->cia_file, ctx); + + rb.Push(ResultSuccess); +} + +void Module::Interface::StopImportTitle(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + auto it = am->import_title_contexts.find(am->importing_title->title_id); + if (it != am->import_title_contexts.end()) { + it->second.state = ImportTitleContextState::RESUMABLE; + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); - LOG_WARNING(Service_AM, "(STUBBED) called"); + LOG_WARNING(Service_AM, "(STUBBED)"); +} + +void Module::Interface::ResumeImportTitle(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const auto media_type = static_cast(rp.Pop()); + const u64 title_id = rp.Pop(); + + if (!am->importing_title || am->importing_title->title_id != title_id || + am->importing_title->media_type != media_type) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + auto it = am->import_title_contexts.find(am->importing_title->title_id); + if (it == am->import_title_contexts.end() || + it->second.state != ImportTitleContextState::RESUMABLE) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + it->second.state = ImportTitleContextState::WAITING_FOR_IMPORT; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + + LOG_WARNING(Service_AM, "(STUBBED) title_id={:016X}", title_id); +} + +void Module::Interface::CancelImportTitle(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + auto it = am->import_title_contexts.find(am->importing_title->title_id); + if (it != am->import_title_contexts.end()) { + it->second.state = ImportTitleContextState::DELETING; + } + + am->importing_title.reset(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + auto it = am->import_title_contexts.find(am->importing_title->title_id); + if (it != am->import_title_contexts.end()) { + it->second.state = ImportTitleContextState::WAITING_FOR_COMMIT; + } + + am->importing_title->cia_file.SetDone(); + am->ScanForTitles(am->importing_title->media_type); + am->importing_title.reset(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void Module::Interface::BeginImportTmd(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + // Create our TMD handle for the app to write to + auto file = std::make_shared( + am->system.Kernel(), std::make_unique(am->importing_title), FileSys::Path{}); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); // No error + rb.PushCopyObjects(file->Connect()); +} + +void Module::Interface::EndImportTmd(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const bool create_context = rp.Pop(); + const auto tmd = rp.PopObject(); + + LOG_DEBUG(Service_AM, ""); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + if (!am->importing_title) { + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } + + auto tmd_file = GetFileBackendFromSession(tmd); + if (tmd_file.Succeeded()) { + rb.Push(tmd_file.Unwrap()->Commit()); + } else { + rb.Push(tmd_file.Code()); + return; + } + + if (create_context) { + const FileSys::TitleMetadata& tmd_info = am->importing_title->cia_file.GetTMD(); + + ImportTitleContext& context = am->import_title_contexts[tmd_info.GetTitleID()]; + context.title_id = tmd_info.GetTitleID(); + context.version = tmd_info.GetTitleVersion(); + context.type = 0; + context.state = ImportTitleContextState::WAITING_FOR_IMPORT; + context.size = 0; + for (size_t i = 0; i < tmd_info.GetContentCount(); i++) { + ImportContentContext content_context; + content_context.content_id = tmd_info.GetContentIDByIndex(i); + content_context.index = static_cast(i); + content_context.state = ImportTitleContextState::WAITING_FOR_IMPORT; + content_context.size = tmd_info.GetContentSizeByIndex(i); + content_context.current_size = 0; + am->import_content_contexts.insert(std::make_pair(context.title_id, content_context)); + + context.size += content_context.size; + } + } +} + +void Module::Interface::CreateImportContentContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 content_count = rp.Pop(); + auto content_buf = rp.PopMappedBuffer(); + + std::vector content_indices(content_count); + content_buf.Read(content_indices.data(), 0, content_buf.GetSize()); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + + LOG_WARNING(Service_AM, "(STUBBED)"); +} + +void Module::Interface::BeginImportContent(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u16 content_index = rp.Pop(); + + LOG_DEBUG(Service_AM, "content_index={}", static_cast(content_index)); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + auto range = am->import_content_contexts.equal_range(am->importing_title->title_id); + auto it = range.first; + for (; it != range.second; it++) + if (it->second.index == content_index) + break; + + if (it == range.second) { + // Index not found + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + // Create our TMD handle for the app to write to + auto file = std::make_shared( + am->system.Kernel(), + std::make_unique(am->importing_title, content_index, it->second), + FileSys::Path{}); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); // No error + rb.PushCopyObjects(file->Connect()); +} + +void Module::Interface::ResumeImportContent(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u16 content_index = rp.Pop(); + + LOG_DEBUG(Service_AM, "content_index={}", static_cast(content_index)); + + if (!am->importing_title) { + // Not importing a title + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + auto range = am->import_content_contexts.equal_range(am->importing_title->title_id); + auto it = range.first; + for (; it != range.second; it++) + if (it->second.index == content_index) + break; + + if (it == range.second || it->second.state != ImportTitleContextState::RESUMABLE) { + // Index not found + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultUnknown); + return; + } + + it->second.state = ImportTitleContextState::WAITING_FOR_IMPORT; + + auto content_file = + std::make_unique(am->importing_title, content_index, it->second); + content_file->SetWritten(it->second.current_size); + + // Create our TMD handle for the app to write to + auto file = std::make_shared(am->system.Kernel(), std::move(content_file), + FileSys::Path{}); + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); + rb.Push(ResultSuccess); // No error + rb.Push(it->second.current_size); + rb.PushCopyObjects(file->Connect()); +} + +void Module::Interface::StopImportContent(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const auto content = rp.PopObject(); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + rb.Push(ResultUnknown); + return; + } + + auto content_file = GetFileBackendFromSession(content); + if (content_file.Failed()) { + rb.Push(content_file.Code()); + return; + } + + content_file.Unwrap()->GetImportContext().state = ImportTitleContextState::RESUMABLE; + + rb.Push(ResultSuccess); +} + +void Module::Interface::CancelImportContent(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const auto content = rp.PopObject(); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + rb.Push(ResultUnknown); + return; + } + + auto content_file = GetFileBackendFromSession(content); + if (content_file.Failed()) { + rb.Push(content_file.Code()); + return; + } + + content_file.Unwrap()->GetImportContext().state = ImportTitleContextState::DELETING; + content_file.Unwrap()->Cancel(am->importing_title->media_type, am->importing_title->title_id); + + rb.Push(ResultSuccess); +} + +void Module::Interface::EndImportContent(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const auto content = rp.PopObject(); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + LOG_DEBUG(Service_AM, ""); + + if (!am->importing_title) { + // Not importing a title + rb.Push(ResultUnknown); + return; + } + + auto content_file = GetFileBackendFromSession(content); + if (content_file.Failed()) { + rb.Push(content_file.Code()); + return; + } + + content_file.Unwrap()->GetImportContext().state = ImportTitleContextState::WAITING_FOR_COMMIT; + + rb.Push(ResultSuccess); +} + +void Module::Interface::GetNumCurrentImportContentContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + if (!am->importing_title) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } + + GetNumImportContentContextsImpl(rp, am->importing_title->title_id, + am->importing_title->media_type); + + LOG_WARNING(Service_AM, "(STUBBED) title_id={:016X}", am->importing_title->title_id); +} + +void Module::Interface::GetCurrentImportContentContextList(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + if (!am->importing_title) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } + + const u32 list_count = rp.Pop(); + + GetImportContentContextListImpl(rp, list_count, am->importing_title->title_id, + am->importing_title->media_type); + + LOG_WARNING(Service_AM, "(STUBBED) title_id={:016X}", am->importing_title->title_id); +} + +void Module::Interface::GetCurrentImportContentContexts(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + if (!am->importing_title) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; + } + + const u32 list_count = rp.Pop(); + + LOG_WARNING(Service_AM, "(STUBBED) title_id={:016X}", am->importing_title->title_id); + + GetImportContentContextsImpl(rp, list_count, am->importing_title->title_id, + am->importing_title->media_type); +} + +void Module::Interface::Sign(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + size_t signature_size = rp.Pop(); + size_t certificate_size = rp.Pop(); + u64 title_id = rp.Pop(); + size_t data_size = rp.Pop(); + + auto& data_buf = rp.PopMappedBuffer(); + auto& signature_buf = rp.PopMappedBuffer(); + auto& certificate_buf = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + + std::vector data(data_size); + data_buf.Read(data.data(), 0, data_size); + + FileSys::Certificate& ct_cert = HW::UniqueData::GetCTCert(); + if (!ct_cert.IsValid()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::NotFound, + ErrorLevel::Permanent)); + return; + } + + FileSys::Certificate ap_cert; + std::string new_issuer_str = + fmt::format("{}-{}", reinterpret_cast(ct_cert.GetIssuer().data()), + reinterpret_cast(ct_cert.GetName().data())); + std::string new_name_str = fmt::format("AP{:016x}", title_id); + + std::array new_issuer = {0}; + std::array new_name = {0}; + memcpy(new_issuer.data(), new_issuer_str.data(), new_issuer_str.size()); + memcpy(new_name.data(), new_name_str.data(), new_name_str.size()); + + ap_cert.BuildECC(ct_cert, new_issuer, new_name, 0); + + HW::ECC::Signature signature = ap_cert.Sign(data); + std::vector certificate = ap_cert.Serialize(); + + signature_buf.Write(signature.rs.data(), 0, std::min(signature_size, signature.rs.size())); + certificate_buf.Write(certificate.data(), 0, std::min(certificate_size, certificate.size())); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(0); } template @@ -2413,49 +3678,225 @@ void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) { [[maybe_unused]] u32 size = rp.Pop(); auto buffer = rp.PopMappedBuffer(); - if (!am->ct_cert.IsValid()) { - LOG_ERROR(Service_AM, "Invalid or missing CTCert"); + LOG_DEBUG(Service_AM, ""); + + const auto& ct_cert = HW::UniqueData::GetCTCert(); + if (!ct_cert.IsValid()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::NotFound, + ErrorLevel::Permanent)); + return; } - buffer.Write(&am->ct_cert, 0, std::min(sizeof(CTCert), buffer.GetSize())); + auto ct_cert_bin = ct_cert.Serialize(); + + buffer.Write(ct_cert_bin.data(), 0, std::min(ct_cert_bin.size(), buffer.GetSize())); IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); rb.Push(ResultSuccess); rb.Push(0); rb.PushMappedBuffer(buffer); } -std::string Module::GetCTCertPath() { - return FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + "CTCert.bin"; +void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id = rp.Pop(); + u64 ticket_id = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X} ticket_id={}", title_id, ticket_id); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto range = am->am_ticket_list.equal_range(title_id); + auto it = range.first; + for (; it != range.second; it++) { + if (it->second == ticket_id) { + break; + } + } + if (range.first == range.second) { + rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success, + ErrorLevel::Success)); + return; + } + + auto path = GetTicketPath(title_id, ticket_id); + FileUtil::Delete(path); + + am->ScanForTickets(); + + rb.Push(ResultSuccess); } -CTCertLoadStatus Module::LoadCTCertFile(CTCert& output) { - if (output.IsValid()) { - return CTCertLoadStatus::Loaded; +void Module::Interface::GetNumTicketIds(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + + auto range = am->am_ticket_list.equal_range(title_id); + u32 count = static_cast(std::distance(range.first, range.second)); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(count); +} + +void Module::Interface::GetTicketIdList(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 list_count = rp.Pop(); + const u64 title_id = rp.Pop(); + [[maybe_unused]] const bool unk = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + + auto out_buf = rp.PopMappedBuffer(); + + u32 index = 0; + for (auto [it, rangeEnd] = am->am_ticket_list.equal_range(title_id); + it != rangeEnd && index < list_count; index++, it++) { + u64 ticket_id = it->second; + out_buf.Write(&ticket_id, index * sizeof(u64), sizeof(u64)); } - std::string file_path = GetCTCertPath(); - if (!FileUtil::Exists(file_path)) { - return CTCertLoadStatus::NotFound; + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(index); +} + +void Module::Interface::GetNumTicketsOfProgram(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id = rp.Pop(); + + LOG_DEBUG(Service_AM, "title_id={:016X}", title_id); + + auto range = am->am_ticket_list.equal_range(title_id); + u32 count = static_cast(std::distance(range.first, range.second)); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(count); +} + +void Module::Interface::ListTicketInfos(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u32 ticket_count = rp.Pop(); + u64 title_id = rp.Pop(); + u32 skip = rp.Pop(); + auto& out_buffer = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_AM, "(STUBBED) called, ticket_count={}", ticket_count); + + auto range = am->am_ticket_list.equal_range(title_id); + auto it = range.first; + std::advance(it, std::min(static_cast(skip), + static_cast(std::distance(range.first, range.second)))); + + u32 written = 0; + for (; it != range.second && written < ticket_count; it++) { + FileSys::Ticket ticket; + if (ticket.Load(title_id, it->second) != Loader::ResultStatus::Success) + continue; + + TicketInfo info = {}; + info.title_id = ticket.GetTitleID(); + info.ticket_id = ticket.GetTicketID(); + info.version = ticket.GetVersion(); + info.size = static_cast(ticket.GetSerializedSize()); + + out_buffer.Write(&info, written * sizeof(TicketInfo), sizeof(TicketInfo)); + written++; } - FileUtil::IOFile file(file_path, "rb"); - if (!file.IsOpen()) { - return CTCertLoadStatus::IOError; + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); // No error + rb.Push(written); +} + +void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + const u32 ticket_buf_size = rp.Pop(); + const u32 keyiv_buf_size = rp.Pop(); + + const u64 title_id = rp.Pop(); + const u64 ticket_id = rp.Pop(); + + auto ticket_buf = rp.PopMappedBuffer(); + auto keyiv_buf = rp.PopMappedBuffer(); + + LOG_DEBUG(Service_AM, "title_id={:016X} ticket_id={:016X}", title_id, ticket_id); + + u32 tid_high = static_cast(title_id >> 32); + if ((tid_high & 0x00048001) == 0x00048001 || tid_high == 0x00040001 || (tid_high & 0x10) != 0) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, ErrorSummary::InvalidArgument, + ErrorLevel::Usage)); + return; } - if (file.GetSize() != sizeof(CTCert)) { - return CTCertLoadStatus::Invalid; + + auto range = am->am_ticket_list.equal_range(title_id); + auto it = range.first; + for (; it != range.second; it++) + if (it->second == ticket_id) + break; + if (it == range.second) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; } - if (file.ReadBytes(&output, sizeof(CTCert)) != sizeof(CTCert)) { - return CTCertLoadStatus::IOError; + + FileSys::Ticket ticket; + if (ticket.Load(title_id, ticket_id) != Loader::ResultStatus::Success) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); + return; } - if (!output.IsValid()) { - output = CTCert(); - return CTCertLoadStatus::Invalid; - } - return CTCertLoadStatus::Loaded; + + std::vector ticket_data(Common::AlignUp(ticket.GetSerializedSize(), 0x10)); + memcpy(ticket_data.data(), ticket.Serialize().data(), ticket.GetSerializedSize()); + + std::vector key(0x10); + std::vector iv(0x10); + + RAND_bytes(key.data(), static_cast(key.size())); + RAND_bytes(iv.data(), static_cast(iv.size())); + + CryptoPP::CBC_Mode::Encryption e(key.data(), key.size(), iv.data()); + e.ProcessData(ticket_data.data(), ticket_data.data(), ticket_data.size()); + + const auto& wrap_key = HW::RSA::GetTicketWrapSlot(); + u32 padding_len = static_cast( + ((CryptoPP::Integer(wrap_key.GetModulus().data(), wrap_key.GetModulus().size()).BitCount() + + 7) / + 8) - + (key.size() + iv.size()) - 3); + + std::vector m; + m.reserve(3 + padding_len + (key.size() + iv.size())); + m.push_back(0x00); + m.push_back(0x01); + for (u32 i = 0; i < padding_len; i++) + m.push_back(0xFF); + m.push_back(0x00); + m.insert(m.end(), key.begin(), key.end()); + m.insert(m.end(), iv.begin(), iv.end()); + + auto rsa_out = wrap_key.ModularExponentiation(m, static_cast(m.size())); + + ticket_buf.Write(ticket_data.data(), 0, + std::min(static_cast(ticket_buf_size), ticket_data.size())); + keyiv_buf.Write(rsa_out.data(), 0, + std::min(static_cast(keyiv_buf_size), rsa_out.size())); + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(ResultSuccess); + rb.Push(static_cast(ticket_data.size())); + rb.Push(static_cast(rsa_out.size())); } Module::Module(Core::System& _system) : system(_system) { ScanForAllTitles(); - LoadCTCertFile(ct_cert); system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex"); } diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 4b78e84b7..e4786905f 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -16,6 +16,7 @@ #include "common/swap.h" #include "core/file_sys/cia_container.h" #include "core/file_sys/file_backend.h" +#include "core/file_sys/ncch_container.h" #include "core/global.h" #include "core/hle/kernel/mutex.h" #include "core/hle/result.h" @@ -38,11 +39,15 @@ namespace Kernel { class Mutex; } +namespace IPC { +class RequestParser; +} + namespace Service::AM { namespace ErrCodes { enum { - CIACurrentlyInstalling = 4, + InvalidImportState = 4, InvalidTID = 31, EmptyCIA = 32, TryingToUninstallSystemApp = 44, @@ -69,30 +74,33 @@ enum class InstallStatus : u32 { ErrorEncrypted, }; -enum class CTCertLoadStatus { - Loaded, - NotFound, - Invalid, - IOError, +enum class ImportTitleContextState : u8 { + NONE = 0, + WAITING_FOR_IMPORT = 1, + RESUMABLE = 2, + WAITING_FOR_COMMIT = 3, + ALREADY_EXISTS = 4, + DELETING = 5, + NEEDS_CLEANUP = 6, }; -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; +struct ImportTitleContext { + u64 title_id; + u16 version; + ImportTitleContextState state; + u32 type; + u64 size; }; -static_assert(sizeof(CTCert) == 0x180, "Invalid CTCert size."); +static_assert(sizeof(ImportTitleContext) == 0x18, "Invalid ImportTitleContext size"); + +struct ImportContentContext { + u32 content_id; + u16 index; + ImportTitleContextState state; + u64 size; + u64 current_size; +}; +static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size"); // Title ID valid length constexpr std::size_t TITLE_ID_VALID_LENGTH = 16; @@ -102,26 +110,101 @@ constexpr u64 TWL_TITLE_ID_FLAG = 0x0000800000000000ULL; // Progress callback for InstallCIA, receives bytes written and total bytes using ProgressCallback = void(std::size_t, std::size_t); +class NCCHCryptoFile final { +public: + NCCHCryptoFile(const std::string& out_file); + + void Write(const u8* buffer, std::size_t length); + bool IsError() { + return is_error; + } + +private: + friend class CIAFile; + FileUtil::IOFile file; + bool is_error = false; + bool decryption_authorized = false; + + std::size_t written = 0; + + NCCH_Header ncch_header; + std::size_t header_size = 0; + bool header_parsed = false; + + bool is_encrypted = false; + std::array + primary_key{}; // for decrypting exheader, exefs header and icon/banner section + std::array secondary_key{}; // for decrypting romfs and .code section + std::array exheader_ctr{}; + std::array exefs_ctr{}; + std::array romfs_ctr{}; + + struct CryptoRegion { + enum Type { + EXHEADER = 0, + EXEFS_HDR = 1, + EXEFS_PRI = 2, + EXEFS_SEC = 3, + ROMFS = 4, + }; + Type type; + size_t offset; + size_t size; + size_t seek_from; + }; + + std::vector regions; + + ExeFs_Header exefs_header; + std::size_t exefs_header_written = 0; + bool exefs_header_processed = false; +}; + +class CIAFile; +void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx); + // A file handled returned for CIAs to be written into and subsequently installed. class CIAFile final : public FileSys::FileBackend { public: - explicit CIAFile(Core::System& system_, Service::FS::MediaType media_type); + 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(); + Result 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; + + Result ProvideTicket(const FileSys::Ticket& ticket); + const FileSys::TitleMetadata& GetTMD(); + CIAInstallState GetCiaInstallState() { + return install_state; + } + + ResultVal WriteContentDataIndexed(u16 content_index, u64 offset, + std::size_t length, const u8* buffer); + u64 GetSize() const override; bool SetSize(u64 size) const override; bool Close() override; void Flush() const override; + void SetDone() { + is_done = true; + } + private: + friend void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx); Core::System& system; + // Sections (tik, tmd, contents) are being imported individually + bool from_cdn; + bool decryption_authorized; + bool is_done = false; + bool is_closed = false; + // Whether it's installing an update, and what step of installation it is at bool is_update = false; CIAInstallState install_state = CIAInstallState::InstallStarted; @@ -133,13 +216,26 @@ private: FileSys::CIAContainer container; std::vector data; std::vector content_written; - std::vector content_files; + std::vector content_file_paths; + u16 current_content_index = -1; + std::unique_ptr current_content_file; Service::FS::MediaType media_type; class DecryptionState; std::unique_ptr decryption_state; }; +class CurrentImportingTitle { +public: + explicit CurrentImportingTitle(Core::System& system_, u64 title_id_, + Service::FS::MediaType media_type_) + : cia_file(system_, media_type_, true), title_id(title_id_), media_type(media_type_) {} + + CIAFile cia_file; + u64 title_id; + Service::FS::MediaType media_type; +}; + // A file handled returned for Tickets to be written into and subsequently installed. class TicketFile final : public FileSys::FileBackend { public: @@ -154,9 +250,78 @@ public: bool Close() override; void Flush() const override; + Result Commit(); + u64 GetTitleID() { + return title_id; + } + u64 GetTicketID() { + return ticket_id; + } + +private: + u64 written = 0; + u64 title_id, ticket_id; + std::vector data; +}; + +// A file handled returned for TMDs to be written into and subsequently installed. +class TMDFile final : public FileSys::FileBackend { +public: + explicit TMDFile(const std::shared_ptr& import_context) + : importing_title(import_context) {} + ~TMDFile(); + + ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, + const u8* buffer) override; + u64 GetSize() const override; + bool SetSize(u64 size) const override; + bool Close() override; + void Flush() const override; + + Result Commit(); + private: u64 written = 0; std::vector data; + std::shared_ptr importing_title; +}; + +// A file handled returned for contents to be written into and subsequently installed. +class ContentFile final : public FileSys::FileBackend { +public: + explicit ContentFile(const std::shared_ptr& import_context, u16 index_, + ImportContentContext& import_context_) + : import_context(import_context_), importing_title(import_context), index(index_) {} + ~ContentFile(); + + ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, + const u8* buffer) override; + u64 GetSize() const override; + bool SetSize(u64 size) const override; + bool Close() override; + void Flush() const override; + + void Cancel(FS::MediaType media_type, u64 title_id); + + ImportContentContext& GetImportContext() { + return import_context; + } + + void SetWritten(u64 written_) { + written = written_; + } + +private: + ImportContentContext& import_context; + + u64 written = 0; + std::shared_ptr importing_title; + u16 index; + + NCCH_Header ncch_header; + size_t ncch_copied = 0; }; /** @@ -168,13 +333,6 @@ private: InstallStatus InstallCIA(const std::string& path, std::function&& update_callback = nullptr); -/** - * Downloads and installs title form the Nintendo Update Service. - * @param title_id the title_id to download - * @returns whether the install was successful or error code - */ -InstallStatus InstallFromNus(u64 title_id, int version = -1); - /** * Get the update title ID for a title * @param titleId the title ID @@ -189,6 +347,11 @@ u64 GetTitleUpdateId(u64 title_id); */ Service::FS::MediaType GetTitleMediaType(u64 titleId); +/** + * Get the .tik path for a title_id and ticket_id. + */ +std::string GetTicketPath(u64 title_id, u64 ticket_id); + /** * Get the .tmd path for a title * @param media_type the media the title exists on @@ -461,6 +624,36 @@ public: */ void GetDeviceID(Kernel::HLERequestContext& ctx); + void GetNumImportTitleContextsImpl(IPC::RequestParser& rp, FS::MediaType media_type, + bool include_installing, bool include_finalizing); + + void GetImportTitleContextListImpl(IPC::RequestParser& rp, FS::MediaType media_type, + u32 list_count, bool include_installing, + bool include_finalizing); + + void GetNumImportTitleContexts(Kernel::HLERequestContext& ctx); + + void GetImportTitleContextList(Kernel::HLERequestContext& ctx); + + void GetImportTitleContexts(Kernel::HLERequestContext& ctx); + + void DeleteImportTitleContext(Kernel::HLERequestContext& ctx); + + void GetNumImportContentContextsImpl(IPC::RequestParser& rp, u64 title_id, + FS::MediaType media_type); + + void GetImportContentContextListImpl(IPC::RequestParser& rp, u32 list_count, u64 title_id, + FS::MediaType media_type); + + void GetImportContentContextsImpl(IPC::RequestParser& rp, u32 list_count, u64 title_id, + FS::MediaType media_type); + + void GetNumImportContentContexts(Kernel::HLERequestContext& ctx); + + void GetImportContentContextList(Kernel::HLERequestContext& ctx); + + void GetImportContentContexts(Kernel::HLERequestContext& ctx); + /** * AM::NeedsCleanup service function * Inputs: @@ -748,6 +941,40 @@ public: */ void EndImportTicket(Kernel::HLERequestContext& ctx); + void BeginImportTitle(Kernel::HLERequestContext& ctx); + + void StopImportTitle(Kernel::HLERequestContext& ctx); + + void ResumeImportTitle(Kernel::HLERequestContext& ctx); + + void CancelImportTitle(Kernel::HLERequestContext& ctx); + + void EndImportTitle(Kernel::HLERequestContext& ctx); + + void BeginImportTmd(Kernel::HLERequestContext& ctx); + + void EndImportTmd(Kernel::HLERequestContext& ctx); + + void CreateImportContentContexts(Kernel::HLERequestContext& ctx); + + void BeginImportContent(Kernel::HLERequestContext& ctx); + + void ResumeImportContent(Kernel::HLERequestContext& ctx); + + void StopImportContent(Kernel::HLERequestContext& ctx); + + void CancelImportContent(Kernel::HLERequestContext& ctx); + + void EndImportContent(Kernel::HLERequestContext& ctx); + + void GetNumCurrentImportContentContexts(Kernel::HLERequestContext& ctx); + + void GetCurrentImportContentContextList(Kernel::HLERequestContext& ctx); + + void GetCurrentImportContentContexts(Kernel::HLERequestContext& ctx); + + void Sign(Kernel::HLERequestContext& ctx); + /** * AM::GetDeviceCert service function * Inputs: @@ -758,6 +985,18 @@ public: */ void GetDeviceCert(Kernel::HLERequestContext& ctx); + void DeleteTicketId(Kernel::HLERequestContext& ctx); + + void GetNumTicketIds(Kernel::HLERequestContext& ctx); + + void GetTicketIdList(Kernel::HLERequestContext& ctx); + + void GetNumTicketsOfProgram(Kernel::HLERequestContext& ctx); + + void ListTicketInfos(Kernel::HLERequestContext& ctx); + + void ExportTicketWrapped(Kernel::HLERequestContext& ctx); + protected: std::shared_ptr am; @@ -765,19 +1004,9 @@ public: std::shared_ptr artic_client = nullptr; }; - /** - * Gets the CTCert.bin path in the host filesystem - * @returns std::string CTCert.bin path in the host filesystem - */ - static std::string GetCTCertPath(); - - /** - * Loads the CTCert.bin file from the filesystem. - * @returns CTCertLoadStatus indicating the file load status. - */ - static CTCertLoadStatus LoadCTCertFile(CTCert& output); - private: + void ScanForTickets(); + /** * Scans the for titles in a storage medium for listing. * @param media_type the storage medium to scan @@ -791,9 +1020,14 @@ private: Core::System& system; bool cia_installing = false; + bool force_old_device_id = false; + bool force_new_device_id = false; std::array, 3> am_title_list; + std::multimap am_ticket_list; std::shared_ptr system_updater_mutex; - CTCert ct_cert{}; + std::shared_ptr importing_title; + std::map import_title_contexts; + std::multimap import_content_contexts; template void serialize(Archive& ar, const unsigned int); diff --git a/src/core/hle/service/am/am_net.cpp b/src/core/hle/service/am/am_net.cpp index eb9470752..e21ad5ebd 100644 --- a/src/core/hle/service/am/am_net.cpp +++ b/src/core/hle/service/am/am_net.cpp @@ -20,21 +20,21 @@ AM_NET::AM_NET(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0008, &AM_NET::GetNumTickets, "GetNumTickets"}, {0x0009, &AM_NET::GetTicketList, "GetTicketList"}, {0x000A, &AM_NET::GetDeviceID, "GetDeviceID"}, - {0x000B, nullptr, "GetNumImportTitleContexts"}, - {0x000C, nullptr, "GetImportTitleContextList"}, - {0x000D, nullptr, "GetImportTitleContexts"}, - {0x000E, nullptr, "DeleteImportTitleContext"}, - {0x000F, nullptr, "GetNumImportContentContexts"}, - {0x0010, nullptr, "GetImportContentContextList"}, - {0x0011, nullptr, "GetImportContentContexts"}, + {0x000B, &AM_NET::GetNumImportTitleContexts, "GetNumImportTitleContexts"}, + {0x000C, &AM_NET::GetImportTitleContextList, "GetImportTitleContextList"}, + {0x000D, &AM_NET::GetImportTitleContexts, "GetImportTitleContexts"}, + {0x000E, &AM_NET::DeleteImportTitleContext, "DeleteImportTitleContext"}, + {0x000F, &AM_NET::GetNumImportContentContexts, "GetNumImportContentContexts"}, + {0x0010, &AM_NET::GetImportContentContextList, "GetImportContentContextList"}, + {0x0011, &AM_NET::GetImportContentContexts, "GetImportContentContexts"}, {0x0012, nullptr, "DeleteImportContentContexts"}, {0x0013, &AM_NET::NeedsCleanup, "NeedsCleanup"}, - {0x0014, nullptr, "DoCleanup"}, + {0x0014, &AM_NET::DoCleanup, "DoCleanup"}, {0x0015, nullptr, "DeleteAllImportContexts"}, {0x0016, nullptr, "DeleteAllTemporaryPrograms"}, {0x0017, nullptr, "ImportTwlBackupLegacy"}, {0x0018, nullptr, "InitializeTitleDatabase"}, - {0x0019, nullptr, "QueryAvailableTitleDatabase"}, + {0x0019, &AM_NET::QueryAvailableTitleDatabase, "QueryAvailableTitleDatabase"}, {0x001A, nullptr, "CalcTwlBackupSize"}, {0x001B, nullptr, "ExportTwlBackup"}, {0x001C, nullptr, "ImportTwlBackup"}, @@ -83,35 +83,35 @@ AM_NET::AM_NET(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0801, &AM_NET::BeginImportTicket, "BeginImportTicket"}, {0x0802, nullptr, "CancelImportTicket"}, {0x0803, &AM_NET::EndImportTicket, "EndImportTicket"}, - {0x0804, nullptr, "BeginImportTitle"}, - {0x0805, nullptr, "StopImportTitle"}, - {0x0806, nullptr, "ResumeImportTitle"}, - {0x0807, nullptr, "CancelImportTitle"}, - {0x0808, nullptr, "EndImportTitle"}, + {0x0804, &AM_NET::BeginImportTitle, "BeginImportTitle"}, + {0x0805, &AM_NET::StopImportTitle, "StopImportTitle"}, + {0x0806, &AM_NET::ResumeImportTitle, "ResumeImportTitle"}, + {0x0807, &AM_NET::CancelImportTitle, "CancelImportTitle"}, + {0x0808, &AM_NET::EndImportTitle, "EndImportTitle"}, {0x0809, nullptr, "CommitImportTitles"}, - {0x080A, nullptr, "BeginImportTmd"}, + {0x080A, &AM_NET::BeginImportTmd, "BeginImportTmd"}, {0x080B, nullptr, "CancelImportTmd"}, - {0x080C, nullptr, "EndImportTmd"}, - {0x080D, nullptr, "CreateImportContentContexts"}, - {0x080E, nullptr, "BeginImportContent"}, - {0x080F, nullptr, "StopImportContent"}, - {0x0810, nullptr, "ResumeImportContent"}, - {0x0811, nullptr, "CancelImportContent"}, - {0x0812, nullptr, "EndImportContent"}, - {0x0813, nullptr, "GetNumCurrentImportContentContexts"}, - {0x0814, nullptr, "GetCurrentImportContentContextList"}, - {0x0815, nullptr, "GetCurrentImportContentContexts"}, - {0x0816, nullptr, "Sign"}, + {0x080C, &AM_NET::EndImportTmd, "EndImportTmd"}, + {0x080D, &AM_NET::CreateImportContentContexts, "CreateImportContentContexts"}, + {0x080E, &AM_NET::BeginImportContent, "BeginImportContent"}, + {0x080F, &AM_NET::StopImportContent, "StopImportContent"}, + {0x0810, &AM_NET::ResumeImportContent, "ResumeImportContent"}, + {0x0811, &AM_NET::CancelImportContent, "CancelImportContent"}, + {0x0812, &AM_NET::EndImportContent, "EndImportContent"}, + {0x0813, &AM_NET::GetNumCurrentImportContentContexts, "GetNumCurrentImportContentContexts"}, + {0x0814, &AM_NET::GetCurrentImportContentContextList, "GetCurrentImportContentContextList"}, + {0x0815, &AM_NET::GetCurrentImportContentContexts, "GetCurrentImportContentContexts"}, + {0x0816, &AM_NET::Sign, "Sign"}, {0x0817, nullptr, "Verify"}, {0x0818, &AM_NET::GetDeviceCert, "GetDeviceCert"}, {0x0819, nullptr, "ImportCertificates"}, {0x081A, nullptr, "ImportCertificate"}, {0x081B, nullptr, "CommitImportTitlesAndUpdateFirmwareAuto"}, - {0x081C, nullptr, "DeleteTicketId"}, - {0x081D, nullptr, "GetNumTicketIds"}, - {0x081E, nullptr, "GetTicketIdList"}, - {0x081F, nullptr, "GetNumTicketsOfProgram"}, - {0x0820, nullptr, "ListTicketInfos"}, + {0x081C, &AM_NET::DeleteTicketId, "DeleteTicketId"}, + {0x081D, &AM_NET::GetNumTicketIds, "GetNumTicketIds"}, + {0x081E, &AM_NET::GetTicketIdList, "GetTicketIdList"}, + {0x081F, &AM_NET::GetNumTicketsOfProgram, "GetNumTicketsOfProgram"}, + {0x0820, &AM_NET::ListTicketInfos, "ListTicketInfos"}, {0x0821, nullptr, "GetRightsOnlyTicketData"}, {0x0822, nullptr, "GetNumCurrentContentInfos"}, {0x0823, nullptr, "FindCurrentContentInfos"}, @@ -120,6 +120,7 @@ AM_NET::AM_NET(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0826, nullptr, "UpdateImportContentContexts"}, {0x0827, nullptr, "DeleteAllDemoLaunchInfos"}, {0x0828, nullptr, "BeginImportTitleForOverWrite"}, + {0x0829, &AM_NET::ExportTicketWrapped, "ExportTicketWrapped"}, // clang-format on }; RegisterHandlers(functions); diff --git a/src/core/hle/service/am/am_sys.cpp b/src/core/hle/service/am/am_sys.cpp index a4e39e6d7..c62d0a485 100644 --- a/src/core/hle/service/am/am_sys.cpp +++ b/src/core/hle/service/am/am_sys.cpp @@ -20,13 +20,13 @@ AM_SYS::AM_SYS(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0008, &AM_SYS::GetNumTickets, "GetNumTickets"}, {0x0009, &AM_SYS::GetTicketList, "GetTicketList"}, {0x000A, &AM_SYS::GetDeviceID, "GetDeviceID"}, - {0x000B, nullptr, "GetNumImportTitleContexts"}, - {0x000C, nullptr, "GetImportTitleContextList"}, - {0x000D, nullptr, "GetImportTitleContexts"}, - {0x000E, nullptr, "DeleteImportTitleContext"}, - {0x000F, nullptr, "GetNumImportContentContexts"}, - {0x0010, nullptr, "GetImportContentContextList"}, - {0x0011, nullptr, "GetImportContentContexts"}, + {0x000B, &AM_SYS::GetNumImportTitleContexts, "GetNumImportTitleContexts"}, + {0x000C, &AM_SYS::GetImportTitleContextList, "GetImportTitleContextList"}, + {0x000D, &AM_SYS::GetImportTitleContexts, "GetImportTitleContexts"}, + {0x000E, &AM_SYS::DeleteImportTitleContext, "DeleteImportTitleContext"}, + {0x000F, &AM_SYS::GetNumImportContentContexts, "GetNumImportContentContexts"}, + {0x0010, &AM_SYS::GetImportContentContextList, "GetImportContentContextList"}, + {0x0011, &AM_SYS::GetImportContentContexts, "GetImportContentContexts"}, {0x0012, nullptr, "DeleteImportContentContexts"}, {0x0013, &AM_SYS::NeedsCleanup, "NeedsCleanup"}, {0x0014, &AM_SYS::DoCleanup, "DoCleanup"}, @@ -42,10 +42,10 @@ AM_SYS::AM_SYS(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x001E, nullptr, "ReadTwlBackupInfo"}, {0x001F, nullptr, "DeleteAllExpiredUserPrograms"}, {0x0020, nullptr, "GetTwlArchiveResourceInfo"}, - {0x0021, nullptr, "GetPersonalizedTicketInfoList"}, + {0x0021, &AM_SYS::GetPersonalizedTicketInfoList, "GetPersonalizedTicketInfoList"}, {0x0022, nullptr, "DeleteAllImportContextsFiltered"}, - {0x0023, nullptr, "GetNumImportTitleContextsFiltered"}, - {0x0024, nullptr, "GetImportTitleContextListFiltered"}, + {0x0023, &AM_SYS::GetNumImportTitleContextsFiltered, "GetNumImportTitleContextsFiltered"}, + {0x0024, &AM_SYS::GetImportTitleContextListFiltered, "GetImportTitleContextListFiltered"}, {0x0025, &AM_SYS::CheckContentRights, "CheckContentRights"}, {0x0026, nullptr, "GetTicketLimitInfos"}, {0x0027, nullptr, "GetDemoLaunchInfos"}, @@ -53,7 +53,7 @@ AM_SYS::AM_SYS(std::shared_ptr am) : Module::Interface(std::move(am), "a {0x0029, nullptr, "DeleteUserProgramsAtomically"}, {0x002A, nullptr, "GetNumExistingContentInfosSystem"}, {0x002B, nullptr, "ListExistingContentInfosSystem"}, - {0x002C, nullptr, "GetProgramInfosIgnorePlatform"}, + {0x002C, &AM_SYS::GetProgramInfosIgnorePlatform, "GetProgramInfosIgnorePlatform"}, {0x002D, &AM_SYS::CheckContentRightsIgnorePlatform, "CheckContentRightsIgnorePlatform"}, {0x1001, &AM_SYS::GetDLCContentInfoCount, "GetDLCContentInfoCount"}, {0x1002, &AM_SYS::FindDLCContentInfos, "FindDLCContentInfos"}, diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index 82963a35a..d6f1b5269 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -29,6 +29,7 @@ #include "core/hle/service/cfg/cfg_s.h" #include "core/hle/service/cfg/cfg_u.h" #include "core/loader/loader.h" +#include "core/hw/unique_data.h" SERVICE_CONSTRUCT_IMPL(Service::CFG::Module) SERIALIZE_EXPORT_IMPL(Service::CFG::Module) @@ -217,11 +218,19 @@ void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) { void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - u8 ret = 0; - if (cfg->secure_info_a_loaded) { - ret = cfg->secure_info_a.unknown; + const auto& secure_info_a = HW::UniqueData::GetSecureInfoA(); + const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB(); + + // Never happens on real hardware, but may happen if user didn't supply a dump. + // Always make sure to have available both secure data kinds or error otherwise. + if (!secure_info_a.IsValid() || !local_friend_code_seed_b.IsValid()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState, + ErrorLevel::Permanent)); } + u8 ret = secure_info_a.body.unknown; + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); rb.Push(ret); @@ -232,20 +241,25 @@ void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) { [[maybe_unused]] u32 out_size = rp.Pop(); auto out_buffer = rp.PopMappedBuffer(); - if (out_buffer.GetSize() < sizeof(SecureInfoA::serial_number)) { + if (out_buffer.GetSize() < sizeof(HW::UniqueData::SecureInfoA::body.serial_number)) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(Result(ErrorDescription::InvalidSize, ErrorModule::Config, ErrorSummary::WrongArgument, ErrorLevel::Permanent)); } + + const auto& secure_info_a = HW::UniqueData::GetSecureInfoA(); + const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB(); + // Never happens on real hardware, but may happen if user didn't supply a dump. // Always make sure to have available both secure data kinds or error otherwise. - if (!cfg->secure_info_a_loaded || !cfg->local_friend_code_seed_b_loaded) { + if (!secure_info_a.IsValid() || !local_friend_code_seed_b.IsValid()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState, ErrorLevel::Permanent)); } - out_buffer.Write(&cfg->secure_info_a.serial_number, 0, sizeof(SecureInfoA::serial_number)); + out_buffer.Write(secure_info_a.body.serial_number.data(), 0, + sizeof(HW::UniqueData::SecureInfoA::body.serial_number)); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -398,27 +412,34 @@ void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ct auto out_buffer = rp.PopMappedBuffer(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - if (out_buffer.GetSize() < sizeof(LocalFriendCodeSeedB)) { + if (out_buffer.GetSize() < sizeof(HW::UniqueData::LocalFriendCodeSeedB)) { rb.Push(Result(ErrorDescription::InvalidSize, ErrorModule::Config, ErrorSummary::WrongArgument, ErrorLevel::Permanent)); } + + const auto& secure_info_a = HW::UniqueData::GetSecureInfoA(); + const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB(); + // Never happens on real hardware, but may happen if user didn't supply a dump. // Always make sure to have available both secure data kinds or error otherwise. - if (!cfg->secure_info_a_loaded || !cfg->local_friend_code_seed_b_loaded) { + if (!secure_info_a.IsValid() || !local_friend_code_seed_b.IsValid()) { rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState, ErrorLevel::Permanent)); } - out_buffer.Write(&cfg->local_friend_code_seed_b, 0, sizeof(LocalFriendCodeSeedB)); + out_buffer.Write(&local_friend_code_seed_b, 0, sizeof(HW::UniqueData::LocalFriendCodeSeedB)); rb.Push(ResultSuccess); } void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + const auto& secure_info_a = HW::UniqueData::GetSecureInfoA(); + const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB(); + // Never happens on real hardware, but may happen if user didn't supply a dump. // Always make sure to have available both secure data kinds or error otherwise. - if (!cfg->secure_info_a_loaded || !cfg->local_friend_code_seed_b_loaded) { + if (!secure_info_a.IsValid() || !local_friend_code_seed_b.IsValid()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState, ErrorLevel::Permanent)); @@ -426,7 +447,7 @@ void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); rb.Push(ResultSuccess); - rb.Push(cfg->local_friend_code_seed_b.friend_code_seed); + rb.Push(local_friend_code_seed_b.body.friend_code_seed); } void Module::Interface::FormatConfig(Kernel::HLERequestContext& ctx) { @@ -601,14 +622,6 @@ Result Module::UpdateConfigNANDSavegame() { return ResultSuccess; } -std::string Module::GetLocalFriendCodeSeedBPath() { - return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/LocalFriendCodeSeed_B"; -} - -std::string Module::GetSecureInfoAPath() { - return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/SecureInfo_A"; -} - Result Module::FormatConfig() { Result res = DeleteConfigNANDSaveFile(); // The delete command fails if the file doesn't exist, so we have to check that too @@ -682,55 +695,6 @@ Result Module::LoadConfigNANDSaveFile() { return FormatConfig(); } -void Module::InvalidateSecureData() { - secure_info_a_loaded = local_friend_code_seed_b_loaded = false; -} - -SecureDataLoadStatus Module::LoadSecureInfoAFile() { - if (secure_info_a_loaded) { - return SecureDataLoadStatus::Loaded; - } - std::string file_path = GetSecureInfoAPath(); - if (!FileUtil::Exists(file_path)) { - return SecureDataLoadStatus::NotFound; - } - FileUtil::IOFile file(file_path, "rb"); - if (!file.IsOpen()) { - return SecureDataLoadStatus::IOError; - } - if (file.GetSize() != sizeof(SecureInfoA)) { - return SecureDataLoadStatus::Invalid; - } - if (file.ReadBytes(&secure_info_a, sizeof(SecureInfoA)) != sizeof(SecureInfoA)) { - return SecureDataLoadStatus::IOError; - } - secure_info_a_loaded = true; - return SecureDataLoadStatus::Loaded; -} - -SecureDataLoadStatus Module::LoadLocalFriendCodeSeedBFile() { - if (local_friend_code_seed_b_loaded) { - return SecureDataLoadStatus::Loaded; - } - std::string file_path = GetLocalFriendCodeSeedBPath(); - if (!FileUtil::Exists(file_path)) { - return SecureDataLoadStatus::NotFound; - } - FileUtil::IOFile file(file_path, "rb"); - if (!file.IsOpen()) { - return SecureDataLoadStatus::IOError; - } - if (file.GetSize() != sizeof(LocalFriendCodeSeedB)) { - return SecureDataLoadStatus::Invalid; - } - if (file.ReadBytes(&local_friend_code_seed_b, sizeof(LocalFriendCodeSeedB)) != - sizeof(LocalFriendCodeSeedB)) { - return SecureDataLoadStatus::IOError; - } - local_friend_code_seed_b_loaded = true; - return SecureDataLoadStatus::Loaded; -} - void Module::LoadMCUConfig() { FileUtil::IOFile mcu_data_file( fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb"); @@ -768,8 +732,6 @@ Module::Module(Core::System& system_) : system(system_) { SetEULAVersion(default_version); UpdateConfigNANDSavegame(); } - LoadSecureInfoAFile(); - LoadLocalFriendCodeSeedBFile(); } Module::~Module() = default; diff --git a/src/core/hle/service/cfg/cfg.h b/src/core/hle/service/cfg/cfg.h index 263cd51e6..f545518e6 100644 --- a/src/core/hle/service/cfg/cfg.h +++ b/src/core/hle/service/cfg/cfg.h @@ -21,6 +21,10 @@ namespace Core { class System; } +namespace HW::UniqueData { +enum class SecureDataLoadStatus; +} + namespace Service::CFG { enum ConfigBlockID { @@ -177,28 +181,6 @@ enum class AccessFlag : u16 { }; DECLARE_ENUM_FLAG_OPERATORS(AccessFlag); -struct SecureInfoA { - std::array signature; - u8 region; - u8 unknown; - std::array serial_number; -}; -static_assert(sizeof(SecureInfoA) == 0x111); - -struct LocalFriendCodeSeedB { - std::array signature; - u64 unknown; - u64 friend_code_seed; -}; -static_assert(sizeof(LocalFriendCodeSeedB) == 0x110); - -enum class SecureDataLoadStatus { - Loaded, - NotFound, - Invalid, - IOError, -}; - class Module final { public: Module(Core::System& system_); @@ -635,34 +617,6 @@ public: */ Result UpdateConfigNANDSavegame(); - /** - * Invalidates the loaded secure data so that it is loaded again. - */ - void InvalidateSecureData(); - /** - * Loads the LocalFriendCodeSeed_B file from NAND. - * @returns LocalFriendCodeSeedBLoadStatus indicating the file load status. - */ - SecureDataLoadStatus LoadSecureInfoAFile(); - - /** - * Loads the LocalFriendCodeSeed_B file from NAND. - * @returns LocalFriendCodeSeedBLoadStatus indicating the file load status. - */ - SecureDataLoadStatus LoadLocalFriendCodeSeedBFile(); - - /** - * Gets the SecureInfo_A path in the host filesystem - * @returns std::string SecureInfo_A path in the host filesystem - */ - std::string GetSecureInfoAPath(); - - /** - * Gets the LocalFriendCodeSeed_B path in the host filesystem - * @returns std::string LocalFriendCodeSeed_B path in the host filesystem - */ - std::string GetLocalFriendCodeSeedBPath(); - /** * Saves MCU specific data */ @@ -678,10 +632,6 @@ private: std::array cfg_config_file_buffer; std::unique_ptr cfg_system_save_data_archive; u32 preferred_region_code = 0; - bool secure_info_a_loaded = false; - SecureInfoA secure_info_a; - bool local_friend_code_seed_b_loaded = false; - LocalFriendCodeSeedB local_friend_code_seed_b; bool preferred_region_chosen = false; MCUData mcu_data{}; diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index 1da247680..148411f80 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -2,6 +2,10 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include +#include +#include #include "common/archives.h" #include "common/assert.h" #include "common/common_types.h" @@ -25,6 +29,8 @@ #include "core/hle/service/am/am.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/fs/fs_user.h" +#include "core/hw/aes/key.h" +#include "core/hw/unique_data.h" SERVICE_CONSTRUCT_IMPL(Service::FS::FS_USER) SERIALIZE_EXPORT_IMPL(Service::FS::FS_USER) @@ -1111,6 +1117,47 @@ void FS_USER::GetArchiveResource(Kernel::HLERequestContext& ctx) { rb.PushRaw(*resource); } +void FS_USER::ExportIntegrityVerificationSeed(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto buf = rp.PopMappedBuffer(); + + constexpr size_t hashed_movable_size = 0x110; + constexpr size_t cipher_movable_size = 0x120; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto& movable_struct = HW::UniqueData::GetMovableSed(); + if (!movable_struct.IsValid()) { + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::FS, ErrorSummary::NotFound, + ErrorLevel::Permanent)); + return; + } + + std::vector movable(cipher_movable_size); + memcpy(movable.data(), &movable_struct, cipher_movable_size); + + auto movable_cmac = HW::AES::GetMovableKey(true); + auto movable_key = HW::AES::GetMovableKey(false); + + CryptoPP::SHA256 hash; + std::array movable_digest; + hash.CalculateDigest(movable_digest.data(), movable.data(), hashed_movable_size); + + CryptoPP::CMAC cmac(movable_cmac.data(), movable_cmac.size()); + std::array cmac_hash; + cmac.Update(movable_digest.data(), movable_digest.size()); + cmac.Final(cmac_hash.data()); + + CryptoPP::CBC_Mode::Encryption{movable_key.data(), movable_key.size(), + cmac_hash.data()} + .ProcessData(movable.data(), movable.data(), cipher_movable_size); + + movable.insert(movable.begin(), cmac_hash.begin(), cmac_hash.end()); + + buf.Write(movable.data(), 0, std::min(buf.GetSize(), movable.size())); + rb.Push(ResultSuccess); +} + void FS_USER::GetFormatInfo(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto archive_id = rp.PopEnum(); @@ -1262,6 +1309,7 @@ void FS_USER::GetSpecialContentIndex(Kernel::HLERequestContext& ctx) { void FS_USER::GetNumSeeds(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + LOG_DEBUG(Service_FS, ""); rb.Push(ResultSuccess); rb.Push(FileSys::GetSeedCount()); } @@ -1269,12 +1317,68 @@ void FS_USER::GetNumSeeds(Kernel::HLERequestContext& ctx) { void FS_USER::AddSeed(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u64 title_id{rp.Pop()}; + LOG_INFO(Service_FS, "Adding seed for title_id={:016X}", title_id); FileSys::Seed::Data seed{rp.PopRaw()}; FileSys::AddSeed({title_id, seed, {}}); IPC::RequestBuilder rb{rp.MakeBuilder(1, 0)}; rb.Push(ResultSuccess); } +void FS_USER::DeleteSeed(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id{rp.Pop()}; + + bool found = FileSys::DeleteSeed(title_id); + + IPC::RequestBuilder rb{rp.MakeBuilder(1, 0)}; + rb.Push(found ? ResultSuccess + : Result(FileSys::ErrCodes::RomFSNotFound, ErrorModule::FS, + ErrorSummary::NotFound, ErrorLevel::Status)); +} + +void FS_USER::GetSeed(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id{rp.Pop()}; + + auto seed = FileSys::GetSeed(title_id); + if (!seed.has_value()) { + IPC::RequestBuilder rb{rp.MakeBuilder(1, 0)}; + rb.Push(Result(FileSys::ErrCodes::RomFSNotFound, ErrorModule::FS, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + IPC::RequestBuilder rb{rp.MakeBuilder(5, 0)}; + rb.Push(ResultSuccess); + rb.PushRaw(seed.value()); +} + +void FS_USER::SetUnknown0x80Data(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id{rp.Pop()}; + + std::array unknown_data = rp.PopRaw>(); + + IPC::RequestBuilder rb{rp.MakeBuilder(1, 0)}; + rb.Push(ResultSuccess); + + LOG_WARNING(Service_FS, "(STUBBED) title_id={:016X}", title_id); +} + +void FS_USER::GetUnknown0x80Data(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + u64 title_id{rp.Pop()}; + + std::array unknown_data = {0}; + + IPC::RequestBuilder rb{rp.MakeBuilder(0x21, 0)}; + rb.Push(Result(FileSys::ErrCodes::RomFSNotFound, ErrorModule::FS, ErrorSummary::NotFound, + ErrorLevel::Status)); + rb.PushRaw(unknown_data); + + LOG_WARNING(Service_FS, "(STUBBED) title_id={:016X}", title_id); +} + void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u64 value = rp.Pop(); @@ -1729,7 +1833,7 @@ FS_USER::FS_USER(Core::System& system) {0x0847, nullptr, "FormatCtrCardUserSaveData"}, {0x0848, nullptr, "GetSdmcCtrRootPath"}, {0x0849, &FS_USER::GetArchiveResource, "GetArchiveResource"}, - {0x084A, nullptr, "ExportIntegrityVerificationSeed"}, + {0x084A, &FS_USER::ExportIntegrityVerificationSeed, "ExportIntegrityVerificationSeed"}, {0x084B, nullptr, "ImportIntegrityVerificationSeed"}, {0x084C, &FS_USER::FormatSaveData, "FormatSaveData"}, {0x084D, nullptr, "GetLegacySubBannerData"}, @@ -1767,7 +1871,11 @@ FS_USER::FS_USER(Core::System& system) {0x0875, &FS_USER::SetSaveDataSecureValue, "SetSaveDataSecureValue" }, {0x0876, &FS_USER::GetSaveDataSecureValue, "GetSaveDataSecureValue" }, {0x087A, &FS_USER::AddSeed, "AddSeed"}, + {0x087B, &FS_USER::GetSeed, "GetSeed"}, + {0x087C, &FS_USER::DeleteSeed, "GetSeed"}, {0x087D, &FS_USER::GetNumSeeds, "GetNumSeeds"}, + {0x0880, &FS_USER::SetUnknown0x80Data, "SetUnknown0x80Data"}, + {0x0881, &FS_USER::GetUnknown0x80Data, "GetUnknown0x80Data"}, {0x0886, nullptr, "CheckUpdatedDat"}, // clang-format on }; diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index d7f11d402..11fbef4db 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -524,6 +524,8 @@ private: */ void GetArchiveResource(Kernel::HLERequestContext& ctx); + void ExportIntegrityVerificationSeed(Kernel::HLERequestContext& ctx); + /** * FS_User::GetFormatInfo service function. * Inputs: @@ -610,17 +612,6 @@ private: */ void GetSpecialContentIndex(Kernel::HLERequestContext& ctx); - /** - * FS_User::GetNumSeeds service function. - * Inputs: - * 0 : 0x087D0000 - * Outputs: - * 0 : 0x087D0080 - * 1 : Result of function, 0 on success, otherwise error code - * 2 : Number of seeds in the SEEDDB - */ - void GetNumSeeds(Kernel::HLERequestContext& ctx); - /** * FS_User::AddSeed service function. * Inputs: @@ -633,6 +624,25 @@ private: */ void AddSeed(Kernel::HLERequestContext& ctx); + void GetSeed(Kernel::HLERequestContext& ctx); + + void DeleteSeed(Kernel::HLERequestContext& ctx); + + /** + * FS_User::GetNumSeeds service function. + * Inputs: + * 0 : 0x087D0000 + * Outputs: + * 0 : 0x087D0080 + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Number of seeds in the SEEDDB + */ + void GetNumSeeds(Kernel::HLERequestContext& ctx); + + void SetUnknown0x80Data(Kernel::HLERequestContext& ctx); + + void GetUnknown0x80Data(Kernel::HLERequestContext& ctx); + /** * FS_User::SetSaveDataSecureValue service function. * Inputs: diff --git a/src/core/hle/service/http/ctr-common-1-cert.h b/src/core/hle/service/http/ctr-common-1-cert.h new file mode 100644 index 000000000..2f75e665b --- /dev/null +++ b/src/core/hle/service/http/ctr-common-1-cert.h @@ -0,0 +1,85 @@ +/* Generated by bin2c, do not edit manually */ + +/* Contents of file ctr-common-1-cert.bin */ +const long int ctr_common_1_cert_bin_size = 1264; +const unsigned char ctr_common_1_cert_bin[1264] = { + 0x2B, 0x7B, 0xB5, 0xB5, 0x8F, 0xD2, 0x86, 0x75, 0xDB, 0x69, 0x02, 0xD9, 0x90, 0xCD, 0xC1, 0x1B, + 0xFC, 0xA9, 0x3D, 0xD8, 0x51, 0x82, 0xEE, 0x75, 0xE1, 0x22, 0x0C, 0x98, 0xFB, 0xE6, 0xB6, 0xE1, + 0xBF, 0xD0, 0x85, 0x15, 0x2F, 0x34, 0xF7, 0x27, 0x5C, 0x6D, 0xAB, 0x23, 0x0E, 0x04, 0x75, 0xCE, + 0x95, 0x57, 0x99, 0x35, 0xFC, 0x30, 0xC2, 0x4E, 0xF7, 0xAE, 0x42, 0x37, 0x94, 0x31, 0xFA, 0x84, + 0xB5, 0x51, 0x58, 0xE7, 0x8B, 0x37, 0xC3, 0x45, 0xB5, 0x7D, 0xAE, 0x27, 0x92, 0x9E, 0xB8, 0xF2, + 0x07, 0x1C, 0xAF, 0x67, 0xA9, 0x1C, 0x27, 0x73, 0x7E, 0x60, 0xD2, 0xCF, 0x37, 0x71, 0x32, 0xD1, + 0x35, 0x51, 0x0A, 0x23, 0xB0, 0x3B, 0x5B, 0x08, 0x91, 0xE1, 0xE1, 0xE9, 0x5D, 0x2B, 0x5B, 0xF0, + 0xE3, 0xBB, 0x5F, 0xBD, 0xF0, 0x9B, 0xB5, 0xD7, 0x37, 0x96, 0x76, 0x54, 0x52, 0xBA, 0x79, 0xC2, + 0x7E, 0xFE, 0x57, 0xD4, 0x4E, 0x5F, 0x05, 0x4A, 0x0D, 0xB6, 0x15, 0x49, 0x08, 0x06, 0xF6, 0x3D, + 0xF9, 0xAB, 0x04, 0x2F, 0xEB, 0x26, 0x21, 0x56, 0x2B, 0x1C, 0xAC, 0x85, 0xB0, 0x5A, 0xC8, 0x4E, + 0x67, 0x9B, 0x8C, 0xAA, 0xB8, 0x47, 0x6E, 0x7B, 0x72, 0x93, 0x7C, 0x32, 0x3C, 0x20, 0xFD, 0x0B, + 0x61, 0x3C, 0x91, 0xC1, 0x26, 0x6F, 0xD8, 0xD7, 0x79, 0x85, 0x34, 0xD1, 0x4D, 0x1C, 0xCC, 0x66, + 0x42, 0xDB, 0xBE, 0xD8, 0x8E, 0xB0, 0x49, 0xEA, 0x0A, 0x68, 0x56, 0x16, 0x17, 0x49, 0x57, 0x82, + 0x7B, 0x5D, 0xBD, 0x1C, 0x0A, 0x97, 0xBB, 0xA5, 0x2E, 0x78, 0xE6, 0x86, 0x4C, 0xC6, 0xD1, 0x76, + 0xCB, 0x30, 0x31, 0xB2, 0x02, 0xF8, 0xDA, 0x0A, 0x50, 0x86, 0xA6, 0x2E, 0x23, 0x2B, 0x44, 0xF9, + 0x51, 0x81, 0xAB, 0x71, 0xE8, 0x6A, 0x61, 0xD9, 0x37, 0x32, 0x9C, 0xDD, 0x91, 0x68, 0x79, 0x97, + 0x04, 0x09, 0xD7, 0x96, 0x3B, 0x52, 0xCF, 0x63, 0x66, 0x25, 0xDD, 0xE4, 0x8A, 0xB6, 0xFE, 0x0D, + 0x15, 0x55, 0xD3, 0xD5, 0xB5, 0x1D, 0x73, 0x32, 0xD0, 0xAD, 0x46, 0xF0, 0x8E, 0x74, 0x1B, 0x70, + 0x39, 0xBA, 0x5A, 0x14, 0xB4, 0x16, 0x38, 0x3C, 0xA3, 0x82, 0x82, 0x1C, 0xCA, 0x29, 0xD4, 0xA1, + 0x5D, 0x26, 0x7D, 0xDA, 0x5B, 0x0B, 0x4C, 0xE8, 0xF8, 0xDF, 0x99, 0x9C, 0x67, 0xA5, 0x43, 0xA6, + 0xCC, 0x38, 0x10, 0xFF, 0xE9, 0xC9, 0x66, 0x04, 0xF7, 0x52, 0x25, 0x7B, 0x28, 0xA3, 0xD7, 0x01, + 0xFE, 0xC2, 0xFE, 0x50, 0xBE, 0x02, 0x99, 0x17, 0x87, 0x71, 0xD4, 0xBB, 0x03, 0x67, 0xFF, 0x8A, + 0x4B, 0x03, 0xEB, 0x43, 0x16, 0xB2, 0x24, 0x04, 0x89, 0x50, 0x22, 0x1F, 0x51, 0xB0, 0xF8, 0xF0, + 0x40, 0x83, 0x56, 0x46, 0xCF, 0x8A, 0xE7, 0x48, 0xB7, 0x50, 0x85, 0x3B, 0x27, 0xDD, 0xBA, 0xA2, + 0x6F, 0xC3, 0x5E, 0x5B, 0xBE, 0xD4, 0xF0, 0x1C, 0x11, 0xC6, 0xD7, 0x91, 0x99, 0x57, 0x55, 0x17, + 0xBC, 0xCC, 0x3D, 0xC7, 0x27, 0xB3, 0xE7, 0x52, 0x3A, 0x2E, 0x39, 0x8A, 0x8F, 0xE2, 0xCB, 0x11, + 0x09, 0xAF, 0xCD, 0xED, 0x61, 0xFC, 0x32, 0x3D, 0xB9, 0xA5, 0x54, 0x09, 0xC6, 0x67, 0x9E, 0xD2, + 0xB2, 0x77, 0xD8, 0x39, 0xC7, 0xFD, 0x3B, 0x7A, 0x4F, 0x27, 0xED, 0x6E, 0x16, 0x4B, 0xFA, 0x4B, + 0x95, 0xD4, 0x47, 0x8D, 0x93, 0x3B, 0x4B, 0x5B, 0xD7, 0x2D, 0x7D, 0x59, 0x34, 0x66, 0x11, 0xF8, + 0xF0, 0x24, 0x5B, 0xC8, 0xB1, 0xAA, 0xE9, 0x92, 0x50, 0xAA, 0xFA, 0xBE, 0xEF, 0x61, 0xBA, 0x17, + 0xDA, 0xCB, 0xD5, 0x7C, 0x80, 0x53, 0x93, 0x90, 0x38, 0xFC, 0x4D, 0xF7, 0x0D, 0x99, 0x83, 0xF2, + 0x3C, 0x54, 0xD9, 0xDF, 0x3A, 0x5D, 0xFF, 0xB5, 0x8F, 0x87, 0x5A, 0xD4, 0x07, 0x60, 0xF1, 0x5B, + 0x84, 0x09, 0xE9, 0xD4, 0x1D, 0xD4, 0xA9, 0x1A, 0xA9, 0x4F, 0xF3, 0xA6, 0x2D, 0x38, 0xBA, 0x52, + 0x1C, 0x92, 0xB5, 0x10, 0x1A, 0x95, 0x4C, 0xEB, 0xDB, 0xE9, 0xD7, 0xB1, 0xF8, 0x01, 0x74, 0x07, + 0xE7, 0x52, 0x91, 0xA7, 0xDD, 0x12, 0x2D, 0x13, 0x25, 0xDE, 0xEE, 0x8B, 0x94, 0x74, 0xD0, 0x4C, + 0x3E, 0x76, 0x98, 0xF5, 0x45, 0xC9, 0xA9, 0x8F, 0xEB, 0x60, 0x54, 0xC2, 0xEA, 0xCB, 0xA1, 0xB7, + 0x49, 0xE8, 0x12, 0x10, 0xF1, 0x2C, 0xE6, 0x05, 0xF7, 0xBC, 0x2A, 0x00, 0xC6, 0x75, 0x0A, 0x08, + 0x14, 0x86, 0x14, 0x5E, 0x42, 0x7C, 0x5B, 0xA2, 0x83, 0x5E, 0x80, 0xCD, 0x0B, 0x42, 0x4E, 0x33, + 0xDE, 0xBB, 0x94, 0x3F, 0x04, 0x5D, 0xAA, 0x82, 0x35, 0x59, 0x51, 0xCB, 0x98, 0x02, 0x13, 0x30, + 0x63, 0x10, 0x8A, 0x93, 0x36, 0x2B, 0xA4, 0x5E, 0x9B, 0x9C, 0xB6, 0x0A, 0x88, 0x17, 0x31, 0x33, + 0x28, 0xFB, 0x44, 0x01, 0xD4, 0x13, 0x57, 0x5E, 0x2C, 0xEB, 0x00, 0x15, 0x8A, 0x76, 0x31, 0x2A, + 0xDA, 0xF6, 0xF1, 0x8A, 0x80, 0xE6, 0xA9, 0xAF, 0xDA, 0x61, 0xC3, 0x75, 0x76, 0x18, 0x55, 0xBF, + 0x49, 0x2B, 0x51, 0xFF, 0x04, 0xF2, 0x6C, 0xD8, 0x78, 0xF1, 0xDE, 0xCB, 0x5E, 0x0E, 0x0D, 0xFB, + 0xFB, 0x76, 0xA2, 0x5C, 0x02, 0x62, 0x6F, 0x04, 0x4F, 0x64, 0xE0, 0x44, 0xD9, 0x6C, 0xA2, 0x12, + 0x1F, 0x46, 0x8D, 0xAC, 0xFF, 0x4D, 0x00, 0x8C, 0xD7, 0x6A, 0x87, 0xF3, 0x28, 0x56, 0xD5, 0x99, + 0xD6, 0xAE, 0x58, 0xE9, 0x38, 0x6B, 0xDF, 0xF1, 0x4C, 0xD4, 0xD0, 0x44, 0x6D, 0x51, 0x74, 0xE8, + 0xC4, 0x4F, 0xDF, 0x3F, 0xF3, 0x26, 0x70, 0xB6, 0x1D, 0xC0, 0x4C, 0x9E, 0x81, 0xC7, 0xC7, 0x78, + 0x30, 0xF7, 0xFA, 0x4D, 0xF5, 0x20, 0xDC, 0x3E, 0xB9, 0xCB, 0xBC, 0x5C, 0x8C, 0x96, 0x04, 0xAC, + 0x32, 0x8F, 0x64, 0xC2, 0x3C, 0x61, 0x16, 0xC3, 0x3A, 0x43, 0x6E, 0xA1, 0xB2, 0xA3, 0x71, 0xA7, + 0xB8, 0x76, 0x21, 0xC2, 0x59, 0x53, 0xE8, 0xC4, 0x23, 0xD7, 0xC0, 0xA5, 0x47, 0xF1, 0x3E, 0xED, + 0x89, 0x1B, 0x61, 0xC7, 0x5D, 0x10, 0x23, 0xBF, 0xE2, 0x98, 0x05, 0xB8, 0x47, 0x1E, 0x24, 0x25, + 0xEB, 0x88, 0x74, 0xD0, 0x6A, 0xC7, 0xEA, 0x78, 0x51, 0x85, 0xB6, 0x61, 0x03, 0xB2, 0xF8, 0x2B, + 0x60, 0x6B, 0x56, 0x27, 0x7F, 0x03, 0xC8, 0xFA, 0xB0, 0xC8, 0xE0, 0x7F, 0x9E, 0xFD, 0xC8, 0x18, + 0xB9, 0x03, 0x13, 0xC2, 0x78, 0x5D, 0x79, 0xA9, 0x66, 0x1D, 0x15, 0x11, 0x3D, 0x22, 0xD3, 0xBF, + 0xB6, 0xF8, 0x6F, 0xF4, 0x64, 0x6C, 0x93, 0xC0, 0xB6, 0xB8, 0xB4, 0x00, 0x54, 0x7E, 0xC2, 0x89, + 0x93, 0xF7, 0x13, 0xFE, 0xF0, 0xFB, 0x9D, 0x81, 0x3D, 0x73, 0xED, 0xC8, 0xDE, 0x92, 0x80, 0x9E, + 0xCB, 0xDB, 0xD3, 0x39, 0xB3, 0x01, 0x85, 0x5B, 0x73, 0x30, 0x35, 0x82, 0xCB, 0x34, 0xD8, 0x93, + 0x90, 0x26, 0x93, 0xC4, 0xE1, 0xB1, 0x72, 0xFF, 0xA8, 0xC1, 0x9B, 0x59, 0x2D, 0xCA, 0x4C, 0xEB, + 0x82, 0x98, 0x3C, 0x09, 0xD7, 0x1D, 0x2E, 0x89, 0xFB, 0xB5, 0x73, 0x3A, 0x8D, 0xA3, 0x40, 0x1A, + 0xCE, 0xE7, 0xD0, 0x70, 0x52, 0x18, 0x8E, 0x6C, 0x9C, 0x92, 0x45, 0x6E, 0xAA, 0x00, 0xBB, 0xB4, + 0xBB, 0x60, 0x3B, 0xF1, 0xD4, 0x1F, 0xCE, 0xA1, 0x0E, 0x90, 0x92, 0x29, 0xFA, 0x83, 0x3B, 0xEC, + 0xB3, 0x1D, 0x4B, 0x68, 0x2C, 0x48, 0x2C, 0x10, 0x23, 0xEF, 0x89, 0x46, 0x57, 0xA8, 0x2A, 0x7F, + 0xBA, 0x5D, 0x9C, 0xCA, 0xA7, 0x48, 0xE6, 0xA6, 0x0A, 0xDE, 0x00, 0x20, 0x01, 0x09, 0xD9, 0x75, + 0xAA, 0x25, 0x3E, 0x45, 0x20, 0xA2, 0x0C, 0x2A, 0x91, 0x18, 0xEF, 0xE6, 0xAA, 0xA2, 0x90, 0x14, + 0x9C, 0xAB, 0xC8, 0x8D, 0x49, 0xC6, 0x72, 0x58, 0xF0, 0x0A, 0x59, 0xBA, 0xFE, 0xAE, 0x73, 0xE5, + 0x48, 0xE0, 0x71, 0xEF, 0xE8, 0x00, 0x2E, 0xB2, 0xC9, 0xF9, 0x1A, 0x59, 0xC5, 0x41, 0x83, 0x00, + 0xDF, 0xE3, 0x26, 0x8F, 0x01, 0x12, 0xDA, 0xE3, 0x8F, 0xE1, 0xAA, 0xC6, 0xBC, 0x0E, 0x48, 0x49, + 0x8E, 0x30, 0x40, 0x8C, 0xAD, 0x80, 0x5B, 0xAF, 0x89, 0x60, 0x33, 0xE6, 0x91, 0x61, 0x6B, 0xF2, + 0x09, 0x7B, 0xCD, 0x54, 0x1D, 0x2B, 0x30, 0x40, 0x02, 0x06, 0xB0, 0x72, 0xE7, 0x5E, 0xE4, 0x53, + 0xD7, 0x25, 0x18, 0x75, 0x40, 0xCB, 0xA9, 0x11, 0xCC, 0x3D, 0xFB, 0x14, 0xEA, 0x40, 0xAF, 0x10, + 0x05, 0xFF, 0xE8, 0xE1, 0x87, 0x86, 0x8E, 0x01, 0xB5, 0x09, 0xB6, 0xE9, 0xD7, 0x26, 0xD7, 0x56, + 0xA4, 0x22, 0x88, 0x44, 0x74, 0xB7, 0x78, 0xD2, 0x81, 0x50, 0xB4, 0x35, 0xC9, 0x14, 0x47, 0xB4, + 0xAC, 0xCE, 0xED, 0xCB, 0xEE, 0xD5, 0x9C, 0x75, 0xDB, 0x18, 0x94, 0x71, 0xC7, 0xB1, 0xA8, 0x42, + 0x70, 0x66, 0xDF, 0x14, 0x6D, 0x05, 0x6A, 0x6A, 0x9B, 0xC4, 0xA2, 0x8C, 0xDB, 0xFE, 0x92, 0x23, + 0x4F, 0xEA, 0x66, 0x89, 0xFB, 0xD9, 0xC1, 0x8D, 0xE3, 0x70, 0xF9, 0x5F, 0x1E, 0x1A, 0x9E, 0x9A, + 0xBC, 0x7E, 0xDD, 0xAB, 0x9D, 0xB9, 0x56, 0xCB, 0xDC, 0xFB, 0x3F, 0x32, 0xCF, 0xC3, 0x17, 0x1B, + 0x25, 0x6E, 0xFF, 0xE8, 0xBD, 0x32, 0x57, 0xA3, 0x8B, 0x06, 0x9B, 0x1F, 0xA7, 0xFC, 0xE7, 0x3A, + 0x89, 0x54, 0xFA, 0x8E, 0x1B, 0xAD, 0xBF, 0x3C, 0xDD, 0x00, 0x49, 0xA3, 0xC1, 0x8D, 0xC8, 0x6D, + 0x8D, 0x43, 0xCC, 0x4E, 0xE5, 0xAE, 0xC9, 0x6C, 0x0C, 0x20, 0x09, 0x77, 0xDA, 0x8C, 0xE7, 0xAC +}; diff --git a/src/core/hle/service/http/ctr-common-1-key.h b/src/core/hle/service/http/ctr-common-1-key.h new file mode 100644 index 000000000..cd5a36799 --- /dev/null +++ b/src/core/hle/service/http/ctr-common-1-key.h @@ -0,0 +1,83 @@ +/* Generated by bin2c, do not edit manually */ + +/* Contents of file ctr-common-1-key.bin */ +const long int ctr_common_1_key_bin_size = 1232; +const unsigned char ctr_common_1_key_bin[1232] = { + 0x8E, 0xCB, 0xB5, 0xCF, 0x61, 0x5F, 0xBD, 0xE0, 0x41, 0x62, 0x2F, 0x18, 0x27, 0xC1, 0x3A, 0x72, + 0x5F, 0x6A, 0xF3, 0x6C, 0xDD, 0x26, 0x28, 0x30, 0xE3, 0x55, 0xAD, 0x2F, 0xF8, 0xE3, 0xA3, 0x0C, + 0x3B, 0x67, 0x26, 0xA1, 0xCD, 0x67, 0x28, 0x7C, 0xBB, 0xA5, 0xA5, 0xF7, 0x01, 0x55, 0x00, 0x87, + 0x09, 0xB7, 0x51, 0xE6, 0xA5, 0x8D, 0x93, 0xD0, 0xE3, 0x8D, 0x45, 0x2A, 0xDB, 0xAF, 0xFC, 0x51, + 0xD8, 0x94, 0xF6, 0xC5, 0xA9, 0x5E, 0xBA, 0x29, 0xD3, 0xB5, 0x5E, 0x98, 0x92, 0x84, 0xEA, 0x31, + 0xDC, 0xDF, 0xFE, 0x0B, 0x63, 0x78, 0xE6, 0xA0, 0x07, 0x6F, 0x33, 0xB4, 0x13, 0x6B, 0xD1, 0x48, + 0x7E, 0x3D, 0x21, 0xD1, 0x0E, 0xB0, 0x19, 0xC1, 0xB9, 0x16, 0xB5, 0xE0, 0x95, 0xFE, 0x99, 0xCD, + 0x82, 0xBA, 0x89, 0x15, 0xCE, 0xF3, 0xB1, 0xB0, 0xA2, 0xE3, 0xC2, 0x7F, 0xE4, 0xF7, 0x65, 0x8F, + 0x77, 0xB9, 0x15, 0x65, 0x4D, 0x3C, 0x42, 0xA6, 0x90, 0xDC, 0x0D, 0xFF, 0xA7, 0xE6, 0x59, 0x3A, + 0xF5, 0x8D, 0xE9, 0xEC, 0x45, 0xB7, 0xA6, 0x1E, 0xBA, 0x0E, 0x5B, 0x99, 0x19, 0x13, 0xE2, 0x9F, + 0x9A, 0x47, 0xDC, 0xDB, 0x9A, 0x71, 0x9E, 0x1C, 0xCF, 0x15, 0xEE, 0xCA, 0x4A, 0x57, 0x38, 0x7E, + 0x65, 0xF5, 0xB3, 0x2C, 0x51, 0xB3, 0x14, 0xC3, 0xF8, 0x9D, 0x58, 0x46, 0xF1, 0xE8, 0xA9, 0x83, + 0xE7, 0x3F, 0x1A, 0x0F, 0xEF, 0x3E, 0x37, 0xAC, 0x5C, 0xFE, 0x11, 0x2F, 0xA3, 0x78, 0x4B, 0xB7, + 0xA4, 0x12, 0x89, 0x87, 0xC5, 0xEB, 0x4E, 0x31, 0x10, 0xB6, 0x8D, 0xE9, 0xE3, 0xD8, 0x88, 0x4D, + 0x4E, 0x89, 0xDC, 0x3E, 0x17, 0x63, 0xC9, 0xA4, 0x75, 0x10, 0xD3, 0x89, 0x65, 0x2C, 0x10, 0x8C, + 0xEE, 0x95, 0x8E, 0x8A, 0x36, 0x48, 0x69, 0x4E, 0xAF, 0x08, 0xB9, 0x7F, 0x12, 0x7C, 0x1D, 0x7A, + 0x41, 0x58, 0xAE, 0xDD, 0x8E, 0x2A, 0xC0, 0x68, 0xED, 0x90, 0x6C, 0x61, 0x78, 0xA7, 0x7F, 0xE2, + 0x7A, 0x0D, 0xDF, 0x75, 0x4B, 0x76, 0x72, 0x13, 0x90, 0xDC, 0x5A, 0x3C, 0xDB, 0x02, 0xA5, 0xCB, + 0x5A, 0xA8, 0x2D, 0x11, 0x77, 0xD3, 0xC6, 0x9D, 0xE8, 0xC7, 0x5A, 0x9E, 0xB2, 0x3D, 0xF5, 0xCB, + 0x98, 0xA2, 0x48, 0xF8, 0xBF, 0x5F, 0x31, 0xB4, 0x83, 0xFB, 0xAE, 0x40, 0x73, 0x02, 0x1A, 0x27, + 0x30, 0x42, 0x87, 0xCE, 0x44, 0x1E, 0x8C, 0x05, 0x7B, 0xAE, 0x9C, 0xED, 0xE0, 0x3A, 0x21, 0x86, + 0x7F, 0xC1, 0xC2, 0x32, 0xE3, 0xB0, 0x1B, 0x65, 0x70, 0xA0, 0xB0, 0x1F, 0xD7, 0x24, 0x3D, 0xA7, + 0x6B, 0x68, 0xA2, 0x6B, 0xA0, 0x23, 0x21, 0x1A, 0x8E, 0xBE, 0xC0, 0xCB, 0xCD, 0xC4, 0x91, 0xFF, + 0xF9, 0x05, 0xCE, 0x78, 0xB7, 0xE9, 0x9F, 0xAC, 0x11, 0xC4, 0x66, 0x31, 0x7C, 0xE2, 0x8A, 0x1C, + 0x2E, 0xBC, 0xE1, 0x0B, 0xC4, 0x91, 0xE0, 0xED, 0xE7, 0x6F, 0x1A, 0x7E, 0x55, 0x7E, 0xAA, 0xD0, + 0xC2, 0xDD, 0x1B, 0xF5, 0xB7, 0x0C, 0x7E, 0x26, 0x8C, 0x31, 0x25, 0xDC, 0x43, 0x21, 0x00, 0x2E, + 0x30, 0x49, 0xC8, 0x1C, 0x65, 0xF9, 0x33, 0x35, 0x9F, 0xBA, 0x16, 0xFA, 0xF6, 0x86, 0xA5, 0x02, + 0xB5, 0x8C, 0x97, 0x75, 0x78, 0x55, 0x96, 0xC6, 0xAC, 0x8D, 0xE8, 0x46, 0xE5, 0x46, 0xF0, 0x95, + 0x53, 0xB6, 0x2C, 0x38, 0x5B, 0xA1, 0x1E, 0x9C, 0x51, 0x05, 0x73, 0xF4, 0xD0, 0x66, 0x71, 0x48, + 0xC2, 0xA0, 0x00, 0xCF, 0xBC, 0x8C, 0xAD, 0xE5, 0xED, 0x77, 0x74, 0xE2, 0xB9, 0xC8, 0x9D, 0x76, + 0x4B, 0x57, 0xF6, 0xFD, 0x52, 0xCA, 0xAD, 0xB7, 0xF0, 0x04, 0x84, 0xF4, 0x82, 0xE0, 0x5F, 0xBE, + 0x91, 0x00, 0xE4, 0xE6, 0x9C, 0xE6, 0x86, 0xEE, 0x64, 0xC1, 0xBC, 0x4A, 0xE6, 0x14, 0x5F, 0xEF, + 0xDB, 0x1E, 0xED, 0x03, 0x79, 0xDB, 0x79, 0x1B, 0x8E, 0x8E, 0x50, 0x55, 0xB5, 0x45, 0xAB, 0xBC, + 0x09, 0x95, 0x6C, 0xC4, 0x13, 0x1A, 0x8A, 0x19, 0x9A, 0x8B, 0xA1, 0xE6, 0xD4, 0xA8, 0xA8, 0x57, + 0x7A, 0xED, 0xCD, 0xFD, 0x8A, 0xF2, 0xFB, 0xE4, 0x2A, 0x96, 0xDF, 0xFF, 0xB0, 0xC9, 0x36, 0xD9, + 0x21, 0x6E, 0xB3, 0x6B, 0x46, 0x38, 0xC6, 0xF0, 0xE3, 0x12, 0x4F, 0x77, 0x9E, 0x59, 0x09, 0xCE, + 0x6E, 0x6F, 0x62, 0xDF, 0xCE, 0x8A, 0xFD, 0x44, 0x64, 0x3E, 0x62, 0x07, 0x2B, 0x13, 0x22, 0xC9, + 0x0D, 0xC4, 0x92, 0x05, 0x3F, 0x80, 0x9D, 0xDC, 0xC4, 0x00, 0x79, 0x09, 0x0B, 0x8A, 0xF6, 0x1C, + 0x20, 0x0F, 0x15, 0xB2, 0xE4, 0x32, 0x07, 0x4A, 0x94, 0xA6, 0x48, 0x52, 0x32, 0xF1, 0x68, 0x9E, + 0x85, 0xAB, 0xC1, 0xA9, 0x43, 0xF5, 0x43, 0xC1, 0x57, 0x9E, 0x30, 0xC3, 0xBC, 0xA2, 0x44, 0x68, + 0xF0, 0x2C, 0xE4, 0xC0, 0x4E, 0x73, 0xFF, 0x94, 0xAB, 0x62, 0x7F, 0x5B, 0x46, 0x0C, 0xB4, 0x32, + 0x59, 0x49, 0x4E, 0xA9, 0xA0, 0x6B, 0xA6, 0xB6, 0xE1, 0x33, 0xA3, 0x30, 0x34, 0x9D, 0x69, 0x0A, + 0xF5, 0x0B, 0x46, 0xAF, 0x12, 0xB2, 0x76, 0xCD, 0xF8, 0x81, 0x64, 0x4A, 0x40, 0xAE, 0x27, 0x0C, + 0xFB, 0x36, 0x4E, 0xB5, 0x3F, 0xA1, 0x9C, 0x27, 0xF7, 0xB0, 0xCC, 0xBE, 0x46, 0x5A, 0x07, 0x01, + 0xF8, 0x31, 0x6E, 0x8B, 0x36, 0x60, 0x5A, 0x35, 0x1B, 0xAE, 0x70, 0x18, 0x31, 0xDF, 0x62, 0x61, + 0x2C, 0x06, 0x8F, 0x03, 0x46, 0x80, 0x3F, 0xC3, 0xAD, 0xE6, 0xE8, 0x5D, 0x01, 0xCC, 0xBB, 0x02, + 0x75, 0x73, 0x26, 0x16, 0xA4, 0xC4, 0xF3, 0x3F, 0x28, 0xC0, 0x69, 0x41, 0xD3, 0x9B, 0x2F, 0xC1, + 0x6D, 0xC0, 0xA4, 0x43, 0xB7, 0x83, 0x91, 0x02, 0x61, 0x68, 0x77, 0x2D, 0x46, 0xA3, 0xC2, 0x60, + 0x21, 0x9D, 0x12, 0x00, 0x02, 0x5F, 0x49, 0x73, 0x96, 0x6E, 0x32, 0xC9, 0x61, 0xB2, 0x8F, 0xEB, + 0x76, 0x5F, 0xBD, 0xD4, 0x49, 0x31, 0xDD, 0xA6, 0xC8, 0x99, 0x03, 0xC8, 0x0C, 0xE9, 0xF9, 0x99, + 0x3A, 0xDE, 0xD7, 0xB7, 0x95, 0x99, 0x76, 0x84, 0xE3, 0x9C, 0x5D, 0x31, 0x9C, 0x63, 0x3F, 0x81, + 0xCF, 0x91, 0x85, 0xC6, 0x49, 0x6C, 0x52, 0x11, 0x35, 0x7E, 0x5F, 0x86, 0x00, 0x5C, 0xA6, 0xA7, + 0xCA, 0x3C, 0x5C, 0x1B, 0xED, 0x10, 0xA4, 0xB8, 0xDC, 0x30, 0x89, 0x3C, 0x19, 0x98, 0x4C, 0xB5, + 0x17, 0x85, 0x58, 0x9D, 0x1F, 0x94, 0xC8, 0x07, 0xED, 0x9B, 0xC0, 0x8A, 0xC3, 0x60, 0xC7, 0xA9, + 0xA0, 0xF8, 0x67, 0xD4, 0x3B, 0x17, 0xBD, 0x67, 0x86, 0x71, 0x1D, 0xB6, 0xA9, 0x46, 0xE3, 0xEE, + 0xAA, 0x38, 0x0D, 0xEC, 0xD8, 0xBB, 0x5C, 0xF2, 0xC8, 0x35, 0x4D, 0x19, 0x26, 0x39, 0xE0, 0x58, + 0xA9, 0x6B, 0x14, 0x84, 0x8D, 0x17, 0xFF, 0xA0, 0x47, 0x6D, 0xDD, 0xFA, 0x97, 0x74, 0xA8, 0x4F, + 0x9C, 0xF1, 0x03, 0x11, 0xCE, 0x69, 0x8F, 0xD6, 0x90, 0x3B, 0x75, 0x61, 0xD0, 0xA1, 0x74, 0x55, + 0x47, 0xC9, 0x32, 0xA7, 0x68, 0xB7, 0x9C, 0x22, 0xA5, 0x31, 0x04, 0x52, 0xA2, 0x74, 0x26, 0xCF, + 0x1E, 0x0C, 0xEE, 0x93, 0xFB, 0x38, 0xFA, 0x08, 0x5B, 0x95, 0xC2, 0x25, 0x3A, 0x7C, 0x8F, 0x14, + 0xD4, 0x7B, 0x24, 0xF5, 0xCF, 0x91, 0x2A, 0xBD, 0x91, 0x9B, 0x19, 0x79, 0xBC, 0x91, 0x44, 0x2D, + 0x33, 0x9D, 0x3F, 0xDD, 0x78, 0x7D, 0x77, 0x6F, 0xF1, 0xD8, 0xAF, 0x1F, 0xC3, 0xC8, 0xA7, 0xBD, + 0xB5, 0xF1, 0x2C, 0xCD, 0x66, 0x3B, 0xE1, 0xB0, 0xC8, 0x9A, 0x87, 0x6A, 0x00, 0xFC, 0x5B, 0x19, + 0xB5, 0x43, 0x99, 0x0B, 0xCF, 0xB8, 0x15, 0x3E, 0x1A, 0x8A, 0x9C, 0x4A, 0xBF, 0x75, 0x9C, 0x04, + 0x71, 0x53, 0xBC, 0x57, 0xBE, 0x99, 0x82, 0xAA, 0xFF, 0x3B, 0x9B, 0x53, 0x62, 0xC3, 0x81, 0x1C, + 0x4B, 0x6A, 0xBF, 0x21, 0x19, 0xF6, 0x5E, 0x9C, 0x77, 0x70, 0x41, 0xCE, 0x02, 0x84, 0x4C, 0x5D, + 0xBA, 0x57, 0x32, 0xE3, 0x3C, 0x48, 0x98, 0xD6, 0x42, 0x1A, 0x5F, 0x8C, 0x67, 0xD8, 0x5A, 0x29, + 0xDA, 0x4F, 0x24, 0xC6, 0xA8, 0xFE, 0xC6, 0x68, 0x86, 0xAE, 0xD5, 0xE0, 0x25, 0x9C, 0x74, 0xCC, + 0xB9, 0x1E, 0x20, 0xE3, 0x70, 0xD7, 0x82, 0x4B, 0x6D, 0x50, 0xB0, 0x91, 0x9D, 0x2B, 0x0A, 0xD5, + 0xEB, 0x42, 0x60, 0x96, 0xAC, 0x5F, 0x1F, 0xD4, 0x2C, 0xF8, 0x20, 0x2B, 0xAA, 0x48, 0xBE, 0x73, + 0x6C, 0x38, 0x3A, 0xAA, 0x60, 0xAF, 0x6D, 0x13, 0xAB, 0xF7, 0x7F, 0x36, 0x18, 0x26, 0xEF, 0x1F, + 0x87, 0xC1, 0x18, 0x6F, 0xEA, 0xF2, 0x1C, 0x84, 0xF3, 0xE0, 0xA2, 0xA9, 0x2A, 0x36, 0xCC, 0xDE, + 0x38, 0x24, 0xCB, 0x23, 0xB3, 0xCC, 0x6F, 0xAA, 0xF3, 0x44, 0x47, 0x01, 0x0B, 0x00, 0xC0, 0x4F, + 0x65, 0xD8, 0xF2, 0xB1, 0x79, 0x12, 0x10, 0x09, 0x5D, 0xFD, 0x63, 0x3A, 0xD7, 0xBE, 0xF3, 0xB2, + 0x9C, 0x0B, 0xCA, 0x0C, 0xE7, 0x4E, 0xE0, 0xBC, 0xFE, 0xB1, 0x0D, 0xB5, 0x87, 0x46, 0xD8, 0x3B, + 0x0B, 0xF1, 0x35, 0xD5, 0x0E, 0x92, 0xB2, 0xD3, 0x04, 0x72, 0x28, 0xA4, 0xC2, 0xB0, 0xA8, 0xD6, + 0xB9, 0xC4, 0x85, 0x97, 0x96, 0xA8, 0xED, 0xA2, 0xE6, 0x5D, 0x22, 0x5C, 0x30, 0x67, 0xA8, 0x8C +}; diff --git a/src/core/hle/service/http/http_c.cpp b/src/core/hle/service/http/http_c.cpp index 8fb9e4300..624dc2b23 100644 --- a/src/core/hle/service/http/http_c.cpp +++ b/src/core/hle/service/http/http_c.cpp @@ -28,6 +28,9 @@ SERIALIZE_EXPORT_IMPL(Service::HTTP::SessionData) namespace Service::HTTP { +#include "ctr-common-1-cert.h" +#include "ctr-common-1-key.h" + namespace ErrCodes { enum { InvalidRequestState = 22, @@ -576,10 +579,15 @@ void HTTP_C::BeginRequest(Kernel::HLERequestContext& ctx) { // For now make every request async in it's own thread. // This always returns success, but the request is only performed when it hasn't started + if (http_context.state == RequestState::NotStarted) { - http_context.request_future = - std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); - http_context.current_copied_data = 0; + if (http_context.method == RequestMethod::Post && !http_context.post_data_added) { + http_context.post_pending_request = true; + } else { + http_context.current_copied_data = 0; + http_context.request_future = + std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); + } } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); @@ -615,9 +623,13 @@ void HTTP_C::BeginRequestAsync(Kernel::HLERequestContext& ctx) { // This always returns success, but the request is only performed when it hasn't started if (http_context.state == RequestState::NotStarted) { - http_context.request_future = - std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); - http_context.current_copied_data = 0; + if (http_context.method == RequestMethod::Post && !http_context.post_data_added) { + http_context.post_pending_request = true; + } else { + http_context.current_copied_data = 0; + http_context.request_future = + std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); + } } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); @@ -946,6 +958,7 @@ void HTTP_C::AddPostDataAscii(Kernel::HLERequestContext& ctx) { Context::Param param_value(name, value); http_context.post_data.emplace(name, param_value); + http_context.post_data_added = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -1004,6 +1017,7 @@ void HTTP_C::AddPostDataBinary(Kernel::HLERequestContext& ctx) { Context::Param param_value(name, value); http_context.post_data.emplace(name, param_value); http_context.force_multipart = true; + http_context.post_data_added = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -1053,6 +1067,7 @@ void HTTP_C::AddPostDataRaw(Kernel::HLERequestContext& ctx) { http_context.post_data_raw.resize(buffer.GetSize()); buffer.Read(http_context.post_data_raw.data(), 0, buffer.GetSize()); + http_context.post_data_added = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -1144,8 +1159,8 @@ void HTTP_C::SendPostDataAsciiImpl(Kernel::HLERequestContext& ctx, bool timeout) Context& http_context = GetContext(context_handle); - if (http_context.state == RequestState::NotStarted) { - LOG_ERROR(Service_HTTP, "Tried to send Post data on a context that has not been started"); + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, "Tried to send Post data on a context that has been started"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ErrorInvalidRequestState); rb.PushMappedBuffer(value_buffer); @@ -1163,6 +1178,7 @@ void HTTP_C::SendPostDataAsciiImpl(Kernel::HLERequestContext& ctx, bool timeout) Context::Param param_value(name, value); http_context.post_data.emplace(name, param_value); + http_context.post_data_added = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -1227,6 +1243,7 @@ void HTTP_C::SendPostDataBinaryImpl(Kernel::HLERequestContext& ctx, bool timeout Context::Param param_value(name, value); http_context.post_data.emplace(name, param_value); + http_context.post_data_added = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -1295,6 +1312,7 @@ void HTTP_C::SendPostDataRawImpl(Kernel::HLERequestContext& ctx, bool timeout) { Context::Param raw_param(value); std::string value_string(value.begin(), value.end()); http_context.post_data.emplace(value_string, raw_param); + http_context.post_data_added = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -1360,20 +1378,129 @@ void HTTP_C::NotifyFinishSendPostData(Kernel::HLERequestContext& ctx) { Context& http_context = GetContext(context_handle); - if (http_context.state == RequestState::NotStarted) { - LOG_ERROR(Service_HTTP, - "Tried to notfy finish Post on a context that has not been started"); + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, "Tried to notfy finish Post on a context that has been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidRequestState); + return; + } + + if (!http_context.post_pending_request) { + LOG_ERROR(Service_HTTP, "Tried to notfy finish Post on a context that has not begun"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidRequestState); + return; + } + + if (!http_context.post_data_added) { + LOG_ERROR(Service_HTTP, "Tried to notfy finish Post on a context that has no post data"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ErrorInvalidRequestState); return; } http_context.finish_post_data.Set(); + http_context.post_pending_request = false; + + http_context.current_copied_data = 0; + http_context.request_future = + std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); } +void HTTP_C::GetResponseData(Kernel::HLERequestContext& ctx) { + GetResponseDataImpl(ctx, false); +} + +void HTTP_C::GetResponseDataTimeout(Kernel::HLERequestContext& ctx) { + GetResponseDataImpl(ctx, true); +} + +void HTTP_C::GetResponseDataImpl(Kernel::HLERequestContext& ctx, bool timeout) { + IPC::RequestParser rp(ctx); + + struct AsyncData { + // Input + u32 context_handle; + bool timeout; + u64 timeout_nanos; + u32 data_max_len; + Kernel::MappedBuffer* data_buffer; + // Output + Result async_res = ResultSuccess; + }; + std::shared_ptr async_data = std::make_shared(); + + async_data->timeout = timeout; + async_data->context_handle = rp.Pop(); + async_data->data_max_len = rp.Pop(); + if (timeout) { + async_data->timeout_nanos = rp.Pop(); + } + async_data->data_buffer = &rp.PopMappedBuffer(); + + if (!PerformStateChecks(ctx, rp, async_data->context_handle)) { + return; + } + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + Context& http_context = GetContext(async_data->context_handle); + + if (async_data->timeout) { + const auto wait_res = http_context.request_future.wait_for( + std::chrono::nanoseconds(async_data->timeout_nanos)); + if (wait_res == std::future_status::timeout) { + async_data->async_res = ErrorTimeout; + } + } else { + http_context.request_future.wait(); + } + + return 0; + }, + [this, async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 0); + if (async_data->async_res != ResultSuccess) { + rb.Push(async_data->async_res); + return; + } + + Context& http_context = GetContext(async_data->context_handle); + auto& headers = http_context.response.headers; + std::vector out; + + if (async_data->timeout) { + LOG_DEBUG(Service_HTTP, "timeout={}", async_data->timeout_nanos); + } else { + LOG_DEBUG(Service_HTTP, ""); + } + + // httplib does not keep the raw HTTP header data, so we need to reconstruct it. + // Sadly, the order of headers is lost, but for now it's good enough. + std::string hdr = + fmt::format("{} {} {}\r\n", http_context.response.version, + http_context.response.status, http_context.response.reason); + out.insert(out.end(), hdr.begin(), hdr.end()); + + for (auto& h : headers) { + hdr = fmt::format("{}: {}\r\n", h.first, h.second); + out.insert(out.end(), hdr.begin(), hdr.end()); + } + + hdr = "\r\n"; + out.insert(out.end(), hdr.begin(), hdr.end()); + + size_t write_size = std::min(out.size(), async_data->data_buffer->GetSize()); + async_data->data_buffer->Write(out.data(), 0, write_size); + + rb.Push(ResultSuccess); + rb.Push(static_cast(write_size)); + }); +} + void HTTP_C::GetResponseHeader(Kernel::HLERequestContext& ctx) { GetResponseHeaderImpl(ctx, false); } @@ -1746,9 +1873,6 @@ void HTTP_C::OpenDefaultClientCertContext(Kernel::HLERequestContext& ctx) { if (!ClCertA.init) { LOG_ERROR(Service_HTTP, "called but ClCertA is missing"); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(static_cast(-1)); - return; } const auto& it = std::find_if(client_certs.begin(), client_certs.end(), @@ -1961,7 +2085,15 @@ bool HTTP_C::PerformStateChecks(Kernel::HLERequestContext& ctx, IPC::RequestPars } void HTTP_C::DecryptClCertA() { + if (!HW::AES::IsNormalKeyAvailable(HW::AES::KeySlotID::SSLKey)) { + LOG_ERROR(Service_HTTP, "NormalKey in KeySlot 0x0D missing"); + return; + } + + HW::AES::AESKey key = HW::AES::GetNormalKey(HW::AES::KeySlotID::SSLKey); static constexpr u32 iv_length = 16; + std::vector cert_file_data; + std::vector key_file_data; FileSys::NCCHArchive archive(0x0004001b00010002, Service::FS::MediaType::NAND); @@ -1972,63 +2104,68 @@ void HTTP_C::DecryptClCertA() { open_mode.read_flag.Assign(1); auto file_result = archive.OpenFile(file_path, open_mode, 0); if (file_result.Failed()) { - LOG_ERROR(Service_HTTP, "ClCertA file missing"); - return; + LOG_ERROR(Service_HTTP, "ClCertA file missing, using default"); + + cert_file_data.resize(ctr_common_1_cert_bin_size); + memcpy(cert_file_data.data(), ctr_common_1_cert_bin, cert_file_data.size()); + + key_file_data.resize(ctr_common_1_key_bin_size); + memcpy(key_file_data.data(), ctr_common_1_key_bin, key_file_data.size()); + } else { + auto romfs = std::move(file_result).Unwrap(); + std::vector romfs_buffer(romfs->GetSize()); + romfs->Read(0, romfs_buffer.size(), romfs_buffer.data()); + romfs->Close(); + + const RomFS::RomFSFile cert_file = + RomFS::GetFile(romfs_buffer.data(), {u"ctr-common-1-cert.bin"}); + if (cert_file.Length() == 0) { + LOG_ERROR(Service_HTTP, "ctr-common-1-cert.bin missing"); + return; + } + if (cert_file.Length() <= iv_length) { + LOG_ERROR(Service_HTTP, "ctr-common-1-cert.bin size is too small. Size: {}", + cert_file.Length()); + return; + } + + cert_file_data.resize(cert_file.Length()); + memcpy(cert_file_data.data(), cert_file.Data(), cert_file.Length()); + + const RomFS::RomFSFile key_file = + RomFS::GetFile(romfs_buffer.data(), {u"ctr-common-1-key.bin"}); + if (key_file.Length() == 0) { + LOG_ERROR(Service_HTTP, "ctr-common-1-key.bin missing"); + return; + } + if (key_file.Length() <= iv_length) { + LOG_ERROR(Service_HTTP, "ctr-common-1-key.bin size is too small. Size: {}", + key_file.Length()); + return; + } + + key_file_data.resize(key_file.Length()); + memcpy(key_file_data.data(), key_file.Data(), key_file.Length()); } - auto romfs = std::move(file_result).Unwrap(); - std::vector romfs_buffer(romfs->GetSize()); - romfs->Read(0, romfs_buffer.size(), romfs_buffer.data()); - romfs->Close(); - - if (!HW::AES::IsNormalKeyAvailable(HW::AES::KeySlotID::SSLKey)) { - LOG_ERROR(Service_HTTP, "NormalKey in KeySlot 0x0D missing"); - return; - } - HW::AES::AESKey key = HW::AES::GetNormalKey(HW::AES::KeySlotID::SSLKey); - - const RomFS::RomFSFile cert_file = - RomFS::GetFile(romfs_buffer.data(), {u"ctr-common-1-cert.bin"}); - if (cert_file.Length() == 0) { - LOG_ERROR(Service_HTTP, "ctr-common-1-cert.bin missing"); - return; - } - if (cert_file.Length() <= iv_length) { - LOG_ERROR(Service_HTTP, "ctr-common-1-cert.bin size is too small. Size: {}", - cert_file.Length()); - return; - } - - std::vector cert_data(cert_file.Length() - iv_length); + std::vector cert_data(cert_file_data.size() - iv_length); using CryptoPP::AES; CryptoPP::CBC_Mode::Decryption aes_cert; std::array cert_iv; - std::memcpy(cert_iv.data(), cert_file.Data(), iv_length); + std::memcpy(cert_iv.data(), cert_file_data.data(), iv_length); aes_cert.SetKeyWithIV(key.data(), AES::BLOCKSIZE, cert_iv.data()); - aes_cert.ProcessData(cert_data.data(), cert_file.Data() + iv_length, - cert_file.Length() - iv_length); + aes_cert.ProcessData(cert_data.data(), cert_file_data.data() + iv_length, + cert_file_data.size() - iv_length); - const RomFS::RomFSFile key_file = - RomFS::GetFile(romfs_buffer.data(), {u"ctr-common-1-key.bin"}); - if (key_file.Length() == 0) { - LOG_ERROR(Service_HTTP, "ctr-common-1-key.bin missing"); - return; - } - if (key_file.Length() <= iv_length) { - LOG_ERROR(Service_HTTP, "ctr-common-1-key.bin size is too small. Size: {}", - key_file.Length()); - return; - } - - std::vector key_data(key_file.Length() - iv_length); + std::vector key_data(key_file_data.size() - iv_length); CryptoPP::CBC_Mode::Decryption aes_key; std::array key_iv; - std::memcpy(key_iv.data(), key_file.Data(), iv_length); + std::memcpy(key_iv.data(), key_file_data.data(), iv_length); aes_key.SetKeyWithIV(key.data(), AES::BLOCKSIZE, key_iv.data()); - aes_key.ProcessData(key_data.data(), key_file.Data() + iv_length, - key_file.Length() - iv_length); + aes_key.ProcessData(key_data.data(), key_file_data.data() + iv_length, + key_file_data.size() - iv_length); ClCertA.certificate = std::move(cert_data); ClCertA.private_key = std::move(key_data); @@ -2069,8 +2206,8 @@ HTTP_C::HTTP_C() : ServiceFramework("http:C", 32) { {0x001D, &HTTP_C::NotifyFinishSendPostData, "NotifyFinishSendPostData"}, {0x001E, &HTTP_C::GetResponseHeader, "GetResponseHeader"}, {0x001F, &HTTP_C::GetResponseHeaderTimeout, "GetResponseHeaderTimeout"}, - {0x0020, nullptr, "GetResponseData"}, - {0x0021, nullptr, "GetResponseDataTimeout"}, + {0x0020, &HTTP_C::GetResponseData, "GetResponseData"}, + {0x0021, &HTTP_C::GetResponseDataTimeout, "GetResponseDataTimeout"}, {0x0022, &HTTP_C::GetResponseStatusCode, "GetResponseStatusCode"}, {0x0023, &HTTP_C::GetResponseStatusCodeTimeout, "GetResponseStatusCodeTimeout"}, {0x0024, &HTTP_C::AddTrustedRootCA, "AddTrustedRootCA"}, diff --git a/src/core/hle/service/http/http_c.h b/src/core/hle/service/http/http_c.h index 20076da7b..90ecd5b66 100644 --- a/src/core/hle/service/http/http_c.h +++ b/src/core/hle/service/http/http_c.h @@ -274,6 +274,8 @@ public: u32 socket_buffer_size; std::vector headers; const ClCertAData* clcert_data; + bool post_data_added = false; + bool post_pending_request = false; Params post_data; std::string post_data_raw; PostDataEncoding post_data_encoding = PostDataEncoding::Auto; @@ -699,6 +701,12 @@ private: */ void GetResponseHeaderTimeout(Kernel::HLERequestContext& ctx); + void GetResponseData(Kernel::HLERequestContext& ctx); + + void GetResponseDataTimeout(Kernel::HLERequestContext& ctx); + + void GetResponseDataImpl(Kernel::HLERequestContext& ctx, bool timeout); + /** * GetResponseHeaderImpl: * Implements GetResponseHeader and GetResponseHeaderTimeout service functions diff --git a/src/core/hw/aes/key.cpp b/src/core/hw/aes/key.cpp index 42fa562eb..776eab670 100644 --- a/src/core/hw/aes/key.cpp +++ b/src/core/hw/aes/key.cpp @@ -7,14 +7,20 @@ #include #include #include +#include +#include #include "common/common_paths.h" #include "common/file_util.h" #include "common/logging/log.h" #include "common/string_util.h" +#include "core/file_sys/certificate.h" +#include "core/file_sys/otp.h" #include "core/hle/service/fs/archive.h" #include "core/hw/aes/arithmetic128.h" #include "core/hw/aes/key.h" +#include "core/hw/default_keys.h" #include "core/hw/rsa/rsa.h" +#include "core/loader/loader.h" namespace HW::AES { @@ -26,8 +32,7 @@ namespace { // On a real 3DS the generation for the normal key is hardware based, and thus the constant can't // get dumped. Generated normal keys are also not accessible on a 3DS. The used formula for // calculating the constant is a software implementation of what the hardware generator does. -constexpr AESKey generator_constant = {{0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45, - 0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A}}; +AESKey generator_constant; AESKey HexToKey(const std::string& hex) { if (hex.size() < 32) { @@ -125,6 +130,12 @@ std::array, NumDlpNfcKeyYs> dlp_nfc_key_y_slots; std::array nfc_secrets; AESIV nfc_iv; +AESKey otp_key; +AESIV otp_iv; + +KeySlot movable_key; +KeySlot movable_cmac; + struct KeyDesc { char key_type; std::size_t slot_id; @@ -132,96 +143,29 @@ struct KeyDesc { bool same_as_before; }; -void LoadBootromKeys() { - constexpr std::array keys = { - {{'X', 0x2C, false}, {'X', 0x2D, true}, {'X', 0x2E, true}, {'X', 0x2F, true}, - {'X', 0x30, false}, {'X', 0x31, true}, {'X', 0x32, true}, {'X', 0x33, true}, - {'X', 0x34, false}, {'X', 0x35, true}, {'X', 0x36, true}, {'X', 0x37, true}, - {'X', 0x38, false}, {'X', 0x39, true}, {'X', 0x3A, true}, {'X', 0x3B, true}, - {'X', 0x3C, false}, {'X', 0x3D, false}, {'X', 0x3E, false}, {'X', 0x3F, false}, - {'Y', 0x4, false}, {'Y', 0x5, false}, {'Y', 0x6, false}, {'Y', 0x7, false}, - {'Y', 0x8, false}, {'Y', 0x9, false}, {'Y', 0xA, false}, {'Y', 0xB, false}, - {'N', 0xC, false}, {'N', 0xD, true}, {'N', 0xE, true}, {'N', 0xF, true}, - {'N', 0x10, false}, {'N', 0x11, true}, {'N', 0x12, true}, {'N', 0x13, true}, - {'N', 0x14, false}, {'N', 0x15, false}, {'N', 0x16, false}, {'N', 0x17, false}, - {'N', 0x18, false}, {'N', 0x19, true}, {'N', 0x1A, true}, {'N', 0x1B, true}, - {'N', 0x1C, false}, {'N', 0x1D, true}, {'N', 0x1E, true}, {'N', 0x1F, true}, - {'N', 0x20, false}, {'N', 0x21, true}, {'N', 0x22, true}, {'N', 0x23, true}, - {'N', 0x24, false}, {'N', 0x25, true}, {'N', 0x26, true}, {'N', 0x27, true}, - {'N', 0x28, true}, {'N', 0x29, false}, {'N', 0x2A, false}, {'N', 0x2B, false}, - {'N', 0x2C, false}, {'N', 0x2D, true}, {'N', 0x2E, true}, {'N', 0x2F, true}, - {'N', 0x30, false}, {'N', 0x31, true}, {'N', 0x32, true}, {'N', 0x33, true}, - {'N', 0x34, false}, {'N', 0x35, true}, {'N', 0x36, true}, {'N', 0x37, true}, - {'N', 0x38, false}, {'N', 0x39, true}, {'N', 0x3A, true}, {'N', 0x3B, true}, - {'N', 0x3C, true}, {'N', 0x3D, false}, {'N', 0x3E, false}, {'N', 0x3F, false}}}; - - // Bootrom sets all these keys when executed, but later some of the normal keys get overwritten - // by other applications e.g. process9. These normal keys thus aren't used by any application - // and have no value for emulation - - const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9; - auto file = FileUtil::IOFile(filepath, "rb"); - if (!file) { - return; - } - - const std::size_t length = file.GetSize(); - if (length != 65536) { - LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length); - return; - } - - constexpr std::size_t KEY_SECTION_START = 55760; - file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section - - AESKey new_key; - for (const auto& key : keys) { - if (!key.same_as_before) { - file.ReadArray(new_key.data(), new_key.size()); - if (!file) { - LOG_ERROR(HW_AES, "Reading from Bootrom9 failed"); - return; - } - } - - LOG_DEBUG(HW_AES, "Loaded Slot{:#02x} Key{} from Bootrom9.", key.slot_id, key.key_type); - - switch (key.key_type) { - case 'X': - key_slots.at(key.slot_id).SetKeyX(new_key); - break; - case 'Y': - key_slots.at(key.slot_id).SetKeyY(new_key); - break; - case 'N': - key_slots.at(key.slot_id).SetNormalKey(new_key); - break; - default: - LOG_ERROR(HW_AES, "Invalid key type {}", key.key_type); - break; - } - } -} - void LoadPresetKeys() { - const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS; - FileUtil::CreateFullPath(filepath); // Create path if not already created + auto s = GetKeysStream(); - boost::iostreams::stream file; - FileUtil::OpenFStream(file, filepath); - if (!file.is_open()) { - return; - } + std::string mode = ""; - while (!file.eof()) { + while (!s.eof()) { std::string line; - std::getline(file, line); + std::getline(s, line); // Ignore empty or commented lines. if (line.empty() || line.starts_with("#")) { continue; } + if (line.starts_with(":")) { + mode = line.substr(1); + continue; + } + + if (mode != "AES") { + continue; + } + const auto parts = Common::SplitString(line, '='); if (parts.size() != 2) { LOG_ERROR(HW_AES, "Failed to parse {}", line); @@ -265,6 +209,31 @@ void LoadPresetKeys() { continue; } + if (name == "generatorConstant") { + generator_constant = key; + continue; + } + + if (name == "otpKey") { + otp_key = key; + continue; + } + + if (name == "otpIV") { + otp_iv = key; + continue; + } + + if (name == "movableKeyY") { + movable_key.SetKeyY(key); + continue; + } + + if (name == "movableCmacY") { + movable_cmac.SetKeyY(key); + continue; + } + if (name == "dlpKeyY") { dlp_nfc_key_y_slots[DlpNfcKeyY::Dlp] = key; continue; @@ -310,15 +279,38 @@ void LoadPresetKeys() { } // namespace +std::istringstream GetKeysStream() { + const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + KEYS_FILE; + FileUtil::CreateFullPath(filepath); // Create path if not already created + + boost::iostreams::stream file; + FileUtil::OpenFStream(file, filepath); + std::istringstream ret; + if (file.is_open()) { + return std::istringstream(std::string(std::istreambuf_iterator(file), {})); + } else { + // The key data is encrypted in the source to prevent easy access to it for unintended + // purposes. + std::vector kiv(16); + std::string s(default_keys_enc_size, ' '); + CryptoPP::CBC_Mode::Decryption(kiv.data(), kiv.size(), kiv.data()) + .ProcessData(reinterpret_cast(s.data()), default_keys_enc, s.size()); + return std::istringstream(s); + } +} + void InitKeys(bool force) { static bool initialized = false; if (initialized && !force) { return; } initialized = true; - HW::RSA::InitSlots(); - LoadBootromKeys(); LoadPresetKeys(); + movable_key.SetKeyX(key_slots[0x35].x); + movable_cmac.SetKeyX(key_slots[0x35].x); + + HW::RSA::InitSlots(); + HW::ECC::InitSlots(); } void SetKeyX(std::size_t slot_id, const AESKey& key) { @@ -371,4 +363,12 @@ const AESIV& GetNfcIv() { return nfc_iv; } +std::pair GetOTPKeyIV() { + return {otp_key, otp_iv}; +} + +const AESKey& GetMovableKey(bool cmac_key) { + return cmac_key ? movable_cmac.normal.value() : movable_key.normal.value(); +} + } // namespace HW::AES diff --git a/src/core/hw/aes/key.h b/src/core/hw/aes/key.h index 584ab284e..c9d0d4f09 100644 --- a/src/core/hw/aes/key.h +++ b/src/core/hw/aes/key.h @@ -6,9 +6,15 @@ #include #include +#include #include #include "common/common_types.h" +namespace FileSys { +class Certificate; +class OTP; +} // namespace FileSys + namespace HW::AES { enum KeySlotID : std::size_t { @@ -74,6 +80,8 @@ constexpr std::size_t AES_BLOCK_SIZE = 16; using AESKey = std::array; using AESIV = std::array; +std::istringstream GetKeysStream(); + void InitKeys(bool force = false); void SetKeyX(std::size_t slot_id, const AESKey& key); @@ -90,5 +98,8 @@ void SelectDlpNfcKeyYIndex(u8 index); bool NfcSecretsAvailable(); const NfcSecret& GetNfcSecret(NfcSecretId secret_id); const AESIV& GetNfcIv(); +std::pair GetOTPKeyIV(); + +const AESKey& GetMovableKey(bool cmac_key); } // namespace HW::AES diff --git a/src/core/hw/default_keys.h b/src/core/hw/default_keys.h new file mode 100644 index 000000000..019d5cfd0 --- /dev/null +++ b/src/core/hw/default_keys.h @@ -0,0 +1,466 @@ +/* Generated by bin2c, do not edit manually */ + +/* Contents of file keys.enc */ +const long int default_keys_enc_size = 7360; +const unsigned char default_keys_enc[7360] = { + 0x4E, 0x81, 0xE9, 0x54, 0xCC, 0xDE, 0xFD, 0x56, 0x7D, 0xD2, 0x72, 0xE6, 0xD9, 0xCD, 0x8E, 0x11, + 0xE1, 0x7F, 0x74, 0xF4, 0xFC, 0x54, 0xA6, 0xA4, 0x27, 0xC2, 0xD7, 0x50, 0xEA, 0xE7, 0xBE, 0xC9, + 0xA7, 0x5E, 0xE0, 0x2E, 0x4A, 0xBE, 0xF5, 0xD5, 0x0D, 0x22, 0x76, 0x2E, 0xB6, 0x80, 0xD8, 0x54, + 0x88, 0x60, 0x77, 0x80, 0xDB, 0xC3, 0x33, 0x8F, 0xF6, 0x83, 0x79, 0xF9, 0x0D, 0x0E, 0xEC, 0xD2, + 0x8D, 0x1C, 0xAB, 0x2C, 0x6B, 0xE7, 0xFE, 0xF1, 0x25, 0x63, 0x37, 0x3A, 0x9D, 0xF1, 0xD9, 0x7D, + 0xB4, 0x9F, 0xA9, 0xA0, 0x25, 0x45, 0x92, 0xBA, 0xCE, 0xEA, 0x68, 0x8F, 0x5E, 0x05, 0x27, 0x0A, + 0x3F, 0xFC, 0x77, 0x67, 0x7C, 0x3B, 0x4C, 0x42, 0x40, 0xFC, 0xE8, 0xFA, 0xC8, 0x87, 0xDA, 0xA2, + 0xE4, 0x67, 0xAC, 0x61, 0x09, 0xB8, 0x4B, 0xF6, 0x87, 0x73, 0xE8, 0x0E, 0xBE, 0x05, 0x68, 0xE8, + 0x1A, 0x31, 0x30, 0x19, 0xB6, 0xE9, 0x41, 0x9F, 0xC0, 0x9D, 0x69, 0xEE, 0x23, 0x36, 0xF3, 0x43, + 0x2C, 0x22, 0x33, 0x1D, 0x94, 0x25, 0xC1, 0xD0, 0xA1, 0x13, 0x55, 0x7D, 0xDE, 0x2B, 0xB3, 0x88, + 0xF6, 0x99, 0x7F, 0x33, 0x37, 0x49, 0xF1, 0xB7, 0x2F, 0x6D, 0x3A, 0x47, 0x27, 0xB0, 0x19, 0xA3, + 0x5C, 0xC0, 0xE2, 0xF0, 0x50, 0xC0, 0xC4, 0xC1, 0x08, 0xDA, 0x29, 0xED, 0x42, 0x22, 0x79, 0xD7, + 0xBD, 0xDA, 0x12, 0x44, 0x78, 0xF3, 0xC5, 0x6E, 0x9E, 0xFC, 0x40, 0x18, 0xA0, 0x0B, 0x96, 0x94, + 0x5B, 0x4F, 0x5E, 0x3A, 0x97, 0xA7, 0x13, 0x5E, 0x47, 0x48, 0xD2, 0x62, 0xFB, 0x6B, 0x56, 0x56, + 0x86, 0xF6, 0x6C, 0x6D, 0x8A, 0xB8, 0x25, 0xDA, 0xB6, 0x0E, 0x0C, 0x29, 0x92, 0x84, 0xCF, 0x0C, + 0x4A, 0x19, 0x4B, 0xCA, 0x0D, 0x86, 0x82, 0x5A, 0x7D, 0x57, 0x49, 0x95, 0x82, 0x2F, 0x48, 0x1D, + 0xC7, 0x51, 0xAD, 0xCB, 0x86, 0x4D, 0x4F, 0x13, 0x6D, 0x7E, 0xE5, 0xF6, 0x2D, 0xD1, 0x36, 0xF9, + 0x6F, 0xEC, 0xCB, 0xC0, 0x2F, 0x5E, 0x85, 0x7C, 0xC9, 0x53, 0x13, 0xAC, 0xB8, 0xBE, 0x51, 0xC7, + 0xDC, 0xB2, 0x0A, 0x37, 0x8B, 0x41, 0x45, 0x36, 0x5E, 0x4C, 0x4A, 0x8D, 0x47, 0x84, 0xF0, 0xF5, + 0x0F, 0x1C, 0x4B, 0x86, 0x6C, 0x32, 0x16, 0x1C, 0x0D, 0x72, 0x70, 0x85, 0x50, 0xC9, 0x69, 0x69, + 0xD0, 0x8B, 0x34, 0x82, 0xA9, 0xE6, 0x84, 0x9C, 0xEE, 0xFE, 0x4E, 0xD6, 0x57, 0x53, 0x72, 0x58, + 0x72, 0x0F, 0xD9, 0xA0, 0xFD, 0xE3, 0x33, 0xE1, 0x8D, 0xC8, 0xD1, 0x04, 0x3E, 0x48, 0x6B, 0x6B, + 0x89, 0x9C, 0x70, 0xCB, 0x1D, 0x8F, 0x79, 0x18, 0x5A, 0xE8, 0x96, 0xEC, 0x19, 0xB6, 0x22, 0x81, + 0xC0, 0x5D, 0x0B, 0xF6, 0x4D, 0x47, 0x2D, 0x04, 0x93, 0x11, 0x07, 0x12, 0x90, 0x46, 0x16, 0x00, + 0x61, 0x8B, 0xC3, 0xAA, 0xBD, 0x49, 0x92, 0xE2, 0x71, 0x9A, 0x69, 0x0B, 0xA9, 0x81, 0x32, 0x48, + 0x77, 0x24, 0xB3, 0x07, 0xEF, 0xAD, 0xCF, 0x7A, 0xB6, 0x9F, 0xF5, 0x9F, 0xBD, 0xA1, 0xF0, 0xEA, + 0x1B, 0xD3, 0x6F, 0xBA, 0xDB, 0xC9, 0x67, 0x1A, 0x3B, 0x7C, 0x43, 0xA5, 0xA8, 0x3A, 0x67, 0x91, + 0x3E, 0x12, 0x6A, 0x67, 0x4A, 0x40, 0x5C, 0x62, 0x67, 0xB2, 0xB4, 0xB2, 0xCB, 0xA8, 0xB9, 0x0C, + 0xF4, 0x3D, 0xA3, 0x94, 0xA9, 0x4E, 0xEB, 0x1C, 0x34, 0x96, 0xA3, 0x62, 0x30, 0xCD, 0xB7, 0x5E, + 0xD5, 0x79, 0xE5, 0x96, 0xF8, 0x1F, 0x89, 0x65, 0x36, 0xC3, 0x1C, 0x08, 0xC0, 0x2C, 0x42, 0x0F, + 0x2A, 0xF1, 0xBB, 0x35, 0x34, 0x52, 0xFA, 0x29, 0xF9, 0x73, 0x00, 0x8E, 0x99, 0x6F, 0xB3, 0xAE, + 0x18, 0xA1, 0x6B, 0xA3, 0x87, 0xF7, 0x38, 0x69, 0x51, 0xFA, 0x23, 0x73, 0x15, 0x23, 0x15, 0xD1, + 0xED, 0x04, 0x44, 0x00, 0xB1, 0x50, 0x2A, 0xCF, 0xED, 0x7B, 0x47, 0xA4, 0xFA, 0x7F, 0x1E, 0x1B, + 0xE0, 0xCE, 0x3F, 0x3D, 0xE6, 0x8D, 0x8E, 0xEA, 0x1D, 0xDC, 0xB2, 0xF7, 0x15, 0xB3, 0xF7, 0xC7, + 0x75, 0x4E, 0xC8, 0x10, 0xBE, 0xCB, 0x1E, 0x02, 0x35, 0xC4, 0xD4, 0x04, 0x2B, 0x8A, 0x16, 0x02, + 0x5B, 0x2F, 0x69, 0x75, 0x6E, 0x1B, 0x4B, 0x93, 0x87, 0xFB, 0x7D, 0x9E, 0xC0, 0xFB, 0x5C, 0xAA, + 0xC8, 0x8B, 0xEC, 0x8F, 0x85, 0xC1, 0x99, 0xC7, 0xD2, 0xAE, 0x07, 0x51, 0x8A, 0xFF, 0x53, 0x92, + 0xEB, 0x0C, 0x01, 0x74, 0x35, 0xEF, 0xCB, 0x78, 0x14, 0x3D, 0x72, 0xBC, 0xF7, 0x6D, 0xC1, 0x3D, + 0x43, 0x00, 0x11, 0x89, 0x1B, 0x72, 0xCC, 0x5F, 0x5B, 0x0E, 0x86, 0x2E, 0x76, 0x99, 0x6A, 0x3D, + 0x91, 0x95, 0x0A, 0x35, 0x51, 0x2A, 0x88, 0x94, 0xEC, 0xEA, 0x78, 0x20, 0x36, 0x8F, 0x62, 0x45, + 0xE4, 0x32, 0x66, 0x7C, 0x9F, 0x25, 0x50, 0xE0, 0x33, 0xBA, 0xEC, 0xC5, 0x97, 0x18, 0xC5, 0x10, + 0x5C, 0xCD, 0x4E, 0x39, 0xE7, 0x71, 0xB3, 0xC1, 0x7D, 0xBA, 0xBA, 0x9A, 0xCB, 0x58, 0xD8, 0xFA, + 0xC8, 0xAB, 0x3A, 0x0E, 0x37, 0x7C, 0xE5, 0xD1, 0x18, 0x0D, 0x13, 0x61, 0x2A, 0x32, 0x14, 0xD7, + 0x5E, 0x5E, 0x77, 0x84, 0x8A, 0xBC, 0xCA, 0xFC, 0x6C, 0x23, 0xF9, 0x30, 0x83, 0x98, 0x15, 0x6C, + 0x66, 0x42, 0x10, 0x27, 0x5D, 0xBB, 0x18, 0x21, 0x42, 0x79, 0xB0, 0x6E, 0x86, 0x0A, 0xE6, 0xB1, + 0x0D, 0xDE, 0x64, 0x90, 0xB2, 0x82, 0xB3, 0x71, 0x91, 0xC6, 0x42, 0xC3, 0x5D, 0x0F, 0xC6, 0x45, + 0x18, 0xD6, 0xD7, 0xE1, 0x6B, 0xC3, 0xB8, 0x8D, 0xD2, 0x59, 0xF9, 0xFF, 0xE8, 0x1C, 0x65, 0xF9, + 0x7B, 0xAF, 0x84, 0x8C, 0x5D, 0xF3, 0x84, 0x9E, 0x49, 0x66, 0x2F, 0xCE, 0x1B, 0x77, 0xA2, 0xC3, + 0x73, 0xB4, 0xA1, 0x0B, 0x61, 0xFF, 0x7D, 0x4B, 0xB2, 0x45, 0x0E, 0xE8, 0x91, 0x61, 0x26, 0x56, + 0xCA, 0x23, 0x71, 0x89, 0x70, 0xF1, 0xAE, 0x3D, 0x34, 0x9F, 0xE9, 0x83, 0x4A, 0xA1, 0xA5, 0xF2, + 0x42, 0x8E, 0xA1, 0xA9, 0x4F, 0x50, 0xE5, 0x8E, 0x2D, 0x03, 0xEB, 0x5C, 0x92, 0xD2, 0xA7, 0xA0, + 0xAB, 0x77, 0x97, 0x0B, 0x97, 0xDE, 0x28, 0x64, 0x6C, 0x0C, 0xA4, 0xB0, 0x3A, 0xF6, 0x81, 0xF9, + 0x59, 0x5E, 0xAA, 0xC2, 0xA0, 0x0D, 0xA3, 0x91, 0x05, 0xDB, 0x38, 0xC3, 0xA7, 0x0C, 0x31, 0x14, + 0xE9, 0xF9, 0x4F, 0xD9, 0x55, 0x6B, 0x31, 0x93, 0x23, 0x8F, 0xBC, 0x67, 0x8E, 0xD6, 0xD5, 0x65, + 0xA0, 0xD9, 0x8A, 0xF9, 0x23, 0xF5, 0x27, 0xD2, 0x67, 0x97, 0x75, 0x97, 0x10, 0x9F, 0x81, 0xAF, + 0xF5, 0x8C, 0xF0, 0xD2, 0x8F, 0xDE, 0x14, 0x59, 0xC0, 0x01, 0xD2, 0xCB, 0x08, 0x37, 0x52, 0xD1, + 0x56, 0x1A, 0xF3, 0xE5, 0x1C, 0xFF, 0x9B, 0xAF, 0x5D, 0x51, 0xA6, 0x0E, 0x88, 0x10, 0xE2, 0x9B, + 0xE0, 0x5E, 0x0D, 0xE3, 0x01, 0x50, 0x17, 0xBB, 0xA1, 0x49, 0x30, 0xA5, 0x7D, 0x73, 0xFC, 0xF8, + 0x0E, 0xA3, 0x8E, 0x1E, 0x6E, 0x67, 0x48, 0x8F, 0xFA, 0xF4, 0x24, 0x33, 0xB7, 0x19, 0x10, 0xBB, + 0x06, 0x74, 0x39, 0x4E, 0xAA, 0x61, 0xA6, 0x24, 0x74, 0xA4, 0x90, 0xD8, 0x41, 0x42, 0xEC, 0xA0, + 0x83, 0x1E, 0x19, 0x60, 0x55, 0x01, 0x86, 0x59, 0x87, 0x69, 0x8A, 0xBE, 0x25, 0xDB, 0xA4, 0xD0, + 0xC2, 0x37, 0x96, 0xE4, 0x67, 0xF6, 0x23, 0x76, 0xD5, 0x55, 0x42, 0x33, 0x0F, 0x93, 0x4C, 0x26, + 0x5F, 0x55, 0x3F, 0xF0, 0xC5, 0x6D, 0x03, 0x39, 0xF1, 0xD3, 0x66, 0x3D, 0x53, 0xEF, 0xA8, 0x3D, + 0x06, 0x03, 0xE6, 0x47, 0x5E, 0xBA, 0x08, 0x2E, 0xB6, 0x99, 0x25, 0xBD, 0x40, 0x7A, 0x53, 0xC6, + 0x13, 0x49, 0xF4, 0xD3, 0xFD, 0xE5, 0x06, 0x0D, 0xA6, 0x75, 0xBB, 0x9F, 0x8D, 0x23, 0x3E, 0x6A, + 0x59, 0x61, 0xFA, 0xB3, 0x10, 0xCF, 0x5A, 0x53, 0xA0, 0xF6, 0x5D, 0x93, 0x1E, 0x4A, 0x5C, 0x2B, + 0x55, 0xAE, 0xD0, 0x6F, 0x15, 0xC1, 0x87, 0x4D, 0x65, 0x52, 0xCC, 0xA1, 0x30, 0x92, 0x5A, 0x58, + 0x6E, 0x3A, 0x81, 0x3D, 0xD4, 0x99, 0x93, 0x9A, 0x22, 0x78, 0x03, 0xD2, 0x08, 0x45, 0x66, 0xD5, + 0x55, 0x8F, 0x3B, 0x42, 0x14, 0xB6, 0x15, 0x04, 0x99, 0x5B, 0x71, 0x37, 0x29, 0x6A, 0x3D, 0x5C, + 0x78, 0x63, 0x05, 0x0F, 0xC9, 0xA1, 0x90, 0xA1, 0xDF, 0x19, 0x62, 0x53, 0x58, 0x26, 0x70, 0x2E, + 0xF6, 0x61, 0x48, 0xE1, 0x8F, 0x30, 0x98, 0x37, 0x9F, 0x82, 0xA0, 0xE2, 0x87, 0x81, 0x09, 0xD8, + 0xC0, 0x4D, 0x70, 0x24, 0x9C, 0x69, 0x90, 0x1A, 0x3B, 0x62, 0x47, 0x7B, 0xD2, 0x5D, 0x83, 0xC6, + 0xFA, 0xFE, 0xE6, 0x99, 0x1B, 0x3D, 0x18, 0x71, 0xA3, 0x15, 0xE8, 0x3A, 0xAD, 0x98, 0xAA, 0xD6, + 0x6C, 0xAD, 0x41, 0x4E, 0x29, 0x50, 0x33, 0xD2, 0xF6, 0xA7, 0x0D, 0x1B, 0x3D, 0xDE, 0x77, 0x44, + 0x77, 0xA4, 0x1B, 0x3F, 0x70, 0x92, 0xDE, 0xB2, 0x97, 0x96, 0x3D, 0x74, 0x7A, 0x24, 0x92, 0xD3, + 0x83, 0xD3, 0x37, 0x13, 0xB1, 0xB2, 0x25, 0xD4, 0x69, 0x6A, 0x8D, 0x44, 0x61, 0xC0, 0x9F, 0x9E, + 0x23, 0x78, 0x8A, 0xE2, 0xAB, 0xB6, 0xBE, 0xC3, 0xCB, 0x08, 0x5C, 0x2A, 0xF9, 0x77, 0x99, 0xA3, + 0x85, 0x3A, 0x71, 0x28, 0xA3, 0xFD, 0x9E, 0x07, 0xC2, 0x93, 0x19, 0x0B, 0xEE, 0x84, 0x6B, 0xE5, + 0xA0, 0x1E, 0xD1, 0x86, 0x5C, 0x91, 0x13, 0x21, 0x62, 0x18, 0x1E, 0xE8, 0x33, 0x48, 0x4C, 0x96, + 0xD8, 0x1F, 0xB1, 0x0D, 0xF0, 0xA6, 0x8D, 0x9A, 0x22, 0x69, 0xE5, 0x2C, 0xBE, 0xFC, 0x7F, 0x6D, + 0x45, 0xC7, 0xB3, 0x61, 0xF6, 0x8F, 0x13, 0xDD, 0xF0, 0x73, 0xD6, 0x08, 0xA1, 0x3E, 0x86, 0xC4, + 0xD1, 0xC1, 0x2E, 0xF6, 0xD2, 0x3A, 0x0E, 0x64, 0xE9, 0x9A, 0x27, 0xA6, 0x48, 0xCF, 0x9D, 0xFC, + 0x85, 0xC2, 0xF4, 0x14, 0x3C, 0x77, 0x6D, 0x1B, 0xE9, 0xD0, 0x8B, 0xA6, 0x80, 0x69, 0x28, 0x98, + 0x3A, 0xC1, 0x0D, 0x90, 0xBD, 0xC8, 0x52, 0x1F, 0x3E, 0x61, 0xDA, 0xF2, 0x4A, 0xF3, 0x64, 0x66, + 0x72, 0x03, 0xEF, 0x34, 0xAD, 0x29, 0xBF, 0xDD, 0xA7, 0x87, 0x75, 0x92, 0x55, 0x13, 0x6F, 0x94, + 0x81, 0xC0, 0x01, 0xA5, 0x34, 0x48, 0x9A, 0xDA, 0x76, 0xC7, 0xE5, 0x87, 0xB7, 0xF4, 0x03, 0x33, + 0xB3, 0x99, 0x17, 0x54, 0x00, 0x82, 0x6A, 0x02, 0xEF, 0xD8, 0x9C, 0x75, 0xF2, 0x68, 0x8C, 0x4D, + 0xEB, 0x19, 0xB5, 0x80, 0x72, 0x52, 0xBA, 0x0D, 0x50, 0x00, 0x35, 0x4D, 0x50, 0xA9, 0x87, 0xA9, + 0xD0, 0x23, 0x3D, 0x92, 0xD6, 0x7F, 0xDB, 0xD9, 0xED, 0x04, 0x1F, 0x0D, 0x15, 0xD9, 0x86, 0x0B, + 0x67, 0xF8, 0xF4, 0x1B, 0x60, 0x44, 0x66, 0x3E, 0x6F, 0x5E, 0xF6, 0x8F, 0x7B, 0x98, 0x03, 0xC0, + 0x3A, 0xB8, 0x49, 0xC2, 0x94, 0xB0, 0x4A, 0x03, 0xD6, 0xDA, 0xCD, 0x01, 0xEA, 0x7C, 0x9D, 0x0A, + 0xED, 0x7E, 0x6A, 0x6C, 0xE9, 0x62, 0x9F, 0xD6, 0x0A, 0x6F, 0xEE, 0x42, 0x6C, 0xC5, 0x3C, 0x70, + 0xAF, 0xDF, 0x69, 0x54, 0x57, 0x67, 0x7E, 0xF1, 0x49, 0x2A, 0xDC, 0x27, 0x37, 0x3C, 0xA5, 0x37, + 0x99, 0x9A, 0xFF, 0x4F, 0x1E, 0x5E, 0xF6, 0xD4, 0x52, 0xE8, 0x3F, 0x90, 0xC6, 0x06, 0xD5, 0x8E, + 0x26, 0xFD, 0x6E, 0xD8, 0xE3, 0x9D, 0x8C, 0x89, 0xE4, 0xE0, 0x7D, 0xEA, 0x3F, 0x02, 0xB4, 0xA1, + 0x6E, 0x1E, 0x7C, 0x49, 0xA5, 0x43, 0x67, 0x24, 0x6B, 0xED, 0x96, 0x83, 0xAB, 0x5A, 0x15, 0xA9, + 0x35, 0x72, 0x50, 0x68, 0x0B, 0x21, 0xF5, 0xF9, 0xD0, 0xC3, 0x73, 0x7F, 0xE9, 0xD4, 0xEF, 0x51, + 0x5B, 0x45, 0xC4, 0x27, 0x2B, 0xAF, 0x47, 0x6F, 0x07, 0x66, 0x72, 0x74, 0xB0, 0xC6, 0x9B, 0x9E, + 0x6A, 0x2D, 0xA2, 0x09, 0x93, 0x83, 0xF6, 0x36, 0xDB, 0x46, 0xC2, 0xEC, 0x56, 0x5B, 0x1F, 0x13, + 0x23, 0x16, 0x6C, 0xAC, 0xD8, 0x31, 0xB2, 0x1A, 0x87, 0x25, 0x55, 0xFE, 0xC4, 0x08, 0x9D, 0x48, + 0x41, 0x72, 0x0B, 0x6A, 0x1C, 0x3E, 0xEF, 0x91, 0x2E, 0xA6, 0x26, 0xD9, 0xF1, 0xF9, 0x0D, 0xD8, + 0xFF, 0xE2, 0x2D, 0x6E, 0x96, 0xEF, 0x72, 0x88, 0xE0, 0x64, 0xFF, 0xE6, 0xE0, 0xA2, 0x93, 0xF8, + 0x66, 0xA6, 0x24, 0xCD, 0x6F, 0x93, 0x1C, 0xA0, 0x72, 0xEA, 0x02, 0x31, 0x89, 0x20, 0x9D, 0x62, + 0x24, 0x3E, 0x01, 0x69, 0x58, 0x06, 0xF5, 0xAA, 0x9D, 0xC3, 0x2F, 0x36, 0xD2, 0xDB, 0x73, 0x7D, + 0x8C, 0x19, 0x95, 0xF2, 0x95, 0x6F, 0x93, 0xA7, 0x35, 0x3B, 0xEB, 0x65, 0xFC, 0x95, 0x4F, 0x74, + 0x6F, 0xE6, 0xC7, 0x8B, 0xEA, 0x28, 0x9B, 0xF1, 0x74, 0x2A, 0xC6, 0x54, 0xFC, 0x84, 0x80, 0xA3, + 0xB2, 0x1B, 0x33, 0x2F, 0xE6, 0x95, 0x42, 0x95, 0xEC, 0xC5, 0x44, 0x19, 0x4C, 0xB8, 0x38, 0x96, + 0xE8, 0x13, 0xBB, 0x4E, 0x75, 0xDB, 0xEB, 0xC5, 0xFB, 0x10, 0x3B, 0x51, 0xEB, 0x4A, 0x23, 0xCA, + 0x43, 0x13, 0x15, 0xD4, 0x19, 0x87, 0xB1, 0xB8, 0x19, 0xFF, 0x5B, 0x06, 0xA7, 0x0F, 0xD3, 0xFF, + 0x5C, 0x8C, 0x89, 0xC3, 0x9C, 0xE2, 0x0C, 0x96, 0x5D, 0xD9, 0x8C, 0x2D, 0xCC, 0x70, 0x87, 0x0C, + 0x1C, 0x11, 0x36, 0x77, 0xE5, 0xF2, 0x60, 0x40, 0xA0, 0xDF, 0x20, 0xD5, 0xB2, 0x71, 0x99, 0xA3, + 0x58, 0x46, 0xB4, 0x5A, 0xAB, 0xE9, 0x33, 0x7C, 0xA4, 0xD5, 0x17, 0x6C, 0x15, 0xA7, 0x82, 0x23, + 0x82, 0x6C, 0x4D, 0x76, 0xFA, 0x94, 0xD0, 0xDE, 0xA3, 0xEB, 0x2F, 0x60, 0xAA, 0x8F, 0x5C, 0x80, + 0xD3, 0x9A, 0x97, 0xBB, 0x23, 0x22, 0xC7, 0xA1, 0x78, 0x2A, 0x1D, 0x3C, 0xF8, 0xF9, 0x98, 0xF4, + 0x11, 0x8B, 0x07, 0xEF, 0x73, 0x58, 0xC6, 0x01, 0xA0, 0x7E, 0x63, 0x9C, 0x9D, 0x24, 0x4A, 0x0F, + 0x4D, 0x8C, 0xD4, 0xA8, 0x34, 0x57, 0x5A, 0x48, 0x19, 0x0B, 0x7C, 0x96, 0x38, 0x7C, 0x24, 0x48, + 0x16, 0xEB, 0x4D, 0xED, 0xF5, 0x62, 0x3D, 0xD6, 0xF8, 0xB2, 0xAD, 0xF3, 0x6A, 0xE1, 0xA0, 0x8D, + 0x08, 0x91, 0xE3, 0x2A, 0x09, 0x04, 0x7C, 0x94, 0xF1, 0x59, 0x13, 0x66, 0x3D, 0xA8, 0x91, 0xFD, + 0xBD, 0x85, 0x72, 0xF6, 0x91, 0xAA, 0x59, 0x0C, 0xC6, 0xE0, 0x84, 0x3A, 0xCB, 0x2B, 0x6C, 0x57, + 0x53, 0x4F, 0xFB, 0x1B, 0x0D, 0x5B, 0xD5, 0xA3, 0x91, 0xC9, 0x6F, 0x33, 0xC9, 0xC6, 0x33, 0xF9, + 0x8A, 0xF6, 0x52, 0x5C, 0x38, 0x17, 0x95, 0x1F, 0x5C, 0x4C, 0xB4, 0x35, 0x9D, 0xE2, 0xAA, 0x14, + 0x77, 0xD3, 0x2E, 0x6C, 0xB3, 0x0A, 0xA3, 0x6E, 0xEA, 0x6E, 0x5D, 0x37, 0xB5, 0x38, 0x9D, 0x68, + 0x7E, 0x2C, 0x6B, 0x6B, 0xB7, 0x97, 0x19, 0x1B, 0x66, 0xA3, 0x13, 0x34, 0xC2, 0x0F, 0x9B, 0xC6, + 0x58, 0x2F, 0xCC, 0x69, 0x2A, 0xAC, 0x02, 0x7F, 0xE4, 0xF7, 0x62, 0x26, 0x18, 0x9F, 0xE0, 0x0F, + 0x5F, 0xB3, 0xD5, 0x30, 0xEE, 0x5C, 0xCA, 0xD3, 0xAB, 0x84, 0x95, 0xF9, 0xE6, 0x42, 0xA7, 0x00, + 0x82, 0x3F, 0x6A, 0x98, 0x07, 0xD0, 0x78, 0x19, 0x26, 0xDC, 0x9B, 0xC6, 0x9C, 0xC4, 0x1E, 0xDB, + 0x6B, 0x16, 0xEF, 0x3E, 0xE1, 0xBE, 0xB0, 0xF7, 0x03, 0xDD, 0xD5, 0xC0, 0x80, 0x0E, 0xCA, 0xAC, + 0xF0, 0x2D, 0xBA, 0x4E, 0xE8, 0x14, 0xCF, 0x57, 0xD4, 0xFE, 0x42, 0x0C, 0xB6, 0x33, 0xC0, 0x6F, + 0xFD, 0xA1, 0xC4, 0xDE, 0x50, 0x05, 0xE1, 0xC0, 0x4D, 0xE1, 0x72, 0x03, 0xFF, 0xBB, 0x34, 0x10, + 0xDB, 0x4E, 0xD0, 0x01, 0x81, 0x13, 0x63, 0x0B, 0xF3, 0xD6, 0xA0, 0xF2, 0x74, 0xBD, 0xA7, 0x15, + 0xB5, 0xAB, 0x38, 0xBE, 0x8A, 0x75, 0x3C, 0xDA, 0xAA, 0x57, 0xF4, 0xF9, 0x7A, 0x29, 0xC8, 0x00, + 0x63, 0xC9, 0xB2, 0xBA, 0x98, 0x13, 0x6F, 0x23, 0x00, 0xDF, 0x98, 0xCE, 0x51, 0x4E, 0xB0, 0xC3, + 0x81, 0x0A, 0x41, 0xCD, 0x2C, 0x27, 0x31, 0xF9, 0xEC, 0x3D, 0x1C, 0x86, 0x20, 0x31, 0x84, 0x17, + 0x59, 0xF1, 0x86, 0x82, 0x5B, 0x68, 0xAC, 0xED, 0x04, 0x81, 0x6D, 0xE8, 0x73, 0x04, 0x29, 0xFE, + 0x94, 0x50, 0x18, 0x82, 0xA4, 0x93, 0x98, 0x00, 0x19, 0xC1, 0xDC, 0xD6, 0x55, 0xB2, 0x5A, 0x63, + 0xF9, 0x3D, 0xA5, 0x2A, 0xC0, 0x6C, 0x93, 0xBD, 0xE8, 0x4C, 0x8C, 0x74, 0xCD, 0xC2, 0x1C, 0x70, + 0xBB, 0xAC, 0xAE, 0xFB, 0x71, 0xDC, 0x43, 0x89, 0x72, 0x77, 0x90, 0x6F, 0x99, 0xC3, 0x42, 0x00, + 0x33, 0xAD, 0x63, 0xE9, 0xFE, 0x04, 0xC3, 0xF7, 0xA1, 0x7D, 0xDD, 0x1E, 0x4B, 0x8C, 0x06, 0xC8, + 0x2C, 0x4A, 0x9A, 0x4C, 0x1E, 0x15, 0xC3, 0xE3, 0x44, 0x05, 0xA7, 0x5D, 0xBE, 0x64, 0x46, 0x0B, + 0x64, 0x40, 0x84, 0xF4, 0xA7, 0x3C, 0xA9, 0x37, 0x19, 0x9A, 0x6B, 0xF5, 0xB2, 0xCC, 0x69, 0xCB, + 0xA0, 0x41, 0xD8, 0x7D, 0x36, 0x64, 0xA6, 0x5E, 0x57, 0xAD, 0xC3, 0x2C, 0xAA, 0x8C, 0x6C, 0x92, + 0x9F, 0x66, 0xEC, 0xB9, 0xE9, 0xC0, 0x41, 0x46, 0x10, 0xAB, 0x20, 0xF3, 0x56, 0x5E, 0xA0, 0xCB, + 0xCA, 0x05, 0x64, 0x40, 0xF2, 0xFC, 0xFA, 0x4F, 0x83, 0xEC, 0xC7, 0xBA, 0xEA, 0x9F, 0x2D, 0x4F, + 0xA3, 0x17, 0xD4, 0x60, 0x13, 0x93, 0x0B, 0xB2, 0xEA, 0xB0, 0x16, 0x6A, 0x95, 0xA7, 0x2D, 0x4C, + 0x94, 0x17, 0xE0, 0x62, 0xA7, 0xFA, 0x0B, 0x8A, 0x9A, 0x55, 0xB6, 0x36, 0x83, 0xB1, 0x2E, 0x11, + 0x6E, 0x98, 0xE4, 0x8E, 0x14, 0x8B, 0xAD, 0x0B, 0x48, 0x37, 0x1B, 0x11, 0x44, 0xE4, 0xA3, 0x5C, + 0x93, 0x17, 0x66, 0xB3, 0x16, 0x9C, 0x4E, 0xB0, 0x66, 0x47, 0x5E, 0x8B, 0xEC, 0x42, 0x18, 0xD7, + 0x7A, 0x51, 0xF8, 0xBA, 0x3C, 0xB6, 0xC7, 0x5E, 0x24, 0x71, 0x1E, 0x65, 0x05, 0x2C, 0x92, 0x0C, + 0xEA, 0x9E, 0x5A, 0x0F, 0x47, 0xF0, 0x05, 0x92, 0xD9, 0xDC, 0x85, 0x3C, 0x04, 0x29, 0x29, 0xC8, + 0x34, 0xD3, 0xAB, 0x64, 0x94, 0xAB, 0xBC, 0x3E, 0xD9, 0xED, 0xB0, 0x4E, 0xEC, 0x76, 0x62, 0x0A, + 0xEC, 0xD1, 0x83, 0x4B, 0x5F, 0xDD, 0x60, 0x69, 0x03, 0xC5, 0x2F, 0x30, 0x0A, 0xEE, 0x7D, 0x96, + 0xBC, 0x86, 0xB0, 0x66, 0x1B, 0x72, 0x61, 0x12, 0x88, 0xE2, 0x2A, 0x84, 0x6B, 0xFF, 0x96, 0xB8, + 0x1A, 0x6B, 0x73, 0x2E, 0xB0, 0xDF, 0xA1, 0xC3, 0x02, 0x6E, 0xCE, 0x55, 0xB6, 0xD1, 0xB6, 0x14, + 0x85, 0xD6, 0x6F, 0x9E, 0xEC, 0x6E, 0xEE, 0x20, 0xF2, 0xF9, 0x68, 0x39, 0x40, 0x97, 0x1D, 0xEC, + 0x4A, 0x1D, 0x7C, 0x5B, 0xAE, 0x87, 0x80, 0xAF, 0x0A, 0xCC, 0x3A, 0x02, 0x5B, 0xC7, 0x64, 0x42, + 0x61, 0x8B, 0xBD, 0xEC, 0x15, 0xFE, 0xF5, 0xC7, 0x17, 0x99, 0x6F, 0x0A, 0x54, 0xCC, 0x2B, 0x91, + 0xA6, 0x5A, 0x44, 0x48, 0xE3, 0x8B, 0x52, 0x69, 0xC2, 0x8E, 0xD6, 0x70, 0xA9, 0x6A, 0x8D, 0x2C, + 0x45, 0xA3, 0xAF, 0xB3, 0x55, 0x57, 0x4B, 0xB6, 0x5B, 0xF4, 0x45, 0xB9, 0x8B, 0xB2, 0xD4, 0x5C, + 0xB2, 0xEA, 0xD3, 0x89, 0x2C, 0xDE, 0x3F, 0x5B, 0xB7, 0x7A, 0x31, 0xA0, 0xEB, 0xA5, 0x11, 0x0A, + 0xE5, 0xB0, 0x98, 0x98, 0xAD, 0xD0, 0x17, 0xA6, 0xF7, 0xCF, 0xB8, 0x88, 0x34, 0xBC, 0xC7, 0x4A, + 0xE8, 0x3E, 0x44, 0x04, 0x94, 0xB8, 0x65, 0x74, 0xCE, 0x69, 0x81, 0x3E, 0x7C, 0x65, 0x7B, 0xEA, + 0x50, 0xE5, 0xF3, 0xB7, 0xB4, 0xC0, 0xE7, 0xFC, 0x64, 0xEA, 0x06, 0xA0, 0xBC, 0x76, 0x8F, 0xD8, + 0xBD, 0x86, 0xF7, 0x5C, 0xD4, 0xC5, 0x50, 0xC6, 0xFD, 0xE3, 0xF5, 0x31, 0xDA, 0xC5, 0x13, 0x81, + 0xCE, 0xE4, 0xB5, 0xA0, 0x88, 0xAA, 0x2D, 0x6D, 0xD3, 0x8E, 0x7B, 0x48, 0x85, 0x40, 0xD7, 0x53, + 0x16, 0x36, 0x8D, 0x4E, 0xAD, 0x0B, 0x2B, 0x8B, 0x5E, 0x3C, 0x9A, 0x69, 0xDA, 0x02, 0xCC, 0x67, + 0xD1, 0x03, 0x19, 0x36, 0xEB, 0xE2, 0xEC, 0xAF, 0xB6, 0xF8, 0x26, 0xA4, 0x20, 0x11, 0x28, 0xFA, + 0xA7, 0xE3, 0x16, 0x87, 0x60, 0x33, 0x56, 0x16, 0xBB, 0xE4, 0xEC, 0xE4, 0xD1, 0x7F, 0x02, 0x01, + 0x5D, 0x3E, 0xE6, 0x18, 0x05, 0xED, 0x1B, 0xA8, 0x29, 0x7F, 0xED, 0x13, 0x04, 0xC5, 0x24, 0x6C, + 0xA4, 0x5F, 0x9A, 0xE4, 0xBC, 0x84, 0x03, 0x2A, 0x6B, 0xB6, 0x38, 0x0B, 0x63, 0x93, 0x2B, 0xA5, + 0xB2, 0x9E, 0x3C, 0x7C, 0x5C, 0x14, 0xA4, 0xC4, 0xFE, 0x57, 0xAE, 0xB0, 0x56, 0xE6, 0xA1, 0x3D, + 0x56, 0x94, 0xA7, 0xC3, 0x7D, 0x63, 0x79, 0x1C, 0xA2, 0x62, 0xCF, 0x9E, 0x7C, 0x0F, 0x87, 0x20, + 0x34, 0x82, 0xD3, 0xDD, 0x00, 0x4D, 0xB5, 0xC4, 0xF6, 0x54, 0x86, 0x2F, 0xB3, 0xDC, 0x70, 0x1C, + 0x00, 0x0A, 0x0C, 0x6C, 0x32, 0x40, 0x5B, 0x4A, 0x51, 0x6E, 0x12, 0xC3, 0x38, 0x70, 0x73, 0xA5, + 0xEB, 0x5D, 0x01, 0xE3, 0x5E, 0xF4, 0x4F, 0x6D, 0xBC, 0xFD, 0x2E, 0xAF, 0xA8, 0x9D, 0xFE, 0x6D, + 0x2B, 0x31, 0x1A, 0x4E, 0xC2, 0x6D, 0xC9, 0xBD, 0xAD, 0x41, 0x56, 0x15, 0x74, 0xD9, 0xAE, 0x4E, + 0x36, 0x79, 0xF1, 0x23, 0x8F, 0xAA, 0x85, 0x05, 0xB3, 0x1A, 0x5B, 0x5D, 0x2C, 0xB4, 0xA4, 0x09, + 0xA1, 0xF4, 0x8D, 0x2B, 0x82, 0x97, 0x9E, 0xDB, 0xEF, 0xA5, 0xB9, 0x9B, 0x43, 0x7D, 0x51, 0xE3, + 0x0B, 0x3A, 0x7C, 0xFD, 0xA4, 0x86, 0xBD, 0x1E, 0x57, 0x57, 0x46, 0x1C, 0xF2, 0x45, 0xD3, 0x3F, + 0xEB, 0x8E, 0x32, 0x12, 0xEC, 0x16, 0x0D, 0x14, 0xF6, 0x47, 0xCA, 0xF6, 0x30, 0xB0, 0xFA, 0x36, + 0xE7, 0xC9, 0xE3, 0x1A, 0x23, 0xB1, 0xF3, 0x88, 0xD0, 0xB8, 0x4B, 0x33, 0xF4, 0xB9, 0xE8, 0x6E, + 0xBD, 0x60, 0xED, 0x78, 0xC3, 0xF6, 0x4D, 0x95, 0x7B, 0x9C, 0x25, 0x41, 0x18, 0x1E, 0x24, 0xA2, + 0xFB, 0x9A, 0x78, 0x03, 0x0A, 0xAB, 0x5D, 0x65, 0x80, 0xE0, 0xC7, 0x82, 0x96, 0xED, 0x73, 0xA1, + 0x9E, 0x16, 0x15, 0xEE, 0xE1, 0x8E, 0x96, 0xBD, 0xCA, 0x0C, 0x74, 0x69, 0xF1, 0x80, 0x63, 0x41, + 0x65, 0x70, 0xA0, 0x44, 0x22, 0x80, 0x34, 0x1E, 0xDE, 0x6A, 0x8B, 0x35, 0x80, 0xD2, 0x79, 0x46, + 0x90, 0x02, 0xD1, 0x39, 0x7A, 0x52, 0x4B, 0x78, 0xD5, 0x75, 0x6D, 0x3D, 0xF2, 0x23, 0x9E, 0x3E, + 0xC9, 0xB8, 0x8F, 0x39, 0xFA, 0x8C, 0x9A, 0x8C, 0xBD, 0x56, 0xDE, 0x99, 0x76, 0xBB, 0xF1, 0xBC, + 0xBA, 0xB8, 0x65, 0x23, 0x42, 0x82, 0xB2, 0x88, 0xD1, 0x2D, 0x65, 0xFB, 0x37, 0x51, 0x8F, 0xB7, + 0xDC, 0x83, 0xAB, 0x03, 0x00, 0x23, 0x22, 0x4B, 0x65, 0x3C, 0x1F, 0xBE, 0x57, 0x93, 0x37, 0xD5, + 0x10, 0xA6, 0xBD, 0xFB, 0xD4, 0x5A, 0xFE, 0xA5, 0x94, 0x51, 0xD6, 0xE5, 0x2D, 0x31, 0xDB, 0x42, + 0xC1, 0x07, 0xBE, 0xAF, 0x5F, 0xE6, 0x36, 0x0C, 0x27, 0x34, 0xDE, 0x9E, 0xA6, 0xCD, 0x5B, 0x66, + 0xF2, 0x86, 0x29, 0xAF, 0x87, 0xA9, 0xC6, 0xF2, 0xBD, 0xE7, 0xFB, 0x08, 0x55, 0xDC, 0xFD, 0x35, + 0x00, 0x4E, 0x55, 0xA7, 0x4E, 0xF5, 0xB7, 0x2D, 0x81, 0xE0, 0xF8, 0x5E, 0x62, 0x4B, 0x32, 0x1F, + 0x50, 0x01, 0x4D, 0xAD, 0x6D, 0x1A, 0x5F, 0xE0, 0xBA, 0x2E, 0xD9, 0x03, 0x57, 0xD5, 0xEC, 0xFF, + 0x66, 0x12, 0xA7, 0xFF, 0xA0, 0xCD, 0x2C, 0xA4, 0xBB, 0x84, 0x20, 0x03, 0xD3, 0x96, 0x7D, 0x5D, + 0x71, 0xCB, 0x9F, 0xE8, 0x23, 0x04, 0x66, 0xC1, 0xD1, 0x28, 0xC8, 0xE3, 0x14, 0xC2, 0xFC, 0xAA, + 0x3D, 0x2E, 0x77, 0x34, 0xBD, 0x70, 0xC5, 0xA5, 0xA0, 0x0F, 0x63, 0xF5, 0x2B, 0x48, 0x8E, 0xC3, + 0x9C, 0x5D, 0xA2, 0x29, 0xFC, 0x80, 0x87, 0x5B, 0x37, 0x17, 0x65, 0x33, 0xF4, 0x15, 0x59, 0x28, + 0x5D, 0xDA, 0x69, 0xFD, 0x0E, 0x1E, 0x22, 0xE4, 0xA7, 0x04, 0xA5, 0xDA, 0x05, 0x72, 0x4F, 0xA7, + 0x08, 0x11, 0x58, 0xAE, 0xA9, 0xD4, 0x61, 0xA5, 0x30, 0x32, 0xBF, 0xB7, 0xA7, 0xD0, 0x87, 0x81, + 0xF6, 0x79, 0x16, 0x43, 0x1F, 0xFB, 0x92, 0xE5, 0x53, 0xA5, 0xC8, 0x4D, 0xA6, 0x71, 0x6E, 0x47, + 0x5E, 0x69, 0x59, 0xE8, 0x50, 0x57, 0x9D, 0x2B, 0x0C, 0x4E, 0xCE, 0x33, 0x43, 0xE1, 0xD2, 0xA0, + 0x8D, 0x7B, 0x83, 0x0A, 0xEB, 0xC0, 0x12, 0x12, 0x08, 0x7A, 0xFA, 0x54, 0x46, 0x58, 0x2F, 0x9C, + 0xDB, 0xE9, 0x33, 0x3F, 0xB6, 0x8B, 0xF5, 0x26, 0xBF, 0xD7, 0xA4, 0x6E, 0x29, 0xB3, 0x2E, 0x7D, + 0x43, 0x5E, 0xCE, 0x69, 0x3C, 0xA7, 0xBA, 0xB9, 0xCF, 0xA4, 0x38, 0xF7, 0x9D, 0x19, 0x0D, 0x7B, + 0xD7, 0x48, 0x42, 0x70, 0x18, 0xF3, 0xB2, 0x06, 0x2A, 0xDF, 0xAF, 0xCB, 0xCE, 0xAC, 0x00, 0x32, + 0x90, 0x1B, 0x05, 0x95, 0x79, 0x52, 0x31, 0x6F, 0x84, 0xFA, 0x0E, 0x72, 0xC1, 0xA1, 0x62, 0x8B, + 0xDE, 0x15, 0x8F, 0x23, 0x86, 0x22, 0x53, 0x65, 0xAA, 0xFC, 0xD6, 0xC1, 0xDC, 0xFE, 0x4F, 0xF4, + 0x85, 0x5F, 0xEB, 0xDC, 0x29, 0x06, 0x60, 0x41, 0x6C, 0x1B, 0xAB, 0x63, 0x87, 0x93, 0x15, 0x35, + 0x20, 0xB2, 0x13, 0x36, 0x7C, 0x19, 0x62, 0x11, 0x3D, 0x33, 0x68, 0xCB, 0xDB, 0x82, 0xBA, 0x41, + 0x01, 0x1B, 0xD7, 0xE8, 0xCC, 0xFC, 0xA9, 0x2E, 0x93, 0xC6, 0xC9, 0xBA, 0x6B, 0x11, 0x43, 0x14, + 0x50, 0xD0, 0x26, 0x59, 0x45, 0xDA, 0x42, 0x23, 0x13, 0x46, 0x5D, 0x1E, 0x49, 0xB0, 0xDB, 0x48, + 0xCF, 0xE6, 0xB8, 0x7C, 0xD8, 0x69, 0x4F, 0xB5, 0xC3, 0x0F, 0x57, 0xB2, 0x1D, 0x10, 0x17, 0x40, + 0x02, 0xA7, 0x7F, 0xE2, 0xFB, 0xCD, 0xBA, 0x3C, 0x6D, 0x14, 0x32, 0x27, 0x75, 0x5C, 0xB5, 0xE8, + 0x1D, 0x85, 0xAD, 0xD7, 0xD6, 0x49, 0x06, 0x96, 0x0C, 0xBA, 0xA7, 0x1C, 0x15, 0x0E, 0x59, 0x1A, + 0x3C, 0xA0, 0x69, 0x6E, 0x96, 0xB6, 0x71, 0x4E, 0x54, 0xD8, 0xA9, 0x58, 0xB0, 0x61, 0xBE, 0x0E, + 0xC8, 0x77, 0xC7, 0x44, 0xD3, 0xBA, 0x9B, 0x64, 0xA6, 0xBF, 0x10, 0xC3, 0x09, 0x6D, 0xB2, 0x70, + 0xC0, 0xCC, 0xCB, 0xD0, 0x87, 0x05, 0xAF, 0x54, 0xA5, 0x7D, 0xA0, 0xDA, 0x8B, 0xB2, 0xE4, 0x1E, + 0x1C, 0x0E, 0x35, 0x3D, 0x00, 0xCC, 0xCB, 0x96, 0x1D, 0xD7, 0xFD, 0xD1, 0xCE, 0xE8, 0xE7, 0x6A, + 0xA2, 0xE8, 0xC4, 0x5F, 0xD0, 0xB9, 0x84, 0x3D, 0xE3, 0x5E, 0x67, 0xA8, 0x60, 0x52, 0x0B, 0x80, + 0x6A, 0x34, 0x9B, 0x12, 0xA2, 0x2E, 0x0E, 0x0E, 0x1B, 0x7B, 0x34, 0xD1, 0xEE, 0xA2, 0xF4, 0xFF, + 0x51, 0xB3, 0x51, 0x62, 0x12, 0x96, 0xB6, 0x4B, 0x00, 0xBB, 0xDC, 0x67, 0xAB, 0xE8, 0x74, 0x55, + 0xCD, 0x0E, 0x70, 0xB8, 0xE4, 0x10, 0x81, 0x7E, 0x1D, 0xF8, 0x2D, 0x0C, 0x7A, 0x38, 0x39, 0xA2, + 0xEB, 0xC8, 0x34, 0x73, 0xF2, 0x59, 0x5D, 0x29, 0xB5, 0xE3, 0x60, 0xD3, 0x55, 0xFA, 0xB1, 0xEB, + 0xBA, 0x6E, 0x21, 0x52, 0x24, 0xDE, 0x74, 0x29, 0xC4, 0x95, 0xA3, 0x7E, 0xEA, 0xC8, 0x75, 0x54, + 0x65, 0xF7, 0xA9, 0xF6, 0x24, 0xAB, 0x90, 0xF4, 0xF0, 0x3F, 0x26, 0xDD, 0x3F, 0xBE, 0xE3, 0xD8, + 0x5A, 0x4C, 0xA8, 0x12, 0x10, 0x61, 0x28, 0xE5, 0xD8, 0xF4, 0x1C, 0xB3, 0x49, 0x29, 0xE0, 0x3C, + 0x4B, 0x3C, 0x3D, 0x8B, 0x38, 0x9A, 0xDE, 0xDF, 0x44, 0x55, 0x6C, 0x5B, 0x8B, 0x7C, 0x96, 0x9D, + 0xB3, 0x9A, 0x60, 0x6A, 0x07, 0xFA, 0xE2, 0x01, 0xEF, 0x8F, 0xF7, 0xBA, 0xDB, 0xEE, 0xFE, 0xF0, + 0x8B, 0x38, 0x06, 0xE0, 0x53, 0x20, 0x3E, 0x0E, 0x0A, 0x9A, 0x00, 0x8D, 0x0A, 0x31, 0xFA, 0xD5, + 0xBE, 0x2E, 0x9B, 0x40, 0x2F, 0x2C, 0x2D, 0xF2, 0x9B, 0x87, 0xD8, 0xC1, 0x6C, 0x15, 0x31, 0xC2, + 0x04, 0xA1, 0x1F, 0xBA, 0xD1, 0xBE, 0x89, 0x99, 0xF8, 0xC7, 0x09, 0x82, 0x6F, 0xAD, 0xDF, 0xC5, + 0x92, 0xCA, 0x99, 0x01, 0x0E, 0xC0, 0xD3, 0x94, 0xF7, 0xB9, 0x16, 0x29, 0xDC, 0x3F, 0xB5, 0xCA, + 0x72, 0x79, 0xEC, 0x04, 0x5B, 0x23, 0xC7, 0x5D, 0x49, 0x7C, 0x6E, 0xB4, 0x74, 0xEB, 0x22, 0x0D, + 0xC4, 0x63, 0x23, 0x9E, 0xD0, 0x1C, 0xF1, 0x28, 0xF7, 0x77, 0x1C, 0x3E, 0x11, 0xE7, 0x64, 0x4E, + 0x81, 0x07, 0xEE, 0x06, 0x69, 0x1A, 0x06, 0x94, 0xC9, 0x48, 0xA8, 0x2C, 0x7B, 0xF1, 0xD5, 0x6E, + 0x41, 0xB7, 0xD0, 0xEF, 0x47, 0x74, 0xA7, 0x4F, 0xCB, 0xB1, 0x18, 0x6F, 0x78, 0x43, 0x3A, 0x88, + 0xC7, 0xEC, 0x2D, 0xF9, 0xD0, 0x3A, 0xE3, 0x5C, 0xFC, 0xA2, 0xDF, 0x88, 0xED, 0x79, 0x04, 0x37, + 0x17, 0xCC, 0xE8, 0x78, 0x17, 0x01, 0x47, 0x69, 0xF6, 0x15, 0xF3, 0x35, 0x6B, 0xCA, 0xBD, 0xBA, + 0x9A, 0xF6, 0xFD, 0x8E, 0x03, 0xA9, 0x2F, 0xD4, 0x4B, 0x83, 0xC2, 0x7E, 0xDF, 0xE4, 0xCC, 0xE3, + 0x43, 0x00, 0x77, 0xA4, 0x6A, 0xD0, 0xA7, 0x77, 0x65, 0xC1, 0xC0, 0x5B, 0x89, 0x9E, 0x4E, 0xA8, + 0x9F, 0x15, 0x9C, 0x05, 0x73, 0xC0, 0x7D, 0x3E, 0xC2, 0x19, 0x3E, 0xFF, 0x52, 0x72, 0x7E, 0x51, + 0xA7, 0xB6, 0xF2, 0xD3, 0x01, 0x24, 0xBA, 0x2D, 0x83, 0x8A, 0xED, 0x3A, 0xE5, 0x21, 0xAE, 0xB7, + 0x6E, 0xD8, 0xCE, 0x9B, 0xE4, 0xE4, 0x38, 0x3F, 0xA5, 0xBA, 0xFA, 0x33, 0x88, 0xCC, 0x77, 0x23, + 0x22, 0x37, 0x1C, 0xE2, 0xA0, 0xF2, 0xBE, 0x53, 0x39, 0xEB, 0xF3, 0x52, 0xED, 0x79, 0xBB, 0xE9, + 0xDE, 0xB0, 0xB5, 0x02, 0xEB, 0xC0, 0xF9, 0xA9, 0x83, 0x09, 0x0E, 0x38, 0x25, 0x46, 0x33, 0x21, + 0xC5, 0x46, 0xEA, 0x58, 0x9C, 0x20, 0xD2, 0x64, 0x89, 0xF4, 0x0C, 0x91, 0x48, 0x0F, 0x36, 0x85, + 0xCB, 0x30, 0x1E, 0x7B, 0x3C, 0x16, 0x9A, 0xB5, 0xA5, 0xB1, 0x82, 0x8A, 0x69, 0xC2, 0xCB, 0x14, + 0xFB, 0x5E, 0xDD, 0x5C, 0xC1, 0x24, 0xF5, 0x64, 0x01, 0xF3, 0x87, 0x82, 0x9A, 0xA2, 0x91, 0x3A, + 0xB5, 0xCC, 0xE9, 0x50, 0x01, 0xCF, 0x73, 0xF1, 0xE1, 0x58, 0xFB, 0x84, 0xF7, 0x67, 0xA0, 0x13, + 0xEC, 0xD1, 0x1B, 0x7A, 0x72, 0xA1, 0x5B, 0x04, 0xEA, 0x68, 0x3D, 0xCA, 0x9B, 0x08, 0x0D, 0x0D, + 0x87, 0x1F, 0x08, 0x5B, 0xE2, 0x9B, 0x72, 0x63, 0xE8, 0x73, 0xD2, 0x79, 0x4D, 0xCF, 0x89, 0x78, + 0x32, 0x16, 0xE3, 0x12, 0xCF, 0x3F, 0x5C, 0x82, 0x51, 0x1D, 0x14, 0x73, 0xC3, 0x52, 0x0A, 0xE4, + 0xF7, 0x79, 0x44, 0x3C, 0x6A, 0x72, 0xA6, 0xA5, 0xAD, 0xC0, 0xBF, 0xDA, 0x75, 0xC4, 0x35, 0x20, + 0xA0, 0x64, 0x7E, 0x22, 0xCE, 0xE5, 0x1C, 0x3F, 0x78, 0xB5, 0x29, 0x3A, 0x45, 0x67, 0xED, 0x25, + 0x80, 0xDE, 0x1B, 0x89, 0x38, 0x6F, 0x16, 0x52, 0x2D, 0x1F, 0x9F, 0xDF, 0xD3, 0xA0, 0xC9, 0x18, + 0x03, 0x61, 0x54, 0xA1, 0x8F, 0x05, 0x8D, 0x4F, 0x00, 0xF8, 0xA7, 0xE1, 0x51, 0x9C, 0x99, 0xA1, + 0xE4, 0xA1, 0x31, 0x16, 0xAC, 0xBC, 0x91, 0xB2, 0x73, 0x5B, 0x36, 0xE1, 0x7A, 0xD9, 0x82, 0xBD, + 0xD2, 0x4C, 0x31, 0x45, 0x1A, 0x55, 0xAF, 0x41, 0xE8, 0x34, 0xD9, 0x86, 0xF1, 0x61, 0xA2, 0x11, + 0x19, 0x78, 0xA1, 0x47, 0x66, 0x42, 0xAE, 0xA8, 0x84, 0x56, 0x0F, 0x34, 0x7A, 0xC5, 0x80, 0x7B, + 0xE6, 0xE3, 0xAF, 0x64, 0x86, 0xE8, 0x2D, 0x54, 0xA2, 0x01, 0x8B, 0xD1, 0x2D, 0x71, 0x76, 0x0A, + 0x62, 0xEA, 0x17, 0x38, 0xF6, 0x19, 0x5F, 0x59, 0xAB, 0xB9, 0x84, 0xCF, 0x1F, 0x7F, 0x8E, 0x31, + 0x92, 0xDF, 0x4E, 0x14, 0x72, 0xAF, 0x56, 0x27, 0x4B, 0x54, 0xDE, 0x57, 0x2B, 0xCE, 0x06, 0x47, + 0x5F, 0x01, 0x5C, 0xE0, 0xA2, 0xFF, 0x0F, 0x1F, 0xBC, 0x3B, 0x53, 0x72, 0x18, 0xEA, 0x94, 0x07, + 0x71, 0xAF, 0x4B, 0x43, 0x02, 0x6A, 0x7E, 0x21, 0xAB, 0x4A, 0x84, 0x1B, 0xA7, 0x62, 0xF4, 0x1F, + 0xB1, 0xD6, 0xDD, 0x08, 0xC4, 0x6C, 0xAB, 0xC0, 0x44, 0xAE, 0x5F, 0x98, 0xB8, 0x45, 0xEC, 0x0D, + 0x8A, 0x1B, 0xE2, 0xE2, 0x54, 0x42, 0x51, 0xF2, 0xF9, 0xDB, 0x11, 0xB5, 0x20, 0xF4, 0xEE, 0xF8, + 0x92, 0x2E, 0x9A, 0x1A, 0x68, 0xE9, 0xCF, 0xC8, 0x8D, 0xCE, 0x33, 0x6E, 0xF1, 0xDD, 0xFF, 0xEB, + 0xB8, 0x49, 0xB8, 0x56, 0x59, 0x32, 0x71, 0xB2, 0x28, 0xF9, 0xA4, 0x98, 0xC3, 0xB8, 0xDF, 0x0F, + 0xCD, 0x06, 0x76, 0x6C, 0xCD, 0x46, 0x8E, 0x2F, 0x91, 0xDE, 0x3C, 0x7D, 0x18, 0xD4, 0x86, 0x15, + 0x43, 0xC7, 0xE5, 0xB3, 0xB0, 0xFF, 0x81, 0x32, 0x3C, 0x3B, 0xE0, 0x98, 0xB7, 0x4C, 0xFD, 0xE9, + 0x13, 0x54, 0x7E, 0xAF, 0x04, 0x56, 0x0F, 0x8E, 0x0A, 0xE1, 0x55, 0xFF, 0x19, 0x84, 0x8D, 0xB9, + 0x63, 0x91, 0xD8, 0x10, 0x27, 0x7B, 0xD5, 0x05, 0x28, 0x5F, 0xF9, 0xE5, 0xFC, 0xA2, 0x25, 0xFB, + 0xE3, 0x36, 0x91, 0x55, 0xBB, 0xA6, 0xE0, 0x64, 0x20, 0xF6, 0x27, 0x23, 0x9E, 0x82, 0xFB, 0x7C, + 0x2A, 0xE5, 0x39, 0x2D, 0xA3, 0x4B, 0xD1, 0x82, 0xF5, 0x68, 0x1F, 0x42, 0xE4, 0x0B, 0xB0, 0x2E, + 0x37, 0x3C, 0x2A, 0x12, 0x61, 0xEC, 0x54, 0x1D, 0xA2, 0xA3, 0x89, 0x54, 0x25, 0xAD, 0x17, 0xE0, + 0x8A, 0xFB, 0xA8, 0xF4, 0x6D, 0xAF, 0xF0, 0x84, 0x12, 0xE1, 0x92, 0x72, 0x9B, 0x41, 0x99, 0xA6, + 0x3E, 0x12, 0x74, 0x28, 0x6F, 0x9C, 0xA3, 0x63, 0xC6, 0x88, 0x76, 0xC6, 0x22, 0x76, 0xEC, 0x48, + 0x2B, 0xB5, 0x41, 0x81, 0x45, 0xBB, 0xCB, 0x4D, 0x9D, 0x77, 0x95, 0x49, 0x5F, 0x43, 0x27, 0x40, + 0xD1, 0x4E, 0xEB, 0x2B, 0xD9, 0x0D, 0x7B, 0xD3, 0x36, 0xAE, 0x18, 0x9E, 0x45, 0x36, 0xA1, 0xA0, + 0xB2, 0x30, 0xF1, 0x82, 0xE5, 0x73, 0x5F, 0xC4, 0x75, 0x43, 0xE9, 0xD7, 0x16, 0xE1, 0x98, 0xB6, + 0x60, 0xEB, 0x43, 0x4D, 0x5C, 0xBE, 0x0C, 0xC1, 0x92, 0x8D, 0x9E, 0x25, 0x3A, 0x55, 0xF6, 0x66, + 0x7C, 0x7F, 0xB4, 0x3F, 0x08, 0xA3, 0x1F, 0xC4, 0xCE, 0x05, 0x0F, 0xBF, 0x99, 0x4D, 0x40, 0x5A, + 0x56, 0xC1, 0x50, 0x87, 0x07, 0xDF, 0xED, 0xA2, 0x43, 0x2A, 0xB1, 0x69, 0x36, 0x44, 0xD3, 0x42, + 0x48, 0x53, 0xF6, 0xD8, 0xA9, 0xD7, 0x61, 0xCB, 0x12, 0x8B, 0xCC, 0x5A, 0xE2, 0x47, 0xCD, 0x8C, + 0xCB, 0xBC, 0x2A, 0x56, 0xE0, 0x00, 0x0F, 0x99, 0x61, 0xE4, 0x4C, 0xAE, 0x83, 0x7D, 0xFB, 0xEF, + 0x61, 0x49, 0x40, 0xF9, 0x37, 0x27, 0x12, 0x34, 0xF4, 0x85, 0xAB, 0x27, 0xB6, 0x96, 0xBF, 0xFE, + 0x00, 0xD1, 0x5A, 0xF6, 0x55, 0x90, 0x64, 0x8C, 0x95, 0xCF, 0x15, 0x02, 0x31, 0x45, 0x4D, 0x70, + 0xC4, 0xF9, 0xFC, 0x57, 0x06, 0x93, 0x46, 0x03, 0xE3, 0x94, 0x3C, 0x94, 0x52, 0x54, 0xED, 0x02, + 0x44, 0x9D, 0x61, 0xB9, 0x74, 0x84, 0xA4, 0x06, 0x9F, 0x1D, 0x38, 0x26, 0xE2, 0x8E, 0x09, 0x11, + 0xA9, 0xAF, 0xA7, 0xFE, 0xE8, 0xFF, 0xFB, 0xF2, 0x07, 0xC7, 0xFF, 0x00, 0xC0, 0x9D, 0xDD, 0x91, + 0x38, 0x4B, 0x65, 0x0F, 0xE5, 0xB2, 0xD1, 0xF2, 0x20, 0x91, 0x19, 0x8C, 0x44, 0xB3, 0x71, 0x6A, + 0x68, 0x3D, 0xE4, 0x4F, 0x56, 0x16, 0xFD, 0x25, 0x61, 0x95, 0x5B, 0xA4, 0xE7, 0xED, 0x8B, 0x07, + 0x25, 0x69, 0xDA, 0xFB, 0xED, 0x6C, 0x60, 0x62, 0x5E, 0x9A, 0x3F, 0x8C, 0xC8, 0xE7, 0xF6, 0xC0, + 0x3F, 0xF1, 0x0E, 0x6A, 0xFE, 0x65, 0xC3, 0x04, 0xCC, 0xBD, 0x6E, 0x01, 0x5A, 0xE5, 0xF1, 0x00, + 0xA6, 0xDC, 0xE7, 0x19, 0x23, 0x6D, 0xB3, 0xB2, 0x72, 0x93, 0x8F, 0x83, 0xFC, 0x1F, 0x3E, 0x8F, + 0x1D, 0x20, 0x68, 0x05, 0x2F, 0x53, 0x7F, 0xE1, 0x71, 0x7F, 0xFC, 0x80, 0x71, 0x8D, 0x51, 0xFF, + 0xE1, 0x57, 0x77, 0x13, 0xDA, 0x99, 0x77, 0x27, 0xF7, 0xDB, 0xAD, 0x43, 0xB7, 0xC1, 0x13, 0x65, + 0x0E, 0xE6, 0x54, 0xCB, 0xC4, 0x3E, 0x3B, 0x49, 0x8E, 0x3F, 0xFE, 0xDC, 0x8D, 0x17, 0x10, 0x72, + 0x12, 0xC2, 0x65, 0xB3, 0x16, 0x05, 0xB2, 0xAC, 0xB5, 0x7D, 0xC8, 0x40, 0x72, 0xC7, 0xCF, 0xC5, + 0x7B, 0xA2, 0x54, 0xA3, 0xC6, 0x7C, 0x2C, 0x24, 0x13, 0x33, 0xEF, 0x8C, 0xC3, 0x5A, 0x07, 0xD1, + 0x18, 0xF8, 0x18, 0xEF, 0x06, 0xC6, 0x5A, 0x78, 0xC6, 0x63, 0x11, 0x09, 0xC2, 0xFC, 0x73, 0x75, + 0xF0, 0x92, 0x7A, 0x90, 0xD9, 0xE4, 0xDB, 0xCF, 0x4F, 0x0A, 0x8D, 0x04, 0x5E, 0xF2, 0x8A, 0x7D, + 0xBD, 0x9E, 0xFD, 0x59, 0x62, 0x0D, 0xE5, 0x53, 0x6B, 0xE0, 0xD5, 0x24, 0xB4, 0x53, 0xD4, 0xAD, + 0xBF, 0xC0, 0x26, 0x86, 0x95, 0x82, 0x80, 0x21, 0x86, 0x18, 0xED, 0xE4, 0xDE, 0x94, 0x9B, 0x5F, + 0x6C, 0x41, 0x90, 0x9E, 0x7B, 0x9C, 0x5F, 0x37, 0x11, 0xD1, 0xFF, 0x17, 0xA3, 0x90, 0xFE, 0x87, + 0xE8, 0x0C, 0x2D, 0x44, 0xD3, 0x7C, 0x2D, 0x63, 0xA4, 0xD0, 0x7D, 0xC3, 0x69, 0x6A, 0x44, 0x65, + 0xD3, 0xB3, 0xEB, 0x77, 0xAA, 0x4E, 0x88, 0xCE, 0xDB, 0x3F, 0x71, 0x9C, 0x67, 0xA6, 0x72, 0xA9, + 0xFD, 0x3E, 0x70, 0x94, 0xB0, 0xA8, 0x82, 0x41, 0xB6, 0x19, 0x87, 0xD8, 0x47, 0x3A, 0x02, 0xCA, + 0x25, 0x17, 0x11, 0x2F, 0xEB, 0xDA, 0x6C, 0x2A, 0x76, 0xA9, 0x19, 0x33, 0xB6, 0x80, 0xBA, 0x88, + 0xDD, 0xAE, 0xCF, 0x31, 0x7F, 0x8C, 0x9B, 0x28, 0x5D, 0x5A, 0xE8, 0xE1, 0x09, 0x3F, 0xF0, 0x25, + 0x88, 0xAF, 0xBC, 0xBB, 0x07, 0x1F, 0x16, 0xCA, 0x74, 0xB3, 0xF0, 0xEE, 0x24, 0x51, 0x80, 0xDD, + 0x21, 0xE0, 0x8A, 0xC7, 0xA4, 0x26, 0x42, 0xFA, 0x0B, 0x4A, 0x7F, 0x8D, 0x41, 0xED, 0x05, 0x1D, + 0x0F, 0xE6, 0xF2, 0x33, 0xF3, 0xA8, 0x27, 0x0E, 0x11, 0x15, 0xED, 0x59, 0x1A, 0x02, 0x8E, 0xCA, + 0x87, 0xCA, 0x09, 0x50, 0x59, 0xC8, 0x1F, 0xA6, 0xC9, 0x60, 0xB3, 0x4D, 0x60, 0x82, 0x12, 0x3F, + 0x83, 0x1B, 0x69, 0x6D, 0xCB, 0x43, 0x39, 0x20, 0x93, 0xDF, 0x53, 0xCD, 0xCA, 0x8F, 0x9F, 0x15, + 0x01, 0xE7, 0xDA, 0x60, 0xAD, 0x2F, 0xCC, 0xBE, 0x09, 0x4E, 0x0F, 0x35, 0x20, 0x6D, 0xCD, 0x32, + 0xB0, 0x51, 0x78, 0x17, 0xEE, 0x06, 0x72, 0x9D, 0x66, 0xB0, 0x0D, 0x09, 0x78, 0xA4, 0x9D, 0x9C, + 0x16, 0x4A, 0x3B, 0xBE, 0x89, 0x6B, 0x1B, 0xD8, 0xBD, 0xAF, 0x00, 0xAE, 0x13, 0x86, 0xE3, 0x38, + 0xB9, 0x15, 0x30, 0xFC, 0x5E, 0x74, 0x5F, 0xFE, 0xB1, 0xEC, 0xCF, 0xB6, 0xE2, 0xBF, 0x63, 0x28, + 0xA7, 0x3E, 0x64, 0xC6, 0xC1, 0x72, 0x86, 0x72, 0xFE, 0x03, 0xB1, 0x60, 0x03, 0x16, 0x6F, 0xDA, + 0xE7, 0x48, 0x33, 0x58, 0x66, 0x07, 0x6C, 0x96, 0x5A, 0x29, 0x7D, 0xBF, 0x1C, 0xA1, 0x1A, 0xC6, + 0x1C, 0x90, 0x6D, 0x5C, 0x94, 0xB4, 0x4A, 0x94, 0x5C, 0x4D, 0xAC, 0x13, 0x07, 0x5A, 0xF8, 0x68, + 0x37, 0x3C, 0x9C, 0x33, 0xF8, 0x7E, 0x85, 0x38, 0x57, 0x28, 0xA9, 0xE3, 0xEE, 0xD8, 0x15, 0x4F, + 0xB7, 0x02, 0x0D, 0x6B, 0xB7, 0xF0, 0x27, 0x1E, 0x23, 0x95, 0x46, 0x50, 0x75, 0x4A, 0x87, 0x0D, + 0x2E, 0xCA, 0xC0, 0x37, 0x6E, 0x20, 0x19, 0x1D, 0xD6, 0x1C, 0xFE, 0x66, 0x67, 0xBB, 0x61, 0x91, + 0xC8, 0x0D, 0x77, 0xB9, 0xE1, 0x03, 0x82, 0x73, 0x40, 0xB5, 0x61, 0x94, 0xC9, 0x71, 0x38, 0xFE, + 0x88, 0x25, 0x3B, 0xD6, 0x67, 0x78, 0x79, 0xBA, 0xE1, 0xBD, 0x12, 0x93, 0x57, 0x91, 0xB8, 0x2C, + 0xEE, 0x4E, 0x48, 0xEC, 0x43, 0x18, 0xFC, 0x33, 0xF3, 0x80, 0x7E, 0xB0, 0x1B, 0xAC, 0xA4, 0x6A, + 0x08, 0xF9, 0x7A, 0x72, 0x7F, 0x0D, 0xAE, 0x5D, 0x80, 0x29, 0xA0, 0x9C, 0x73, 0xF1, 0xD5, 0xB5, + 0x56, 0x5F, 0xF7, 0xDD, 0xDE, 0x51, 0xB2, 0x40, 0xDA, 0x13, 0x8B, 0x57, 0x34, 0xDA, 0x52, 0x6E, + 0xCF, 0x2D, 0x12, 0x60, 0x80, 0x48, 0x84, 0x23, 0xB0, 0x8A, 0x5D, 0x34, 0xA1, 0x29, 0x70, 0x7A, + 0xED, 0x01, 0x44, 0x3B, 0xFC, 0x73, 0x22, 0x37, 0x08, 0x95, 0xD8, 0x7D, 0x07, 0xEF, 0x6B, 0xCD, + 0x46, 0x73, 0x6A, 0xD3, 0xAE, 0x13, 0x7D, 0xAE, 0xAF, 0x30, 0xA6, 0xD6, 0x68, 0x10, 0x1E, 0x1C, + 0x2D, 0x64, 0xF3, 0xA9, 0xE6, 0xE1, 0x10, 0x46, 0xC3, 0x12, 0xFB, 0x29, 0x6A, 0x15, 0xA6, 0x06, + 0x5C, 0x9E, 0xD3, 0x32, 0xE4, 0xCD, 0x96, 0x29, 0xDD, 0xB3, 0x05, 0xEA, 0x27, 0xA5, 0x95, 0x66, + 0xB0, 0x79, 0x64, 0x70, 0x26, 0x7C, 0x46, 0x39, 0x18, 0x79, 0x8D, 0xDC, 0x83, 0x6F, 0x11, 0xF0, + 0x05, 0xC5, 0xDC, 0x54, 0xEB, 0xB7, 0xA1, 0x13, 0x4B, 0x13, 0x28, 0x49, 0x91, 0xCC, 0x2A, 0x26, + 0xB5, 0xF0, 0x25, 0x8E, 0xF4, 0xC0, 0xFF, 0x2F, 0x37, 0xD5, 0x57, 0x7E, 0x25, 0xBF, 0xBE, 0x91, + 0x5C, 0x83, 0x6D, 0xC5, 0x4A, 0xDE, 0x7C, 0xC8, 0x37, 0xEA, 0x0F, 0x51, 0xEE, 0x18, 0xDE, 0xC8, + 0xCC, 0xA6, 0xCE, 0x07, 0xB9, 0x12, 0x1A, 0xB9, 0x7F, 0x08, 0x44, 0x34, 0x15, 0x58, 0x32, 0x69, + 0x8C, 0xEA, 0x44, 0x8B, 0x05, 0x15, 0x7A, 0xD7, 0xBB, 0xD2, 0x0B, 0x57, 0x25, 0x11, 0x75, 0x3A, + 0x47, 0xD4, 0xA7, 0x37, 0x40, 0x0C, 0x0A, 0x25, 0x21, 0x85, 0x2E, 0x95, 0x5F, 0x15, 0x15, 0x8D, + 0x8E, 0xBB, 0x0B, 0x6D, 0xE8, 0x58, 0x3F, 0x0C, 0x1D, 0xAD, 0xE0, 0x61, 0x7F, 0xE5, 0x58, 0xD5, + 0x11, 0x95, 0x90, 0x2B, 0x1D, 0x28, 0x58, 0xE2, 0xB9, 0x9D, 0x52, 0x65, 0x98, 0xE4, 0xF7, 0x4C, + 0x3B, 0x63, 0x3F, 0x9A, 0xBC, 0x17, 0x38, 0xA5, 0xAC, 0x28, 0x37, 0x66, 0x38, 0xC3, 0x79, 0xC1, + 0x43, 0x2E, 0xAB, 0x26, 0x22, 0xA6, 0xCF, 0xB3, 0x9F, 0xBE, 0xAC, 0x7A, 0xF4, 0x31, 0xCA, 0x8D, + 0x03, 0xDD, 0xE6, 0xCC, 0xB3, 0x08, 0x57, 0x9C, 0x1E, 0xC2, 0xC6, 0x73, 0xB5, 0x54, 0x95, 0xD8, + 0xC6, 0xC2, 0xF1, 0x69, 0x4F, 0x1E, 0xE6, 0xB1, 0x77, 0x3E, 0x02, 0x86, 0x26, 0x06, 0x2B, 0x34, + 0xA1, 0x51, 0xC0, 0x3D, 0x8D, 0xC6, 0xD6, 0x4A, 0xD8, 0x29, 0x18, 0x4C, 0x0B, 0xEB, 0x74, 0x1C, + 0xDC, 0x13, 0x62, 0xB4, 0xEE, 0x96, 0xF4, 0x48, 0x09, 0x5D, 0x60, 0xD3, 0xAC, 0x85, 0xDF, 0x9A, + 0x10, 0xAB, 0x1A, 0x94, 0x31, 0x74, 0xF3, 0x1D, 0x76, 0x87, 0xB2, 0x3A, 0xD1, 0xF7, 0x93, 0x79, + 0x67, 0x37, 0x6A, 0x97, 0x1F, 0xB9, 0x9E, 0xE4, 0xA1, 0xD3, 0xEF, 0xB4, 0xE6, 0xB2, 0x76, 0x9B, + 0xAA, 0x68, 0x2E, 0xA3, 0x18, 0x23, 0xE4, 0xA9, 0xAF, 0xFC, 0x8D, 0xBE, 0x19, 0x18, 0xB6, 0xF9, + 0xCE, 0xD2, 0xE2, 0xB5, 0xEC, 0x06, 0x43, 0xDD, 0x5C, 0x19, 0xE9, 0xC0, 0x63, 0x08, 0x87, 0xAE, + 0xDE, 0x34, 0xAB, 0xDA, 0xE8, 0xA3, 0x47, 0x85, 0xD5, 0xB1, 0x7A, 0xC2, 0x0F, 0x6A, 0x82, 0x5A, + 0xA1, 0xA1, 0x1D, 0xCF, 0xC3, 0xB9, 0xA4, 0x87, 0x79, 0x01, 0x4A, 0x59, 0xB0, 0xBB, 0xB0, 0x01, + 0x1E, 0x3C, 0x7A, 0xB7, 0xAF, 0xAF, 0xF7, 0x5A, 0x53, 0x17, 0x4B, 0xEB, 0x83, 0x15, 0xB2, 0x9F, + 0x02, 0x3B, 0x19, 0xDA, 0x8C, 0x91, 0x00, 0xF3, 0x78, 0x52, 0xF6, 0xB5, 0x7A, 0x90, 0x1D, 0x77, + 0x33, 0x35, 0x85, 0x06, 0x7D, 0x5F, 0x0D, 0xF1, 0xC6, 0x2B, 0x9D, 0x6E, 0xEA, 0x9F, 0xC3, 0xC6, + 0x87, 0xCD, 0x65, 0x74, 0xEB, 0x42, 0x5B, 0x21, 0x5F, 0xE3, 0xAA, 0x66, 0x44, 0xE2, 0xBE, 0x91, + 0xC8, 0xF9, 0xDA, 0xEC, 0x04, 0x8C, 0x5B, 0x5B, 0x1F, 0x52, 0x09, 0x57, 0x26, 0x2E, 0x95, 0xFD, + 0xCC, 0xF1, 0xDD, 0xB1, 0xBB, 0x98, 0x33, 0xE6, 0x1A, 0xBE, 0x44, 0x1C, 0xC2, 0xD1, 0x33, 0xB7, + 0xE2, 0x26, 0xDE, 0xC5, 0x0E, 0x28, 0x6A, 0x30, 0xD3, 0x81, 0x42, 0xDA, 0x48, 0x0E, 0x6F, 0xB2, + 0x33, 0xF3, 0xB3, 0x0C, 0xE1, 0x29, 0xB0, 0x77, 0xA0, 0x6C, 0xCB, 0x17, 0xE1, 0x81, 0x66, 0x8F, + 0x77, 0xDD, 0x3E, 0x5D, 0x46, 0x84, 0xF2, 0x9B, 0x7F, 0xAE, 0x2A, 0x8A, 0xCF, 0xB6, 0xB5, 0x92, + 0xF0, 0x8B, 0x1C, 0xF4, 0x38, 0x7F, 0x14, 0x26, 0x00, 0x4B, 0x90, 0x55, 0x11, 0x35, 0xE5, 0x84, + 0x50, 0x5A, 0x79, 0x53, 0x5B, 0xB3, 0xB2, 0xD7, 0xAB, 0x6D, 0x87, 0xC5, 0x94, 0xF9, 0x6F, 0xEA, + 0x74, 0xA0, 0x6F, 0x72, 0x76, 0x66, 0x76, 0xBB, 0x74, 0xF4, 0x4A, 0x69, 0x37, 0x7C, 0xC9, 0x0D, + 0x3F, 0xDE, 0x47, 0xE9, 0x56, 0xC0, 0x97, 0x9A, 0x3A, 0xB0, 0x05, 0xDC, 0x99, 0xA4, 0x9C, 0x1C, + 0x49, 0x57, 0xBE, 0xB0, 0xE1, 0xB0, 0xDC, 0xAD, 0xC1, 0xCC, 0x31, 0xB1, 0x4B, 0xE2, 0x63, 0x7F, + 0x7A, 0xB4, 0x84, 0x55, 0x5E, 0xEF, 0x85, 0x4B, 0xD7, 0x5D, 0x60, 0x9B, 0x82, 0x47, 0x3C, 0x45, + 0x68, 0x5F, 0xCB, 0x59, 0x0E, 0xA3, 0x62, 0x61, 0xE6, 0x1B, 0x7D, 0x29, 0x36, 0x04, 0x57, 0x7E, + 0x73, 0xC6, 0x92, 0x52, 0x1C, 0x07, 0x47, 0xA1, 0x46, 0x9C, 0x55, 0x68, 0xFB, 0xC1, 0x11, 0x5A, + 0x85, 0x08, 0x09, 0xE7, 0xF9, 0x81, 0xC8, 0x0A, 0xEC, 0x5A, 0x46, 0x49, 0x5A, 0x84, 0xB0, 0xEF, + 0x90, 0x79, 0xD8, 0xC5, 0x26, 0xBE, 0x1E, 0xF5, 0x5E, 0xBE, 0x6F, 0x39, 0xD0, 0x96, 0xD1, 0x3B, + 0xAD, 0xD7, 0x2A, 0x91, 0xB5, 0x48, 0x58, 0x10, 0x79, 0x9B, 0x05, 0x98, 0x66, 0xAD, 0xF8, 0x38, + 0xC4, 0xF6, 0x56, 0xCC, 0xF3, 0x7D, 0x4B, 0xB6, 0x97, 0xD6, 0xE8, 0x8B, 0xC5, 0xFB, 0x83, 0x69, + 0x46, 0xE1, 0x4E, 0xF5, 0x67, 0xD5, 0x7B, 0x06, 0x8C, 0x5A, 0x82, 0x5C, 0x60, 0x33, 0xB1, 0xD3, + 0x50, 0x30, 0x52, 0x4E, 0xDA, 0x85, 0xBA, 0x98, 0x30, 0xB4, 0xAB, 0x22, 0x49, 0xC9, 0xD8, 0xB2, + 0xE0, 0x63, 0x1F, 0x12, 0x32, 0x1E, 0xD6, 0x05, 0x21, 0x86, 0x58, 0x53, 0x4F, 0xEA, 0x2A, 0x59, + 0x75, 0x35, 0x2D, 0x1A, 0x82, 0xEF, 0x8C, 0x71, 0x3B, 0xCD, 0x78, 0x32, 0xE8, 0xD2, 0x30, 0x12, + 0x79, 0x22, 0x4F, 0x4D, 0xAE, 0xFF, 0xA5, 0x48, 0x3C, 0xCA, 0x5F, 0x6A, 0x14, 0xB4, 0x96, 0xB7, + 0x6C, 0xD9, 0xC1, 0xD7, 0x24, 0xF7, 0xDE, 0x14, 0x70, 0x70, 0x14, 0xEE, 0x68, 0x4F, 0x39, 0x36, + 0xA9, 0xE0, 0x27, 0xEC, 0xFE, 0x03, 0x5D, 0x2D, 0xBD, 0x66, 0x8E, 0xA7, 0xB4, 0x1F, 0xA0, 0x94, + 0xBE, 0x51, 0xDE, 0x44, 0xEC, 0xE4, 0x6D, 0xFD, 0xAA, 0xAF, 0x9C, 0x3A, 0x41, 0x87, 0xF8, 0x4E, + 0x8A, 0xF4, 0x08, 0xC5, 0xB3, 0xD0, 0xB9, 0x1B, 0x23, 0x77, 0x7F, 0x39, 0x9D, 0xAE, 0x0C, 0xAA, + 0x63, 0xB3, 0x99, 0x84, 0xFB, 0xCE, 0x79, 0x7A, 0x34, 0x78, 0x36, 0xA2, 0x38, 0xF8, 0xF7, 0x2E, + 0x0C, 0x7D, 0xBD, 0xBB, 0xF8, 0x5B, 0x5F, 0x33, 0x92, 0x9E, 0x01, 0xEA, 0x85, 0x77, 0xA7, 0xB0, + 0xA5, 0x06, 0xEB, 0xF3, 0x75, 0x5A, 0x2E, 0xDD, 0xD6, 0x7A, 0x07, 0xE4, 0x24, 0x24, 0xC7, 0x76, + 0x52, 0xAD, 0x7B, 0x3C, 0x45, 0x29, 0xB8, 0x01, 0x32, 0xE5, 0x85, 0x0F, 0x2F, 0x50, 0x19, 0x54, + 0x20, 0x67, 0x58, 0x8B, 0x65, 0xFA, 0x4D, 0x4E, 0xA9, 0x70, 0xC4, 0x9A, 0x3C, 0xF3, 0xB0, 0x35, + 0x02, 0x13, 0x5B, 0xD6, 0xB2, 0x63, 0xBF, 0x2E, 0xB4, 0xB6, 0xC1, 0x7C, 0x8B, 0x75, 0xF6, 0x99, + 0x67, 0xBC, 0xC6, 0xA1, 0xB1, 0x58, 0xF1, 0x72, 0x3A, 0x92, 0x31, 0x5C, 0x2C, 0x2E, 0x1D, 0xF3, + 0x09, 0x9E, 0xD3, 0x18, 0xDB, 0x39, 0x11, 0x3D, 0xE1, 0x9D, 0x7B, 0xE3, 0x4D, 0x16, 0xCF, 0x4F, + 0x2B, 0xF9, 0x97, 0xEF, 0x6A, 0x3A, 0x30, 0xAE, 0x6B, 0x90, 0x85, 0x51, 0x14, 0x78, 0xD9, 0xDF, + 0xDC, 0x65, 0x3C, 0x83, 0xF5, 0xE9, 0xE6, 0xB5, 0x8B, 0x42, 0x8C, 0xBE, 0x05, 0x78, 0xE4, 0x9A, + 0xBA, 0x21, 0xD4, 0x30, 0x48, 0x89, 0x92, 0xE4, 0x7E, 0xF9, 0x43, 0x4D, 0x2C, 0xDF, 0xDE, 0x8E, + 0x63, 0xFB, 0x40, 0xDD, 0x0E, 0x2C, 0x34, 0x4F, 0x44, 0xAE, 0x29, 0xA2, 0x48, 0x58, 0x60, 0xC8, + 0xC7, 0x64, 0x1F, 0x69, 0x99, 0xD1, 0x01, 0x91, 0x81, 0x42, 0x54, 0x10, 0xBE, 0x82, 0x18, 0x39, + 0x78, 0xE4, 0x4E, 0xFA, 0xB6, 0xE6, 0x48, 0xB8, 0x36, 0x65, 0xDF, 0x00, 0xF0, 0x12, 0x60, 0xB3, + 0x74, 0x28, 0x1E, 0x68, 0xF1, 0x40, 0x9A, 0x29, 0xA1, 0xBB, 0x21, 0x9D, 0x96, 0x61, 0x8F, 0x85, + 0x6C, 0x88, 0x58, 0x91, 0x79, 0xF6, 0x88, 0x3A, 0x9A, 0x54, 0xC0, 0xE5, 0x13, 0x88, 0x30, 0x4A, + 0x65, 0xE1, 0x8D, 0x0D, 0x10, 0x61, 0xD8, 0xA5, 0x90, 0x02, 0xED, 0xA6, 0xE9, 0x49, 0xD9, 0xC7, + 0x86, 0x2C, 0xFF, 0xAC, 0xD6, 0x4E, 0xED, 0x5C, 0x4F, 0xA2, 0x8E, 0xF9, 0x18, 0x3B, 0xDE, 0x16, + 0x04, 0xD2, 0x75, 0xEC, 0x15, 0x9F, 0xF0, 0x01, 0xB5, 0xE7, 0x0C, 0x96, 0xBE, 0xC4, 0xBE, 0xEA, + 0xDB, 0xB7, 0x2B, 0xFC, 0x73, 0x6A, 0x1D, 0x0B, 0x74, 0xD8, 0x64, 0x57, 0xD0, 0xB9, 0x4F, 0x9A, + 0x72, 0x74, 0x07, 0xC5, 0x8D, 0xDB, 0x81, 0x4C, 0x13, 0x77, 0xCD, 0xDA, 0x01, 0x8E, 0xDF, 0xFF, + 0xCA, 0x11, 0x62, 0x37, 0xC9, 0xAC, 0xFD, 0x94, 0xF4, 0xCC, 0x42, 0xC7, 0x9B, 0xD1, 0xF9, 0x4D, + 0x85, 0x3B, 0xDC, 0xBF, 0xFC, 0x20, 0x4D, 0xE1, 0x52, 0xCD, 0x29, 0xF1, 0x7D, 0x2A, 0x54, 0xA1, + 0x2E, 0x18, 0x7B, 0xDD, 0x05, 0xE0, 0x36, 0x7D, 0x7C, 0x40, 0x11, 0x9A, 0xC8, 0xE1, 0x63, 0x39, + 0x7D, 0x72, 0x54, 0xB2, 0x1C, 0xE1, 0x40, 0x13, 0x6A, 0x1F, 0x76, 0xB1, 0xAD, 0x75, 0xE3, 0x24, + 0x7B, 0x3F, 0xA9, 0xCA, 0xFD, 0x28, 0x76, 0x6F, 0x65, 0x63, 0xA7, 0xCC, 0x71, 0x84, 0xE3, 0x04, + 0xC5, 0x05, 0x17, 0x5A, 0x1F, 0xD0, 0xEA, 0x69, 0xBB, 0x7A, 0xE1, 0xA1, 0xB0, 0xFB, 0xE0, 0xD2, + 0x70, 0x1F, 0x6B, 0x5C, 0x86, 0xE4, 0xDE, 0x8C, 0x5C, 0xC8, 0x36, 0xA9, 0xDD, 0x5D, 0x13, 0x82, + 0xDB, 0x6E, 0x93, 0x00, 0x77, 0x8C, 0xE1, 0xD3, 0x9A, 0x0C, 0x4D, 0xF4, 0x5A, 0x10, 0xDB, 0xBF, + 0x3D, 0xD0, 0x6C, 0x4E, 0xEC, 0x64, 0xA2, 0xF4, 0x5D, 0x29, 0x80, 0x4B, 0xE7, 0xA1, 0x14, 0xAE, + 0xB4, 0x78, 0x8B, 0x6E, 0xCB, 0xA1, 0xB2, 0x02, 0x35, 0xC7, 0x4E, 0x58, 0x7C, 0x98, 0x46, 0x05, + 0xCE, 0x56, 0x83, 0xB4, 0x5E, 0x82, 0x65, 0xF1, 0xC9, 0x9B, 0x29, 0xAC, 0x42, 0xEB, 0xE5, 0xF1, + 0x1D, 0x1A, 0x11, 0x0C, 0x63, 0xAD, 0xFD, 0xCF, 0x40, 0x68, 0xF1, 0xB3, 0xF4, 0x62, 0xB9, 0x9B, + 0x6C, 0x6C, 0x13, 0x94, 0xAF, 0x82, 0x80, 0xE0, 0xBA, 0x6C, 0xCB, 0x84, 0x0D, 0xA0, 0xE4, 0xAA, + 0x15, 0x8F, 0xAD, 0x29, 0xCE, 0x79, 0x7B, 0xF6, 0x70, 0x3F, 0xCC, 0x8B, 0x92, 0xA9, 0xC5, 0x17, + 0xBA, 0xE0, 0xF0, 0x9B, 0x96, 0x8E, 0x7F, 0xA0, 0x2F, 0x2C, 0x5F, 0x54, 0xF6, 0x5A, 0x9E, 0xBB, + 0xAC, 0x6C, 0xE3, 0xBF, 0xC4, 0x1C, 0xAB, 0x14, 0xEB, 0x12, 0xCE, 0xD8, 0xA5, 0x44, 0xCC, 0x4F, + 0x4B, 0x08, 0x4F, 0x2C, 0x00, 0x81, 0xD5, 0x17, 0x22, 0x07, 0x42, 0xCA, 0xFD, 0x49, 0xAD, 0x06, + 0x95, 0xC8, 0xD1, 0xC7, 0x3E, 0x39, 0x34, 0x1C, 0x41, 0x99, 0xC2, 0xAB, 0x8A, 0xED, 0x50, 0x12, + 0xE8, 0xC7, 0x75, 0x52, 0x19, 0x2B, 0xD4, 0xCC, 0xD3, 0xFA, 0x84, 0xE7, 0x0C, 0xCE, 0xE3, 0x93, + 0xCA, 0x60, 0xE5, 0xB7, 0x06, 0xDB, 0x84, 0xEE, 0x79, 0xA7, 0x54, 0x76, 0xE9, 0x46, 0x85, 0xEA, + 0x4F, 0xF3, 0xA1, 0xEF, 0x10, 0xC1, 0x4C, 0x12, 0xB0, 0xEE, 0x23, 0xDD, 0x81, 0x3A, 0x6E, 0x9F, + 0x01, 0x03, 0x04, 0x7C, 0x6D, 0x47, 0x84, 0xB7, 0xE7, 0x19, 0xE3, 0x4E, 0xCF, 0x23, 0x3A, 0xB2, + 0x23, 0x33, 0x00, 0xCB, 0x07, 0x78, 0x51, 0x8B, 0x0C, 0x30, 0x7A, 0x1F, 0x41, 0x14, 0x75, 0xFF, + 0x9E, 0x43, 0x81, 0xF4, 0x15, 0x89, 0x8B, 0xB7, 0x2B, 0xBC, 0x62, 0x48, 0x64, 0xD9, 0x26, 0xBE, + 0xEA, 0x22, 0xB6, 0x22, 0xB8, 0x6F, 0x2B, 0xB6, 0x9B, 0x8F, 0xC7, 0x63, 0x03, 0x83, 0xA7, 0x22, + 0xF8, 0x5C, 0x08, 0x87, 0x70, 0xA2, 0xCB, 0x6B, 0xD6, 0xC9, 0xF6, 0x59, 0x60, 0x8C, 0x10, 0xFA, + 0x3C, 0xAD, 0x15, 0x1F, 0x8F, 0x18, 0x01, 0x2F, 0xB9, 0x2C, 0x01, 0x59, 0x76, 0x18, 0xE6, 0x55, + 0x98, 0x23, 0x33, 0xA9, 0x05, 0xFC, 0x4C, 0xF3, 0x9A, 0xCB, 0xBA, 0x42, 0x60, 0x0C, 0x50, 0xEB, + 0x69, 0xF1, 0x22, 0x73, 0x03, 0x4B, 0x38, 0x74, 0xBF, 0xBB, 0x7B, 0x4C, 0x7F, 0x30, 0xF0, 0x21, + 0x8C, 0x73, 0x24, 0x69, 0x1F, 0x7F, 0xF4, 0x18, 0x0E, 0x4F, 0xB7, 0x99, 0x8D, 0x5A, 0xE9, 0xF7, + 0x79, 0x8B, 0x25, 0xCB, 0xC7, 0xEA, 0x8C, 0xA7, 0x36, 0x33, 0x72, 0x78, 0x2B, 0x9B, 0x4F, 0xA6, + 0x53, 0x10, 0xFA, 0xF7, 0x1C, 0x66, 0xBB, 0x7C, 0x72, 0x18, 0xBE, 0x91, 0x5C, 0x8C, 0xED, 0x75, + 0x3C, 0x35, 0xF4, 0x49, 0xAD, 0xB0, 0x49, 0x67, 0x05, 0x6B, 0x96, 0x46, 0xF9, 0x2D, 0xAF, 0x8E, + 0x90, 0x3A, 0x3C, 0x35, 0xF5, 0x66, 0x7E, 0xE8, 0x08, 0x04, 0x0D, 0xBF, 0x6E, 0x4E, 0x3F, 0xAD, + 0x0A, 0xB9, 0x06, 0xDF, 0x4B, 0xD1, 0x9E, 0x5D, 0x69, 0x13, 0x4B, 0xCD, 0xB6, 0xE7, 0x4A, 0x12, + 0xF2, 0x94, 0xE7, 0xBF, 0xE3, 0x48, 0x65, 0xF2, 0xD4, 0x2A, 0xCF, 0x17, 0xC2, 0xF8, 0x50, 0xC5, + 0xF9, 0x3B, 0x11, 0x04, 0x5D, 0x84, 0xE6, 0x9D, 0x97, 0x31, 0xDB, 0x72, 0xD0, 0x0F, 0x3E, 0x9D, + 0xE2, 0x5B, 0x81, 0xBF, 0xA3, 0xD1, 0xB3, 0x5D, 0xCF, 0x1D, 0x70, 0xDA, 0x96, 0xA3, 0x67, 0xA5, + 0xF0, 0x5E, 0xA8, 0xD0, 0x15, 0x47, 0x3D, 0xAE, 0xA4, 0x33, 0x80, 0xF4, 0x90, 0x1B, 0x27, 0xD1, + 0x4F, 0x5F, 0x2B, 0x1B, 0xA4, 0xEE, 0x5F, 0x3F, 0x0F, 0xC2, 0x70, 0x13, 0x42, 0x42, 0x98, 0xA1, + 0xC4, 0x0C, 0x2B, 0xB1, 0x4E, 0x69, 0xF2, 0xBC, 0xB5, 0x21, 0xD2, 0x11, 0xA4, 0xC0, 0x3D, 0xB5, + 0xBE, 0x4D, 0x0E, 0x84, 0x9B, 0xF0, 0xB0, 0x0F, 0xE2, 0x23, 0xCB, 0x66, 0x1D, 0x71, 0x4F, 0x20, + 0x2F, 0x3C, 0x9D, 0xC8, 0x3C, 0x11, 0xA7, 0x47, 0xA4, 0x3A, 0x46, 0xB0, 0x83, 0xA1, 0xD5, 0x34, + 0x71, 0xA9, 0x8B, 0xDB, 0xBA, 0x6F, 0x49, 0xDC, 0x69, 0xC9, 0xBF, 0xFE, 0x6C, 0x12, 0x58, 0x3B, + 0xDB, 0x42, 0x39, 0xF7, 0x63, 0xB2, 0xA4, 0xFD, 0x30, 0x48, 0x45, 0x4E, 0x02, 0x15, 0xBD, 0x4B, + 0xC3, 0x59, 0xF9, 0x0B, 0x87, 0x1F, 0xEB, 0x90, 0x0E, 0x69, 0x0C, 0xDE, 0x15, 0xD8, 0x7C, 0xA6, + 0xD2, 0xFC, 0xFA, 0x5F, 0xAE, 0x9B, 0x7D, 0x76, 0x0B, 0xE8, 0x53, 0xE4, 0x10, 0xC6, 0xF0, 0x3D, + 0x33, 0x7A, 0x9E, 0x4E, 0x57, 0x0F, 0x58, 0xB6, 0x13, 0x95, 0x89, 0x6D, 0x84, 0xC6, 0xB2, 0x22, + 0x37, 0xF9, 0x99, 0x27, 0xCC, 0x10, 0xA2, 0x14, 0x84, 0x0F, 0x9A, 0xE7, 0xB9, 0x52, 0xFC, 0xB3, + 0x3F, 0x97, 0x07, 0x67, 0xB5, 0xE5, 0x00, 0x32, 0x4D, 0x90, 0x4D, 0x6A, 0xFE, 0x17, 0xE1, 0xAC, + 0x58, 0x00, 0x69, 0xC8, 0x7A, 0x87, 0x60, 0x10, 0xB5, 0x9C, 0x64, 0xFC, 0xAE, 0xD8, 0x86, 0x88, + 0x5A, 0x76, 0xED, 0x72, 0x97, 0x19, 0x90, 0xD1, 0xE3, 0xAF, 0x6E, 0x07, 0x2F, 0x7A, 0xBB, 0xC6, + 0x0D, 0x63, 0x53, 0x23, 0x00, 0xA3, 0x8A, 0x88, 0x2B, 0x21, 0x9C, 0x3A, 0x9D, 0x86, 0x16, 0xE9, +}; diff --git a/src/core/hw/ecc.cpp b/src/core/hw/ecc.cpp new file mode 100644 index 000000000..5f9cff029 --- /dev/null +++ b/src/core/hw/ecc.cpp @@ -0,0 +1,212 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/common_paths.h" +#include "common/file_util.h" +#include "core/hw/aes/key.h" +#include "core/hw/ecc.h" +#include "cryptopp/osrng.h" + +namespace HW::ECC { + +PublicKey root_public; + +CryptoPPInteger PrivateKey::AsCryptoPPInteger() const { + return CryptoPP::Integer(x.data(), x.size(), CryptoPP::Integer::UNSIGNED, + CryptoPP::BIG_ENDIAN_ORDER); +} + +CryptoPPECCPrivateKey PrivateKey::AsCryptoPPPrivateKey() const { + CryptoPPECCPrivateKey private_key_cpp; + CryptoPP::AutoSeededRandomPool prng; + + private_key_cpp.Initialize(CryptoPP::ASN1::sect233r1(), AsCryptoPPInteger()); + if (!private_key_cpp.Validate(prng, 3)) { + LOG_ERROR(HW, "Failed to verify ECC private key"); + } + + return private_key_cpp; +} + +CryptoPPPoint PublicKey::AsCryptoPPPoint() const { + return CryptoPP::EC2N::Point(CryptoPP::PolynomialMod2(x.data(), x.size()), + CryptoPP::PolynomialMod2(y.data(), y.size())); +} + +CryptoPPECCPublicKey PublicKey::AsCryptoPPPublicKey() const { + CryptoPPECCPublicKey public_key_cpp; + + public_key_cpp.Initialize(CryptoPP::ASN1::sect233r1(), AsCryptoPPPoint()); + return public_key_cpp; +} + +std::vector HexToVector(const std::string& hex) { + std::vector vector(hex.size() / 2); + for (std::size_t i = 0; i < vector.size(); ++i) { + vector[i] = static_cast(std::stoi(hex.substr(i * 2, 2), nullptr, 16)); + } + + return vector; +} + +void InitSlots() { + static bool initialized = false; + if (initialized) + return; + initialized = true; + + auto s = HW::AES::GetKeysStream(); + + std::string mode = ""; + + while (!s.eof()) { + std::string line; + std::getline(s, line); + + // Ignore empty or commented lines. + if (line.empty() || line.starts_with("#")) { + continue; + } + + if (line.starts_with(":")) { + mode = line.substr(1); + continue; + } + + if (mode != "ECC") { + continue; + } + + const auto parts = Common::SplitString(line, '='); + if (parts.size() != 2) { + LOG_ERROR(HW_RSA, "Failed to parse {}", line); + continue; + } + + const std::string& name = parts[0]; + + std::vector key; + try { + key = HexToVector(parts[1]); + } catch (const std::logic_error& e) { + LOG_ERROR(HW_RSA, "Invalid key {}: {}", parts[1], e.what()); + continue; + } + + if (name == "rootPublicXY") { + memcpy(root_public.xy.data(), key.data(), std::min(root_public.xy.size(), key.size())); + continue; + } + } +} + +PrivateKey CreateECCPrivateKey(std::span private_key_x, bool fix_up) { + CryptoPPECCPrivateKey private_key; + CryptoPPInteger privk_x(private_key_x.data(), private_key_x.size(), CryptoPP::Integer::UNSIGNED, + CryptoPP::BIG_ENDIAN_ORDER); + + // The ECC library Nintendo used to generate private keys does not limit the private key + // size to be inside the subgroup order. To fix this, we do a modulo operation with the + // subgroup order, otherwise CryptoPP will fail to use the key. + if (fix_up) { + CryptoPP::DL_GroupParameters_EC params(CryptoPP::ASN1::sect233r1()); + privk_x = privk_x % params.GetSubgroupOrder(); + } + private_key.Initialize(CryptoPP::ASN1::sect233r1(), privk_x); + + PrivateKey ret; + private_key.GetPrivateExponent().Encode(ret.x.data(), ret.x.size()); + return ret; +} + +PublicKey CreateECCPublicKey(std::span public_key_xy) { + ASSERT_MSG(public_key_xy.size() <= sizeof(PublicKey::xy), "Invalid public key length"); + + PublicKey ret; + memcpy(ret.xy.data(), public_key_xy.data(), ret.xy.size()); + return ret; +} + +Signature CreateECCSignature(std::span signature_rs) { + ASSERT_MSG(signature_rs.size() <= sizeof(Signature::rs), "Invalid signature length"); + + Signature ret; + memcpy(ret.rs.data(), signature_rs.data(), ret.rs.size()); + return ret; +} + +PublicKey MakePublicKey(const CryptoPPECCPrivateKey& private_key_cpp) { + CryptoPPECCPublicKey public_key_cpp; + PublicKey public_key; + + private_key_cpp.MakePublicKey(public_key_cpp); + + public_key_cpp.GetPublicElement().x.Encode(public_key.x.data(), public_key.x.size()); + public_key_cpp.GetPublicElement().y.Encode(public_key.y.data(), public_key.y.size()); + + return public_key; +} + +PublicKey MakePublicKey(const PrivateKey& private_key) { + return MakePublicKey(private_key.AsCryptoPPPrivateKey()); +} + +std::pair GenerateKeyPair() { + CryptoPPECCPrivateKey private_key_cpp; + PrivateKey private_key; + + CryptoPP::AutoSeededRandomPool prng; + + private_key_cpp.Initialize(prng, CryptoPP::ASN1::sect233r1()); + private_key_cpp.GetPrivateExponent().Encode(private_key.x.data(), private_key.x.size()); + + return std::make_pair(private_key, MakePublicKey(private_key_cpp)); +} + +Signature Sign(std::span data, PrivateKey private_key) { + CryptoPP::ECDSA::Signer signer( + private_key.AsCryptoPPPrivateKey()); + CryptoPP::AutoSeededRandomPool prng; + + Signature ret; + + signer.SignMessage(prng, data.data(), data.size(), ret.rs.data()); + return ret; +} + +bool Verify(std::span data, Signature signature, PublicKey public_key) { + CryptoPP::ECDSA::Verifier verifier( + public_key.AsCryptoPPPublicKey()); + + return verifier.VerifyMessage(data.data(), data.size(), signature.rs.data(), + signature.rs.size()); +} + +std::vector Agree(PrivateKey private_key, PublicKey others_public_key) { + CryptoPP::ECDH::Domain domain( + CryptoPP::ASN1::sect233r1()); + CryptoPP::DL_GroupParameters_EC params(CryptoPP::ASN1::sect233r1()); + std::vector agreement(domain.AgreedValueLength()); + + std::vector private_encoded(domain.PrivateKeyLength()); + private_key.AsCryptoPPInteger().Encode(private_encoded.data(), private_encoded.size()); + + std::vector others_public_encoded(params.GetEncodedElementSize(true)); + params.EncodeElement(true, others_public_key.AsCryptoPPPoint(), others_public_encoded.data()); + + if (!domain.Agree(agreement.data(), private_encoded.data(), others_public_encoded.data())) { + LOG_ERROR(HW, "ECDH agreement failed"); + } + + return agreement; +} + +const PublicKey& GetRootPublicKey() { + return root_public; +} + +} // namespace HW::ECC \ No newline at end of file diff --git a/src/core/hw/ecc.h b/src/core/hw/ecc.h new file mode 100644 index 000000000..581f45aed --- /dev/null +++ b/src/core/hw/ecc.h @@ -0,0 +1,70 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include "common/common_types.h" +#include "cryptopp/eccrypto.h" + +namespace HW::ECC { +// All the supported 3DS ECC operations use the sect233r1 curve, +// so we default to sizes for this curve only. +static constexpr size_t INT_SIZE = 0x1E; + +using CryptoPPInteger = CryptoPP::Integer; +using CryptoPPPoint = CryptoPP::EC2N::Point; + +using CryptoPPECCPrivateKey = CryptoPP::ECDSA::PrivateKey; +using CryptoPPECCPublicKey = CryptoPP::ECDSA::PublicKey; + +struct PrivateKey { + std::array x; + + CryptoPPInteger AsCryptoPPInteger() const; + + CryptoPPECCPrivateKey AsCryptoPPPrivateKey() const; +}; + +union PublicKey { + struct { + std::array x; + std::array y; + }; + std::array xy; + + CryptoPPPoint AsCryptoPPPoint() const; + + CryptoPPECCPublicKey AsCryptoPPPublicKey() const; +}; + +union Signature { + struct { + std::array r; + std::array s; + }; + std::array rs; +}; + +void InitSlots(); + +PrivateKey CreateECCPrivateKey(std::span private_key_x, bool fix_up = false); +PublicKey CreateECCPublicKey(std::span public_key_xy); +Signature CreateECCSignature(std::span signature_rs); + +PublicKey MakePublicKey(const CryptoPPECCPrivateKey& private_key_cpp); +PublicKey MakePublicKey(const PrivateKey& private_key); +std::pair GenerateKeyPair(); + +Signature Sign(std::span data, PrivateKey private_key); +bool Verify(std::span data, Signature signature, PublicKey public_key); + +std::vector Agree(PrivateKey private_key, PublicKey others_public_key); + +const PublicKey& GetRootPublicKey(); +} // namespace HW::ECC \ No newline at end of file diff --git a/src/core/hw/rsa/rsa.cpp b/src/core/hw/rsa/rsa.cpp index 1a96d5523..9104b31c6 100644 --- a/src/core/hw/rsa/rsa.cpp +++ b/src/core/hw/rsa/rsa.cpp @@ -3,6 +3,8 @@ // Refer to the license.txt file included. #include +#include +#include #include #include #include @@ -11,39 +13,82 @@ #include "common/common_paths.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "core/hw/aes/key.h" #include "core/hw/rsa/rsa.h" +#include "cryptopp/osrng.h" +#include "cryptopp/rsa.h" namespace HW::RSA { -namespace { -std::vector HexToBytes(const std::string& hex) { - std::vector bytes; - - for (unsigned int i = 0; i < hex.length(); i += 2) { - std::string byteString = hex.substr(i, 2); - u8 byte = static_cast(std::strtol(byteString.c_str(), nullptr, 16)); - bytes.push_back(byte); - } - return bytes; -}; -} // namespace - constexpr std::size_t SlotSize = 4; std::array rsa_slots; -std::vector RsaSlot::GetSignature(std::span message) const { +RsaSlot ticket_wrap_slot; +RsaSlot secure_info_slot; +RsaSlot local_friend_code_seed_slot; + +std::vector RsaSlot::ModularExponentiation(std::span message, + int out_size_bytes) const { CryptoPP::Integer sig = CryptoPP::ModularExponentiation(CryptoPP::Integer(message.data(), message.size()), CryptoPP::Integer(exponent.data(), exponent.size()), CryptoPP::Integer(modulus.data(), modulus.size())); - std::stringstream ss; - ss << std::hex << sig; - CryptoPP::HexDecoder decoder; - decoder.Put(reinterpret_cast(ss.str().data()), ss.str().size()); - decoder.MessageEnd(); - std::vector result(decoder.MaxRetrievable()); - decoder.Get(result.data(), result.size()); - return HexToBytes(ss.str()); + + std::vector result((out_size_bytes == -1) ? sig.MinEncodedSize() : out_size_bytes); + sig.Encode(result.data(), result.size()); + return result; +} + +std::vector RsaSlot::Sign(std::span message) const { + if (private_d.empty()) { + LOG_ERROR(HW, "Cannot sign, RSA slot does not have a private key"); + return {}; + } + + CryptoPP::RSASS::PrivateKey private_key; + private_key.Initialize(CryptoPP::Integer(modulus.data(), modulus.size()), + CryptoPP::Integer(exponent.data(), exponent.size()), + CryptoPP::Integer(private_d.data(), private_d.size())); + + CryptoPP::RSASS::Signer signer(private_key); + CryptoPP::AutoSeededRandomPool prng; + std::vector ret(signer.SignatureLength()); + + signer.SignMessage(prng, message.data(), message.size(), ret.data()); + + return ret; +} + +bool RsaSlot::Verify(std::span message, std::span signature) const { + CryptoPP::RSASS::PublicKey public_key; + public_key.Initialize(CryptoPP::Integer(modulus.data(), modulus.size()), + CryptoPP::Integer(exponent.data(), exponent.size())); + + CryptoPP::RSASS::Verifier verifier(public_key); + + return verifier.VerifyMessage(message.data(), message.size(), signature.data(), + signature.size()); +} + +std::vector HexToVector(const std::string& hex) { + std::vector vector(hex.size() / 2); + for (std::size_t i = 0; i < vector.size(); ++i) { + vector[i] = static_cast(std::stoi(hex.substr(i * 2, 2), nullptr, 16)); + } + + return vector; +} + +std::optional> ParseKeySlotName(const std::string& full_name) { + std::size_t slot; + char type; + int end; + if (std::sscanf(full_name.c_str(), "slot0x%zX%c%n", &slot, &type, &end) == 2 && + end == static_cast(full_name.size())) { + return std::make_pair(slot, type); + } else { + return std::nullopt; + } } void InitSlots() { @@ -52,62 +97,119 @@ void InitSlots() { return; initialized = true; - const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9; - FileUtil::IOFile file(filepath, "rb"); - if (!file) { - return; + auto s = HW::AES::GetKeysStream(); + + std::string mode = ""; + + while (!s.eof()) { + std::string line; + std::getline(s, line); + + // Ignore empty or commented lines. + if (line.empty() || line.starts_with("#")) { + continue; + } + + if (line.starts_with(":")) { + mode = line.substr(1); + continue; + } + + if (mode != "RSA") { + continue; + } + + const auto parts = Common::SplitString(line, '='); + if (parts.size() != 2) { + LOG_ERROR(HW_RSA, "Failed to parse {}", line); + continue; + } + + const std::string& name = parts[0]; + + std::vector key; + try { + key = HexToVector(parts[1]); + } catch (const std::logic_error& e) { + LOG_ERROR(HW_RSA, "Invalid key {}: {}", parts[1], e.what()); + continue; + } + + if (name == "ticketWrapExp") { + ticket_wrap_slot.SetExponent(key); + continue; + } + + if (name == "ticketWrapMod") { + ticket_wrap_slot.SetModulus(key); + continue; + } + + if (name == "secureInfoExp") { + secure_info_slot.SetExponent(key); + continue; + } + + if (name == "secureInfoMod") { + secure_info_slot.SetModulus(key); + continue; + } + + if (name == "lfcsExp") { + local_friend_code_seed_slot.SetExponent(key); + continue; + } + + if (name == "lfcsMod") { + local_friend_code_seed_slot.SetModulus(key); + continue; + } + + const auto key_slot = ParseKeySlotName(name); + if (!key_slot) { + LOG_ERROR(HW_RSA, "Invalid key name '{}'", name); + continue; + } + + if (key_slot->first >= SlotSize) { + LOG_ERROR(HW_RSA, "Out of range key slot ID {:#X}", key_slot->first); + continue; + } + + switch (key_slot->second) { + case 'X': + rsa_slots.at(key_slot->first).SetExponent(key); + break; + case 'M': + rsa_slots.at(key_slot->first).SetModulus(key); + break; + case 'P': + rsa_slots.at(key_slot->first).SetPrivateD(key); + break; + default: + LOG_ERROR(HW_RSA, "Invalid key type '{}'", key_slot->second); + break; + } } - - const std::size_t length = file.GetSize(); - if (length != 65536) { - LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length); - return; - } - - constexpr std::size_t RSA_MODULUS_POS = 0xB3E0; - file.Seek(RSA_MODULUS_POS, SEEK_SET); - std::vector modulus(256); - file.ReadArray(modulus.data(), modulus.size()); - - constexpr std::size_t RSA_EXPONENT_POS = 0xB4E0; - file.Seek(RSA_EXPONENT_POS, SEEK_SET); - std::vector exponent(256); - file.ReadArray(exponent.data(), exponent.size()); - - rsa_slots[0] = RsaSlot(std::move(exponent), std::move(modulus)); - // TODO(B3N30): Initalize the other slots. But since they aren't used at all, we can skip them - // for now } -RsaSlot GetSlot(std::size_t slot_id) { +static RsaSlot empty_slot; +const RsaSlot& GetSlot(std::size_t slot_id) { if (slot_id >= rsa_slots.size()) - return RsaSlot{}; + return empty_slot; return rsa_slots[slot_id]; } -std::vector CreateASN1Message(std::span data) { - static constexpr std::array asn1_header = { - {0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x31, 0x30, 0x0D, 0x06, - 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}}; +const RsaSlot& GetTicketWrapSlot() { + return ticket_wrap_slot; +} - std::vector message(asn1_header.begin(), asn1_header.end()); - CryptoPP::SHA256 sha; - message.resize(message.size() + CryptoPP::SHA256::DIGESTSIZE); - sha.CalculateDigest(message.data() + asn1_header.size(), data.data(), data.size()); - return message; +const RsaSlot& GetSecureInfoSlot() { + return secure_info_slot; +} + +const RsaSlot& GetLocalFriendCodeSeedSlot() { + return local_friend_code_seed_slot; } } // namespace HW::RSA diff --git a/src/core/hw/rsa/rsa.h b/src/core/hw/rsa/rsa.h index 0c94171a9..b6dd2c1ca 100644 --- a/src/core/hw/rsa/rsa.h +++ b/src/core/hw/rsa/rsa.h @@ -15,23 +15,57 @@ public: RsaSlot() = default; RsaSlot(std::vector exponent, std::vector modulus) : init(true), exponent(std::move(exponent)), modulus(std::move(modulus)) {} - std::vector GetSignature(std::span message) const; + + std::vector ModularExponentiation(std::span message, + int out_size_bytes = -1) const; + + std::vector Sign(std::span message) const; + + bool Verify(std::span message, std::span signature) const; explicit operator bool() const { // TODO(B3N30): Maybe check if exponent and modulus are vailid return init; } + void SetExponent(const std::vector& e) { + exponent = e; + } + + const std::vector& GetExponent() const { + return exponent; + } + + void SetModulus(const std::vector& m) { + modulus = m; + } + + const std::vector& GetModulus() const { + return modulus; + } + + void SetPrivateD(const std::vector& d) { + private_d = d; + } + + const std::vector& GetPrivateD() const { + return private_d; + } + private: bool init = false; std::vector exponent; std::vector modulus; + std::vector private_d; }; void InitSlots(); -RsaSlot GetSlot(std::size_t slot_id); +const RsaSlot& GetSlot(std::size_t slot_id); -std::vector CreateASN1Message(std::span data); +const RsaSlot& GetTicketWrapSlot(); + +const RsaSlot& GetSecureInfoSlot(); +const RsaSlot& GetLocalFriendCodeSeedSlot(); } // namespace HW::RSA diff --git a/src/core/hw/unique_data.cpp b/src/core/hw/unique_data.cpp new file mode 100644 index 000000000..8010d3c60 --- /dev/null +++ b/src/core/hw/unique_data.cpp @@ -0,0 +1,228 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/common_paths.h" +#include "core/file_sys/certificate.h" +#include "core/file_sys/otp.h" +#include "core/hw/aes/key.h" +#include "core/hw/ecc.h" +#include "core/hw/rsa/rsa.h" +#include "core/hw/unique_data.h" +#include "core/loader/loader.h" + +namespace HW::UniqueData { + +static SecureInfoA secure_info_a; +static bool secure_info_a_signature_valid = false; +static LocalFriendCodeSeedB local_friend_code_seed_b; +static bool local_friend_code_seed_b_signature_valid = false; +static FileSys::OTP otp; +static FileSys::Certificate ct_cert; +static MovableSedFull movable; +static bool movable_signature_valid = false; + +bool SecureInfoA::VerifySignature() const { + return HW::RSA::GetSecureInfoSlot().Verify( + std::span(reinterpret_cast(&body), sizeof(body)), signature); +} + +bool LocalFriendCodeSeedB::VerifySignature() const { + return HW::RSA::GetLocalFriendCodeSeedSlot().Verify( + std::span(reinterpret_cast(&body), sizeof(body)), signature); +} + +bool MovableSed::VerifySignature() const { + return lfcs.VerifySignature(); +} + +SecureDataLoadStatus LoadSecureInfoA() { + if (secure_info_a.IsValid()) { + return secure_info_a_signature_valid ? SecureDataLoadStatus::Loaded + : SecureDataLoadStatus::InvalidSignature; + } + std::string file_path = GetSecureInfoAPath(); + if (!FileUtil::Exists(file_path)) { + return SecureDataLoadStatus::NotFound; + } + FileUtil::IOFile file(file_path, "rb"); + if (!file.IsOpen()) { + return SecureDataLoadStatus::IOError; + } + if (file.GetSize() != sizeof(SecureInfoA)) { + return SecureDataLoadStatus::Invalid; + } + if (file.ReadBytes(&secure_info_a, sizeof(SecureInfoA)) != sizeof(SecureInfoA)) { + secure_info_a.Invalidate(); + return SecureDataLoadStatus::IOError; + } + + secure_info_a_signature_valid = secure_info_a.VerifySignature(); + if (!secure_info_a_signature_valid) { + LOG_WARNING(HW, "SecureInfo_A signature check failed"); + } + + return secure_info_a_signature_valid ? SecureDataLoadStatus::Loaded + : SecureDataLoadStatus::InvalidSignature; +} + +SecureDataLoadStatus LoadLocalFriendCodeSeedB() { + if (local_friend_code_seed_b.IsValid()) { + return local_friend_code_seed_b_signature_valid ? SecureDataLoadStatus::Loaded + : SecureDataLoadStatus::InvalidSignature; + } + std::string file_path = GetLocalFriendCodeSeedBPath(); + if (!FileUtil::Exists(file_path)) { + return SecureDataLoadStatus::NotFound; + } + FileUtil::IOFile file(file_path, "rb"); + if (!file.IsOpen()) { + return SecureDataLoadStatus::IOError; + } + if (file.GetSize() != sizeof(LocalFriendCodeSeedB)) { + return SecureDataLoadStatus::Invalid; + } + if (file.ReadBytes(&local_friend_code_seed_b, sizeof(LocalFriendCodeSeedB)) != + sizeof(LocalFriendCodeSeedB)) { + local_friend_code_seed_b.Invalidate(); + return SecureDataLoadStatus::IOError; + } + + local_friend_code_seed_b_signature_valid = local_friend_code_seed_b.VerifySignature(); + if (!local_friend_code_seed_b_signature_valid) { + LOG_WARNING(HW, "LocalFriendCodeSeed_B signature check failed"); + } + + return local_friend_code_seed_b_signature_valid ? SecureDataLoadStatus::Loaded + : SecureDataLoadStatus::InvalidSignature; +} + +SecureDataLoadStatus LoadOTP() { + if (otp.Valid()) { + return SecureDataLoadStatus::Loaded; + } + + const std::string filepath = GetOTPPath(); + + auto otp_keyiv = HW::AES::GetOTPKeyIV(); + + auto loader_status = otp.Load(filepath, otp_keyiv.first, otp_keyiv.second); + if (loader_status != Loader::ResultStatus::Success) { + otp.Invalidate(); + ct_cert.Invalidate(); + return loader_status == Loader::ResultStatus::ErrorNotFound ? SecureDataLoadStatus::NotFound + : SecureDataLoadStatus::Invalid; + } + + constexpr const char* issuer_ret = "Nintendo CA - G3_NintendoCTR2prod"; + constexpr const char* issuer_dev = "Nintendo CA - G3_NintendoCTR2dev"; + std::array issuer = {0}; + if (otp.IsDev()) { + memcpy(issuer.data(), issuer_dev, strlen(issuer_dev)); + } else { + memcpy(issuer.data(), issuer_ret, strlen(issuer_ret)); + } + std::string name_str = fmt::format("CT{:08X}-{:02X}", otp.GetDeviceID(), otp.GetSystemType()); + std::array name = {0}; + memcpy(name.data(), name_str.data(), name_str.size()); + + ct_cert.BuildECC(issuer, name, otp.GetCTCertExpiration(), + HW::ECC::CreateECCPrivateKey(otp.GetCTCertPrivateKey(), true), + HW::ECC::CreateECCSignature(otp.GetCTCertSignature())); + + if (!ct_cert.VerifyMyself(HW::ECC::GetRootPublicKey())) { + LOG_ERROR(HW, "CTCert failed verification"); + otp.Invalidate(); + ct_cert.Invalidate(); + return SecureDataLoadStatus::IOError; + } + + return SecureDataLoadStatus::Loaded; +} + +SecureDataLoadStatus LoadMovable() { + if (movable.IsValid()) { + return movable_signature_valid ? SecureDataLoadStatus::Loaded + : SecureDataLoadStatus::InvalidSignature; + } + std::string file_path = GetMovablePath(); + if (!FileUtil::Exists(file_path)) { + return SecureDataLoadStatus::NotFound; + } + FileUtil::IOFile file(file_path, "rb"); + if (!file.IsOpen()) { + return SecureDataLoadStatus::IOError; + } + if (file.GetSize() != sizeof(MovableSedFull)) { + if (file.GetSize() == sizeof(MovableSed)) { + LOG_WARNING(HW, "Uninitialized movable.sed files are not supported"); + } + return SecureDataLoadStatus::Invalid; + } + if (file.ReadBytes(&movable, sizeof(MovableSedFull)) != sizeof(MovableSedFull)) { + movable.Invalidate(); + return SecureDataLoadStatus::IOError; + } + + movable_signature_valid = movable.VerifySignature(); + if (!movable_signature_valid) { + LOG_WARNING(HW, "LocalFriendCodeSeed_B signature check failed"); + } + + return movable_signature_valid ? SecureDataLoadStatus::Loaded + : SecureDataLoadStatus::InvalidSignature; +} + +std::string GetSecureInfoAPath() { + return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/SecureInfo_A"; +} + +std::string GetLocalFriendCodeSeedBPath() { + return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/LocalFriendCodeSeed_B"; +} + +std::string GetOTPPath() { + return FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + "otp.bin"; +} + +std::string GetMovablePath() { + return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "private/movable.sed"; +} + +SecureInfoA& GetSecureInfoA() { + LoadSecureInfoA(); + + return secure_info_a; +} + +LocalFriendCodeSeedB& GetLocalFriendCodeSeedB() { + LoadLocalFriendCodeSeedB(); + + return local_friend_code_seed_b; +} + +FileSys::Certificate& HW::UniqueData::GetCTCert() { + LoadOTP(); + + return ct_cert; +} + +FileSys::OTP& GetOTP() { + LoadOTP(); + + return otp; +} +MovableSedFull& GetMovableSed() { + LoadMovable(); + + return movable; +} +void InvalidateSecureData() { + secure_info_a.Invalidate(); + local_friend_code_seed_b.Invalidate(); + otp.Invalidate(); + ct_cert.Invalidate(); + movable.Invalidate(); +} + +} // namespace HW::UniqueData \ No newline at end of file diff --git a/src/core/hw/unique_data.h b/src/core/hw/unique_data.h new file mode 100644 index 000000000..afb5fd3fc --- /dev/null +++ b/src/core/hw/unique_data.h @@ -0,0 +1,133 @@ +// Copyright 2024 Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/common_types.h" + +namespace FileSys { +class Certificate; +class OTP; +} // namespace FileSys + +namespace HW::UniqueData { + +struct SecureInfoA { + std::array signature; + struct { + u8 region; + u8 unknown; + std::array serial_number; + } body; + + bool IsValid() const { + for (auto c : body.serial_number) { + if (c != 0) { + return true; + } + } + + return false; + } + + void Invalidate() { + memset(body.serial_number.data(), 0, body.serial_number.size()); + } + + bool VerifySignature() const; +}; +static_assert(sizeof(SecureInfoA) == 0x111); + +struct LocalFriendCodeSeedB { + std::array signature; + struct { + u64 unknown; + u64 friend_code_seed; + } body; + + bool IsValid() const { + return body.friend_code_seed != 0; + } + + void Invalidate() { + body.friend_code_seed = 0; + } + + bool VerifySignature() const; +}; +static_assert(sizeof(LocalFriendCodeSeedB) == 0x110); + +struct MovableSed { + static constexpr std::array seed_magic{0x53, 0x45, 0x45, 0x44}; + + std::array magic; + u32 seed_info; + LocalFriendCodeSeedB lfcs; + std::array key_y; + + bool IsValid() const { + return magic == seed_magic; + } + + void Invalidate() { + magic = {0x0, 0x0, 0x0, 0x0}; + } + + bool VerifySignature() const; +}; +static_assert(sizeof(MovableSed) == 0x120); + +struct MovableSedFull { + struct { + MovableSed sed; + std::array unknown; + } body; + std::array aes_mac; + + bool IsValid() const { + return body.sed.magic == MovableSed::seed_magic; + } + + void Invalidate() { + body.sed.magic = {0x0, 0x0, 0x0, 0x0}; + } + + bool VerifySignature() const { + // TODO(PabloMK7): Implement AES MAC verification + return body.sed.VerifySignature(); + } +}; +static_assert(sizeof(MovableSedFull) == 0x140); + +enum class SecureDataLoadStatus { + Loaded = 0, + InvalidSignature = 1, + + NotFound = -1, + Invalid = -2, + IOError = -3, +}; + +SecureDataLoadStatus LoadSecureInfoA(); +SecureDataLoadStatus LoadLocalFriendCodeSeedB(); +SecureDataLoadStatus LoadOTP(); +SecureDataLoadStatus LoadMovable(); + +std::string GetSecureInfoAPath(); +std::string GetLocalFriendCodeSeedBPath(); +std::string GetOTPPath(); + +std::string GetMovablePath(); + +SecureInfoA& GetSecureInfoA(); +LocalFriendCodeSeedB& GetLocalFriendCodeSeedB(); +FileSys::Certificate& GetCTCert(); +FileSys::OTP& GetOTP(); +MovableSedFull& GetMovableSed(); + +void InvalidateSecureData(); +} // namespace HW::UniqueData \ No newline at end of file diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index a47d5ae1d..20824944d 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -75,6 +75,7 @@ enum class ResultStatus { ErrorEncrypted, ErrorGbaTitle, ErrorArtic, + ErrorNotFound, }; constexpr u32 MakeMagic(char a, char b, char c, char d) {