diff --git a/README.md b/README.md
index d2227b767..b68033674 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,13 @@
+AzaharPlus is a fork of the Azahar 3DS emulator that restores some features.
+
+Each version is the same as the corresponding version of Azahar exept for these features:
+- Support of 3DS files. If a file works with earlier Citra forks, it works with AzaharPlus.
+- Ability to download system files from official servers. No need for an actual 3DS.
+
+Below is the readme from Azahar, unchanged.
+
+---
+


diff --git a/dist/apple/Info.plist.in b/dist/apple/Info.plist.in
index d12a21f8e..22cfab071 100644
--- a/dist/apple/Info.plist.in
+++ b/dist/apple/Info.plist.in
@@ -31,6 +31,7 @@
CFBundleTypeExtensions
+ 3ds3dsxccicxi
diff --git a/dist/org.azahar_emu.Azahar.xml b/dist/org.azahar_emu.Azahar.xml
index 6f884c9ea..0fa7afbae 100644
--- a/dist/org.azahar_emu.Azahar.xml
+++ b/dist/org.azahar_emu.Azahar.xml
@@ -16,6 +16,7 @@
CTR Cart Image
+
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index 5a1ef0502..3c6399c66 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -63,6 +63,7 @@ android {
// The application ID refers to Lime3DS to allow for
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
applicationId = "io.github.lime3ds.android"
+
minSdk = 28
targetSdk = 35
versionCode = autoVersion
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
index 583e59863..5baa05b64 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
@@ -186,6 +186,8 @@ object NativeLibrary {
external fun unlinkConsole()
+ external fun downloadTitleFromNus(title: Long): InstallStatus
+
private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object()
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt
new file mode 100644
index 000000000..3f5abfd14
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt
@@ -0,0 +1,152 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.citra.citra_emu.NativeLibrary.InstallStatus
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.DialogProgressBarBinding
+import org.citra.citra_emu.viewmodel.GamesViewModel
+import org.citra.citra_emu.viewmodel.SystemFilesViewModel
+
+class DownloadSystemFilesDialogFragment : DialogFragment() {
+ private var _binding: DialogProgressBarBinding? = null
+ private val binding get() = _binding!!
+
+ private val downloadViewModel: SystemFilesViewModel by activityViewModels()
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ private lateinit var titles: LongArray
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ _binding = DialogProgressBarBinding.inflate(layoutInflater)
+
+ titles = requireArguments().getLongArray(TITLES)!!
+
+ binding.progressText.visibility = View.GONE
+
+ binding.progressBar.min = 0
+ binding.progressBar.max = titles.size
+ if (downloadViewModel.isDownloading.value != true) {
+ binding.progressBar.progress = 0
+ }
+
+ isCancelable = false
+ return MaterialAlertDialogBuilder(requireContext())
+ .setView(binding.root)
+ .setTitle(R.string.downloading_files)
+ .setMessage(R.string.downloading_files_description)
+ .setNegativeButton(android.R.string.cancel, null)
+ .create()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ downloadViewModel.progress.collectLatest { binding.progressBar.progress = it }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ downloadViewModel.result.collect {
+ when (it) {
+ InstallStatus.Success -> {
+ downloadViewModel.clear()
+ dismiss()
+ MessageDialogFragment.newInstance(R.string.download_success, 0)
+ .show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
+ gamesViewModel.setShouldSwapData(true)
+ }
+
+ InstallStatus.ErrorFailedToOpenFile,
+ InstallStatus.ErrorEncrypted,
+ InstallStatus.ErrorFileNotFound,
+ InstallStatus.ErrorInvalid,
+ InstallStatus.ErrorAborted -> {
+ downloadViewModel.clear()
+ dismiss()
+ MessageDialogFragment.newInstance(
+ R.string.download_failed,
+ R.string.download_failed_description
+ ).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
+ gamesViewModel.setShouldSwapData(true)
+ }
+
+ InstallStatus.Cancelled -> {
+ downloadViewModel.clear()
+ dismiss()
+ MessageDialogFragment.newInstance(
+ R.string.download_cancelled,
+ R.string.download_cancelled_description
+ ).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
+ }
+
+ // Do nothing on null
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ // Consider using WorkManager here. While the home menu can only really amount to
+ // about 150MBs, this could be a problem on inconsistent networks
+ downloadViewModel.download(titles)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val alertDialog = dialog as AlertDialog
+ val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
+ negativeButton.setOnClickListener {
+ downloadViewModel.cancel()
+ dialog?.setTitle(R.string.cancelling)
+ binding.progressBar.isIndeterminate = true
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ companion object {
+ const val TAG = "DownloadSystemFilesDialogFragment"
+
+ const val TITLES = "Titles"
+
+ fun newInstance(titles: LongArray): DownloadSystemFilesDialogFragment {
+ val dialog = DownloadSystemFilesDialogFragment()
+ val args = Bundle()
+ args.putLongArray(TITLES, titles)
+ dialog.arguments = args
+ return dialog
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt
index 382a42773..2fcab82e6 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt
@@ -45,7 +45,7 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
- private var show3DSFileWarning: Boolean = true
+ private var show3DSFileWarning: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
index 432a06aa0..e7ff2ba34 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
@@ -1,4 +1,4 @@
-// Copyright Citra Emulator Project / Azahar Emulator Project
+// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -121,14 +121,8 @@ class HomeSettingsFragment : Fragment() {
}
),
HomeSetting(
- R.string.install_game_content,
- R.string.install_game_content_description,
- R.drawable.ic_install,
- { mainActivity.ciaFileInstaller.launch(true) }
- ),
- HomeSetting(
- R.string.setup_system_files,
- R.string.setup_system_files_description,
+ R.string.system_files,
+ R.string.system_files_description,
R.drawable.ic_system_update,
{
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@@ -136,6 +130,12 @@ class HomeSettingsFragment : Fragment() {
?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment)
}
),
+ HomeSetting(
+ R.string.install_game_content,
+ R.string.install_game_content_description,
+ R.drawable.ic_install,
+ { mainActivity.ciaFileInstaller.launch(true) }
+ ),
HomeSetting(
R.string.share_log,
R.string.share_log_description,
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
index 7b76149d3..4c79082f1 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
@@ -1,61 +1,76 @@
-// Copyright Citra Emulator Project / Azahar Emulator Project
+// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.fragments
-import android.content.DialogInterface
+import android.content.res.Resources
import android.os.Bundle
+import android.text.Html
import android.text.method.LinkMovementMethod
-import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.AdapterView
import android.widget.ArrayAdapter
-import android.widget.FrameLayout
-import android.widget.LinearLayout
-import android.widget.RadioButton
-import android.widget.RadioGroup
-import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
-import androidx.core.text.HtmlCompat
-import androidx.core.widget.doOnTextChanged
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.progressindicator.CircularProgressIndicator
-import com.google.android.material.textview.MaterialTextView
+import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.transition.MaterialSharedAxis
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
-import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
+import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.viewmodel.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel
+import org.citra.citra_emu.viewmodel.SystemFilesViewModel
class SystemFilesFragment : Fragment() {
private var _binding: FragmentSystemFilesBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
+ private val systemFilesViewModel: SystemFilesViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
+ private lateinit var regionValues: IntArray
+
+ private val systemTypeDropdown = DropdownItem(R.array.systemFileTypeValues)
+ private val systemRegionDropdown = DropdownItem(R.array.systemFileRegionValues)
+
+ private val SYS_TYPE = "SysType"
+ private val REGION = "Region"
private val REGION_START = "RegionStart"
private val homeMenuMap: MutableMap = mutableMapOf()
- private var setupStateCached: BooleanArray? = null
- private lateinit var regionValues: IntArray
+
+ private val WARNING_SHOWN = "SystemFilesWarningShown"
+
+ private class DropdownItem(val valuesId: Int) : AdapterView.OnItemClickListener {
+ var position = 0
+
+ fun getValue(resources: Resources): Int {
+ return resources.getIntArray(valuesId)[position]
+ }
+
+ override fun onItemClick(p0: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ this.position = position
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -77,15 +92,61 @@ class SystemFilesFragment : Fragment() {
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false)
+ val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+ if (!preferences.getBoolean(WARNING_SHOWN, false)) {
+ MessageDialogFragment.newInstance(
+ R.string.home_menu_warning,
+ R.string.home_menu_warning_description
+ ).show(childFragmentManager, MessageDialogFragment.TAG)
+ preferences.edit()
+ .putBoolean(WARNING_SHOWN, true)
+ .apply()
+ }
+
+ binding.toolbarSystemFiles.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
// TODO: Remove workaround for text filtering issue in material components when fixed
// https://github.com/material-components/material-components-android/issues/1464
+ binding.dropdownSystemType.isSaveEnabled = false
+ binding.dropdownSystemRegion.isSaveEnabled = false
binding.dropdownSystemRegionStart.isSaveEnabled = false
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ systemFilesViewModel.shouldRefresh.collect {
+ if (it) {
+ reloadUi()
+ systemFilesViewModel.setShouldRefresh(false)
+ }
+ }
+ }
+ }
+
reloadUi()
if (savedInstanceState != null) {
+ setDropdownSelection(
+ binding.dropdownSystemType,
+ systemTypeDropdown,
+ savedInstanceState.getInt(SYS_TYPE)
+ )
+ setDropdownSelection(
+ binding.dropdownSystemRegion,
+ systemRegionDropdown,
+ savedInstanceState.getInt(REGION)
+ )
binding.dropdownSystemRegionStart
.setText(savedInstanceState.getString(REGION_START), false)
}
+
+ setInsets()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putInt(SYS_TYPE, systemTypeDropdown.position)
+ outState.putInt(REGION, systemRegionDropdown.position)
+ outState.putString(REGION_START, binding.dropdownSystemRegionStart.text.toString())
}
override fun onResume() {
@@ -98,41 +159,6 @@ class SystemFilesFragment : Fragment() {
SystemSaveGame.save()
}
- private fun showProgressDialog(
- main_title: CharSequence,
- main_text: CharSequence
- ): AlertDialog? {
- val context = requireContext()
- val progressIndicator = CircularProgressIndicator(context).apply {
- isIndeterminate = true
- layoutParams = FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.WRAP_CONTENT,
- FrameLayout.LayoutParams.WRAP_CONTENT,
- Gravity.CENTER // Center the progress indicator
- ).apply {
- setMargins(50, 50, 50, 50) // Add margins (left, top, right, bottom)
- }
- }
-
- val pleaseWaitText = MaterialTextView(context).apply {
- text = main_text
- }
-
- val container = LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- gravity = Gravity.CENTER
- setPadding(40, 40, 40, 40) // Optional: Add padding to the entire layout
- addView(pleaseWaitText)
- addView(progressIndicator)
- }
-
- return MaterialAlertDialogBuilder(context)
- .setTitle(main_title)
- .setView(container)
- .setCancelable(false)
- .show()
- }
-
private fun reloadUi() {
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
@@ -150,167 +176,31 @@ class SystemFilesFragment : Fragment() {
gamesViewModel.setShouldSwapData(true)
}
- binding.setupSystemFilesDescription?.apply {
- text = HtmlCompat.fromHtml(
- context.getString(R.string.setup_system_files_preamble),
- HtmlCompat.FROM_HTML_MODE_COMPACT
- )
- movementMethod = LinkMovementMethod.getInstance()
- }
-
- binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
- binding.buttonUnlinkConsoleData.setOnClickListener {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.delete_system_files)
- .setMessage(HtmlCompat.fromHtml(
- requireContext().getString(R.string.delete_system_files_description),
- HtmlCompat.FROM_HTML_MODE_COMPACT
- ))
- .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
- NativeLibrary.unlinkConsole()
- binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
- }
- .setNegativeButton(android.R.string.cancel, null)
- .show()
- }
-
- binding.buttonSetUpSystemFiles.setOnClickListener {
- val inflater = LayoutInflater.from(context)
- val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
- var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
-
- val progressDialog = showProgressDialog(
- getText(R.string.setup_system_files),
- getString(R.string.setup_system_files_detect)
- )
-
- CoroutineScope(Dispatchers.IO).launch {
- val setupState = setupStateCached ?: NativeLibrary.areSystemTitlesInstalled().also {
- setupStateCached = it
- }
-
- withContext(Dispatchers.Main) {
- progressDialog?.dismiss()
-
- inputBinding.editTextInput.setText(textInputValue)
- inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
- textInputValue = text.toString()
- }
-
- val buttonGroup = context?.let { it1 -> RadioGroup(it1) }!!
-
- val buttonO3ds = context?.let { it1 ->
- RadioButton(it1).apply {
- text = context.getString(R.string.setup_system_files_o3ds)
- isChecked = false
- }
- }!!
-
- val buttonN3ds = context?.let { it1 ->
- RadioButton(it1).apply {
- text = context.getString(R.string.setup_system_files_n3ds)
- isChecked = false
- }
- }!!
-
- val textO3ds: String
- val textN3ds: String
-
- val colorO3ds: Int
- val colorN3ds: Int
-
- if (!setupStateCached!![0]) {
- textO3ds = getString(R.string.setup_system_files_possible)
- colorO3ds = R.color.citra_primary_blue
-
- textN3ds = getString(R.string.setup_system_files_o3ds_needed)
- colorN3ds = R.color.citra_primary_yellow
-
- buttonN3ds.isEnabled = false
- } else {
- textO3ds = getString(R.string.setup_system_files_completed)
- colorO3ds = R.color.citra_primary_green
-
- if (!setupStateCached!![1]) {
- textN3ds = getString(R.string.setup_system_files_possible)
- colorN3ds = R.color.citra_primary_blue
- } else {
- textN3ds = getString(R.string.setup_system_files_completed)
- colorN3ds = R.color.citra_primary_green
- }
- }
-
- val tooltipO3ds = context?.let { it1 ->
- MaterialTextView(it1).apply {
- text = textO3ds
- textSize = 12f
- setTextColor(ContextCompat.getColor(requireContext(), colorO3ds))
- }
- }
-
- val tooltipN3ds = context?.let { it1 ->
- MaterialTextView(it1).apply {
- text = textN3ds
- textSize = 12f
- setTextColor(ContextCompat.getColor(requireContext(), colorN3ds))
- }
- }
-
- buttonGroup.apply {
- addView(buttonO3ds)
- addView(tooltipO3ds)
- addView(buttonN3ds)
- addView(tooltipN3ds)
- }
-
- inputBinding.root.apply {
- addView(buttonGroup)
- }
-
- val dialog = context?.let {
- MaterialAlertDialogBuilder(it)
- .setView(inputBinding.root)
- .setTitle(getString(R.string.setup_system_files_enter_address))
- .setPositiveButton(android.R.string.ok) { diag, _ ->
- if (textInputValue.isNotEmpty() && !(!buttonO3ds.isChecked && !buttonN3ds.isChecked)) {
- preferences.edit()
- .putString("last_artic_base_addr", textInputValue)
- .apply()
- val menu = Game(
- title = getString(R.string.artic_base),
- path = if (buttonO3ds.isChecked) {
- "articinio://$textInputValue"
- } else {
- "articinin://$textInputValue"
- },
- filename = ""
- )
- val progressDialog2 = showProgressDialog(
- getText(R.string.setup_system_files),
- getString(
- R.string.setup_system_files_preparing
- )
- )
-
- CoroutineScope(Dispatchers.IO).launch {
- NativeLibrary.uninstallSystemFiles(buttonO3ds.isChecked)
- withContext(Dispatchers.Main) {
- setupStateCached = null
- progressDialog2?.dismiss()
- val action =
- HomeNavigationDirections.actionGlobalEmulationActivity(
- menu
- )
- binding.root.findNavController().navigate(action)
- }
- }
- }
- }
- .setNegativeButton(android.R.string.cancel) { _, _ -> }
- .show()
- }
- }
+ if (!NativeLibrary.areKeysAvailable()) {
+ binding.apply {
+ systemType.isEnabled = false
+ systemRegion.isEnabled = false
+ buttonDownloadHomeMenu.isEnabled = false
+ textKeysMissing.visibility = View.VISIBLE
+ textKeysMissingHelp.visibility = View.VISIBLE
+ textKeysMissingHelp.text =
+ Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY)
+ textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance()
}
+ } else {
+ populateDownloadOptions()
+ }
+
+ binding.buttonDownloadHomeMenu.setOnClickListener {
+ val titleIds = NativeLibrary.getSystemTitleIds(
+ systemTypeDropdown.getValue(resources),
+ systemRegionDropdown.getValue(resources)
+ )
+
+ DownloadSystemFilesDialogFragment.newInstance(titleIds).show(
+ childFragmentManager,
+ DownloadSystemFilesDialogFragment.TAG
+ )
}
populateHomeMenuOptions()
@@ -326,6 +216,51 @@ class SystemFilesFragment : Fragment() {
}
}
+ private fun populateDropdown(
+ dropdown: MaterialAutoCompleteTextView,
+ valuesId: Int,
+ dropdownItem: DropdownItem
+ ) {
+ val valuesAdapter = ArrayAdapter.createFromResource(
+ requireContext(),
+ valuesId,
+ R.layout.support_simple_spinner_dropdown_item
+ )
+ dropdown.setAdapter(valuesAdapter)
+ dropdown.onItemClickListener = dropdownItem
+ }
+
+ private fun setDropdownSelection(
+ dropdown: MaterialAutoCompleteTextView,
+ dropdownItem: DropdownItem,
+ selection: Int
+ ) {
+ if (dropdown.adapter != null) {
+ dropdown.setText(dropdown.adapter.getItem(selection).toString(), false)
+ }
+ dropdownItem.position = selection
+ }
+
+ private fun populateDownloadOptions() {
+ populateDropdown(binding.dropdownSystemType, R.array.systemFileTypes, systemTypeDropdown)
+ populateDropdown(
+ binding.dropdownSystemRegion,
+ R.array.systemFileRegions,
+ systemRegionDropdown
+ )
+
+ setDropdownSelection(
+ binding.dropdownSystemType,
+ systemTypeDropdown,
+ systemTypeDropdown.position
+ )
+ setDropdownSelection(
+ binding.dropdownSystemRegion,
+ systemRegionDropdown,
+ systemRegionDropdown.position
+ )
+ }
+
private fun populateHomeMenuOptions() {
regionValues = resources.getIntArray(R.array.systemFileRegionValues)
val regionEntries = resources.getStringArray(R.array.systemFileRegions)
@@ -350,4 +285,30 @@ class SystemFilesFragment : Fragment() {
binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false)
}
}
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpAppBar = binding.toolbarSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
+ mlpAppBar.leftMargin = leftInsets
+ mlpAppBar.rightMargin = rightInsets
+ binding.toolbarSystemFiles.layoutParams = mlpAppBar
+
+ val mlpScrollSystemFiles =
+ binding.scrollSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
+ mlpScrollSystemFiles.leftMargin = leftInsets
+ mlpScrollSystemFiles.rightMargin = rightInsets
+ binding.scrollSystemFiles.layoutParams = mlpScrollSystemFiles
+
+ binding.scrollSystemFiles.updatePadding(bottom = barInsets.bottom)
+
+ windowInsets
+ }
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt
index 7fb6825dd..689b4ec25 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt
@@ -63,7 +63,7 @@ class Game(
val allExtensions: Set get() = extensions + badExtensions
val extensions: Set = HashSet(
- listOf("3dsx", "elf", "axf", "cci", "cxi", "app")
+ listOf("3ds", "3dsx", "elf", "axf", "cci", "cxi", "app")
)
val badExtensions: Set = HashSet(
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt
new file mode 100644
index 000000000..d4f654d5c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt
@@ -0,0 +1,139 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.viewmodel
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.NativeLibrary.InstallStatus
+import org.citra.citra_emu.utils.Log
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.CoroutineContext
+import kotlin.math.min
+
+class SystemFilesViewModel : ViewModel() {
+ private var job: Job
+ private val coroutineContext: CoroutineContext
+ get() = Dispatchers.IO + job
+
+ val isDownloading get() = _isDownloading.asStateFlow()
+ private val _isDownloading = MutableStateFlow(false)
+
+ val progress get() = _progress.asStateFlow()
+ private val _progress = MutableStateFlow(0)
+
+ val result get() = _result.asStateFlow()
+ private val _result = MutableStateFlow(null)
+
+ val shouldRefresh get() = _shouldRefresh.asStateFlow()
+ private val _shouldRefresh = MutableStateFlow(false)
+
+ private var cancelled = false
+
+ private val RETRY_AMOUNT = 3
+
+ init {
+ job = Job()
+ clear()
+ }
+
+ fun setShouldRefresh(refresh: Boolean) {
+ _shouldRefresh.value = refresh
+ }
+
+ fun setProgress(progress: Int) {
+ _progress.value = progress
+ }
+
+ fun download(titles: LongArray) {
+ if (isDownloading.value) {
+ return
+ }
+ clear()
+ _isDownloading.value = true
+ Log.debug("System menu download started.")
+
+ val minExecutors = min(Runtime.getRuntime().availableProcessors(), titles.size)
+ val segment = (titles.size / minExecutors)
+ val atomicProgress = AtomicInteger(0)
+ for (i in 0 until minExecutors) {
+ val titlesSegment = if (i < minExecutors - 1) {
+ titles.copyOfRange(i * segment, (i + 1) * segment)
+ } else {
+ titles.copyOfRange(i * segment, titles.size)
+ }
+
+ CoroutineScope(coroutineContext).launch {
+ titlesSegment.forEach { title: Long ->
+ // Notify UI of cancellation before ending coroutine
+ if (cancelled) {
+ _result.value = InstallStatus.ErrorAborted
+ cancelled = false
+ }
+
+ // Takes a moment to see if the coroutine was cancelled
+ yield()
+
+ // Retry downloading a title repeatedly
+ for (j in 0 until RETRY_AMOUNT) {
+ val result = tryDownloadTitle(title)
+ if (result == InstallStatus.Success) {
+ break
+ } else if (j == RETRY_AMOUNT - 1) {
+ _result.value = result
+ return@launch
+ }
+ Log.warning("Download for title{$title} failed, retrying in 3s...")
+ delay(3000L)
+ }
+
+ Log.debug("Successfully installed title - $title")
+ setProgress(atomicProgress.incrementAndGet())
+
+ Log.debug("System File Progress - ${atomicProgress.get()} / ${titles.size}")
+ if (atomicProgress.get() == titles.size) {
+ _result.value = InstallStatus.Success
+ setShouldRefresh(true)
+ }
+ }
+ }
+ }
+ }
+
+ private fun tryDownloadTitle(title: Long): InstallStatus {
+ val result = NativeLibrary.downloadTitleFromNus(title)
+ if (result != InstallStatus.Success) {
+ Log.error("Failed to install title $title with error - $result")
+ }
+ return result
+ }
+
+ fun clear() {
+ Log.debug("Clearing")
+ job.cancelChildren()
+ job = Job()
+ _progress.value = 0
+ _result.value = null
+ _isDownloading.value = false
+ cancelled = false
+ }
+
+ fun cancel() {
+ Log.debug("Canceling system file download.")
+ cancelled = true
+ job.cancelChildren()
+ job = Job()
+ _progress.value = 0
+ _result.value = InstallStatus.Cancelled
+ }
+}
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 02cc329af..0b1d6f4dc 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -464,6 +464,17 @@ void Java_org_citra_citra_1emu_NativeLibrary_uninstallSystemFiles(JNIEnv* env,
: Core::SystemTitleSet::New3ds);
}
+jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jobject obj,
+ jlong title) {
+ [[maybe_unused]] const auto title_id = static_cast(title);
+ Service::AM::InstallStatus status = Service::AM::InstallFromNus(title_id);
+ if (status != Service::AM::InstallStatus::Success) {
+ return IDCache::GetJavaCiaInstallStatus(status);
+ }
+ return IDCache::GetJavaCiaInstallStatus(Service::AM::InstallStatus::Success);
+}
+
[[maybe_unused]] static bool CheckKgslPresent() {
constexpr auto KgslPath{"/dev/kgsl-3d0"};
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml b/src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml
new file mode 100644
index 000000000..6c833f876
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/fragment_system_files.xml b/src/android/app/src/main/res/layout/fragment_system_files.xml
index bae1cda1c..734579956 100644
--- a/src/android/app/src/main/res/layout/fragment_system_files.xml
+++ b/src/android/app/src/main/res/layout/fragment_system_files.xml
@@ -18,7 +18,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back"
- app:title="@string/setup_system_files" />
+ app:title="@string/system_files" />
@@ -39,48 +39,77 @@
android:paddingBottom="16dp">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+ android:layout_marginTop="16dp"
+ android:text="@string/download" />
+
+
+
+
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 23bda49f5..4937e61fb 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -297,6 +297,17 @@
6
+
+ @string/system_type_minimal
+ @string/system_type_old_3ds
+ @string/system_type_new_3ds
+
+
+ 1
+ 2
+ 4
+
+
@string/mono@string/stereo
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 225995808..ded70fc01 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -2,7 +2,7 @@
- Azahar
+ AzaharPlusThis software will run applications for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you can begin with emulating, please select a folder to store Azahar\'s user data in.\n\nWhat\'s this:\nWiki - Citra Android user data and storageAzaharAzahar
@@ -148,12 +148,15 @@
System Files
+ System FilesPerform system file operations such as installing system files or booting the Home Menu
+ Download system files to get Mii files, boot the HOME menu, and moreConnect to Artic Setup ToolAzahar Artic Setup Tool. Notes:
This operation will install console unique data to Azahar, do not share your user or nand folders after performing the setup process!
While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the console later from the System Files tab in the emulator options menu.
Do not go online with both Azahar and your 3DS console at the same time after setting up system files, as this could cause issues.
Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended).
Both setup modes will work regardless of the model of the console running the setup tool.
]]>Fetching current system files status, please wait...Unlink Console Unique Data
Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar.
Your friend list will reset and you will be logged out of your NNID/PNID account.
System files and eshop titles obtained through Azahar will become inaccessible until the same console is linked again using the setup tool (save data will not be lost).
Continue?]]>
+ Download System FilesOld 3DS SetupNew 3DS SetupSetup is possible.
@@ -162,9 +165,25 @@
Enter Artic Setup Tool addressPreparing setup, please wait...Boot the HOME Menu
+ System Type
+ Download
+ Azahar is missing keys to download system files.
+ How to get keys?]]>Show HOME menu apps in Applications listRun System Setup when the HOME Menu is launched
+ Minimal
+ Old 3DS
+ New 3DS
+ Downloading Files…
+ Please do not close the app.
+ Download Failed
+ Please make sure you are connected to the internet and try again.
+ Download Complete!
+ Download Cancelled
+ Please restart the download to prevent issues with having incomplete system files.HOME Menu
+ System Files Warning
+ Due to how slow Android\'s storage access framework is for accessing Azahar\'s files, downloading multiple versions of system files can dramatically slow down loading for applications, save states, and the Applications list. Only download the files that you require to avoid any issues with loading speeds.Buttons
diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp
index 94318da55..fda67b54e 100644
--- a/src/citra_qt/citra_qt.cpp
+++ b/src/citra_qt/citra_qt.cpp
@@ -1250,11 +1250,17 @@ bool GMainWindow::LoadROM(const QString& filename) {
break;
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
- QMessageBox::critical(this, tr("App Encrypted"),
- tr("Your app is encrypted. "
- ""
- "Please check our blog for more info."));
+ QMessageBox::critical(
+ this, tr("ROM Encrypted"),
+ tr("Your ROM is encrypted. Please follow the guides to redump your "
+ "game "
+ "cartridges or "
+ "installed "
+ "titles."));
break;
}
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
@@ -2271,11 +2277,10 @@ 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("CIA Encrypted"),
- tr("Your CIA file is encrypted. "
- ""
- "Please check our blog for more info."));
+ QMessageBox::critical(this, tr("Encrypted File"),
+ tr("%1 must be decrypted "
+ "before being used with Azahar. A real 3DS is required.")
+ .arg(filename));
break;
case Service::AM::InstallStatus::ErrorFileNotFound:
QMessageBox::critical(this, tr("Unable to find File"),
@@ -3421,8 +3426,8 @@ static bool IsSingleFileDropEvent(const QMimeData* mime) {
return mime->hasUrls() && mime->urls().length() == 1;
}
-static const std::array AcceptedExtensions = {"cci", "cxi", "bin", "3dsx",
- "app", "elf", "axf"};
+static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin",
+ "3dsx", "app", "elf", "axf"};
static bool IsCorrectFileExtension(const QMimeData* mime) {
const QString& filename = mime->urls().at(0).toLocalFile();
diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/citra_qt/configuration/configure_debug.ui
index 2caafc919..e21b4163b 100644
--- a/src/citra_qt/configuration/configure_debug.ui
+++ b/src/citra_qt/configuration/configure_debug.ui
@@ -217,16 +217,6 @@
-
-
- <html><head/><body>Underclocking can increase performance but may cause the application to freeze.<br/>Overclocking may reduce lag in applications but also might cause freezes</p></body></html>
-
-
- Qt::RichText
-
-
-
- <html><head/><body><p>Enables the use of the ARM JIT compiler for emulating the 3DS CPUs. Don't disable unless for debugging purposes</p></body></html>
@@ -236,14 +226,14 @@
-
+ Enable debug renderer
-
+ Dump command buffers
@@ -253,33 +243,43 @@
+
+
+
+ Miscellaneous
+
+
+
+
+
+ <html><head/><body><p>Introduces a delay to the first ever launched app thread if LLE modules are enabled, to allow them to initialize.</p></body></html>
+
+
+ Delay app start for LLE module initialization
+
+
+
+
+
+
+ Force deterministic async operations
+
+
+ <html><head/><body><p>Forces all async operations to run on the main thread, making them deterministic. Do not enable if you don't know what you are doing.</p></body></html>
+
+
+
+
+
+
-
-
- Miscellaneous
+
+
+ <html><head/><body><p>CPU Clock Speed Information<br/>Underclocking can increase performance but may cause the application to freeze.<br/>Overclocking may reduce lag in applications but also might cause freezes</p></body></html>
+
+
+ Qt::RichText
-
-
-
-
- <html><head/><body><p>Introduces a delay to the first ever launched app thread if LLE modules are enabled, to allow them to initialize.</p></body></html>
-
-
- Delay app start for LLE module initialization
-
-
-
-
-
-
- Force deterministic async operations
-
-
- <html><head/><body><p>Forces all async operations to run on the main thread, making them deterministic. Do not enable if you don't know what you are doing.</p></body></html>
-
-
-
-
diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp
index 4ce036394..99fb6c040 100644
--- a/src/citra_qt/configuration/configure_system.cpp
+++ b/src/citra_qt/configuration/configure_system.cpp
@@ -238,16 +238,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
&ConfigureSystem::RefreshConsoleID);
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
- connect(ui->button_linked_console, &QPushButton::clicked, this,
- &ConfigureSystem::UnlinkConsole);
- connect(ui->combo_country, qOverload(&QComboBox::currentIndexChanged), this,
- [this](int index) {
- CheckCountryValid(static_cast(ui->combo_country->itemData(index).toInt()));
- });
- connect(ui->region_combobox, qOverload(&QComboBox::currentIndexChanged), this,
- [this]([[maybe_unused]] int index) {
- CheckCountryValid(static_cast(ui->combo_country->currentData().toInt()));
- });
+ connect(ui->button_start_download, &QPushButton::clicked, this,
+ &ConfigureSystem::DownloadFromNUS);
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
ui->button_secure_info->setEnabled(false);
@@ -255,7 +247,11 @@ 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);
+#ifdef todotodo
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetSecureInfoAPath());
+#else
+ InstallSecureData(file_path_qtstr.toStdString(), cfg->GetSecureInfoAPath());
+#endif
});
connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] {
ui->button_friend_code_seed->setEnabled(false);
@@ -264,6 +260,7 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A "
"LocalFriendCodeSeed_B);;All Files (*.*)"));
ui->button_friend_code_seed->setEnabled(true);
+#ifdef todotodo
InstallSecureData(file_path_qtstr.toStdString(),
HW::UniqueData::GetLocalFriendCodeSeedBPath());
});
@@ -281,6 +278,16 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
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());
+#else
+ InstallSecureData(file_path_qtstr.toStdString(), cfg->GetLocalFriendCodeSeedBPath());
+ });
+ connect(ui->button_ct_cert, &QPushButton::clicked, this, [this] {
+ ui->button_ct_cert->setEnabled(false);
+ const QString file_path_qtstr = QFileDialog::getOpenFileName(
+ this, tr("Select CTCert"), QString(), tr("CTCert.bin (*.bin);;All Files (*.*)"));
+ ui->button_ct_cert->setEnabled(true);
+ InstallCTCert(file_path_qtstr.toStdString());
+#endif
});
for (u8 i = 0; i < country_names.size(); i++) {
@@ -288,10 +295,36 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
ui->combo_country->addItem(tr(country_names.at(i)), i);
}
}
- ui->label_country_invalid->setVisible(false);
- ui->label_country_invalid->setStyleSheet(QStringLiteral("QLabel { color: #ff3333; }"));
SetupPerGameUI();
+
+ ui->combo_download_set->setCurrentIndex(0); // set to Minimal
+ ui->combo_download_region->setCurrentIndex(0); // set to the base region
+
+ HW::AES::InitKeys(true);
+ bool keys_available = HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) &&
+ HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure2);
+ for (u8 i = 0; i < HW::AES::MaxCommonKeySlot && keys_available; i++) {
+ HW::AES::SelectCommonKeyIndex(i);
+ if (!HW::AES::IsNormalKeyAvailable(HW::AES::KeySlotID::TicketCommonKey)) {
+ keys_available = false;
+ break;
+ }
+ }
+ if (keys_available) {
+ ui->button_start_download->setEnabled(true);
+ ui->combo_download_set->setEnabled(true);
+ ui->combo_download_region->setEnabled(true);
+ ui->label_nus_download->setText(tr("Download System Files from Nintendo servers"));
+ } else {
+ ui->button_start_download->setEnabled(false);
+ ui->combo_download_set->setEnabled(false);
+ ui->combo_download_region->setEnabled(false);
+ ui->label_nus_download->setTextInteractionFlags(Qt::TextBrowserInteraction);
+ ui->label_nus_download->setOpenExternalLinks(true);
+ ui->label_nus_download->setText(tr("Azahar is missing keys to download system files."));
+ }
+
ConfigureTime();
}
@@ -300,19 +333,6 @@ ConfigureSystem::~ConfigureSystem() = default;
void ConfigureSystem::SetConfiguration() {
enabled = !system.IsPoweredOn();
- if (!Settings::IsConfiguringGlobal()) {
- ConfigurationShared::SetHighlight(ui->region_label,
- !Settings::values.region_value.UsingGlobal());
- const bool is_region_global = Settings::values.region_value.UsingGlobal();
- ui->region_combobox->setCurrentIndex(
- is_region_global ? ConfigurationShared::USE_GLOBAL_INDEX
- : static_cast(Settings::values.region_value.GetValue()) +
- ConfigurationShared::USE_GLOBAL_OFFSET + 1);
- } else {
- // The first item is "auto-select" with actual value -1, so plus one here will do the trick
- ui->region_combobox->setCurrentIndex(Settings::values.region_value.GetValue() + 1);
- }
-
ui->combo_init_clock->setCurrentIndex(static_cast(Settings::values.init_clock.GetValue()));
QDateTime date_time;
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
@@ -374,7 +394,6 @@ void ConfigureSystem::ReadSystemSettings() {
// set the country code
country_code = cfg->GetCountryCode();
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
- CheckCountryValid(country_code);
// set whether system setup is needed
system_setup = cfg->IsSystemSetupNeeded();
@@ -391,16 +410,15 @@ void ConfigureSystem::ReadSystemSettings() {
play_coin = Service::PTM::Module::GetPlayCoins();
ui->spinBox_play_coins->setValue(play_coin);
+ // set firmware download region
+ ui->combo_download_region->setCurrentIndex(static_cast(cfg->GetRegionValue()));
+
// Refresh secure data status
RefreshSecureDataStatus();
}
void ConfigureSystem::ApplyConfiguration() {
if (enabled) {
- ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value,
- ui->region_combobox,
- [](s32 index) { return index - 1; });
-
bool modified = false;
// apply username
@@ -591,51 +609,6 @@ void ConfigureSystem::RefreshMAC() {
ui->label_mac->setText(tr("MAC: %1").arg(QString::fromStdString(mac_address)));
}
-void ConfigureSystem::UnlinkConsole() {
- QMessageBox::StandardButton reply;
- QString warning_text =
- tr("This action will unlink your real console from Azahar, with the following "
- "consequences:
Your OTP, SecureInfo and LocalFriendCodeSeed will be removed "
- "from Azahar.
Your friend list will reset and you will be logged out of your "
- "NNID/PNID account.
System files and eshop titles obtained through Azahar will "
- "become inaccessible until the same console is linked again (save data will not be "
- "lost).
Continue?");
- reply =
- QMessageBox::warning(this, tr("Warning"), warning_text, QMessageBox::No | QMessageBox::Yes);
- if (reply == QMessageBox::No) {
- return;
- }
-
- HW::UniqueData::UnlinkConsole();
- RefreshSecureDataStatus();
-}
-
-void ConfigureSystem::CheckCountryValid(u8 country) {
- // TODO(PabloMK7): Make this per-game compatible
- if (!Settings::IsConfiguringGlobal())
- return;
-
- s32 region = ui->region_combobox->currentIndex() - 1;
- QString label_text;
-
- if (region != Settings::REGION_VALUE_AUTO_SELECT &&
- !cfg->IsValidRegionCountry(static_cast(region), country)) {
- label_text = tr("Invalid country for configured region");
- }
- if (HW::UniqueData::GetSecureInfoA().IsValid()) {
- region = static_cast(cfg->GetRegionValue(true));
- if (!cfg->IsValidRegionCountry(static_cast(region), country)) {
- if (!label_text.isEmpty()) {
- label_text += QString::fromStdString("\n");
- }
- label_text += tr("Invalid country for console unique data");
- }
- }
-
- ui->label_country_invalid->setText(label_text);
- ui->label_country_invalid->setVisible(!label_text.isEmpty());
-}
-
void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) {
std::string from =
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
@@ -646,9 +619,24 @@ void ConfigureSystem::InstallSecureData(const std::string& from_path, const std:
FileUtil::CreateFullPath(to);
FileUtil::Copy(from, to);
HW::UniqueData::InvalidateSecureData();
+ 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::Copy(from, to);
+ RefreshSecureDataStatus();
+}
+
+// todotodo
+#ifdef todotodo
void ConfigureSystem::RefreshSecureDataStatus() {
auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) {
switch (status) {
@@ -676,16 +664,38 @@ void ConfigureSystem::RefreshSecureDataStatus() {
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()));
-
- if (HW::UniqueData::IsFullConsoleLinked()) {
- ui->linked_console->setVisible(true);
- ui->button_otp->setEnabled(false);
- ui->button_secure_info->setEnabled(false);
- ui->button_friend_code_seed->setEnabled(false);
- } else {
- ui->linked_console->setVisible(false);
- }
}
+#endif
+
+//--
+void ConfigureSystem::RefreshSecureDataStatus() {
+ auto status_to_str = [](Service::CFG::SecureDataLoadStatus status) {
+ switch (status) {
+ case Service::CFG::SecureDataLoadStatus::Loaded:
+ return "Loaded";
+ case Service::CFG::SecureDataLoadStatus::NotFound:
+ return "Not Found";
+ case Service::CFG::SecureDataLoadStatus::Invalid:
+ return "Invalid";
+ case Service::CFG::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()));
+ 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))))
+ .c_str()));
+}
+//--
void ConfigureSystem::RetranslateUI() {
ui->retranslateUi(this);
@@ -698,7 +708,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
ui->enable_required_online_lle_modules->setEnabled(
Settings::values.enable_required_online_lle_modules.UsingGlobal());
- ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
return;
}
@@ -710,7 +719,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_init_ticks_type->setVisible(false);
ui->label_init_ticks_value->setVisible(false);
ui->label_console_id->setVisible(false);
- ui->label_mac->setVisible(false);
ui->label_sound->setVisible(false);
ui->label_language->setVisible(false);
ui->label_country->setVisible(false);
@@ -732,7 +740,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->edit_init_ticks_value->setVisible(false);
ui->toggle_system_setup->setVisible(false);
ui->button_regenerate_console_id->setVisible(false);
- ui->button_regenerate_mac->setVisible(false);
// Apps can change the state of the plugin loader, so plugins load
// to a chainloaded app with specific parameters. Don't allow
// the plugin loader state to be configured per-game as it may
@@ -740,7 +747,9 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_plugin_loader->setVisible(false);
ui->plugin_loader->setVisible(false);
ui->allow_plugin_loader->setVisible(false);
- ui->group_real_console_unique_data->setVisible(false);
+ // Disable the system firmware downloader.
+ ui->label_nus_download->setVisible(false);
+ ui->body_nus_download->setVisible(false);
ConfigurationShared::SetColoredTristate(ui->toggle_new_3ds, Settings::values.is_new_3ds,
is_new_3ds);
@@ -749,7 +758,45 @@ void ConfigureSystem::SetupPerGameUI() {
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
Settings::values.enable_required_online_lle_modules,
required_online_lle_modules);
- ConfigurationShared::SetColoredComboBox(
- ui->region_combobox, ui->region_label,
- static_cast(Settings::values.region_value.GetValue(true) + 1));
+}
+
+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."));
+ }
+
+ 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 af7c81200..45abf5f2c 100644
--- a/src/citra_qt/configuration/configure_system.h
+++ b/src/citra_qt/configuration/configure_system.h
@@ -56,10 +56,13 @@ private:
void CheckCountryValid(u8 country);
void InstallSecureData(const std::string& from_path, const std::string& to_path);
+ void InstallCTCert(const std::string& from_path);
void RefreshSecureDataStatus();
void SetupPerGameUI();
+ void DownloadFromNUS();
+
private:
std::unique_ptr ui;
Core::System& system;
diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui
index e46f90c85..86b7fb7f5 100644
--- a/src/citra_qt/configuration/configure_system.ui
+++ b/src/citra_qt/configuration/configure_system.ui
@@ -6,7 +6,7 @@
00
- 653
+ 535619
@@ -14,26 +14,11 @@
Form
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
- 660
+ 0480
@@ -64,83 +49,21 @@
System Settings
-
+ Enable New 3DS mode
-
+ Use LLE applets (if installed)
-
-
-
- Enable required LLE modules for
-online features (if installed)
-
-
- Enables the LLE modules needed for online multiplayer, eShop access, etc.
-
-
-
-
-
-
- Region:
-
-
-
-
-
-
- Auto-select
-
-
-
-
- JPN
-
-
-
-
- USA
-
-
-
-
- EUR
-
-
-
-
- AUS
-
-
-
-
- CHN
-
-
-
-
- KOR
-
-
-
-
- TWN
-
-
-
-
-
@@ -153,21 +76,21 @@ online features (if installed)
-
+ Username
-
+ Birthday
-
+
@@ -238,14 +161,14 @@ online features (if installed)
-
+ Language
-
+ Note: this can be overridden when region setting is auto-select
@@ -312,14 +235,14 @@ online features (if installed)
-
+ Sound output mode
-
+
@@ -338,31 +261,24 @@ online features (if installed)
-
+ Country
-
+
-
-
-
-
-
-
-
-
+ Clock
-
+
@@ -376,28 +292,28 @@ online features (if installed)
-
+ Startup time
-
+ yyyy-MM-ddTHH:mm:ss
-
+ Offset time
-
+
@@ -421,14 +337,14 @@ online features (if installed)
-
+ Initial System Ticks
-
+
@@ -442,14 +358,14 @@ online features (if installed)
-
+ Initial System Ticks Override
-
+
@@ -462,21 +378,21 @@ online features (if installed)
-
+ Play Coins
-
+ 300
-
+ <html><head/><body><p>Number of steps per hour reported by the pedometer. Range from 0 to 65,535.</p></body></html>
@@ -486,28 +402,28 @@ online features (if installed)
-
+ 9999
-
+ Run System Setup when Home Menu is launched
-
+ Console ID:
-
+
@@ -523,50 +439,114 @@ online features (if installed)
-
-
-
- MAC:
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Regenerate
-
-
-
-
+ 3GX Plugin Loader:
-
+ Enable 3GX plugin loader
-
+ Allow applications to change plugin loader state
+
+
+
+ Download System Files from Nintendo servers
+
+
+
+
+
+
+
+
+
+
+ Minimal
+
+
+
+
+ Old 3DS
+
+
+
+
+ New 3DS
+
+
+
+
+
+
+
+
+ JPN
+
+
+
+
+ USA
+
+
+
+
+ EUR
+
+
+
+
+ AUS
+
+
+
+
+ CHN
+
+
+
+
+ KOR
+
+
+
+
+ TWN
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Download
+
+
+
+
+
+
@@ -576,168 +556,97 @@ online features (if installed)
Real Console Unique Data
-
-
-
-
-
-
-
-
-
- Your real console is linked to Azahar.
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Unlink
-
-
-
-
-
-
-
-
-
- OTP
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
-
-
-
- SecureInfo_A/B
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
-
-
-
- LocalFriendCodeSeed_A/B
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
-
-
-
-
+
- movable.sed
+ SecureInfo_A/B
-
-
+
+
-
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+
+
+
+ LocalFriendCodeSeed_A/B
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+
+
+
+ CTCert
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
@@ -802,7 +711,6 @@ online features (if installed)
spinBox_play_coinsspinBox_steps_per_hourbutton_regenerate_console_id
- button_regenerate_mac
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index cac036aaa..b41b1e2fd 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -1039,7 +1039,7 @@ void GameList::LoadInterfaceLayout() {
}
const QStringList GameList::supported_file_extensions = {
- QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
+ QStringLiteral("3ds"), QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")};
void GameList::RefreshGameDirectory() {
diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h
index 09dc5ab4e..dd553a963 100644
--- a/src/citra_qt/game_list_p.h
+++ b/src/citra_qt/game_list_p.h
@@ -184,9 +184,11 @@ public:
if (UISettings::values.game_list_icon_size.GetValue() !=
UISettings::GameListIconSize::NoIcon)
setData(GetDefaultIcon(large), Qt::DecorationRole);
+/* todotodo
if (is_encrypted) {
setData(QObject::tr("Unsupported encrypted application"), TitleRole);
}
+*/
return;
}
diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h
index 392954cf8..6fa7fbf1b 100644
--- a/src/citra_qt/uisettings.h
+++ b/src/citra_qt/uisettings.h
@@ -94,7 +94,7 @@ struct Values {
Settings::Setting game_list_row_2{GameListText::FileName, "row2"};
Settings::Setting game_list_hide_no_icon{false, "hideNoIcon"};
Settings::Setting game_list_single_line_mode{false, "singleLineMode"};
- Settings::Setting show_3ds_files_warning{true, "show_3ds_files_warning"};
+ Settings::Setting show_3ds_files_warning{false, "show_3ds_files_warning"};
// Compatibility List
Settings::Setting show_compat_column{true, "show_compat_column"};
diff --git a/src/common/common_paths.h b/src/common/common_paths.h
index bb73c049e..01e18e9d9 100644
--- a/src/common/common_paths.h
+++ b/src/common/common_paths.h
@@ -81,5 +81,6 @@
// Sys files
#define SHARED_FONT "shared_font.bin"
#define KEYS_FILE "keys.txt"
+#define AES_KEYS "aes_keys.txt"
#define BOOTROM9 "boot9.bin"
#define SECRET_SECTOR "sector0x96.bin"
diff --git a/src/common/file_util.h b/src/common/file_util.h
index a4881b57b..c32b66e68 100644
--- a/src/common/file_util.h
+++ b/src/common/file_util.h
@@ -386,7 +386,15 @@ public:
[[nodiscard]] size_t ReadSpan(std::span data) {
static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable.");
+#ifdef todotodo
return ReadImpl(data.data(), data.size(), sizeof(T));
+#else
+ if (!IsOpen()) {
+ return 0;
+ }
+
+ return std::fread(data.data(), sizeof(T), data.size(), m_file);
+#endif
}
/**
@@ -408,7 +416,15 @@ public:
[[nodiscard]] size_t WriteSpan(std::span data) {
static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable.");
+#ifdef todotodo
return WriteImpl(data.data(), data.size(), sizeof(T));
+#else
+ if (!IsOpen()) {
+ return 0;
+ }
+
+ return std::fwrite(data.data(), sizeof(T), data.size(), m_file);
+#endif
}
[[nodiscard]] bool IsOpen() const {
diff --git a/src/common/hacks/hack_list.cpp b/src/common/hacks/hack_list.cpp
index c5702df5e..637876f09 100644
--- a/src/common/hacks/hack_list.cpp
+++ b/src/common/hacks/hack_list.cpp
@@ -70,6 +70,7 @@ HackManager hack_manager = {
},
}},
+#ifdef todotodo
{HackType::ONLINE_LLE_REQUIRED,
HackEntry{
.mode = HackAllowMode::FORCE,
@@ -107,6 +108,7 @@ HackManager hack_manager = {
0x000400000D40D200,
},
}},
+#endif
{HackType::REGION_FROM_SECURE,
HackEntry{
diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp
index a3f97a4f1..0739ac4c0 100644
--- a/src/core/file_sys/ncch_container.cpp
+++ b/src/core/file_sys/ncch_container.cpp
@@ -137,7 +137,9 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
return Loader::ResultStatus::Success;
}
+#ifdef todotodo
for (int i = 0; i < 2; i++) {
+#endif
if (!file->IsOpen()) {
return Loader::ResultStatus::Error;
}
@@ -164,6 +166,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
// Verify we are loading the correct file type...
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
+#ifdef todotodo
// We may be loading a crypto file, try again
if (i == 0) {
file.reset();
@@ -172,8 +175,13 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
} else {
return Loader::ResultStatus::ErrorInvalidFormat;
}
+#else
+ return Loader::ResultStatus::ErrorInvalidFormat;
+#endif
}
+#ifdef todotodo
}
+#endif
if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
@@ -192,7 +200,9 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->IsOpen()) {
size_t file_size;
+#ifdef todotodo
for (int i = 0; i < 2; i++) {
+#endif
file_size = file->GetSize();
// Reset read pointer in case this file has been read before.
@@ -215,6 +225,7 @@ Loader::ResultStatus NCCHContainer::Load() {
// Verify we are loading the correct file type...
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
+#ifdef todotodo
// We may be loading a crypto file, try again
if (i == 0) {
file = HW::UniqueData::OpenUniqueCryptoFile(
@@ -222,14 +233,146 @@ Loader::ResultStatus NCCHContainer::Load() {
} else {
return Loader::ResultStatus::ErrorInvalidFormat;
}
+#else
+ return Loader::ResultStatus::ErrorInvalidFormat;
+#endif
}
+#ifdef todotodo
}
+#endif
if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
}
has_header = true;
+ bool failed_to_decrypt = false;
+ 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;
+ }
if (ncch_header.content_size == file_size) {
// The NCCH is a proto version, which does not use media size units
@@ -237,10 +380,12 @@ Loader::ResultStatus NCCHContainer::Load() {
block_size = 1;
}
+#ifdef todotodo
if (!ncch_header.no_crypto) {
// Encrypted NCCH are not supported
return Loader::ResultStatus::ErrorEncrypted;
}
+#endif
// System archives and DLC don't have an extended header but have RomFS
// Proto apps don't have an ext header size
@@ -254,6 +399,26 @@ 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));
@@ -323,6 +488,7 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
return Loader::ResultStatus::Error;
+#ifdef todotodo
if (file->IsCrypto()) {
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
@@ -330,6 +496,16 @@ Loader::ResultStatus NCCHContainer::Load() {
exefs_file = std::make_unique(filepath, "rb");
}
+#else
+ 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 = std::make_unique(filepath, "rb");
+#endif
has_exefs = true;
}
@@ -457,6 +633,17 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
: (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) {
@@ -466,6 +653,10 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
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)) {
@@ -476,6 +667,9 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
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;
@@ -607,18 +801,34 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf
// We reopen the file, to allow its position to be independent from file's
std::unique_ptr romfs_file_inner;
+#ifdef todotodo
if (file->IsCrypto()) {
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
} else {
romfs_file_inner = std::make_unique(filepath, "rb");
}
+#else
+ romfs_file_inner = std::make_unique(filepath, "rb");
+#endif
if (!romfs_file_inner->IsOpen())
return Loader::ResultStatus::Error;
+#ifdef todotodo
std::shared_ptr direct_romfs =
std::make_shared(std::move(romfs_file_inner), romfs_offset, romfs_size);
+#else
+ 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);
+ }
+#endif
const auto path =
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
@@ -636,8 +846,10 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf
}
Loader::ResultStatus NCCHContainer::DumpRomFS(const std::string& target_path) {
+#ifdef todotodo
if (file->IsCrypto())
return Loader::ResultStatus::ErrorEncrypted;
+#endif
std::shared_ptr direct_romfs;
Loader::ResultStatus result = ReadRomFS(direct_romfs, false);
diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h
index 214f91998..36ca95138 100644
--- a/src/core/file_sys/ncch_container.h
+++ b/src/core/file_sys/ncch_container.h
@@ -348,6 +348,14 @@ private:
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/romfs_reader.cpp b/src/core/file_sys/romfs_reader.cpp
index 7f847efde..0c980b9d2 100644
--- a/src/core/file_sys/romfs_reader.cpp
+++ b/src/core/file_sys/romfs_reader.cpp
@@ -29,6 +29,11 @@ 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;
}
@@ -43,6 +48,11 @@ 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 a427c77d0..f147b996a 100644
--- a/src/core/file_sys/romfs_reader.h
+++ b/src/core/file_sys/romfs_reader.h
@@ -47,7 +47,13 @@ class DirectRomFSReader : public RomFSReader {
public:
DirectRomFSReader(std::unique_ptr&& file, std::size_t file_offset,
std::size_t data_size)
- : file(std::move(file)), file_offset(file_offset), data_size(data_size) {}
+ : is_encrypted(false), file(std::move(file)), file_offset(file_offset), data_size(data_size) {}
+
+ DirectRomFSReader(std::unique_ptr&& 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) {}
~DirectRomFSReader() override = default;
@@ -62,8 +68,12 @@ public:
bool CacheReady(std::size_t file_offset, std::size_t length) override;
private:
+ bool is_encrypted;
std::unique_ptr file;
+ std::array key;
+ std::array ctr;
u64 file_offset;
+ u64 crypto_offset;
u64 data_size;
// Total cache size: 128KB
@@ -86,8 +96,12 @@ 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/signature.h b/src/core/file_sys/signature.h
index 66bd55ea4..0d7b8cbbd 100644
--- a/src/core/file_sys/signature.h
+++ b/src/core/file_sys/signature.h
@@ -4,6 +4,7 @@
#pragma once
+#include "common/assert.h"
#include "common/common_types.h"
#include "common/logging/log.h"
@@ -36,4 +37,4 @@ inline u32 GetSignatureSize(u32 signature_type) {
LOG_ERROR(Common_Filesystem, "Bad signature {}", signature_type);
return 0;
}
-} // namespace FileSys
\ No newline at end of file
+} // namespace FileSys
diff --git a/src/core/file_sys/ticket.cpp b/src/core/file_sys/ticket.cpp
index 1979755a7..f156d37bd 100644
--- a/src/core/file_sys/ticket.cpp
+++ b/src/core/file_sys/ticket.cpp
@@ -64,7 +64,9 @@ Loader::ResultStatus Ticket::DoTitlekeyFixup() {
Loader::ResultStatus Ticket::Load(std::span file_data, std::size_t offset) {
std::size_t total_size = static_cast(file_data.size() - offset);
+/* todotodo
serialized_size = total_size;
+*/
if (total_size < sizeof(u32))
return Loader::ResultStatus::Error;
@@ -88,6 +90,7 @@ 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));
+/* todotodo
std::size_t content_index_start = body_end;
if (total_size < content_index_start + (2 * sizeof(u32)))
return Loader::ResultStatus::Error;
@@ -103,6 +106,8 @@ Loader::ResultStatus Ticket::Load(std::span file_data, std::size_t off
content_index.resize(content_index_size);
std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size);
+*/
+
return Loader::ResultStatus::Success;
}
diff --git a/src/core/file_sys/ticket.h b/src/core/file_sys/ticket.h
index ed6d379c0..385e5fb87 100644
--- a/src/core/file_sys/ticket.h
+++ b/src/core/file_sys/ticket.h
@@ -43,8 +43,9 @@ public:
u8 audit;
INSERT_PADDING_BYTES(0x42);
std::array limits;
+ std::array content_index;
};
- static_assert(sizeof(Body) == 0x164, "Ticket body structure size is wrong");
+ static_assert(sizeof(Body) == 0x210, "Ticket body structure size is wrong");
#pragma pack(pop)
Loader::ResultStatus DoTitlekeyFixup();
diff --git a/src/core/hle/applets/erreula.cpp b/src/core/hle/applets/erreula.cpp
index 0454cdb57..f8b51a33c 100644
--- a/src/core/hle/applets/erreula.cpp
+++ b/src/core/hle/applets/erreula.cpp
@@ -44,12 +44,22 @@ Result ErrEula::ReceiveParameterImpl(const Service::APT::MessageParameter& param
}
Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
+#ifdef todotodo
memcpy(¶m, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param)));
// Do something here, like showing error codes, or prompting for EULA agreement.
if (param.type == DisplayType::Agree) {
param.result = 1;
}
+#else
+ startup_param = parameter.buffer;
+#endif
+
+//--
+ // 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
+//--
// Let the application know that we're closing.
Finalize();
@@ -57,8 +67,13 @@ Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
}
Result ErrEula::Finalize() {
+#ifdef todotodo
std::vector buffer(sizeof(param));
memcpy(buffer.data(), ¶m, buffer.size());
+#else
+ std::vector buffer(startup_param.size());
+ std::fill(buffer.begin(), buffer.end(), 0);
+#endif
CloseApplet(nullptr, buffer);
return ResultSuccess;
}
diff --git a/src/core/hle/applets/erreula.h b/src/core/hle/applets/erreula.h
index 49f41bb88..57ccaf466 100644
--- a/src/core/hle/applets/erreula.h
+++ b/src/core/hle/applets/erreula.h
@@ -54,7 +54,8 @@ private:
std::shared_ptr framebuffer_memory;
/// Parameter received by the applet on start.
- ErrEulaParam param{};
+// ErrEulaParam param{};
+ std::vector startup_param;
};
} // namespace HLE::Applets
diff --git a/src/core/hle/kernel/ipc.cpp b/src/core/hle/kernel/ipc.cpp
index 17b5b20f0..a3bc29095 100644
--- a/src/core/hle/kernel/ipc.cpp
+++ b/src/core/hle/kernel/ipc.cpp
@@ -205,12 +205,21 @@ 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.
+#ifdef todotodo
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();
+#else
+ target_address =
+ dst_process->vm_manager
+ .MapBackingMemoryToBase(Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE,
+ buffer, static_cast(buffer->GetSize()),
+ Kernel::MemoryState::Shared)
+ .Unwrap();
+#endif
// Change the permissions and state of the guard pages.
const VAddr low_guard_address = target_address;
diff --git a/src/core/hle/kernel/shared_page.h b/src/core/hle/kernel/shared_page.h
index 17ae8d2eb..517cf30d6 100644
--- a/src/core/hle/kernel/shared_page.h
+++ b/src/core/hle/kernel/shared_page.h
@@ -47,6 +47,9 @@ union BatteryState {
using MacAddress = std::array;
+// Default MAC address in the Nintendo 3DS range
+constexpr MacAddress DefaultMac = {0x40, 0xF4, 0x07, 0x00, 0x00, 0x00};
+
enum class WifiLinkLevel : u8 {
Off = 0,
Poor = 1,
diff --git a/src/core/hle/service/ac/ac.cpp b/src/core/hle/service/ac/ac.cpp
index f719d919d..cbb4bd1ba 100644
--- a/src/core/hle/service/ac/ac.cpp
+++ b/src/core/hle/service/ac/ac.cpp
@@ -106,10 +106,26 @@ void Module::Interface::GetStatus(Kernel::HLERequestContext& ctx) {
void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
+/* todotodo
+*/
+//--
+ 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);
+#ifdef todotodo
rb.Push(static_cast(WifiStatus::STATUS_CONNECTED_SLOT1));
+#else
+ rb.Push(static_cast(can_reach_internet ? (Settings::values.is_new_3ds
+ ? WifiStatus::STATUS_CONNECTED_N3DS
+ : WifiStatus::STATUS_CONNECTED_O3DS)
+ : WifiStatus::STATUS_DISCONNECTED));
+#endif
}
void Module::Interface::GetInfraPriority(Kernel::HLERequestContext& ctx) {
diff --git a/src/core/hle/service/ac/ac.h b/src/core/hle/service/ac/ac.h
index 4f72ad1cc..1d3340e1a 100644
--- a/src/core/hle/service/ac/ac.h
+++ b/src/core/hle/service/ac/ac.h
@@ -90,7 +90,7 @@ public:
* AC::GetWifiStatus service function
* Outputs:
* 1 : Result of function, 0 on success, otherwise error code
- * 2 : WifiStatus
+ * 2 : Output connection type, 0 = none, 1 = Old3DS Internet, 2 = New3DS Internet.
*/
void GetWifiStatus(Kernel::HLERequestContext& ctx);
@@ -169,9 +169,14 @@ protected:
};
enum class WifiStatus {
STATUS_DISCONNECTED = 0,
+#ifdef todotodo
STATUS_CONNECTED_SLOT1 = (1 << 0),
STATUS_CONNECTED_SLOT2 = (1 << 1),
STATUS_CONNECTED_SLOT3 = (1 << 2),
+#else
+ STATUS_CONNECTED_O3DS = 1,
+ STATUS_CONNECTED_N3DS = 2,
+#endif
};
struct ACConfig {
diff --git a/src/core/hle/service/act/act_errors.h b/src/core/hle/service/act/act_errors.h
index ee6efabf1..2e1ff2ac8 100644
--- a/src/core/hle/service/act/act_errors.h
+++ b/src/core/hle/service/act/act_errors.h
@@ -18,6 +18,7 @@ enum {
NotInitialized = 101,
AlreadyInitialized = 102,
AcStatusDisconnected = 103,
+ ErrDesc103 = 103,
ErrDesc104 = 104,
Busy = 111,
ErrDesc112 = 112,
@@ -317,6 +318,7 @@ enum {
NotInitialized = 220501, // 022-0501
AlreadyInitialized = 220502, // 022-0502
AcStatusDisconnected = 225103, // 022-5103
+ ErrCode225103 = 225103, // 022-5103
ErrCode225104 = 225104, // 022-5104
Busy = 220511, // 022-0511
ErrCode225112 = 225112, // 022-5112
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index 50b5a6b25..caedca100 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -89,12 +89,42 @@ 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;
};
NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) {
+#ifdef todotodo
if (encrypted_content) {
// A console unique crypto file is used to store the decrypted NCCH file. This is done
// to prevent Azahar being used as a tool to download easy shareable decrypted contents
@@ -108,15 +138,20 @@ NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_conte
if (!file->IsOpen()) {
is_error = true;
}
+#else
+ file = std::make_unique(out_file, "wb");
+#endif
}
void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
if (is_error)
return;
+#ifdef todotodo
if (is_not_ncch) {
file->WriteBytes(buffer, length);
}
+#endif
const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
@@ -130,10 +165,14 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
if (!header_parsed && header_size == sizeof(NCCH_Header)) {
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
+#ifdef todotodo
// Most likely DS contents, store without additional operations
is_not_ncch = true;
file->WriteBytes(&ncch_header, sizeof(ncch_header));
file->WriteBytes(buffer, length);
+#else
+ is_error = true;
+#endif
return;
}
@@ -420,7 +459,7 @@ void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ct
}
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),
+ : system(system_), from_cdn(from_cdn_), decryption_authorized(true), 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.
@@ -457,6 +496,7 @@ Result CIAFile::WriteTicket() {
ErrorLevel::Permanent};
}
+#ifdef todotodo
const auto& ticket = container.GetTicket();
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
@@ -471,6 +511,7 @@ Result CIAFile::WriteTicket() {
// TODO: Correct result code.
return FileSys::ResultFileNotFound;
}
+#endif
install_state = CIAInstallState::TicketLoaded;
return ResultSuccess;
@@ -519,12 +560,25 @@ Result CIAFile::WriteTitleMetadata(std::span tmd_data, std::size_t off
auto content_count = container.GetTitleMetadata().GetContentCount();
content_written.resize(content_count);
+#ifdef todotodo
current_content_file.reset();
current_content_index = -1;
content_file_paths.clear();
+#else
+ content_files.clear();
+#endif
for (std::size_t i = 0; i < content_count; i++) {
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
+#ifdef todotodo
content_file_paths.emplace_back(path);
+#else
+ 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;
+ }
+#endif
}
if (container.GetTitleMetadata().HasEncryptedContent()) {
@@ -561,6 +615,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
@@ -579,6 +634,7 @@ 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.
+#ifdef todotodo
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
if (i != current_content_index) {
current_content_index = static_cast(i);
@@ -588,6 +644,10 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length,
}
auto& file = *current_content_file;
+#else
+ FileSys::TitleMetadata tmd = container.GetTitleMetadata();
+ auto& file = content_files[i];
+#endif
std::vector temp(buffer + (range_min - offset),
buffer + (range_min - offset) + available_to_write);
@@ -595,12 +655,7 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length,
decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
}
- file.Write(temp.data(), temp.size());
- if (file.IsError()) {
- // This can never happen in real HW
- return Result(ErrCodes::InvalidImportState, ErrorModule::AM,
- ErrorSummary::InvalidState, ErrorLevel::Permanent);
- }
+ file.WriteBytes(temp.data(), temp.size());
// Keep tabs on how much of this content ID has been written so new range_min
// values can be calculated.
@@ -610,7 +665,7 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length,
}
}
- return length;
+ return success ? length : 0;
}
ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush,
@@ -732,17 +787,13 @@ ResultVal CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
}
file.Write(temp.data(), temp.size());
- if (file.IsError()) {
- // This can never happen in real HW
- return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
- ErrorLevel::Permanent);
- }
+ 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 temp.size();
+ return success ? temp.size() : 0;
}
u64 CIAFile::GetSize() const {
@@ -848,6 +899,12 @@ 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;
}
@@ -865,6 +922,11 @@ Result TicketFile::Commit() {
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.");
@@ -964,8 +1026,10 @@ InstallStatus InstallCIA(const std::string& path,
Core::System::GetInstance(),
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
- if (container.GetTitleMetadata().HasEncryptedContent()) {
- LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path);
+ 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);
return InstallStatus::ErrorEncrypted;
}
@@ -975,8 +1039,12 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorFailedToOpenFile;
}
+#ifdef todotodo
std::vector buffer;
buffer.resize(0x10000);
+#else
+ std::array buffer;
+#endif
auto file_size = file.GetSize();
std::size_t total_bytes_read = 0;
while (total_bytes_read != file_size) {
@@ -1035,6 +1103,96 @@ 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);
@@ -1831,35 +1989,34 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0,
title_count * sizeof(u64));
async_data->title_info_out = &rp.PopMappedBuffer();
+#ifdef todotodo
+ // nim 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 importing context is present and not
+ // committed. If that's the case, return not found
+ Result result = ResultSuccess;
+ for (auto tid : title_id_list) {
+ for (auto& import_ctx : am->import_title_contexts) {
+ if (import_ctx.first == tid &&
+ (import_ctx.second.state == ImportTitleContextState::WAITING_FOR_IMPORT ||
+ import_ctx.second.state == ImportTitleContextState::WAITING_FOR_COMMIT ||
+ import_ctx.second.state == ImportTitleContextState::RESUMABLE)) {
+ LOG_DEBUG(Service_AM, "title pending commit title_id={:016X}", tid);
+ result = Result(ErrorDescription::NotFound, ErrorModule::AM,
+ ErrorSummary::InvalidState, ErrorLevel::Permanent);
+ }
+ }
+ }
+ if (result.IsSuccess())
+ result = GetTitleInfoFromList(title_id_list, media_type, title_info_out);
+#else
ctx.RunAsync(
- [this, async_data](Kernel::HLERequestContext& ctx) {
- // nim 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 importing
- // context is present and not committed. If that's the case, return not found
- for (auto tid : async_data->title_id_list) {
- for (auto& import_ctx : am->import_title_contexts) {
- if (import_ctx.first == tid &&
- (import_ctx.second.state ==
- ImportTitleContextState::WAITING_FOR_IMPORT ||
- import_ctx.second.state ==
- ImportTitleContextState::WAITING_FOR_COMMIT ||
- import_ctx.second.state == ImportTitleContextState::RESUMABLE)) {
- LOG_DEBUG(Service_AM, "title pending commit title_id={:016X}", tid);
- async_data->res =
- Result(ErrorDescription::NotFound, ErrorModule::AM,
- ErrorSummary::InvalidState, ErrorLevel::Permanent);
- }
- }
- }
-
- if (async_data->res.IsSuccess()) {
- async_data->res = GetTitleInfoFromList(async_data->title_id_list,
+ [async_data](Kernel::HLERequestContext& ctx) {
+ async_data->res = GetTitleInfoFromList(async_data->title_id_list,
async_data->media_type, async_data->out);
- }
return 0;
},
[async_data](Kernel::HLERequestContext& ctx) {
@@ -1873,6 +2030,7 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
rb.PushMappedBuffer(*async_data->title_info_out);
},
true);
+#endif
}
}
@@ -2416,6 +2574,7 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
std::scoped_lock lock(am->am_lists_mutex);
+#ifdef todotodo
auto range = am->am_ticket_list.equal_range(title_id);
if (range.first == range.second) {
rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success,
@@ -2429,8 +2588,10 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
}
am->am_ticket_list.erase(range.first, range.second);
+#endif
rb.Push(ResultSuccess);
+ LOG_WARNING(Service_AM, "(STUBBED) called title_id=0x{:016x}", title_id);
}
void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
@@ -2439,11 +2600,19 @@ void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "");
std::scoped_lock lock(am->am_lists_mutex);
+#ifdef todotodo
u32 ticket_count = static_cast(am->am_ticket_list.size());
+#else
+ u32 ticket_count = 0;
+ for (const auto& title_list : am->am_title_list) {
+ ticket_count += static_cast(title_list.size());
+ }
+#endif
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) {
@@ -2456,6 +2625,7 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
u32 tickets_written = 0;
std::scoped_lock lock(am->am_lists_mutex);
+#ifdef todotodo
auto it = am->am_ticket_list.begin();
std::advance(it, std::min(static_cast(ticket_index), am->am_ticket_list.size()));
@@ -2463,11 +2633,22 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
it++, tickets_written++) {
ticket_tids_out.Write(&it->first, tickets_written * sizeof(u64), sizeof(u64));
}
+#else
+ 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;
+ }
+#endif
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) {
@@ -2475,6 +2656,7 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "");
+#ifdef todotodo
const auto& otp = HW::UniqueData::GetOTP();
if (!otp.Valid()) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@@ -2490,6 +2672,13 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
if (am->force_old_device_id) {
deviceID &= ~0x80000000;
}
+#else
+ const u32 deviceID = am->ct_cert.IsValid() ? am->ct_cert.GetDeviceID() : 0;
+
+ if (deviceID == 0) {
+ LOG_ERROR(Service_AM, "Invalid or missing CTCert");
+ }
+#endif
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
rb.Push(ResultSuccess);
@@ -2505,6 +2694,7 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
rb.Push(ResultSuccess);
+#ifdef todotodo
u32 count = 0;
for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) {
if ((include_installing &&
@@ -2517,6 +2707,9 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
}
rb.Push(count);
+#else
+ rb.Push(static_cast(am->import_title_contexts.size()));
+#endif
}
void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp,
@@ -2717,6 +2910,7 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "(STUBBED) media_type=0x{:02x}", media_type);
+#ifdef todotodo
bool needs_cleanup = false;
for (auto& import_ctx : am->import_title_contexts) {
if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) {
@@ -2732,10 +2926,15 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
}
}
}
+#endif
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess);
+#ifdef todotodo
rb.Push(needs_cleanup);
+#else
+ rb.Push(false);
+#endif
}
void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
@@ -2744,6 +2943,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "(STUBBED) called, media_type={:#02x}", media_type);
+#ifdef todotodo
for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) {
if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) {
it = am->import_content_contexts.erase(it);
@@ -2759,6 +2959,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
it++;
}
}
+#endif
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
@@ -2775,6 +2976,7 @@ void Module::Interface::QueryAvailableTitleDatabase(Kernel::HLERequestContext& c
LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type);
}
+#ifdef todotodo
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@@ -2859,6 +3061,48 @@ void Module::Interface::GetImportTitleContextListFiltered(Kernel::HLERequestCont
LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
}
+#else
+void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
+ IPC::RequestParser rp(ctx);
+ [[maybe_unused]] u32 ticket_count = rp.Pop();
+ [[maybe_unused]] auto& buffer = rp.PopMappedBuffer();
+
+ std::scoped_lock lock(am->am_lists_mutex);
+
+ 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);
+}
+
+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);
+
+ 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);
+
+ LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
+}
+#endif
void Module::Interface::CheckContentRights(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@@ -2899,8 +3143,13 @@ void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) {
if (am->cia_installing) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+#ifdef todotodo
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent));
+#else
+ rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
+ ErrorSummary::InvalidState, ErrorLevel::Permanent));
+#endif
return;
}
@@ -2924,8 +3173,13 @@ void Module::Interface::BeginImportProgramTemporarily(Kernel::HLERequestContext&
if (am->cia_installing) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+#ifdef todotodo
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent));
+#else
+ rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
+ ErrorSummary::InvalidState, ErrorLevel::Permanent));
+#endif
return;
}
@@ -2975,7 +3229,25 @@ void Module::Interface::EndImportProgramWithoutCommit(Kernel::HLERequestContext&
}
void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) {
+#ifdef todotodo
CommitImportTitlesImpl(ctx, false, false);
+#else
+ IPC::RequestParser rp(ctx);
+ [[maybe_unused]] const auto media_type = static_cast(rp.Pop());
+ [[maybe_unused]] const u32 title_count = rp.Pop();
+ [[maybe_unused]] const u8 database = rp.Pop();
+ const auto buffer = rp.PopMappedBuffer();
+
+ // Note: This function is basically a no-op for us since we don't use title.db or ticket.db
+ // files to keep track of installed titles.
+ am->ScanForAllTitles();
+
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+ rb.Push(ResultSuccess);
+ rb.PushMappedBuffer(buffer);
+
+ LOG_WARNING(Service_AM, "(STUBBED)");
+#endif
}
/// Wraps all File operations to allow adding an offset to them.
@@ -3452,45 +3724,39 @@ 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");
}
+#ifdef todotodo
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
const auto ticket = rp.PopObject();
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
auto ticket_file = GetFileBackendFromSession(ticket);
if (ticket_file.Succeeded()) {
- struct AsyncData {
- Service::AM::TicketFile* ticket_file;
-
- Result res{0};
- };
- std::shared_ptr async_data = std::make_shared();
- async_data->ticket_file = ticket_file.Unwrap();
-
- ctx.RunAsync(
- [this, async_data](Kernel::HLERequestContext& ctx) {
- async_data->res = async_data->ticket_file->Commit();
-
- std::scoped_lock lock(am->am_lists_mutex);
- am->am_ticket_list.insert(std::make_pair(async_data->ticket_file->GetTitleID(),
- async_data->ticket_file->GetTicketID()));
-
- LOG_DEBUG(Service_AM, "EndImportTicket: title_id={:016X} ticket_id={:016X}",
- async_data->ticket_file->GetTitleID(),
- async_data->ticket_file->GetTicketID());
- return 0;
- },
- [async_data](Kernel::HLERequestContext& ctx) {
- IPC::RequestBuilder rb(ctx, 1, 0);
- rb.Push(async_data->res);
- },
- true);
+ rb.Push(ticket_file.Unwrap()->Commit());
+ am->am_ticket_list.insert(std::make_pair(ticket_file.Unwrap()->GetTitleID(),
+ ticket_file.Unwrap()->GetTicketID()));
} else {
- IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ticket_file.Code());
}
+
+ LOG_DEBUG(Service_AM, "title_id={:016X} ticket_id={:016X}", ticket_file.Unwrap()->GetTitleID(),
+ ticket_file.Unwrap()->GetTicketID());
}
+#else
+void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
+ IPC::RequestParser rp(ctx);
+ [[maybe_unused]] const auto ticket = rp.PopObject();
+
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+ rb.Push(ResultSuccess);
+
+ LOG_WARNING(Service_AM, "(STUBBED) called");
+}
+#endif
void Module::Interface::BeginImportTitle(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@@ -3621,6 +3887,7 @@ void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) {
}
am->importing_title->cia_file.SetDone();
+ am->ScanForTitles(am->importing_title->media_type);
am->importing_title.reset();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@@ -3994,10 +4261,12 @@ void Module::serialize(Archive& ar, const unsigned int) {
ar & cia_installing;
ar & force_old_device_id;
ar & force_new_device_id;
+ ar & am_title_list;
ar & system_updater_mutex;
}
SERIALIZE_IMPL(Module)
+#ifdef todotodo
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 size = rp.Pop();
@@ -4021,6 +4290,52 @@ void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
rb.Push(0);
rb.PushMappedBuffer(buffer);
}
+#else
+void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
+ IPC::RequestParser rp(ctx);
+ [[maybe_unused]] u32 size = rp.Pop();
+ auto buffer = rp.PopMappedBuffer();
+
+ if (!am->ct_cert.IsValid()) {
+ LOG_ERROR(Service_AM, "Invalid or missing CTCert");
+ }
+
+ buffer.Write(&am->ct_cert, 0, std::min(sizeof(CTCert), buffer.GetSize()));
+ IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
+ rb.Push(ResultSuccess);
+ rb.Push(0);
+ rb.PushMappedBuffer(buffer);
+}
+#endif
+
+std::string Module::GetCTCertPath() {
+ return FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + "CTCert.bin";
+}
+
+CTCertLoadStatus Module::LoadCTCertFile(CTCert& output) {
+ if (output.IsValid()) {
+ return CTCertLoadStatus::Loaded;
+ }
+ std::string file_path = GetCTCertPath();
+ if (!FileUtil::Exists(file_path)) {
+ return CTCertLoadStatus::NotFound;
+ }
+ FileUtil::IOFile file(file_path, "rb");
+ if (!file.IsOpen()) {
+ return CTCertLoadStatus::IOError;
+ }
+ if (file.GetSize() != sizeof(CTCert)) {
+ return CTCertLoadStatus::Invalid;
+ }
+ if (file.ReadBytes(&output, sizeof(CTCert)) != sizeof(CTCert)) {
+ return CTCertLoadStatus::IOError;
+ }
+ if (!output.IsValid()) {
+ output = CTCert();
+ return CTCertLoadStatus::Invalid;
+ }
+ return CTCertLoadStatus::Loaded;
+}
void Module::Interface::CommitImportTitlesAndUpdateFirmwareAuto(Kernel::HLERequestContext& ctx) {
CommitImportTitlesImpl(ctx, true, true);
@@ -4051,7 +4366,11 @@ void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) {
auto path = GetTicketPath(title_id, ticket_id);
FileUtil::Delete(path);
+#ifdef todotodo
am->am_ticket_list.erase(it);
+#else
+ am->ScanForTickets();
+#endif
rb.Push(ResultSuccess);
}
@@ -4233,6 +4552,7 @@ void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) {
Module::Module(Core::System& _system) : system(_system) {
FileUtil::CreateFullPath(GetTicketDirectory());
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 afac4dae5..e27c5f739 100644
--- a/src/core/hle/service/am/am.h
+++ b/src/core/hle/service/am/am.h
@@ -51,6 +51,7 @@ namespace Service::AM {
namespace ErrCodes {
enum {
InvalidImportState = 4,
+ CIACurrentlyInstalling = 4,
InvalidTID = 31,
EmptyCIA = 32,
TryingToUninstallSystemApp = 44,
@@ -87,6 +88,13 @@ enum class ImportTitleContextState : u8 {
NEEDS_CLEANUP = 6,
};
+enum class CTCertLoadStatus {
+ Loaded,
+ NotFound,
+ Invalid,
+ IOError,
+};
+
struct ImportTitleContext {
u64 title_id;
u16 version;
@@ -105,6 +113,24 @@ struct ImportContentContext {
};
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
+struct CTCert {
+ u32_be signature_type{};
+ std::array signature_r{};
+ std::array signature_s{};
+ INSERT_PADDING_BYTES(0x40) {};
+ std::array issuer{};
+ u32_be key_type{};
+ std::array key_id{};
+ u32_be expiration_time{};
+ std::array public_key_x{};
+ std::array public_key_y{};
+ INSERT_PADDING_BYTES(0x3C) {};
+
+ bool IsValid() const;
+ u32 GetDeviceID() const;
+};
+static_assert(sizeof(CTCert) == 0x180, "Invalid CTCert size.");
+
// Title ID valid length
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
@@ -126,7 +152,7 @@ private:
friend class CIAFile;
std::unique_ptr file;
bool is_error = false;
- bool is_not_ncch = false;
+// bool is_not_ncch = false;
bool decryption_authorized = false;
std::size_t written = 0;
@@ -223,6 +249,7 @@ private:
std::vector content_file_paths;
u16 current_content_index = -1;
std::unique_ptr current_content_file;
+ std::vector content_files;
Service::FS::MediaType media_type;
class DecryptionState;
@@ -334,6 +361,13 @@ 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
@@ -1022,6 +1056,18 @@ public:
force_new_device_id = true;
}
+ /**
+ * 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();
@@ -1054,6 +1100,7 @@ private:
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;
diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp
index 4d04af017..31e75f62c 100644
--- a/src/core/hle/service/cfg/cfg.cpp
+++ b/src/core/hle/service/cfg/cfg.cpp
@@ -453,6 +453,7 @@ void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) {
void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
+#ifdef todotodo
const auto& secure_info_a = HW::UniqueData::GetSecureInfoA();
const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB();
@@ -465,12 +466,19 @@ void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
}
u8 ret = secure_info_a.body.unknown;
+#else
+ u8 ret = 0;
+ if (cfg->secure_info_a_loaded) {
+ ret = cfg->secure_info_a.unknown;
+ }
+#endif
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess);
rb.Push(ret);
}
+#ifdef todotodo
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop();
@@ -500,6 +508,32 @@ void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
rb.Push(ResultSuccess);
rb.PushMappedBuffer(out_buffer);
}
+#else
+void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
+ IPC::RequestParser rp(ctx);
+ [[maybe_unused]] u32 out_size = rp.Pop();
+ auto out_buffer = rp.PopMappedBuffer();
+
+ if (out_buffer.GetSize() < sizeof(SecureInfoA::serial_number)) {
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+ rb.Push(Result(ErrorDescription::InvalidSize, ErrorModule::Config,
+ ErrorSummary::WrongArgument, ErrorLevel::Permanent));
+ }
+ // 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) {
+ 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));
+
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+ rb.Push(ResultSuccess);
+ rb.PushMappedBuffer(out_buffer);
+}
+#endif
void Module::Interface::SetUUIDClockSequence(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@@ -645,6 +679,7 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx)
rb.Push(cfg->UpdateConfigNANDSavegame());
}
+#ifdef todotodo
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop();
@@ -688,6 +723,44 @@ void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
rb.Push(ResultSuccess);
rb.Push(local_friend_code_seed_b.body.friend_code_seed);
}
+#else
+void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
+ IPC::RequestParser rp(ctx);
+ [[maybe_unused]] u32 out_size = rp.Pop();
+ auto out_buffer = rp.PopMappedBuffer();
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+
+ if (out_buffer.GetSize() < sizeof(LocalFriendCodeSeedB)) {
+ rb.Push(Result(ErrorDescription::InvalidSize, ErrorModule::Config,
+ ErrorSummary::WrongArgument, ErrorLevel::Permanent));
+ }
+ // 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) {
+ rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState,
+ ErrorLevel::Permanent));
+ }
+
+ out_buffer.Write(&cfg->local_friend_code_seed_b, 0, sizeof(LocalFriendCodeSeedB));
+ rb.Push(ResultSuccess);
+}
+
+void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
+ IPC::RequestParser rp(ctx);
+
+ // 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) {
+ IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+ rb.Push(Result(ErrorDescription::NotFound, ErrorModule::Config, ErrorSummary::InvalidState,
+ ErrorLevel::Permanent));
+ }
+
+ IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
+ rb.Push(ResultSuccess);
+ rb.Push(cfg->local_friend_code_seed_b.friend_code_seed);
+}
+#endif
void Module::Interface::FormatConfig(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@@ -863,6 +936,14 @@ 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
@@ -938,6 +1019,55 @@ 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");
@@ -976,6 +1106,8 @@ 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 7536680c0..885320a91 100644
--- a/src/core/hle/service/cfg/cfg.h
+++ b/src/core/hle/service/cfg/cfg.h
@@ -181,6 +181,28 @@ 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_);
@@ -634,6 +656,35 @@ public:
*/
void SaveMacAddress();
+ /**
+ * 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();
+
+
private:
void UpdatePreferredRegionCode();
SystemLanguage GetRawSystemLanguage();
@@ -644,6 +695,10 @@ 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{};
std::string mac_address{};
diff --git a/src/core/hw/aes/key.cpp b/src/core/hw/aes/key.cpp
index 776eab670..3e2a9cee1 100644
--- a/src/core/hw/aes/key.cpp
+++ b/src/core/hw/aes/key.cpp
@@ -32,7 +32,10 @@ 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.
-AESKey generator_constant;
+//AESKey generator_constant;
+
+constexpr AESKey generator_constant = {{0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45,
+ 0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A}};
AESKey HexToKey(const std::string& hex) {
if (hex.size() < 32) {
@@ -143,6 +146,78 @@ 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;
+ }
+ }
+}
+
+#ifdef todotodo
void LoadPresetKeys() {
auto s = GetKeysStream();
@@ -276,6 +351,112 @@ void LoadPresetKeys() {
}
}
}
+#else
+void LoadPresetKeys() {
+ const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS;
+ FileUtil::CreateFullPath(filepath); // Create path if not already created
+
+ boost::iostreams::stream file;
+ FileUtil::OpenFStream(file, filepath);
+ if (!file.is_open()) {
+ return;
+ }
+
+ while (!file.eof()) {
+ std::string line;
+ std::getline(file, line);
+
+ // Ignore empty or commented lines.
+ if (line.empty() || line.starts_with("#")) {
+ continue;
+ }
+
+ const auto parts = Common::SplitString(line, '=');
+ if (parts.size() != 2) {
+ LOG_ERROR(HW_AES, "Failed to parse {}", line);
+ continue;
+ }
+
+ const std::string& name = parts[0];
+
+ const auto nfc_secret = ParseNfcSecretName(name);
+ if (nfc_secret) {
+ auto value = HexToVector(parts[1]);
+ if (nfc_secret->first >= nfc_secrets.size()) {
+ LOG_ERROR(HW_AES, "Invalid NFC secret index {}", nfc_secret->first);
+ } else if (nfc_secret->second == "Phrase") {
+ nfc_secrets[nfc_secret->first].phrase = value;
+ } else if (nfc_secret->second == "Seed") {
+ nfc_secrets[nfc_secret->first].seed = value;
+ } else if (nfc_secret->second == "HmacKey") {
+ nfc_secrets[nfc_secret->first].hmac_key = value;
+ } else {
+ LOG_ERROR(HW_AES, "Invalid NFC secret '{}'", name);
+ }
+ continue;
+ }
+
+ AESKey key;
+ try {
+ key = HexToKey(parts[1]);
+ } catch (const std::logic_error& e) {
+ LOG_ERROR(HW_AES, "Invalid key {}: {}", parts[1], e.what());
+ continue;
+ }
+
+ const auto common_key = ParseCommonKeyName(name);
+ if (common_key) {
+ if (common_key >= common_key_y_slots.size()) {
+ LOG_ERROR(HW_AES, "Invalid common key index {}", common_key.value());
+ } else {
+ common_key_y_slots[common_key.value()] = key;
+ }
+ continue;
+ }
+
+ if (name == "dlpKeyY") {
+ dlp_nfc_key_y_slots[DlpNfcKeyY::Dlp] = key;
+ continue;
+ }
+
+ if (name == "nfcKeyY") {
+ dlp_nfc_key_y_slots[DlpNfcKeyY::Nfc] = key;
+ continue;
+ }
+
+ if (name == "nfcIv") {
+ nfc_iv = key;
+ continue;
+ }
+
+ const auto key_slot = ParseKeySlotName(name);
+ if (!key_slot) {
+ LOG_ERROR(HW_AES, "Invalid key name '{}'", name);
+ continue;
+ }
+
+ if (key_slot->first >= MaxKeySlotID) {
+ LOG_ERROR(HW_AES, "Out of range key slot ID {:#X}", key_slot->first);
+ continue;
+ }
+
+ switch (key_slot->second) {
+ case 'X':
+ key_slots.at(key_slot->first).SetKeyX(key);
+ break;
+ case 'Y':
+ key_slots.at(key_slot->first).SetKeyY(key);
+ break;
+ case 'N':
+ key_slots.at(key_slot->first).SetNormalKey(key);
+ break;
+ default:
+ LOG_ERROR(HW_AES, "Invalid key type '{}'", key_slot->second);
+ break;
+ }
+ }
+}
+#endif
} // namespace
@@ -305,6 +486,8 @@ void InitKeys(bool force) {
return;
}
initialized = true;
+ HW::RSA::InitSlots();
+ LoadBootromKeys();
LoadPresetKeys();
movable_key.SetKeyX(key_slots[0x35].x);
movable_cmac.SetKeyX(key_slots[0x35].x);
diff --git a/src/core/hw/ecc.cpp b/src/core/hw/ecc.cpp
index 5033317a2..d262cccdc 100644
--- a/src/core/hw/ecc.cpp
+++ b/src/core/hw/ecc.cpp
@@ -6,6 +6,7 @@
#include
#include
#include "common/assert.h"
+#include
#include "common/common_paths.h"
#include "common/file_util.h"
#include "common/logging/log.h"
diff --git a/src/core/hw/rsa/rsa.cpp b/src/core/hw/rsa/rsa.cpp
index 4181144f6..542bc020f 100644
--- a/src/core/hw/rsa/rsa.cpp
+++ b/src/core/hw/rsa/rsa.cpp
@@ -21,6 +21,19 @@
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;
@@ -92,6 +105,23 @@ std::optional> ParseKeySlotName(const std::string&
}
}
+std::vector RsaSlot::GetSignature(std::span message) 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());
+}
+
+// todotodo
+#ifdef todotodo
void InitSlots() {
static bool initialized = false;
if (initialized)
@@ -193,6 +223,42 @@ void InitSlots() {
}
}
}
+#endif
+
+//--
+void InitSlots() {
+ static bool initialized = false;
+ if (initialized)
+ return;
+ initialized = true;
+
+ const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
+ FileUtil::IOFile file(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 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
+}
+//--
static RsaSlot empty_slot;
const RsaSlot& GetSlot(std::size_t slot_id) {
@@ -201,6 +267,31 @@ const RsaSlot& GetSlot(std::size_t slot_id) {
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}};
+
+ 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& GetTicketWrapSlot() {
return ticket_wrap_slot;
}
diff --git a/src/core/hw/rsa/rsa.h b/src/core/hw/rsa/rsa.h
index b6dd2c1ca..4041af7ff 100644
--- a/src/core/hw/rsa/rsa.h
+++ b/src/core/hw/rsa/rsa.h
@@ -23,6 +23,8 @@ public:
bool Verify(std::span message, std::span signature) const;
+ std::vector GetSignature(std::span message) const;
+
explicit operator bool() const {
// TODO(B3N30): Maybe check if exponent and modulus are vailid
return init;
@@ -68,4 +70,5 @@ const RsaSlot& GetTicketWrapSlot();
const RsaSlot& GetSecureInfoSlot();
const RsaSlot& GetLocalFriendCodeSeedSlot();
+std::vector CreateASN1Message(std::span data);
} // namespace HW::RSA
diff --git a/src/core/loader/artic.h b/src/core/loader/artic.h
index f5b148364..07fb6b255 100644
--- a/src/core/loader/artic.h
+++ b/src/core/loader/artic.h
@@ -26,6 +26,23 @@ public:
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port,
ArticInitMode init_mode);
+ Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port)
+ : AppLoader(system_, FileUtil::IOFile()) {
+ client = std::make_shared(server_addr, server_port);
+ client->SetCommunicationErrorCallback([&system_](const std::string& msg) {
+ system_.SetStatus(Core::System::ResultStatus::ErrorArticDisconnected,
+ msg.empty() ? nullptr : msg.c_str());
+ });
+ client->SetArticReportTrafficCallback(
+ [&system_](u32 bytes) { system_.ReportArticTraffic(bytes); });
+ client->SetReportArticEventCallback([&system_](u64 event) {
+ Core::PerfStats::PerfArticEventBits ev =
+ static_cast(event & 0xFFFFFFFF);
+ bool set = (event > 32) != 0;
+ system_.ReportPerfArticEvent(ev, set);
+ });
+ }
+
~Apploader_Artic() override;
/**
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index c09760e36..4dd766a37 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -48,7 +48,7 @@ FileType GuessFromExtension(const std::string& extension_) {
if (extension == ".elf" || extension == ".axf")
return FileType::ELF;
- if (extension == ".cci")
+ if (extension == ".cci" || extension == ".3ds")
return FileType::CCI;
if (extension == ".cxi" || extension == ".app")
@@ -112,12 +112,14 @@ static std::unique_ptr GetFileLoader(Core::System& system, FileUtil::
return std::make_unique(system, std::move(file), filepath);
case FileType::ARTIC: {
+#ifdef todotodo
Apploader_Artic::ArticInitMode mode = Apploader_Artic::ArticInitMode::NONE;
if (filename.starts_with("articinio://")) {
mode = Apploader_Artic::ArticInitMode::O3DS;
} else if (filename.starts_with("articinin://")) {
mode = Apploader_Artic::ArticInitMode::N3DS;
}
+#endif
auto strToUInt = [](const std::string& str) -> int {
char* pEnd = NULL;
unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10);
@@ -136,7 +138,11 @@ static std::unique_ptr GetFileLoader(Core::System& system, FileUtil::
server_addr = server_addr.substr(0, pos);
}
}
+#ifdef todotodo
return std::make_unique(system, server_addr, port, mode);
+#else
+ return std::make_unique(system, server_addr, port);
+#endif
}
default: