Restore features

This commit is contained in:
AzaharPlus 2025-03-31 12:39:28 +02:00
parent 5ade69f5f4
commit 3e64dc0c8c
51 changed files with 2462 additions and 765 deletions

View File

@ -1,3 +1,13 @@
<b>AzaharPlus</b> 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.
---
![Azahar Emulator](https://azahar-emu.org/resources/images/logo/azahar-name-and-logo.svg) ![Azahar Emulator](https://azahar-emu.org/resources/images/logo/azahar-name-and-logo.svg)
![GitHub Release](https://img.shields.io/github/v/release/azahar-emu/azahar?label=Current%20Release) ![GitHub Release](https://img.shields.io/github/v/release/azahar-emu/azahar?label=Current%20Release)

View File

@ -31,6 +31,7 @@
<dict> <dict>
<key>CFBundleTypeExtensions</key> <key>CFBundleTypeExtensions</key>
<array> <array>
<string>3ds</string>
<string>3dsx</string> <string>3dsx</string>
<string>cci</string> <string>cci</string>
<string>cxi</string> <string>cxi</string>

View File

@ -16,6 +16,7 @@
<expanded-acronym>CTR Cart Image</expanded-acronym> <expanded-acronym>CTR Cart Image</expanded-acronym>
<icon name="azahar"/> <icon name="azahar"/>
<glob pattern="*.cci"/> <glob pattern="*.cci"/>
<glob pattern="*.3ds"/>
<magic><match value="NCSD" type="string" offset="256"/></magic> <magic><match value="NCSD" type="string" offset="256"/></magic>
</mime-type> </mime-type>

View File

@ -63,6 +63,7 @@ android {
// The application ID refers to Lime3DS to allow for // The application ID refers to Lime3DS to allow for
// the Play Store listing, which was originally set up for Lime3DS, to still be used. // the Play Store listing, which was originally set up for Lime3DS, to still be used.
applicationId = "io.github.lime3ds.android" applicationId = "io.github.lime3ds.android"
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 35
versionCode = autoVersion versionCode = autoVersion

View File

@ -186,6 +186,8 @@ object NativeLibrary {
external fun unlinkConsole() external fun unlinkConsole()
external fun downloadTitleFromNus(title: Long): InstallStatus
private var coreErrorAlertResult = false private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object() private val coreErrorAlertLock = Object()

View File

@ -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
}
}
}

View File

@ -45,7 +45,7 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels() private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
private var show3DSFileWarning: Boolean = true private var show3DSFileWarning: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -1,4 +1,4 @@
// Copyright Citra Emulator Project / Azahar Emulator Project // Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -121,14 +121,8 @@ class HomeSettingsFragment : Fragment() {
} }
), ),
HomeSetting( HomeSetting(
R.string.install_game_content, R.string.system_files,
R.string.install_game_content_description, R.string.system_files_description,
R.drawable.ic_install,
{ mainActivity.ciaFileInstaller.launch(true) }
),
HomeSetting(
R.string.setup_system_files,
R.string.setup_system_files_description,
R.drawable.ic_system_update, R.drawable.ic_system_update,
{ {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -136,6 +130,12 @@ class HomeSettingsFragment : Fragment() {
?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment) ?.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( HomeSetting(
R.string.share_log, R.string.share_log,
R.string.share_log_description, R.string.share_log_description,

View File

@ -1,61 +1,76 @@
// Copyright Citra Emulator Project / Azahar Emulator Project // Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
package org.citra.citra_emu.fragments package org.citra.citra_emu.fragments
import android.content.DialogInterface import android.content.res.Resources
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.FrameLayout import androidx.core.view.ViewCompat
import android.widget.LinearLayout import androidx.core.view.WindowInsetsCompat
import android.widget.RadioButton import androidx.core.view.updatePadding
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.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.textview.MaterialTextView
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.HomeNavigationDirections import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R 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.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.model.Game import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.viewmodel.GamesViewModel import org.citra.citra_emu.viewmodel.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel import org.citra.citra_emu.viewmodel.HomeViewModel
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
class SystemFilesFragment : Fragment() { class SystemFilesFragment : Fragment() {
private var _binding: FragmentSystemFilesBinding? = null private var _binding: FragmentSystemFilesBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
private val systemFilesViewModel: SystemFilesViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel 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 REGION_START = "RegionStart"
private val homeMenuMap: MutableMap<String, String> = mutableMapOf() private val homeMenuMap: MutableMap<String, String> = 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -77,15 +92,61 @@ class SystemFilesFragment : Fragment() {
homeViewModel.setNavigationVisibility(visible = false, animated = true) homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) 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 // TODO: Remove workaround for text filtering issue in material components when fixed
// https://github.com/material-components/material-components-android/issues/1464 // https://github.com/material-components/material-components-android/issues/1464
binding.dropdownSystemType.isSaveEnabled = false
binding.dropdownSystemRegion.isSaveEnabled = false
binding.dropdownSystemRegionStart.isSaveEnabled = false binding.dropdownSystemRegionStart.isSaveEnabled = false
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
systemFilesViewModel.shouldRefresh.collect {
if (it) {
reloadUi()
systemFilesViewModel.setShouldRefresh(false)
}
}
}
}
reloadUi() reloadUi()
if (savedInstanceState != null) { if (savedInstanceState != null) {
setDropdownSelection(
binding.dropdownSystemType,
systemTypeDropdown,
savedInstanceState.getInt(SYS_TYPE)
)
setDropdownSelection(
binding.dropdownSystemRegion,
systemRegionDropdown,
savedInstanceState.getInt(REGION)
)
binding.dropdownSystemRegionStart binding.dropdownSystemRegionStart
.setText(savedInstanceState.getString(REGION_START), false) .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() { override fun onResume() {
@ -98,41 +159,6 @@ class SystemFilesFragment : Fragment() {
SystemSaveGame.save() 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() { private fun reloadUi() {
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
@ -150,167 +176,31 @@ class SystemFilesFragment : Fragment() {
gamesViewModel.setShouldSwapData(true) gamesViewModel.setShouldSwapData(true)
} }
binding.setupSystemFilesDescription?.apply { if (!NativeLibrary.areKeysAvailable()) {
text = HtmlCompat.fromHtml( binding.apply {
context.getString(R.string.setup_system_files_preamble), systemType.isEnabled = false
HtmlCompat.FROM_HTML_MODE_COMPACT systemRegion.isEnabled = false
) buttonDownloadHomeMenu.isEnabled = false
movementMethod = LinkMovementMethod.getInstance() textKeysMissing.visibility = View.VISIBLE
} textKeysMissingHelp.visibility = View.VISIBLE
textKeysMissingHelp.text =
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked() Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY)
binding.buttonUnlinkConsoleData.setOnClickListener { textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance()
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()
}
}
} }
} else {
populateDownloadOptions()
}
binding.buttonDownloadHomeMenu.setOnClickListener {
val titleIds = NativeLibrary.getSystemTitleIds(
systemTypeDropdown.getValue(resources),
systemRegionDropdown.getValue(resources)
)
DownloadSystemFilesDialogFragment.newInstance(titleIds).show(
childFragmentManager,
DownloadSystemFilesDialogFragment.TAG
)
} }
populateHomeMenuOptions() 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() { private fun populateHomeMenuOptions() {
regionValues = resources.getIntArray(R.array.systemFileRegionValues) regionValues = resources.getIntArray(R.array.systemFileRegionValues)
val regionEntries = resources.getStringArray(R.array.systemFileRegions) val regionEntries = resources.getStringArray(R.array.systemFileRegions)
@ -350,4 +285,30 @@ class SystemFilesFragment : Fragment() {
binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false) 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
}
} }

View File

@ -63,7 +63,7 @@ class Game(
val allExtensions: Set<String> get() = extensions + badExtensions val allExtensions: Set<String> get() = extensions + badExtensions
val extensions: Set<String> = HashSet( val extensions: Set<String> = HashSet(
listOf("3dsx", "elf", "axf", "cci", "cxi", "app") listOf("3ds", "3dsx", "elf", "axf", "cci", "cxi", "app")
) )
val badExtensions: Set<String> = HashSet( val badExtensions: Set<String> = HashSet(

View File

@ -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<InstallStatus?>(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
}
}

View File

@ -464,6 +464,17 @@ void Java_org_citra_citra_1emu_NativeLibrary_uninstallSystemFiles(JNIEnv* env,
: Core::SystemTitleSet::New3ds); : 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<u64>(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() { [[maybe_unused]] static bool CheckKgslPresent() {
constexpr auto KgslPath{"/dev/kgsl-3d0"}; constexpr auto KgslPath{"/dev/kgsl-3d0"};

View File

@ -0,0 +1,219 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_about"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_about"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_system_files"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back"
app:title="@string/system_files" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_system_files"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="vertical"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:orientation="vertical"
android:paddingBottom="16dp"
android:layout_weight="1">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/download_system_files"
android:textAlignment="viewStart" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_type"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/system_type"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_region"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/emulated_region"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_region"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_download_home_menu"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/download" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_keys_missing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/keys_missing"
android:textAlignment="viewStart"
android:visibility="gone" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_keys_missing_help"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:visibility="gone"
tools:text="How to get keys?" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:orientation="vertical"
android:paddingBottom="16dp"
android:layout_weight="1">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/boot_home_menu"
android:textAlignment="viewStart" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_region_start"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/emulated_region"
android:enabled="false"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_region_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_start_home_menu"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="@string/start" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_run_system_setup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/run_system_setup"
android:textAlignment="viewStart"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_run_system_setup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_run_system_setup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_show_apps"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/show_home_apps"
android:textAlignment="viewStart"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_show_apps"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_show_apps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -18,7 +18,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/setup_system_files" /> app:title="@string/system_files" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@ -39,48 +39,77 @@
android:paddingBottom="16dp"> android:paddingBottom="16dp">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/setup_system_files"
android:textAlignment="viewStart" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/setupSystemFilesDescription"
style="@style/TextAppearance.Material3.TitleSmall" style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/download_system_files"
android:textAlignment="viewStart" /> android:textAlignment="viewStart" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_type"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/system_type"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_region"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/emulated_region"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_region"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<Button <Button
android:id="@+id/button_set_up_system_files" android:id="@+id/button_download_home_menu"
style="@style/Widget.Material3.Button.UnelevatedButton" style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/setup_tool_connect" /> android:layout_marginTop="16dp"
android:text="@string/download" />
<Button
android:id="@+id/button_unlink_console_data"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete_system_files" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:background="?android:attr/listDivider"
android:padding="40px" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium" android:id="@+id/text_keys_missing"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="16dp"
android:text="@string/keys_missing"
android:textAlignment="viewStart"
android:visibility="gone" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_keys_missing_help"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:visibility="gone"
tools:text="How to get keys?" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="@string/boot_home_menu" android:text="@string/boot_home_menu"
android:textAlignment="viewStart" /> android:textAlignment="viewStart" />

View File

@ -297,6 +297,17 @@
<item>6</item> <item>6</item>
</integer-array> </integer-array>
<string-array name="systemFileTypes">
<item>@string/system_type_minimal</item>
<item>@string/system_type_old_3ds</item>
<item>@string/system_type_new_3ds</item>
</string-array>
<integer-array name="systemFileTypeValues">
<item>1</item>
<item>2</item>
<item>4</item>
</integer-array>
<string-array name="soundOutputModes"> <string-array name="soundOutputModes">
<item>@string/mono</item> <item>@string/mono</item>
<item>@string/stereo</item> <item>@string/stereo</item>

View File

@ -2,7 +2,7 @@
<resources> <resources>
<!-- General application strings --> <!-- General application strings -->
<string name="app_name" translatable="false">Azahar</string> <string name="app_name" translatable="false">AzaharPlus</string>
<string name="app_disclaimer">This 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:\n<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'>Wiki - Citra Android user data and storage</a></string> <string name="app_disclaimer">This 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:\n<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'>Wiki - Citra Android user data and storage</a></string>
<string name="app_notification_channel_name" translatable="false">Azahar</string> <string name="app_notification_channel_name" translatable="false">Azahar</string>
<string name="app_notification_channel_id" translatable="false">Azahar</string> <string name="app_notification_channel_id" translatable="false">Azahar</string>
@ -148,12 +148,15 @@
<!-- System files strings --> <!-- System files strings -->
<string name="setup_system_files">System Files</string> <string name="setup_system_files">System Files</string>
<string name="system_files">System Files</string>
<string name="setup_system_files_description">Perform system file operations such as installing system files or booting the Home Menu</string> <string name="setup_system_files_description">Perform system file operations such as installing system files or booting the Home Menu</string>
<string name="system_files_description">Download system files to get Mii files, boot the HOME menu, and more</string>
<string name="setup_tool_connect">Connect to Artic Setup Tool</string> <string name="setup_tool_connect">Connect to Artic Setup Tool</string>
<string name="setup_system_files_preamble"><![CDATA[Azahar needs console unique data and firmware files from a real console to be able to use some of its features. Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a>.<br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders after performing the setup process!</b></li><li>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.</li><li>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.</li><li>Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul>]]></string> <string name="setup_system_files_preamble"><![CDATA[Azahar needs console unique data and firmware files from a real console to be able to use some of its features. Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a>.<br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders after performing the setup process!</b></li><li>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.</li><li>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.</li><li>Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul>]]></string>
<string name="setup_system_files_detect">Fetching current system files status, please wait...</string> <string name="setup_system_files_detect">Fetching current system files status, please wait...</string>
<string name="delete_system_files">Unlink Console Unique Data</string> <string name="delete_system_files">Unlink Console Unique Data</string>
<string name="delete_system_files_description"><![CDATA[This action will unlink your real console from Azahar, with the following consequences:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar.</li><li>Your friend list will reset and you will be logged out of your NNID/PNID account.</li><li>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).</li></ul><br>Continue?]]></string> <string name="delete_system_files_description"><![CDATA[This action will unlink your real console from Azahar, with the following consequences:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar.</li><li>Your friend list will reset and you will be logged out of your NNID/PNID account.</li><li>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).</li></ul><br>Continue?]]></string>
<string name="download_system_files">Download System Files</string>
<string name="setup_system_files_o3ds">Old 3DS Setup</string> <string name="setup_system_files_o3ds">Old 3DS Setup</string>
<string name="setup_system_files_n3ds">New 3DS Setup</string> <string name="setup_system_files_n3ds">New 3DS Setup</string>
<string name="setup_system_files_possible">Setup is possible.</string> <string name="setup_system_files_possible">Setup is possible.</string>
@ -162,9 +165,25 @@
<string name="setup_system_files_enter_address">Enter Artic Setup Tool address</string> <string name="setup_system_files_enter_address">Enter Artic Setup Tool address</string>
<string name="setup_system_files_preparing">Preparing setup, please wait...</string> <string name="setup_system_files_preparing">Preparing setup, please wait...</string>
<string name="boot_home_menu">Boot the HOME Menu</string> <string name="boot_home_menu">Boot the HOME Menu</string>
<string name="system_type">System Type</string>
<string name="download">Download</string>
<string name="keys_missing">Azahar is missing keys to download system files.</string>
<string name="how_to_get_keys"><![CDATA[<a href="https://web.archive.org/web/20240304203412/https://citra-emu.org/wiki/aes-keys/">How to get keys?</a>]]></string>
<string name="show_home_apps">Show HOME menu apps in Applications list</string> <string name="show_home_apps">Show HOME menu apps in Applications list</string>
<string name="run_system_setup">Run System Setup when the HOME Menu is launched</string> <string name="run_system_setup">Run System Setup when the HOME Menu is launched</string>
<string name="system_type_minimal">Minimal</string>
<string name="system_type_old_3ds">Old 3DS</string>
<string name="system_type_new_3ds">New 3DS</string>
<string name="downloading_files">Downloading Files…</string>
<string name="downloading_files_description">Please do not close the app.</string>
<string name="download_failed">Download Failed</string>
<string name="download_failed_description">Please make sure you are connected to the internet and try again.</string>
<string name="download_success">Download Complete!</string>
<string name="download_cancelled">Download Cancelled</string>
<string name="download_cancelled_description">Please restart the download to prevent issues with having incomplete system files.</string>
<string name="home_menu">HOME Menu</string> <string name="home_menu">HOME Menu</string>
<string name="home_menu_warning">System Files Warning</string>
<string name="home_menu_warning_description">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.</string>
<!-- Generic buttons (Shared with lots of stuff) --> <!-- Generic buttons (Shared with lots of stuff) -->
<string name="generic_buttons">Buttons</string> <string name="generic_buttons">Buttons</string>

View File

@ -1250,11 +1250,17 @@ bool GMainWindow::LoadROM(const QString& filename) {
break; break;
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
QMessageBox::critical(this, tr("App Encrypted"), QMessageBox::critical(
tr("Your app is encrypted. <br/>" this, tr("ROM Encrypted"),
"<a " tr("Your ROM is encrypted. <br/>Please follow the guides to redump your "
"href='https://azahar-emu.org/blog/game-loading-changes/'>" "<a "
"Please check our blog for more info.</a>")); "href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
break; break;
} }
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: 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)); QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename));
break; break;
case Service::AM::InstallStatus::ErrorEncrypted: case Service::AM::InstallStatus::ErrorEncrypted:
QMessageBox::critical(this, tr("CIA Encrypted"), QMessageBox::critical(this, tr("Encrypted File"),
tr("Your CIA file is encrypted.<br/>" tr("%1 must be decrypted "
"<a " "before being used with Azahar. A real 3DS is required.")
"href='https://azahar-emu.org/blog/game-loading-changes/'>" .arg(filename));
"Please check our blog for more info.</a>"));
break; break;
case Service::AM::InstallStatus::ErrorFileNotFound: case Service::AM::InstallStatus::ErrorFileNotFound:
QMessageBox::critical(this, tr("Unable to find File"), 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; return mime->hasUrls() && mime->urls().length() == 1;
} }
static const std::array<std::string, 8> AcceptedExtensions = {"cci", "cxi", "bin", "3dsx", static const std::array<std::string, 8> AcceptedExtensions = {"cci", "3ds", "cxi", "bin",
"app", "elf", "axf"}; "3dsx", "app", "elf", "axf"};
static bool IsCorrectFileExtension(const QMimeData* mime) { static bool IsCorrectFileExtension(const QMimeData* mime) {
const QString& filename = mime->urls().at(0).toLocalFile(); const QString& filename = mime->urls().at(0).toLocalFile();

View File

@ -217,16 +217,6 @@
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_cpu_clock_info">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;Underclocking can increase performance but may cause the application to freeze.&lt;br/&gt;Overclocking may reduce lag in applications but also might cause freezes&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="toggle_cpu_jit"> <widget class="QCheckBox" name="toggle_cpu_jit">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enables the use of the ARM JIT compiler for emulating the 3DS CPUs. Don't disable unless for debugging purposes&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enables the use of the ARM JIT compiler for emulating the 3DS CPUs. Don't disable unless for debugging purposes&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -236,14 +226,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="3" column="0">
<widget class="QCheckBox" name="toggle_renderer_debug"> <widget class="QCheckBox" name="toggle_renderer_debug">
<property name="text"> <property name="text">
<string>Enable debug renderer</string> <string>Enable debug renderer</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="4" column="0">
<widget class="QCheckBox" name="toggle_dump_command_buffers"> <widget class="QCheckBox" name="toggle_dump_command_buffers">
<property name="text"> <property name="text">
<string>Dump command buffers</string> <string>Dump command buffers</string>
@ -253,33 +243,43 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Miscellaneous</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QCheckBox" name="delay_start_for_lle_modules">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Introduces a delay to the first ever launched app thread if LLE modules are enabled, to allow them to initialize.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Delay app start for LLE module initialization</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="deterministic_async_operations">
<property name="text">
<string>Force deterministic async operations</string>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupBox_5"> <widget class="QLabel" name="label_cpu_clock_info">
<property name="title"> <property name="text">
<string>Miscellaneous</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;CPU Clock Speed Information&lt;br/&gt;Underclocking can increase performance but may cause the application to freeze.&lt;br/&gt;Overclocking may reduce lag in applications but also might cause freezes&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property> </property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0">
<widget class="QCheckBox" name="delay_start_for_lle_modules">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Introduces a delay to the first ever launched app thread if LLE modules are enabled, to allow them to initialize.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Delay app start for LLE module initialization</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="deterministic_async_operations">
<property name="text">
<string>Force deterministic async operations</string>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>

View File

@ -238,16 +238,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this, connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
&ConfigureSystem::RefreshConsoleID); &ConfigureSystem::RefreshConsoleID);
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC); connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
connect(ui->button_linked_console, &QPushButton::clicked, this, connect(ui->button_start_download, &QPushButton::clicked, this,
&ConfigureSystem::UnlinkConsole); &ConfigureSystem::DownloadFromNUS);
connect(ui->combo_country, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int index) {
CheckCountryValid(static_cast<u8>(ui->combo_country->itemData(index).toInt()));
});
connect(ui->region_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this]([[maybe_unused]] int index) {
CheckCountryValid(static_cast<u8>(ui->combo_country->currentData().toInt()));
});
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] { connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
ui->button_secure_info->setEnabled(false); ui->button_secure_info->setEnabled(false);
@ -255,7 +247,11 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
this, tr("Select SecureInfo_A/B"), QString(), this, tr("Select SecureInfo_A/B"), QString(),
tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)")); tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)"));
ui->button_secure_info->setEnabled(true); ui->button_secure_info->setEnabled(true);
#ifdef todotodo
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetSecureInfoAPath()); 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] { connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] {
ui->button_friend_code_seed->setEnabled(false); ui->button_friend_code_seed->setEnabled(false);
@ -264,6 +260,7 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A " tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A "
"LocalFriendCodeSeed_B);;All Files (*.*)")); "LocalFriendCodeSeed_B);;All Files (*.*)"));
ui->button_friend_code_seed->setEnabled(true); ui->button_friend_code_seed->setEnabled(true);
#ifdef todotodo
InstallSecureData(file_path_qtstr.toStdString(), InstallSecureData(file_path_qtstr.toStdString(),
HW::UniqueData::GetLocalFriendCodeSeedBPath()); 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 (*.*)")); this, tr("Select movable.sed"), QString(), tr("Sed file (*.sed);;All Files (*.*)"));
ui->button_movable->setEnabled(true); ui->button_movable->setEnabled(true);
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetMovablePath()); 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++) { 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->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(); 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(); ConfigureTime();
} }
@ -300,19 +333,6 @@ ConfigureSystem::~ConfigureSystem() = default;
void ConfigureSystem::SetConfiguration() { void ConfigureSystem::SetConfiguration() {
enabled = !system.IsPoweredOn(); 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<int>(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<u8>(Settings::values.init_clock.GetValue())); ui->combo_init_clock->setCurrentIndex(static_cast<u8>(Settings::values.init_clock.GetValue()));
QDateTime date_time; QDateTime date_time;
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue()); date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
@ -374,7 +394,6 @@ void ConfigureSystem::ReadSystemSettings() {
// set the country code // set the country code
country_code = cfg->GetCountryCode(); country_code = cfg->GetCountryCode();
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code)); ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
CheckCountryValid(country_code);
// set whether system setup is needed // set whether system setup is needed
system_setup = cfg->IsSystemSetupNeeded(); system_setup = cfg->IsSystemSetupNeeded();
@ -391,16 +410,15 @@ void ConfigureSystem::ReadSystemSettings() {
play_coin = Service::PTM::Module::GetPlayCoins(); play_coin = Service::PTM::Module::GetPlayCoins();
ui->spinBox_play_coins->setValue(play_coin); ui->spinBox_play_coins->setValue(play_coin);
// set firmware download region
ui->combo_download_region->setCurrentIndex(static_cast<int>(cfg->GetRegionValue()));
// Refresh secure data status // Refresh secure data status
RefreshSecureDataStatus(); RefreshSecureDataStatus();
} }
void ConfigureSystem::ApplyConfiguration() { void ConfigureSystem::ApplyConfiguration() {
if (enabled) { if (enabled) {
ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value,
ui->region_combobox,
[](s32 index) { return index - 1; });
bool modified = false; bool modified = false;
// apply username // apply username
@ -591,51 +609,6 @@ void ConfigureSystem::RefreshMAC() {
ui->label_mac->setText(tr("MAC: %1").arg(QString::fromStdString(mac_address))); 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:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed "
"from Azahar.</li><li>Your friend list will reset and you will be logged out of your "
"NNID/PNID account.</li><li>System files and eshop titles obtained through Azahar will "
"become inaccessible until the same console is linked again (save data will not be "
"lost).</li></ul><br>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<u32>(region), country)) {
label_text = tr("Invalid country for configured region");
}
if (HW::UniqueData::GetSecureInfoA().IsValid()) {
region = static_cast<u32>(cfg->GetRegionValue(true));
if (!cfg->IsValidRegionCountry(static_cast<u32>(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) { void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) {
std::string from = std::string from =
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault); 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::CreateFullPath(to);
FileUtil::Copy(from, to); FileUtil::Copy(from, to);
HW::UniqueData::InvalidateSecureData(); HW::UniqueData::InvalidateSecureData();
cfg->InvalidateSecureData();
RefreshSecureDataStatus(); 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() { void ConfigureSystem::RefreshSecureDataStatus() {
auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) { auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) {
switch (status) { switch (status) {
@ -676,16 +664,38 @@ void ConfigureSystem::RefreshSecureDataStatus() {
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str())); tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str()));
ui->label_movable_status->setText( ui->label_movable_status->setText(
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadMovable())).c_str())); 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::CFG::SecureDataLoadStatus>(
Service::AM::Module::LoadCTCertFile(ct_cert))))
.c_str()));
}
//--
void ConfigureSystem::RetranslateUI() { void ConfigureSystem::RetranslateUI() {
ui->retranslateUi(this); ui->retranslateUi(this);
@ -698,7 +708,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal()); ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
ui->enable_required_online_lle_modules->setEnabled( ui->enable_required_online_lle_modules->setEnabled(
Settings::values.enable_required_online_lle_modules.UsingGlobal()); Settings::values.enable_required_online_lle_modules.UsingGlobal());
ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
return; return;
} }
@ -710,7 +719,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_init_ticks_type->setVisible(false); ui->label_init_ticks_type->setVisible(false);
ui->label_init_ticks_value->setVisible(false); ui->label_init_ticks_value->setVisible(false);
ui->label_console_id->setVisible(false); ui->label_console_id->setVisible(false);
ui->label_mac->setVisible(false);
ui->label_sound->setVisible(false); ui->label_sound->setVisible(false);
ui->label_language->setVisible(false); ui->label_language->setVisible(false);
ui->label_country->setVisible(false); ui->label_country->setVisible(false);
@ -732,7 +740,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->edit_init_ticks_value->setVisible(false); ui->edit_init_ticks_value->setVisible(false);
ui->toggle_system_setup->setVisible(false); ui->toggle_system_setup->setVisible(false);
ui->button_regenerate_console_id->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 // Apps can change the state of the plugin loader, so plugins load
// to a chainloaded app with specific parameters. Don't allow // to a chainloaded app with specific parameters. Don't allow
// the plugin loader state to be configured per-game as it may // 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->label_plugin_loader->setVisible(false);
ui->plugin_loader->setVisible(false); ui->plugin_loader->setVisible(false);
ui->allow_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, ConfigurationShared::SetColoredTristate(ui->toggle_new_3ds, Settings::values.is_new_3ds,
is_new_3ds); is_new_3ds);
@ -749,7 +758,45 @@ void ConfigureSystem::SetupPerGameUI() {
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules, ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
Settings::values.enable_required_online_lle_modules, Settings::values.enable_required_online_lle_modules,
required_online_lle_modules); required_online_lle_modules);
ConfigurationShared::SetColoredComboBox( }
ui->region_combobox, ui->region_label,
static_cast<u32>(Settings::values.region_value.GetValue(true) + 1)); void ConfigureSystem::DownloadFromNUS() {
ui->button_start_download->setEnabled(false);
const auto mode =
static_cast<Core::SystemTitleSet>(1 << ui->combo_download_set->currentIndex());
const auto region = static_cast<u32>(ui->combo_download_region->currentIndex());
const std::vector<u64> titles = Core::GetSystemTitleIds(mode, region);
QProgressDialog progress(tr("Downloading files..."), tr("Cancel"), 0,
static_cast<int>(titles.size()), this);
progress.setWindowModality(Qt::WindowModal);
QFutureWatcher<void> future_watcher;
QObject::connect(&future_watcher, &QFutureWatcher<void>::finished, &progress,
&QProgressDialog::reset);
QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher,
&QFutureWatcher<void>::cancel);
QObject::connect(&future_watcher, &QFutureWatcher<void>::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);
} }

View File

@ -56,10 +56,13 @@ private:
void CheckCountryValid(u8 country); void CheckCountryValid(u8 country);
void InstallSecureData(const std::string& from_path, const std::string& to_path); void InstallSecureData(const std::string& from_path, const std::string& to_path);
void InstallCTCert(const std::string& from_path);
void RefreshSecureDataStatus(); void RefreshSecureDataStatus();
void SetupPerGameUI(); void SetupPerGameUI();
void DownloadFromNUS();
private: private:
std::unique_ptr<Ui::ConfigureSystem> ui; std::unique_ptr<Ui::ConfigureSystem> ui;
Core::System& system; Core::System& system;

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>653</width> <width>535</width>
<height>619</height> <height>619</height>
</rect> </rect>
</property> </property>
@ -14,26 +14,11 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_scrollbar"> <layout class="QVBoxLayout" name="verticalLayout_scrollbar">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item> <item>
<widget class="QScrollArea" name="scrollArea"> <widget class="QScrollArea" name="scrollArea">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>660</width> <width>0</width>
<height>480</height> <height>480</height>
</size> </size>
</property> </property>
@ -64,83 +49,21 @@
<string>System Settings</string> <string>System Settings</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="1" column="0">
<widget class="QCheckBox" name="toggle_new_3ds"> <widget class="QCheckBox" name="toggle_new_3ds">
<property name="text"> <property name="text">
<string>Enable New 3DS mode</string> <string>Enable New 3DS mode</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="2" column="0">
<widget class="QCheckBox" name="toggle_lle_applets"> <widget class="QCheckBox" name="toggle_lle_applets">
<property name="text"> <property name="text">
<string>Use LLE applets (if installed)</string> <string>Use LLE applets (if installed)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QCheckBox" name="enable_required_online_lle_modules">
<property name="text">
<string>Enable required LLE modules for
online features (if installed)</string>
</property>
<property name="toolTip">
<string>Enables the LLE modules needed for online multiplayer, eShop access, etc.</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="region_label">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="QComboBox" name="region_combobox">
<item>
<property name="text">
<string>Auto-select</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">JPN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">USA</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">EUR</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AUS</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">CHN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">KOR</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TWN</string>
</property>
</item>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="edit_username"> <widget class="QLineEdit" name="edit_username">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@ -153,21 +76,21 @@ online features (if installed)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_username"> <widget class="QLabel" name="label_username">
<property name="text"> <property name="text">
<string>Username</string> <string>Username</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_birthday"> <widget class="QLabel" name="label_birthday">
<property name="text"> <property name="text">
<string>Birthday</string> <string>Birthday</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_birthday2"> <layout class="QHBoxLayout" name="horizontalLayout_birthday2">
<item> <item>
<widget class="QComboBox" name="combo_birthmonth"> <widget class="QComboBox" name="combo_birthmonth">
@ -238,14 +161,14 @@ online features (if installed)</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_language"> <widget class="QLabel" name="label_language">
<property name="text"> <property name="text">
<string>Language</string> <string>Language</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="5" column="1">
<widget class="QComboBox" name="combo_language"> <widget class="QComboBox" name="combo_language">
<property name="toolTip"> <property name="toolTip">
<string>Note: this can be overridden when region setting is auto-select</string> <string>Note: this can be overridden when region setting is auto-select</string>
@ -312,14 +235,14 @@ online features (if installed)</string>
</item> </item>
</widget> </widget>
</item> </item>
<item row="7" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_sound"> <widget class="QLabel" name="label_sound">
<property name="text"> <property name="text">
<string>Sound output mode</string> <string>Sound output mode</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="6" column="1">
<widget class="QComboBox" name="combo_sound"> <widget class="QComboBox" name="combo_sound">
<item> <item>
<property name="text"> <property name="text">
@ -338,31 +261,24 @@ online features (if installed)</string>
</item> </item>
</widget> </widget>
</item> </item>
<item row="8" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_country"> <widget class="QLabel" name="label_country">
<property name="text"> <property name="text">
<string>Country</string> <string>Country</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="7" column="1">
<widget class="QComboBox" name="combo_country"/> <widget class="QComboBox" name="combo_country"/>
</item> </item>
<item row="9" column="1"> <item row="8" column="0">
<widget class="QLabel" name="label_country_invalid">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_init_clock"> <widget class="QLabel" name="label_init_clock">
<property name="text"> <property name="text">
<string>Clock</string> <string>Clock</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1"> <item row="8" column="1">
<widget class="QComboBox" name="combo_init_clock"> <widget class="QComboBox" name="combo_init_clock">
<item> <item>
<property name="text"> <property name="text">
@ -376,28 +292,28 @@ online features (if installed)</string>
</item> </item>
</widget> </widget>
</item> </item>
<item row="11" column="0"> <item row="9" column="0">
<widget class="QLabel" name="label_init_time"> <widget class="QLabel" name="label_init_time">
<property name="text"> <property name="text">
<string>Startup time</string> <string>Startup time</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="1"> <item row="9" column="1">
<widget class="QDateTimeEdit" name="edit_init_time"> <widget class="QDateTimeEdit" name="edit_init_time">
<property name="displayFormat"> <property name="displayFormat">
<string>yyyy-MM-ddTHH:mm:ss</string> <string>yyyy-MM-ddTHH:mm:ss</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="0"> <item row="9" column="0">
<widget class="QLabel" name="label_init_time_offset"> <widget class="QLabel" name="label_init_time_offset">
<property name="text"> <property name="text">
<string>Offset time</string> <string>Offset time</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="1"> <item row="9" column="1">
<layout class="QGridLayout" name="edit_init_time_offset_grid"> <layout class="QGridLayout" name="edit_init_time_offset_grid">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QSpinBox" name="edit_init_time_offset_days"> <widget class="QSpinBox" name="edit_init_time_offset_days">
@ -421,14 +337,14 @@ online features (if installed)</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="13" column="0"> <item row="10" column="0">
<widget class="QLabel" name="label_init_ticks_type"> <widget class="QLabel" name="label_init_ticks_type">
<property name="text"> <property name="text">
<string>Initial System Ticks</string> <string>Initial System Ticks</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="1"> <item row="10" column="1">
<widget class="QComboBox" name="combo_init_ticks_type"> <widget class="QComboBox" name="combo_init_ticks_type">
<item> <item>
<property name="text"> <property name="text">
@ -442,14 +358,14 @@ online features (if installed)</string>
</item> </item>
</widget> </widget>
</item> </item>
<item row="14" column="0"> <item row="11" column="0">
<widget class="QLabel" name="label_init_ticks_value"> <widget class="QLabel" name="label_init_ticks_value">
<property name="text"> <property name="text">
<string>Initial System Ticks Override</string> <string>Initial System Ticks Override</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="14" column="1"> <item row="11" column="1">
<widget class="QLineEdit" name="edit_init_ticks_value"> <widget class="QLineEdit" name="edit_init_ticks_value">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@ -462,21 +378,21 @@ online features (if installed)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="0"> <item row="12" column="0">
<widget class="QLabel" name="label_play_coins"> <widget class="QLabel" name="label_play_coins">
<property name="text"> <property name="text">
<string>Play Coins</string> <string>Play Coins</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="1"> <item row="12" column="1">
<widget class="QSpinBox" name="spinBox_play_coins"> <widget class="QSpinBox" name="spinBox_play_coins">
<property name="maximum"> <property name="maximum">
<number>300</number> <number>300</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="16" column="0"> <item row="13" column="0">
<widget class="QLabel" name="label_steps_per_hour"> <widget class="QLabel" name="label_steps_per_hour">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Number of steps per hour reported by the pedometer. Range from 0 to 65,535.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Number of steps per hour reported by the pedometer. Range from 0 to 65,535.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -486,28 +402,28 @@ online features (if installed)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="16" column="1"> <item row="13" column="1">
<widget class="QSpinBox" name="spinBox_steps_per_hour"> <widget class="QSpinBox" name="spinBox_steps_per_hour">
<property name="maximum"> <property name="maximum">
<number>9999</number> <number>9999</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="17" column="1"> <item row="14" column="1">
<widget class="QCheckBox" name="toggle_system_setup"> <widget class="QCheckBox" name="toggle_system_setup">
<property name="text"> <property name="text">
<string>Run System Setup when Home Menu is launched</string> <string>Run System Setup when Home Menu is launched</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="18" column="0"> <item row="15" column="0">
<widget class="QLabel" name="label_console_id"> <widget class="QLabel" name="label_console_id">
<property name="text"> <property name="text">
<string>Console ID:</string> <string>Console ID:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="18" column="1"> <item row="15" column="1">
<widget class="QPushButton" name="button_regenerate_console_id"> <widget class="QPushButton" name="button_regenerate_console_id">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
@ -523,50 +439,114 @@ online features (if installed)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="19" column="0"> <item row="16" column="0">
<widget class="QLabel" name="label_mac">
<property name="text">
<string>MAC:</string>
</property>
</widget>
</item>
<item row="19" column="1">
<widget class="QPushButton" name="button_regenerate_mac">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Regenerate</string>
</property>
</widget>
</item>
<item row="20" column="0">
<widget class="QLabel" name="label_plugin_loader"> <widget class="QLabel" name="label_plugin_loader">
<property name="text"> <property name="text">
<string>3GX Plugin Loader:</string> <string>3GX Plugin Loader:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="20" column="1"> <item row="16" column="1">
<widget class="QCheckBox" name="plugin_loader"> <widget class="QCheckBox" name="plugin_loader">
<property name="text"> <property name="text">
<string>Enable 3GX plugin loader</string> <string>Enable 3GX plugin loader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="21" column="1"> <item row="17" column="1">
<widget class="QCheckBox" name="allow_plugin_loader"> <widget class="QCheckBox" name="allow_plugin_loader">
<property name="text"> <property name="text">
<string>Allow applications to change plugin loader state</string> <string>Allow applications to change plugin loader state</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="18" column="0">
<widget class="QLabel" name="label_nus_download">
<property name="text">
<string>Download System Files from Nintendo servers</string>
</property>
</widget>
</item>
<item row="18" column="1">
<widget class="QWidget" name="body_nus_download">
<layout class="QHBoxLayout" name="horizontalLayout_nus_download">
<item>
<widget class="QComboBox" name="combo_download_set">
<item>
<property name="text">
<string>Minimal</string>
</property>
</item>
<item>
<property name="text">
<string>Old 3DS</string>
</property>
</item>
<item>
<property name="text">
<string>New 3DS</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="combo_download_region">
<item>
<property name="text">
<string>JPN</string>
</property>
</item>
<item>
<property name="text">
<string>USA</string>
</property>
</item>
<item>
<property name="text">
<string>EUR</string>
</property>
</item>
<item>
<property name="text">
<string>AUS</string>
</property>
</item>
<item>
<property name="text">
<string>CHN</string>
</property>
</item>
<item>
<property name="text">
<string>KOR</string>
</property>
</item>
<item>
<property name="text">
<string>TWN</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_start_download">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -576,168 +556,97 @@ online features (if installed)</string>
<string>Real Console Unique Data</string> <string>Real Console Unique Data</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout1"> <layout class="QGridLayout" name="gridLayout1">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="group_real_console_unique_data_core">
<layout class="QGridLayout" name="gridLayout2">
<item row="0" column="0" colspan="2">
<widget class="QWidget" name="linked_console">
<layout class="QHBoxLayout" name="horizontalLayout_linked_console">
<item>
<widget class="QLabel" name="label_linked_console">
<property name="text">
<string>Your real console is linked to Azahar.</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_linked_console">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Unlink</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_otp">
<property name="text">
<string>OTP</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QWidget" name="otp">
<layout class="QHBoxLayout" name="horizontalLayout_otp">
<item>
<widget class="QLabel" name="label_otp_status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_otp">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Choose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_secure_info">
<property name="text">
<string>SecureInfo_A/B</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QWidget" name="secure_info">
<layout class="QHBoxLayout" name="horizontalLayout_secure_info">
<item>
<widget class="QLabel" name="label_secure_info_status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_secure_info">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Choose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_friend_code_seed">
<property name="text">
<string>LocalFriendCodeSeed_A/B</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QWidget" name="friend_code_seed">
<layout class="QHBoxLayout" name="horizontalLayout_friend_code_seed">
<item>
<widget class="QLabel" name="label_friend_code_seed_status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_friend_code_seed">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Choose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_movable"> <widget class="QLabel" name="label_secure_info">
<property name="text"> <property name="text">
<string>movable.sed</string> <string>SecureInfo_A/B</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QWidget" name="movable"> <widget class="QWidget" name="secure_info">
<layout class="QHBoxLayout" name="horizontalLayout_movable"> <layout class="QHBoxLayout" name="horizontalLayout_secure_info">
<item> <item>
<widget class="QLabel" name="label_movable_status"> <widget class="QLabel" name="label_secure_info_status">
<property name="text"> <property name="text">
<string/> <string/>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="button_movable"> <widget class="QPushButton" name="button_secure_info">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Choose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_friend_code_seed">
<property name="text">
<string>LocalFriendCodeSeed_A/B</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QWidget" name="friend_code_seed">
<layout class="QHBoxLayout" name="horizontalLayout_friend_code_seed">
<item>
<widget class="QLabel" name="label_friend_code_seed_status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_friend_code_seed">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Choose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_ct_cert">
<property name="text">
<string>CTCert</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QWidget" name="ct_cert">
<layout class="QHBoxLayout" name="horizontalLayout_ct_cert">
<item>
<widget class="QLabel" name="label_ct_cert_status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_ct_cert">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -802,7 +711,6 @@ online features (if installed)</string>
<tabstop>spinBox_play_coins</tabstop> <tabstop>spinBox_play_coins</tabstop>
<tabstop>spinBox_steps_per_hour</tabstop> <tabstop>spinBox_steps_per_hour</tabstop>
<tabstop>button_regenerate_console_id</tabstop> <tabstop>button_regenerate_console_id</tabstop>
<tabstop>button_regenerate_mac</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>

View File

@ -1039,7 +1039,7 @@ void GameList::LoadInterfaceLayout() {
} }
const QStringList GameList::supported_file_extensions = { 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")}; QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")};
void GameList::RefreshGameDirectory() { void GameList::RefreshGameDirectory() {

View File

@ -184,9 +184,11 @@ public:
if (UISettings::values.game_list_icon_size.GetValue() != if (UISettings::values.game_list_icon_size.GetValue() !=
UISettings::GameListIconSize::NoIcon) UISettings::GameListIconSize::NoIcon)
setData(GetDefaultIcon(large), Qt::DecorationRole); setData(GetDefaultIcon(large), Qt::DecorationRole);
/* todotodo
if (is_encrypted) { if (is_encrypted) {
setData(QObject::tr("Unsupported encrypted application"), TitleRole); setData(QObject::tr("Unsupported encrypted application"), TitleRole);
} }
*/
return; return;
} }

View File

@ -94,7 +94,7 @@ struct Values {
Settings::Setting<GameListText> game_list_row_2{GameListText::FileName, "row2"}; Settings::Setting<GameListText> game_list_row_2{GameListText::FileName, "row2"};
Settings::Setting<bool> game_list_hide_no_icon{false, "hideNoIcon"}; Settings::Setting<bool> game_list_hide_no_icon{false, "hideNoIcon"};
Settings::Setting<bool> game_list_single_line_mode{false, "singleLineMode"}; Settings::Setting<bool> game_list_single_line_mode{false, "singleLineMode"};
Settings::Setting<bool> show_3ds_files_warning{true, "show_3ds_files_warning"}; Settings::Setting<bool> show_3ds_files_warning{false, "show_3ds_files_warning"};
// Compatibility List // Compatibility List
Settings::Setting<bool> show_compat_column{true, "show_compat_column"}; Settings::Setting<bool> show_compat_column{true, "show_compat_column"};

View File

@ -81,5 +81,6 @@
// Sys files // Sys files
#define SHARED_FONT "shared_font.bin" #define SHARED_FONT "shared_font.bin"
#define KEYS_FILE "keys.txt" #define KEYS_FILE "keys.txt"
#define AES_KEYS "aes_keys.txt"
#define BOOTROM9 "boot9.bin" #define BOOTROM9 "boot9.bin"
#define SECRET_SECTOR "sector0x96.bin" #define SECRET_SECTOR "sector0x96.bin"

View File

@ -386,7 +386,15 @@ public:
[[nodiscard]] size_t ReadSpan(std::span<T> data) { [[nodiscard]] size_t ReadSpan(std::span<T> data) {
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable."); static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
#ifdef todotodo
return ReadImpl(data.data(), data.size(), sizeof(T)); 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<const T> data) { [[nodiscard]] size_t WriteSpan(std::span<const T> data) {
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable."); static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
#ifdef todotodo
return WriteImpl(data.data(), data.size(), sizeof(T)); 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 { [[nodiscard]] bool IsOpen() const {

View File

@ -70,6 +70,7 @@ HackManager hack_manager = {
}, },
}}, }},
#ifdef todotodo
{HackType::ONLINE_LLE_REQUIRED, {HackType::ONLINE_LLE_REQUIRED,
HackEntry{ HackEntry{
.mode = HackAllowMode::FORCE, .mode = HackAllowMode::FORCE,
@ -107,6 +108,7 @@ HackManager hack_manager = {
0x000400000D40D200, 0x000400000D40D200,
}, },
}}, }},
#endif
{HackType::REGION_FROM_SECURE, {HackType::REGION_FROM_SECURE,
HackEntry{ HackEntry{

View File

@ -137,7 +137,9 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
return Loader::ResultStatus::Success; return Loader::ResultStatus::Success;
} }
#ifdef todotodo
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
#endif
if (!file->IsOpen()) { if (!file->IsOpen()) {
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
} }
@ -164,6 +166,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
// Verify we are loading the correct file type... // Verify we are loading the correct file type...
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
#ifdef todotodo
// We may be loading a crypto file, try again // We may be loading a crypto file, try again
if (i == 0) { if (i == 0) {
file.reset(); file.reset();
@ -172,8 +175,13 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
} else { } else {
return Loader::ResultStatus::ErrorInvalidFormat; return Loader::ResultStatus::ErrorInvalidFormat;
} }
#else
return Loader::ResultStatus::ErrorInvalidFormat;
#endif
} }
#ifdef todotodo
} }
#endif
if (file->IsCrypto()) { if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto"); LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
@ -192,7 +200,9 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->IsOpen()) { if (file->IsOpen()) {
size_t file_size; size_t file_size;
#ifdef todotodo
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
#endif
file_size = file->GetSize(); file_size = file->GetSize();
// Reset read pointer in case this file has been read before. // 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... // Verify we are loading the correct file type...
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
#ifdef todotodo
// We may be loading a crypto file, try again // We may be loading a crypto file, try again
if (i == 0) { if (i == 0) {
file = HW::UniqueData::OpenUniqueCryptoFile( file = HW::UniqueData::OpenUniqueCryptoFile(
@ -222,14 +233,146 @@ Loader::ResultStatus NCCHContainer::Load() {
} else { } else {
return Loader::ResultStatus::ErrorInvalidFormat; return Loader::ResultStatus::ErrorInvalidFormat;
} }
#else
return Loader::ResultStatus::ErrorInvalidFormat;
#endif
} }
#ifdef todotodo
} }
#endif
if (file->IsCrypto()) { if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto"); LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
} }
has_header = true; 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<u8, 16> 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<u8, 32> 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<u8, CryptoPP::SHA256::DIGESTSIZE> 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<u8, 4> {
return std::array<u8, 4>{
static_cast<u8>(value >> 24),
static_cast<u8>((value >> 16) & 0xFF),
static_cast<u8>((value >> 8) & 0xFF),
static_cast<u8>(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) { if (ncch_header.content_size == file_size) {
// The NCCH is a proto version, which does not use media size units // The NCCH is a proto version, which does not use media size units
@ -237,10 +380,12 @@ Loader::ResultStatus NCCHContainer::Load() {
block_size = 1; block_size = 1;
} }
#ifdef todotodo
if (!ncch_header.no_crypto) { if (!ncch_header.no_crypto) {
// Encrypted NCCH are not supported // Encrypted NCCH are not supported
return Loader::ResultStatus::ErrorEncrypted; return Loader::ResultStatus::ErrorEncrypted;
} }
#endif
// System archives and DLC don't have an extended header but have RomFS // System archives and DLC don't have an extended header but have RomFS
// Proto apps don't have an ext header size // Proto apps don't have an ext header size
@ -254,6 +399,26 @@ Loader::ResultStatus NCCHContainer::Load() {
return Loader::ResultStatus::Error; 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<CryptoPP::byte*>(&exheader_header);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(
primary_key.data(), primary_key.size(), exheader_ctr.data())
.ProcessData(data, data, sizeof(exheader_header));
}
}
const auto mods_path = const auto mods_path =
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
GetModId(ncch_header.program_id)); GetModId(ncch_header.program_id));
@ -323,6 +488,7 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
#ifdef todotodo
if (file->IsCrypto()) { if (file->IsCrypto()) {
exefs_file = HW::UniqueData::OpenUniqueCryptoFile( exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
@ -330,6 +496,16 @@ Loader::ResultStatus NCCHContainer::Load() {
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb"); exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
} }
#else
if (is_encrypted) {
CryptoPP::byte* data = reinterpret_cast<CryptoPP::byte*>(&exefs_header);
CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(primary_key.data(),
primary_key.size(), exefs_ctr.data())
.ProcessData(data, data, sizeof(exefs_header));
}
exefs_file = std::make_unique<FileUtil::IOFile>(filepath, "rb");
#endif
has_exefs = true; 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); : (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset);
exefs_file->Seek(section_offset, SEEK_SET); exefs_file->Seek(section_offset, SEEK_SET);
std::array<u8, 16> key;
if (strcmp(section.name, "icon") == 0 || strcmp(section.name, "banner") == 0) {
key = primary_key;
} else {
key = secondary_key;
}
CryptoPP::CTR_Mode<CryptoPP::AES>::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; size_t section_size = is_proto ? Common::AlignUp(section.size, 0x10) : section.size;
if (strcmp(section.name, ".code") == 0 && is_compressed) { if (strcmp(section.name, ".code") == 0 && is_compressed) {
@ -466,6 +653,10 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
temp_buffer.size()) temp_buffer.size())
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
if (is_encrypted) {
dec.ProcessData(&temp_buffer[0], &temp_buffer[0], section.size);
}
// Decompress .code section... // Decompress .code section...
buffer.resize(LZSS_GetDecompressedSize(temp_buffer)); buffer.resize(LZSS_GetDecompressedSize(temp_buffer));
if (!LZSS_Decompress(temp_buffer, buffer)) { if (!LZSS_Decompress(temp_buffer, buffer)) {
@ -476,6 +667,9 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
buffer.resize(section_size); buffer.resize(section_size);
if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size) if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size)
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
if (is_encrypted) {
dec.ProcessData(buffer.data(), buffer.data(), section.size);
}
} }
return Loader::ResultStatus::Success; return Loader::ResultStatus::Success;
@ -607,18 +801,34 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<RomFSReader>& romf
// We reopen the file, to allow its position to be independent from file's // We reopen the file, to allow its position to be independent from file's
std::unique_ptr<FileUtil::IOFile> romfs_file_inner; std::unique_ptr<FileUtil::IOFile> romfs_file_inner;
#ifdef todotodo
if (file->IsCrypto()) { if (file->IsCrypto()) {
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile( romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
} else { } else {
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb"); romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
} }
#else
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
#endif
if (!romfs_file_inner->IsOpen()) if (!romfs_file_inner->IsOpen())
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
#ifdef todotodo
std::shared_ptr<RomFSReader> direct_romfs = std::shared_ptr<RomFSReader> direct_romfs =
std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner), romfs_offset, romfs_size); std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner), romfs_offset, romfs_size);
#else
std::shared_ptr<RomFSReader> direct_romfs;
if (is_encrypted) {
direct_romfs =
std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner), romfs_offset,
romfs_size, secondary_key, romfs_ctr, 0x1000);
} else {
direct_romfs = std::make_shared<DirectRomFSReader>(std::move(romfs_file_inner),
romfs_offset, romfs_size);
}
#endif
const auto path = const auto path =
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
@ -636,8 +846,10 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<RomFSReader>& romf
} }
Loader::ResultStatus NCCHContainer::DumpRomFS(const std::string& target_path) { Loader::ResultStatus NCCHContainer::DumpRomFS(const std::string& target_path) {
#ifdef todotodo
if (file->IsCrypto()) if (file->IsCrypto())
return Loader::ResultStatus::ErrorEncrypted; return Loader::ResultStatus::ErrorEncrypted;
#endif
std::shared_ptr<RomFSReader> direct_romfs; std::shared_ptr<RomFSReader> direct_romfs;
Loader::ResultStatus result = ReadRomFS(direct_romfs, false); Loader::ResultStatus result = ReadRomFS(direct_romfs, false);

View File

@ -348,6 +348,14 @@ private:
bool is_loaded = false; bool is_loaded = false;
bool is_compressed = false; bool is_compressed = false;
bool is_encrypted = false;
// for decrypting exheader, exefs header and icon/banner section
std::array<u8, 16> primary_key{};
std::array<u8, 16> secondary_key{}; // for decrypting romfs and .code section
std::array<u8, 16> exheader_ctr{};
std::array<u8, 16> exefs_ctr{};
std::array<u8, 16> romfs_ctr{};
u32 ncch_offset = 0; // Offset to NCCH header, can be 0 for NCCHs or non-zero for CIAs/NCSDs u32 ncch_offset = 0; // Offset to NCCH header, can be 0 for NCCHs or non-zero for CIAs/NCSDs
u32 exefs_offset = 0; u32 exefs_offset = 0;
u32 partition = 0; u32 partition = 0;

View File

@ -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 // Skip cache if the read is too big
if (segments.size() == 1 && segments[0].second > cache_line_size) { if (segments.size() == 1 && segments[0].second > cache_line_size) {
length = file->ReadAtBytes(buffer, length, file_offset + offset); length = file->ReadAtBytes(buffer, length, file_offset + offset);
if (is_encrypted) {
CryptoPP::CTR_Mode<CryptoPP::AES>::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); LOG_TRACE(Service_FS, "RomFS Cache SKIP: offset={}, length={}", offset, length);
return 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 (!cache_entry.first) {
// If not found, read from disk and cache the data // If not found, read from disk and cache the data
read_size = file->ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page); read_size = file->ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page);
if (is_encrypted && read_size) {
CryptoPP::CTR_Mode<CryptoPP::AES>::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, LOG_TRACE(Service_FS, "RomFS Cache MISS: page={}, length={}, into={}", page, seg.second,
(seg.first - page)); (seg.first - page));
} else { } else {

View File

@ -47,7 +47,13 @@ class DirectRomFSReader : public RomFSReader {
public: public:
DirectRomFSReader(std::unique_ptr<FileUtil::IOFile>&& file, std::size_t file_offset, DirectRomFSReader(std::unique_ptr<FileUtil::IOFile>&& file, std::size_t file_offset,
std::size_t data_size) 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<FileUtil::IOFile>&& file, std::size_t file_offset, std::size_t data_size,
const std::array<u8, 16>& key, const std::array<u8, 16>& 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; ~DirectRomFSReader() override = default;
@ -62,8 +68,12 @@ public:
bool CacheReady(std::size_t file_offset, std::size_t length) override; bool CacheReady(std::size_t file_offset, std::size_t length) override;
private: private:
bool is_encrypted;
std::unique_ptr<FileUtil::IOFile> file; std::unique_ptr<FileUtil::IOFile> file;
std::array<u8, 16> key;
std::array<u8, 16> ctr;
u64 file_offset; u64 file_offset;
u64 crypto_offset;
u64 data_size; u64 data_size;
// Total cache size: 128KB // Total cache size: 128KB
@ -86,8 +96,12 @@ private:
template <class Archive> template <class Archive>
void serialize(Archive& ar, const unsigned int) { void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<RomFSReader>(*this); ar& boost::serialization::base_object<RomFSReader>(*this);
ar & is_encrypted;
ar & file; ar & file;
ar & key;
ar & ctr;
ar & file_offset; ar & file_offset;
ar & crypto_offset;
ar & data_size; ar & data_size;
} }
friend class boost::serialization::access; friend class boost::serialization::access;

View File

@ -4,6 +4,7 @@
#pragma once #pragma once
#include "common/assert.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/logging/log.h" #include "common/logging/log.h"

View File

@ -64,7 +64,9 @@ Loader::ResultStatus Ticket::DoTitlekeyFixup() {
Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t offset) { Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t offset) {
std::size_t total_size = static_cast<std::size_t>(file_data.size() - offset); std::size_t total_size = static_cast<std::size_t>(file_data.size() - offset);
/* todotodo
serialized_size = total_size; serialized_size = total_size;
*/
if (total_size < sizeof(u32)) if (total_size < sizeof(u32))
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
@ -88,6 +90,7 @@ Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t off
std::memcpy(ticket_signature.data(), &file_data[offset + sizeof(u32)], signature_size); std::memcpy(ticket_signature.data(), &file_data[offset + sizeof(u32)], signature_size);
std::memcpy(&ticket_body, &file_data[offset + body_start], sizeof(Body)); std::memcpy(&ticket_body, &file_data[offset + body_start], sizeof(Body));
/* todotodo
std::size_t content_index_start = body_end; std::size_t content_index_start = body_end;
if (total_size < content_index_start + (2 * sizeof(u32))) if (total_size < content_index_start + (2 * sizeof(u32)))
return Loader::ResultStatus::Error; return Loader::ResultStatus::Error;
@ -103,6 +106,8 @@ Loader::ResultStatus Ticket::Load(std::span<const u8> file_data, std::size_t off
content_index.resize(content_index_size); content_index.resize(content_index_size);
std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size); std::memcpy(content_index.data(), &file_data[offset + content_index_start], content_index_size);
*/
return Loader::ResultStatus::Success; return Loader::ResultStatus::Success;
} }

View File

@ -43,8 +43,9 @@ public:
u8 audit; u8 audit;
INSERT_PADDING_BYTES(0x42); INSERT_PADDING_BYTES(0x42);
std::array<u8, 0x40> limits; std::array<u8, 0x40> limits;
std::array<u8, 0xAC> 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) #pragma pack(pop)
Loader::ResultStatus DoTitlekeyFixup(); Loader::ResultStatus DoTitlekeyFixup();

View File

@ -44,12 +44,22 @@ Result ErrEula::ReceiveParameterImpl(const Service::APT::MessageParameter& param
} }
Result ErrEula::Start(const Service::APT::MessageParameter& parameter) { Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
#ifdef todotodo
memcpy(&param, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param))); memcpy(&param, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param)));
// Do something here, like showing error codes, or prompting for EULA agreement. // Do something here, like showing error codes, or prompting for EULA agreement.
if (param.type == DisplayType::Agree) { if (param.type == DisplayType::Agree) {
param.result = 1; 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. // Let the application know that we're closing.
Finalize(); Finalize();
@ -57,8 +67,13 @@ Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
} }
Result ErrEula::Finalize() { Result ErrEula::Finalize() {
#ifdef todotodo
std::vector<u8> buffer(sizeof(param)); std::vector<u8> buffer(sizeof(param));
memcpy(buffer.data(), &param, buffer.size()); memcpy(buffer.data(), &param, buffer.size());
#else
std::vector<u8> buffer(startup_param.size());
std::fill(buffer.begin(), buffer.end(), 0);
#endif
CloseApplet(nullptr, buffer); CloseApplet(nullptr, buffer);
return ResultSuccess; return ResultSuccess;
} }

View File

@ -54,7 +54,8 @@ private:
std::shared_ptr<Kernel::SharedMemory> framebuffer_memory; std::shared_ptr<Kernel::SharedMemory> framebuffer_memory;
/// Parameter received by the applet on start. /// Parameter received by the applet on start.
ErrEulaParam param{}; // ErrEulaParam param{};
std::vector<u8> startup_param;
}; };
} // namespace HLE::Applets } // namespace HLE::Applets

View File

@ -205,12 +205,21 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem
buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size); buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size);
// Map the guard pages and mapped pages at once. // Map the guard pages and mapped pages at once.
#ifdef todotodo
auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase( auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase(
Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer, Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer,
static_cast<u32>(buffer->GetSize()), Kernel::MemoryState::Shared); static_cast<u32>(buffer->GetSize()), Kernel::MemoryState::Shared);
ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address"); ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address");
target_address = target_address_result.Unwrap(); 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<u32>(buffer->GetSize()),
Kernel::MemoryState::Shared)
.Unwrap();
#endif
// Change the permissions and state of the guard pages. // Change the permissions and state of the guard pages.
const VAddr low_guard_address = target_address; const VAddr low_guard_address = target_address;

View File

@ -47,6 +47,9 @@ union BatteryState {
using MacAddress = std::array<u8, 6>; using MacAddress = std::array<u8, 6>;
// Default MAC address in the Nintendo 3DS range
constexpr MacAddress DefaultMac = {0x40, 0xF4, 0x07, 0x00, 0x00, 0x00};
enum class WifiLinkLevel : u8 { enum class WifiLinkLevel : u8 {
Off = 0, Off = 0,
Poor = 1, Poor = 1,

View File

@ -106,10 +106,26 @@ void Module::Interface::GetStatus(Kernel::HLERequestContext& ctx) {
void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) { void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
/* todotodo
*/
//--
bool can_reach_internet = false;
std::shared_ptr<SOC::SOC_U> socu_module = SOC::GetService(ac->system);
if (socu_module) {
can_reach_internet = socu_module->GetDefaultInterfaceInfo().has_value();
}
//--
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
#ifdef todotodo
rb.Push<u32>(static_cast<u32>(WifiStatus::STATUS_CONNECTED_SLOT1)); rb.Push<u32>(static_cast<u32>(WifiStatus::STATUS_CONNECTED_SLOT1));
#else
rb.Push<u32>(static_cast<u32>(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) { void Module::Interface::GetInfraPriority(Kernel::HLERequestContext& ctx) {

View File

@ -90,7 +90,7 @@ public:
* AC::GetWifiStatus service function * AC::GetWifiStatus service function
* Outputs: * Outputs:
* 1 : Result of function, 0 on success, otherwise error code * 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); void GetWifiStatus(Kernel::HLERequestContext& ctx);
@ -169,9 +169,14 @@ protected:
}; };
enum class WifiStatus { enum class WifiStatus {
STATUS_DISCONNECTED = 0, STATUS_DISCONNECTED = 0,
#ifdef todotodo
STATUS_CONNECTED_SLOT1 = (1 << 0), STATUS_CONNECTED_SLOT1 = (1 << 0),
STATUS_CONNECTED_SLOT2 = (1 << 1), STATUS_CONNECTED_SLOT2 = (1 << 1),
STATUS_CONNECTED_SLOT3 = (1 << 2), STATUS_CONNECTED_SLOT3 = (1 << 2),
#else
STATUS_CONNECTED_O3DS = 1,
STATUS_CONNECTED_N3DS = 2,
#endif
}; };
struct ACConfig { struct ACConfig {

View File

@ -18,6 +18,7 @@ enum {
NotInitialized = 101, NotInitialized = 101,
AlreadyInitialized = 102, AlreadyInitialized = 102,
AcStatusDisconnected = 103, AcStatusDisconnected = 103,
ErrDesc103 = 103,
ErrDesc104 = 104, ErrDesc104 = 104,
Busy = 111, Busy = 111,
ErrDesc112 = 112, ErrDesc112 = 112,
@ -317,6 +318,7 @@ enum {
NotInitialized = 220501, // 022-0501 NotInitialized = 220501, // 022-0501
AlreadyInitialized = 220502, // 022-0502 AlreadyInitialized = 220502, // 022-0502
AcStatusDisconnected = 225103, // 022-5103 AcStatusDisconnected = 225103, // 022-5103
ErrCode225103 = 225103, // 022-5103
ErrCode225104 = 225104, // 022-5104 ErrCode225104 = 225104, // 022-5104
Busy = 220511, // 022-0511 Busy = 220511, // 022-0511
ErrCode225112 = 225112, // 022-5112 ErrCode225112 = 225112, // 022-5112

View File

@ -89,12 +89,42 @@ struct TicketInfo {
static_assert(sizeof(TicketInfo) == 0x18, "Ticket info structure size is wrong"); 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 { class CIAFile::DecryptionState {
public: public:
std::vector<CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption> content; std::vector<CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption> content;
}; };
NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) { NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) {
#ifdef todotodo
if (encrypted_content) { if (encrypted_content) {
// A console unique crypto file is used to store the decrypted NCCH file. This is done // 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 // 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()) { if (!file->IsOpen()) {
is_error = true; is_error = true;
} }
#else
file = std::make_unique<FileUtil::IOFile>(out_file, "wb");
#endif
} }
void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) { void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
if (is_error) if (is_error)
return; return;
#ifdef todotodo
if (is_not_ncch) { if (is_not_ncch) {
file->WriteBytes(buffer, length); file->WriteBytes(buffer, length);
} }
#endif
const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes) 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 (!header_parsed && header_size == sizeof(NCCH_Header)) {
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) { if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
#ifdef todotodo
// Most likely DS contents, store without additional operations // Most likely DS contents, store without additional operations
is_not_ncch = true; is_not_ncch = true;
file->WriteBytes(&ncch_header, sizeof(ncch_header)); file->WriteBytes(&ncch_header, sizeof(ncch_header));
file->WriteBytes(buffer, length); file->WriteBytes(buffer, length);
#else
is_error = true;
#endif
return; 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_) 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<DecryptionState>()) { decryption_state(std::make_unique<DecryptionState>()) {
// If data is being installing from CDN, provide a fake header to the container so that // If data is being installing from CDN, provide a fake header to the container so that
// it's not uninitialized. // it's not uninitialized.
@ -457,6 +496,7 @@ Result CIAFile::WriteTicket() {
ErrorLevel::Permanent}; ErrorLevel::Permanent};
} }
#ifdef todotodo
const auto& ticket = container.GetTicket(); const auto& ticket = container.GetTicket();
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID()); const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
@ -471,6 +511,7 @@ Result CIAFile::WriteTicket() {
// TODO: Correct result code. // TODO: Correct result code.
return FileSys::ResultFileNotFound; return FileSys::ResultFileNotFound;
} }
#endif
install_state = CIAInstallState::TicketLoaded; install_state = CIAInstallState::TicketLoaded;
return ResultSuccess; return ResultSuccess;
@ -519,12 +560,25 @@ Result CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t off
auto content_count = container.GetTitleMetadata().GetContentCount(); auto content_count = container.GetTitleMetadata().GetContentCount();
content_written.resize(content_count); content_written.resize(content_count);
#ifdef todotodo
current_content_file.reset(); current_content_file.reset();
current_content_index = -1; current_content_index = -1;
content_file_paths.clear(); content_file_paths.clear();
#else
content_files.clear();
#endif
for (std::size_t i = 0; i < content_count; i++) { for (std::size_t i = 0; i < content_count; i++) {
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update); auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
#ifdef todotodo
content_file_paths.emplace_back(path); 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()) { if (container.GetTitleMetadata().HasEncryptedContent()) {
@ -561,6 +615,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
// has been written since we might get a written buffer which contains multiple .app // has been written since we might get a written buffer which contains multiple .app
// contents or only part of a larger .app's contents. // contents or only part of a larger .app's contents.
const u64 offset_max = offset + length; const u64 offset_max = offset + length;
bool success = true;
for (std::size_t i = 0; i < content_written.size(); i++) { for (std::size_t i = 0; i < content_written.size(); i++) {
if (content_written[i] < container.GetContentSize(i)) { if (content_written[i] < container.GetContentSize(i)) {
// The size, minimum unwritten offset, and maximum unwritten offset of this content // The size, minimum unwritten offset, and maximum unwritten offset of this content
@ -579,6 +634,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
// Since the incoming TMD has already been written, we can use GetTitleContentPath // Since the incoming TMD has already been written, we can use GetTitleContentPath
// to get the content paths to write to. // to get the content paths to write to.
#ifdef todotodo
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata(); const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
if (i != current_content_index) { if (i != current_content_index) {
current_content_index = static_cast<u16>(i); current_content_index = static_cast<u16>(i);
@ -588,6 +644,10 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
} }
auto& file = *current_content_file; auto& file = *current_content_file;
#else
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
auto& file = content_files[i];
#endif
std::vector<u8> temp(buffer + (range_min - offset), std::vector<u8> temp(buffer + (range_min - offset),
buffer + (range_min - offset) + available_to_write); buffer + (range_min - offset) + available_to_write);
@ -595,12 +655,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size()); decryption_state->content[i].ProcessData(temp.data(), temp.data(), temp.size());
} }
file.Write(temp.data(), temp.size()); file.WriteBytes(temp.data(), temp.size());
if (file.IsError()) {
// This can never happen in real HW
return Result(ErrCodes::InvalidImportState, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent);
}
// Keep tabs on how much of this content ID has been written so new range_min // Keep tabs on how much of this content ID has been written so new range_min
// values can be calculated. // values can be calculated.
@ -610,7 +665,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
} }
} }
return length; return success ? length : 0;
} }
ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush, ResultVal<std::size_t> CIAFile::Write(u64 offset, std::size_t length, bool flush,
@ -732,17 +787,13 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
} }
file.Write(temp.data(), temp.size()); file.Write(temp.data(), temp.size());
if (file.IsError()) { bool success = !file.IsError();
// This can never happen in real HW
return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent);
}
content_written[content_index] += temp.size(); content_written[content_index] += temp.size();
LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index, LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index,
content_written[content_index]); content_written[content_index]);
return temp.size(); return success ? temp.size() : 0;
} }
u64 CIAFile::GetSize() const { u64 CIAFile::GetSize() const {
@ -848,6 +899,12 @@ bool TicketFile::SetSize(u64 size) const {
} }
bool TicketFile::Close() { 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; return true;
} }
@ -865,6 +922,11 @@ Result TicketFile::Commit() {
ticket_id = ticket.GetTicketID(); ticket_id = ticket.GetTicketID();
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), 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 // Save ticket
if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) { if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) {
LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile."); LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile.");
@ -964,8 +1026,10 @@ InstallStatus InstallCIA(const std::string& path,
Core::System::GetInstance(), Core::System::GetInstance(),
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID())); Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
if (container.GetTitleMetadata().HasEncryptedContent()) { bool title_key_available = container.GetTicket().GetTitleKey().has_value();
LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path); 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; return InstallStatus::ErrorEncrypted;
} }
@ -975,8 +1039,12 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorFailedToOpenFile; return InstallStatus::ErrorFailedToOpenFile;
} }
#ifdef todotodo
std::vector<u8> buffer; std::vector<u8> buffer;
buffer.resize(0x10000); buffer.resize(0x10000);
#else
std::array<u8, 0x10000> buffer;
#endif
auto file_size = file.GetSize(); auto file_size = file.GetSize();
std::size_t total_bytes_read = 0; std::size_t total_bytes_read = 0;
while (total_bytes_read != file_size) { while (total_bytes_read != file_size) {
@ -1035,6 +1103,96 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorInvalid; 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<u8> 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<u32_le>(cetk_response->size()),
.tmd_size = static_cast<u32_le>(tmd_response->size()),
.meta_size = 0,
};
for (u16 i = 0; i < content_count; ++i) {
fake_header.SetContentPresent(i);
}
std::vector<u8> 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, &current_offset](std::vector<u8>& 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) { u64 GetTitleUpdateId(u64 title_id) {
// Real services seem to just discard and replace the whole high word. // Real services seem to just discard and replace the whole high word.
return (title_id & 0xFFFFFFFF) | (static_cast<u64>(TID_HIGH_UPDATE) << 32); return (title_id & 0xFFFFFFFF) | (static_cast<u64>(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, async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0,
title_count * sizeof(u64)); title_count * sizeof(u64));
async_data->title_info_out = &rp.PopMappedBuffer(); 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( ctx.RunAsync(
[this, async_data](Kernel::HLERequestContext& ctx) { [async_data](Kernel::HLERequestContext& ctx) {
// nim checks if the current importing title already exists during installation. async_data->res = GetTitleInfoFromList(async_data->title_id_list,
// 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->media_type, async_data->out); async_data->media_type, async_data->out);
}
return 0; return 0;
}, },
[async_data](Kernel::HLERequestContext& ctx) { [async_data](Kernel::HLERequestContext& ctx) {
@ -1873,6 +2030,7 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
rb.PushMappedBuffer(*async_data->title_info_out); rb.PushMappedBuffer(*async_data->title_info_out);
}, },
true); true);
#endif
} }
} }
@ -2416,6 +2574,7 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
std::scoped_lock lock(am->am_lists_mutex); std::scoped_lock lock(am->am_lists_mutex);
#ifdef todotodo
auto range = am->am_ticket_list.equal_range(title_id); auto range = am->am_ticket_list.equal_range(title_id);
if (range.first == range.second) { if (range.first == range.second) {
rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success, 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); am->am_ticket_list.erase(range.first, range.second);
#endif
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
LOG_WARNING(Service_AM, "(STUBBED) called title_id=0x{:016x}", title_id);
} }
void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) { void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
@ -2439,11 +2600,19 @@ void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, ""); LOG_DEBUG(Service_AM, "");
std::scoped_lock lock(am->am_lists_mutex); std::scoped_lock lock(am->am_lists_mutex);
#ifdef todotodo
u32 ticket_count = static_cast<u32>(am->am_ticket_list.size()); u32 ticket_count = static_cast<u32>(am->am_ticket_list.size());
#else
u32 ticket_count = 0;
for (const auto& title_list : am->am_title_list) {
ticket_count += static_cast<u32>(title_list.size());
}
#endif
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push(ticket_count); rb.Push(ticket_count);
LOG_WARNING(Service_AM, "(STUBBED) called ticket_count=0x{:08x}", ticket_count);
} }
void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) { void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
@ -2456,6 +2625,7 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
u32 tickets_written = 0; u32 tickets_written = 0;
std::scoped_lock lock(am->am_lists_mutex); std::scoped_lock lock(am->am_lists_mutex);
#ifdef todotodo
auto it = am->am_ticket_list.begin(); auto it = am->am_ticket_list.begin();
std::advance(it, std::min(static_cast<size_t>(ticket_index), am->am_ticket_list.size())); std::advance(it, std::min(static_cast<size_t>(ticket_index), am->am_ticket_list.size()));
@ -2463,11 +2633,22 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
it++, tickets_written++) { it++, tickets_written++) {
ticket_tids_out.Write(&it->first, tickets_written * sizeof(u64), sizeof(u64)); 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<u32>(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); IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push(tickets_written); rb.Push(tickets_written);
rb.PushMappedBuffer(ticket_tids_out); 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) { void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
@ -2475,6 +2656,7 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, ""); LOG_DEBUG(Service_AM, "");
#ifdef todotodo
const auto& otp = HW::UniqueData::GetOTP(); const auto& otp = HW::UniqueData::GetOTP();
if (!otp.Valid()) { if (!otp.Valid()) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -2490,6 +2672,13 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
if (am->force_old_device_id) { if (am->force_old_device_id) {
deviceID &= ~0x80000000; 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); IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -2505,6 +2694,7 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
#ifdef todotodo
u32 count = 0; u32 count = 0;
for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) { for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) {
if ((include_installing && if ((include_installing &&
@ -2517,6 +2707,9 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
} }
rb.Push<u32>(count); rb.Push<u32>(count);
#else
rb.Push<u32>(static_cast<u32>(am->import_title_contexts.size()));
#endif
} }
void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp, 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); LOG_DEBUG(Service_AM, "(STUBBED) media_type=0x{:02x}", media_type);
#ifdef todotodo
bool needs_cleanup = false; bool needs_cleanup = false;
for (auto& import_ctx : am->import_title_contexts) { for (auto& import_ctx : am->import_title_contexts) {
if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) { 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); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
#ifdef todotodo
rb.Push<bool>(needs_cleanup); rb.Push<bool>(needs_cleanup);
#else
rb.Push<bool>(false);
#endif
} }
void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) { 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); 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();) { for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) {
if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) { if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) {
it = am->import_content_contexts.erase(it); it = am->import_content_contexts.erase(it);
@ -2759,6 +2959,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
it++; it++;
} }
} }
#endif
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
@ -2775,6 +2976,7 @@ void Module::Interface::QueryAvailableTitleDatabase(Kernel::HLERequestContext& c
LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type); LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type);
} }
#ifdef todotodo
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) { void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(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); 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<u32>();
[[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>();
u8 filter = rp.Pop<u8>();
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<u32>();
const u8 media_type = rp.Pop<u8>();
const u8 filter = rp.Pop<u8>();
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) { void Module::Interface::CheckContentRights(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
@ -2899,8 +3143,13 @@ void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) {
if (am->cia_installing) { if (am->cia_installing) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
#ifdef todotodo
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent)); ErrorLevel::Permanent));
#else
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent));
#endif
return; return;
} }
@ -2924,8 +3173,13 @@ void Module::Interface::BeginImportProgramTemporarily(Kernel::HLERequestContext&
if (am->cia_installing) { if (am->cia_installing) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
#ifdef todotodo
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState, rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent)); ErrorLevel::Permanent));
#else
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent));
#endif
return; return;
} }
@ -2975,7 +3229,25 @@ void Module::Interface::EndImportProgramWithoutCommit(Kernel::HLERequestContext&
} }
void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) { void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) {
#ifdef todotodo
CommitImportTitlesImpl(ctx, false, false); CommitImportTitlesImpl(ctx, false, false);
#else
IPC::RequestParser rp(ctx);
[[maybe_unused]] const auto media_type = static_cast<FS::MediaType>(rp.Pop<u8>());
[[maybe_unused]] const u32 title_count = rp.Pop<u32>();
[[maybe_unused]] const u8 database = rp.Pop<u8>();
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. /// 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); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
rb.Push(ResultSuccess); // No error rb.Push(ResultSuccess); // No error
rb.PushCopyObjects(file->Connect()); rb.PushCopyObjects(file->Connect());
LOG_WARNING(Service_AM, "(STUBBED) called");
} }
#ifdef todotodo
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) { void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
const auto ticket = rp.PopObject<Kernel::ClientSession>(); const auto ticket = rp.PopObject<Kernel::ClientSession>();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket); auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket);
if (ticket_file.Succeeded()) { if (ticket_file.Succeeded()) {
struct AsyncData { rb.Push(ticket_file.Unwrap()->Commit());
Service::AM::TicketFile* ticket_file; am->am_ticket_list.insert(std::make_pair(ticket_file.Unwrap()->GetTitleID(),
ticket_file.Unwrap()->GetTicketID()));
Result res{0};
};
std::shared_ptr<AsyncData> async_data = std::make_shared<AsyncData>();
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);
} else { } else {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ticket_file.Code()); 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<Kernel::ClientSession>();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
LOG_WARNING(Service_AM, "(STUBBED) called");
}
#endif
void Module::Interface::BeginImportTitle(Kernel::HLERequestContext& ctx) { void Module::Interface::BeginImportTitle(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
@ -3621,6 +3887,7 @@ void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) {
} }
am->importing_title->cia_file.SetDone(); am->importing_title->cia_file.SetDone();
am->ScanForTitles(am->importing_title->media_type);
am->importing_title.reset(); am->importing_title.reset();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -3994,10 +4261,12 @@ void Module::serialize(Archive& ar, const unsigned int) {
ar & cia_installing; ar & cia_installing;
ar & force_old_device_id; ar & force_old_device_id;
ar & force_new_device_id; ar & force_new_device_id;
ar & am_title_list;
ar & system_updater_mutex; ar & system_updater_mutex;
} }
SERIALIZE_IMPL(Module) SERIALIZE_IMPL(Module)
#ifdef todotodo
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) { void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 size = rp.Pop<u32>(); [[maybe_unused]] u32 size = rp.Pop<u32>();
@ -4021,6 +4290,52 @@ void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
rb.Push(0); rb.Push(0);
rb.PushMappedBuffer(buffer); rb.PushMappedBuffer(buffer);
} }
#else
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 size = rp.Pop<u32>();
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) { void Module::Interface::CommitImportTitlesAndUpdateFirmwareAuto(Kernel::HLERequestContext& ctx) {
CommitImportTitlesImpl(ctx, true, true); CommitImportTitlesImpl(ctx, true, true);
@ -4051,7 +4366,11 @@ void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) {
auto path = GetTicketPath(title_id, ticket_id); auto path = GetTicketPath(title_id, ticket_id);
FileUtil::Delete(path); FileUtil::Delete(path);
#ifdef todotodo
am->am_ticket_list.erase(it); am->am_ticket_list.erase(it);
#else
am->ScanForTickets();
#endif
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
} }
@ -4233,6 +4552,7 @@ void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) {
Module::Module(Core::System& _system) : system(_system) { Module::Module(Core::System& _system) : system(_system) {
FileUtil::CreateFullPath(GetTicketDirectory()); FileUtil::CreateFullPath(GetTicketDirectory());
ScanForAllTitles(); ScanForAllTitles();
LoadCTCertFile(ct_cert);
system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex"); system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex");
} }

View File

@ -51,6 +51,7 @@ namespace Service::AM {
namespace ErrCodes { namespace ErrCodes {
enum { enum {
InvalidImportState = 4, InvalidImportState = 4,
CIACurrentlyInstalling = 4,
InvalidTID = 31, InvalidTID = 31,
EmptyCIA = 32, EmptyCIA = 32,
TryingToUninstallSystemApp = 44, TryingToUninstallSystemApp = 44,
@ -87,6 +88,13 @@ enum class ImportTitleContextState : u8 {
NEEDS_CLEANUP = 6, NEEDS_CLEANUP = 6,
}; };
enum class CTCertLoadStatus {
Loaded,
NotFound,
Invalid,
IOError,
};
struct ImportTitleContext { struct ImportTitleContext {
u64 title_id; u64 title_id;
u16 version; u16 version;
@ -105,6 +113,24 @@ struct ImportContentContext {
}; };
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size"); static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
struct CTCert {
u32_be signature_type{};
std::array<u8, 0x1E> signature_r{};
std::array<u8, 0x1E> signature_s{};
INSERT_PADDING_BYTES(0x40) {};
std::array<char, 0x40> issuer{};
u32_be key_type{};
std::array<char, 0x40> key_id{};
u32_be expiration_time{};
std::array<u8, 0x1E> public_key_x{};
std::array<u8, 0x1E> 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 // Title ID valid length
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16; constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
@ -126,7 +152,7 @@ private:
friend class CIAFile; friend class CIAFile;
std::unique_ptr<FileUtil::IOFile> file; std::unique_ptr<FileUtil::IOFile> file;
bool is_error = false; bool is_error = false;
bool is_not_ncch = false; // bool is_not_ncch = false;
bool decryption_authorized = false; bool decryption_authorized = false;
std::size_t written = 0; std::size_t written = 0;
@ -223,6 +249,7 @@ private:
std::vector<std::string> content_file_paths; std::vector<std::string> content_file_paths;
u16 current_content_index = -1; u16 current_content_index = -1;
std::unique_ptr<NCCHCryptoFile> current_content_file; std::unique_ptr<NCCHCryptoFile> current_content_file;
std::vector<FileUtil::IOFile> content_files;
Service::FS::MediaType media_type; Service::FS::MediaType media_type;
class DecryptionState; class DecryptionState;
@ -334,6 +361,13 @@ private:
InstallStatus InstallCIA(const std::string& path, InstallStatus InstallCIA(const std::string& path,
std::function<ProgressCallback>&& update_callback = nullptr); std::function<ProgressCallback>&& 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 * Get the update title ID for a title
* @param titleId the title ID * @param titleId the title ID
@ -1022,6 +1056,18 @@ public:
force_new_device_id = true; 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: private:
void ScanForTickets(); void ScanForTickets();
@ -1054,6 +1100,7 @@ private:
std::multimap<u64, u64> am_ticket_list; std::multimap<u64, u64> am_ticket_list;
std::shared_ptr<Kernel::Mutex> system_updater_mutex; std::shared_ptr<Kernel::Mutex> system_updater_mutex;
CTCert ct_cert{};
std::shared_ptr<CurrentImportingTitle> importing_title; std::shared_ptr<CurrentImportingTitle> importing_title;
std::map<u64, ImportTitleContext> import_title_contexts; std::map<u64, ImportTitleContext> import_title_contexts;
std::multimap<u64, ImportContentContext> import_content_contexts; std::multimap<u64, ImportContentContext> import_content_contexts;

View File

@ -453,6 +453,7 @@ void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) {
void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) { void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
#ifdef todotodo
const auto& secure_info_a = HW::UniqueData::GetSecureInfoA(); const auto& secure_info_a = HW::UniqueData::GetSecureInfoA();
const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB(); 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; 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); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push<u8>(ret); rb.Push<u8>(ret);
} }
#ifdef todotodo
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) { void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop<u32>(); [[maybe_unused]] u32 out_size = rp.Pop<u32>();
@ -500,6 +508,32 @@ void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.PushMappedBuffer(out_buffer); rb.PushMappedBuffer(out_buffer);
} }
#else
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
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) { void Module::Interface::SetUUIDClockSequence(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
@ -645,6 +679,7 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx)
rb.Push(cfg->UpdateConfigNANDSavegame()); rb.Push(cfg->UpdateConfigNANDSavegame());
} }
#ifdef todotodo
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) { void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop<u32>(); [[maybe_unused]] u32 out_size = rp.Pop<u32>();
@ -688,6 +723,44 @@ void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push<u64>(local_friend_code_seed_b.body.friend_code_seed); rb.Push<u64>(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<u32>();
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<u64>(cfg->local_friend_code_seed_b.friend_code_seed);
}
#endif
void Module::Interface::FormatConfig(Kernel::HLERequestContext& ctx) { void Module::Interface::FormatConfig(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);
@ -863,6 +936,14 @@ Result Module::UpdateConfigNANDSavegame() {
return ResultSuccess; 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 Module::FormatConfig() {
Result res = DeleteConfigNANDSaveFile(); Result res = DeleteConfigNANDSaveFile();
// The delete command fails if the file doesn't exist, so we have to check that too // 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(); 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() { void Module::LoadMCUConfig() {
FileUtil::IOFile mcu_data_file( FileUtil::IOFile mcu_data_file(
fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb"); fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb");
@ -976,6 +1106,8 @@ Module::Module(Core::System& system_) : system(system_) {
SetEULAVersion(default_version); SetEULAVersion(default_version);
UpdateConfigNANDSavegame(); UpdateConfigNANDSavegame();
} }
LoadSecureInfoAFile();
LoadLocalFriendCodeSeedBFile();
} }
Module::~Module() = default; Module::~Module() = default;

View File

@ -181,6 +181,28 @@ enum class AccessFlag : u16 {
}; };
DECLARE_ENUM_FLAG_OPERATORS(AccessFlag); DECLARE_ENUM_FLAG_OPERATORS(AccessFlag);
struct SecureInfoA {
std::array<u8, 0x100> signature;
u8 region;
u8 unknown;
std::array<u8, 0xF> serial_number;
};
static_assert(sizeof(SecureInfoA) == 0x111);
struct LocalFriendCodeSeedB {
std::array<u8, 0x100> signature;
u64 unknown;
u64 friend_code_seed;
};
static_assert(sizeof(LocalFriendCodeSeedB) == 0x110);
enum class SecureDataLoadStatus {
Loaded,
NotFound,
Invalid,
IOError,
};
class Module final { class Module final {
public: public:
Module(Core::System& system_); Module(Core::System& system_);
@ -634,6 +656,35 @@ public:
*/ */
void SaveMacAddress(); 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: private:
void UpdatePreferredRegionCode(); void UpdatePreferredRegionCode();
SystemLanguage GetRawSystemLanguage(); SystemLanguage GetRawSystemLanguage();
@ -644,6 +695,10 @@ private:
std::array<u8, CONFIG_SAVEFILE_SIZE> cfg_config_file_buffer; std::array<u8, CONFIG_SAVEFILE_SIZE> cfg_config_file_buffer;
std::unique_ptr<FileSys::ArchiveBackend> cfg_system_save_data_archive; std::unique_ptr<FileSys::ArchiveBackend> cfg_system_save_data_archive;
u32 preferred_region_code = 0; 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; bool preferred_region_chosen = false;
MCUData mcu_data{}; MCUData mcu_data{};
std::string mac_address{}; std::string mac_address{};

View File

@ -32,7 +32,10 @@ namespace {
// On a real 3DS the generation for the normal key is hardware based, and thus the constant can't // 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 // 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. // 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) { AESKey HexToKey(const std::string& hex) {
if (hex.size() < 32) { if (hex.size() < 32) {
@ -143,6 +146,78 @@ struct KeyDesc {
bool same_as_before; bool same_as_before;
}; };
void LoadBootromKeys() {
constexpr std::array<KeyDesc, 80> 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() { void LoadPresetKeys() {
auto s = GetKeysStream(); 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<boost::iostreams::file_descriptor_source> file;
FileUtil::OpenFStream<std::ios_base::in>(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 } // namespace
@ -305,6 +486,8 @@ void InitKeys(bool force) {
return; return;
} }
initialized = true; initialized = true;
HW::RSA::InitSlots();
LoadBootromKeys();
LoadPresetKeys(); LoadPresetKeys();
movable_key.SetKeyX(key_slots[0x35].x); movable_key.SetKeyX(key_slots[0x35].x);
movable_cmac.SetKeyX(key_slots[0x35].x); movable_cmac.SetKeyX(key_slots[0x35].x);

View File

@ -6,6 +6,7 @@
#include <boost/iostreams/device/file_descriptor.hpp> #include <boost/iostreams/device/file_descriptor.hpp>
#include <boost/iostreams/stream.hpp> #include <boost/iostreams/stream.hpp>
#include "common/assert.h" #include "common/assert.h"
#include <common/string_util.h>
#include "common/common_paths.h" #include "common/common_paths.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "common/logging/log.h" #include "common/logging/log.h"

View File

@ -21,6 +21,19 @@
namespace HW::RSA { namespace HW::RSA {
namespace {
std::vector<u8> HexToBytes(const std::string& hex) {
std::vector<u8> bytes;
for (unsigned int i = 0; i < hex.length(); i += 2) {
std::string byteString = hex.substr(i, 2);
u8 byte = static_cast<u8>(std::strtol(byteString.c_str(), nullptr, 16));
bytes.push_back(byte);
}
return bytes;
};
} // namespace
constexpr std::size_t SlotSize = 4; constexpr std::size_t SlotSize = 4;
std::array<RsaSlot, SlotSize> rsa_slots; std::array<RsaSlot, SlotSize> rsa_slots;
@ -92,6 +105,23 @@ std::optional<std::pair<std::size_t, char>> ParseKeySlotName(const std::string&
} }
} }
std::vector<u8> RsaSlot::GetSignature(std::span<const u8> 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<unsigned char*>(ss.str().data()), ss.str().size());
decoder.MessageEnd();
std::vector<u8> result(decoder.MaxRetrievable());
decoder.Get(result.data(), result.size());
return HexToBytes(ss.str());
}
// todotodo
#ifdef todotodo
void InitSlots() { void InitSlots() {
static bool initialized = false; static bool initialized = false;
if (initialized) 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<u8> 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<u8> 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; static RsaSlot empty_slot;
const RsaSlot& GetSlot(std::size_t slot_id) { 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]; return rsa_slots[slot_id];
} }
std::vector<u8> CreateASN1Message(std::span<const u8> data) {
static constexpr std::array<u8, 224> 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<u8> 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() { const RsaSlot& GetTicketWrapSlot() {
return ticket_wrap_slot; return ticket_wrap_slot;
} }

View File

@ -23,6 +23,8 @@ public:
bool Verify(std::span<const u8> message, std::span<const u8> signature) const; bool Verify(std::span<const u8> message, std::span<const u8> signature) const;
std::vector<u8> GetSignature(std::span<const u8> message) const;
explicit operator bool() const { explicit operator bool() const {
// TODO(B3N30): Maybe check if exponent and modulus are vailid // TODO(B3N30): Maybe check if exponent and modulus are vailid
return init; return init;
@ -68,4 +70,5 @@ const RsaSlot& GetTicketWrapSlot();
const RsaSlot& GetSecureInfoSlot(); const RsaSlot& GetSecureInfoSlot();
const RsaSlot& GetLocalFriendCodeSeedSlot(); const RsaSlot& GetLocalFriendCodeSeedSlot();
std::vector<u8> CreateASN1Message(std::span<const u8> data);
} // namespace HW::RSA } // namespace HW::RSA

View File

@ -26,6 +26,23 @@ public:
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port, Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port,
ArticInitMode init_mode); ArticInitMode init_mode);
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port)
: AppLoader(system_, FileUtil::IOFile()) {
client = std::make_shared<Network::ArticBase::Client>(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<Core::PerfStats::PerfArticEventBits>(event & 0xFFFFFFFF);
bool set = (event > 32) != 0;
system_.ReportPerfArticEvent(ev, set);
});
}
~Apploader_Artic() override; ~Apploader_Artic() override;
/** /**

View File

@ -48,7 +48,7 @@ FileType GuessFromExtension(const std::string& extension_) {
if (extension == ".elf" || extension == ".axf") if (extension == ".elf" || extension == ".axf")
return FileType::ELF; return FileType::ELF;
if (extension == ".cci") if (extension == ".cci" || extension == ".3ds")
return FileType::CCI; return FileType::CCI;
if (extension == ".cxi" || extension == ".app") if (extension == ".cxi" || extension == ".app")
@ -112,12 +112,14 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileUtil::
return std::make_unique<AppLoader_NCCH>(system, std::move(file), filepath); return std::make_unique<AppLoader_NCCH>(system, std::move(file), filepath);
case FileType::ARTIC: { case FileType::ARTIC: {
#ifdef todotodo
Apploader_Artic::ArticInitMode mode = Apploader_Artic::ArticInitMode::NONE; Apploader_Artic::ArticInitMode mode = Apploader_Artic::ArticInitMode::NONE;
if (filename.starts_with("articinio://")) { if (filename.starts_with("articinio://")) {
mode = Apploader_Artic::ArticInitMode::O3DS; mode = Apploader_Artic::ArticInitMode::O3DS;
} else if (filename.starts_with("articinin://")) { } else if (filename.starts_with("articinin://")) {
mode = Apploader_Artic::ArticInitMode::N3DS; mode = Apploader_Artic::ArticInitMode::N3DS;
} }
#endif
auto strToUInt = [](const std::string& str) -> int { auto strToUInt = [](const std::string& str) -> int {
char* pEnd = NULL; char* pEnd = NULL;
unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10); unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10);
@ -136,7 +138,11 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileUtil::
server_addr = server_addr.substr(0, pos); server_addr = server_addr.substr(0, pos);
} }
} }
#ifdef todotodo
return std::make_unique<Apploader_Artic>(system, server_addr, port, mode); return std::make_unique<Apploader_Artic>(system, server_addr, port, mode);
#else
return std::make_unique<Apploader_Artic>(system, server_addr, port);
#endif
} }
default: default: