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)
![GitHub Release](https://img.shields.io/github/v/release/azahar-emu/azahar?label=Current%20Release)

View File

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

View File

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

View File

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

View File

@ -186,6 +186,8 @@ object NativeLibrary {
external fun unlinkConsole()
external fun downloadTitleFromNus(title: Long): InstallStatus
private var coreErrorAlertResult = false
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 homeViewModel: HomeViewModel by activityViewModels()
private var show3DSFileWarning: Boolean = true
private var show3DSFileWarning: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
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
// Refer to the license.txt file included.
@ -121,14 +121,8 @@ class HomeSettingsFragment : Fragment() {
}
),
HomeSetting(
R.string.install_game_content,
R.string.install_game_content_description,
R.drawable.ic_install,
{ mainActivity.ciaFileInstaller.launch(true) }
),
HomeSetting(
R.string.setup_system_files,
R.string.setup_system_files_description,
R.string.system_files,
R.string.system_files_description,
R.drawable.ic_system_update,
{
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -136,6 +130,12 @@ class HomeSettingsFragment : Fragment() {
?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment)
}
),
HomeSetting(
R.string.install_game_content,
R.string.install_game_content_description,
R.drawable.ic_install,
{ mainActivity.ciaFileInstaller.launch(true) }
),
HomeSetting(
R.string.share_log,
R.string.share_log_description,

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
// Refer to the license.txt file included.
package org.citra.citra_emu.fragments
import android.content.DialogInterface
import android.content.res.Resources
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.widget.doOnTextChanged
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.textview.MaterialTextView
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.viewmodel.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel
import org.citra.citra_emu.viewmodel.SystemFilesViewModel
class SystemFilesFragment : Fragment() {
private var _binding: FragmentSystemFilesBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private val systemFilesViewModel: SystemFilesViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
private lateinit var regionValues: IntArray
private val systemTypeDropdown = DropdownItem(R.array.systemFileTypeValues)
private val systemRegionDropdown = DropdownItem(R.array.systemFileRegionValues)
private val SYS_TYPE = "SysType"
private val REGION = "Region"
private val REGION_START = "RegionStart"
private val homeMenuMap: MutableMap<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?) {
super.onCreate(savedInstanceState)
@ -77,15 +92,61 @@ class SystemFilesFragment : Fragment() {
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false)
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
if (!preferences.getBoolean(WARNING_SHOWN, false)) {
MessageDialogFragment.newInstance(
R.string.home_menu_warning,
R.string.home_menu_warning_description
).show(childFragmentManager, MessageDialogFragment.TAG)
preferences.edit()
.putBoolean(WARNING_SHOWN, true)
.apply()
}
binding.toolbarSystemFiles.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
}
// TODO: Remove workaround for text filtering issue in material components when fixed
// https://github.com/material-components/material-components-android/issues/1464
binding.dropdownSystemType.isSaveEnabled = false
binding.dropdownSystemRegion.isSaveEnabled = false
binding.dropdownSystemRegionStart.isSaveEnabled = false
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
systemFilesViewModel.shouldRefresh.collect {
if (it) {
reloadUi()
systemFilesViewModel.setShouldRefresh(false)
}
}
}
}
reloadUi()
if (savedInstanceState != null) {
setDropdownSelection(
binding.dropdownSystemType,
systemTypeDropdown,
savedInstanceState.getInt(SYS_TYPE)
)
setDropdownSelection(
binding.dropdownSystemRegion,
systemRegionDropdown,
savedInstanceState.getInt(REGION)
)
binding.dropdownSystemRegionStart
.setText(savedInstanceState.getString(REGION_START), false)
}
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(SYS_TYPE, systemTypeDropdown.position)
outState.putInt(REGION, systemRegionDropdown.position)
outState.putString(REGION_START, binding.dropdownSystemRegionStart.text.toString())
}
override fun onResume() {
@ -98,41 +159,6 @@ class SystemFilesFragment : Fragment() {
SystemSaveGame.save()
}
private fun showProgressDialog(
main_title: CharSequence,
main_text: CharSequence
): AlertDialog? {
val context = requireContext()
val progressIndicator = CircularProgressIndicator(context).apply {
isIndeterminate = true
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER // Center the progress indicator
).apply {
setMargins(50, 50, 50, 50) // Add margins (left, top, right, bottom)
}
}
val pleaseWaitText = MaterialTextView(context).apply {
text = main_text
}
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setPadding(40, 40, 40, 40) // Optional: Add padding to the entire layout
addView(pleaseWaitText)
addView(progressIndicator)
}
return MaterialAlertDialogBuilder(context)
.setTitle(main_title)
.setView(container)
.setCancelable(false)
.show()
}
private fun reloadUi() {
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
@ -150,167 +176,31 @@ class SystemFilesFragment : Fragment() {
gamesViewModel.setShouldSwapData(true)
}
binding.setupSystemFilesDescription?.apply {
text = HtmlCompat.fromHtml(
context.getString(R.string.setup_system_files_preamble),
HtmlCompat.FROM_HTML_MODE_COMPACT
)
movementMethod = LinkMovementMethod.getInstance()
}
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
binding.buttonUnlinkConsoleData.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.delete_system_files)
.setMessage(HtmlCompat.fromHtml(
requireContext().getString(R.string.delete_system_files_description),
HtmlCompat.FROM_HTML_MODE_COMPACT
))
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
NativeLibrary.unlinkConsole()
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
binding.buttonSetUpSystemFiles.setOnClickListener {
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
val progressDialog = showProgressDialog(
getText(R.string.setup_system_files),
getString(R.string.setup_system_files_detect)
)
CoroutineScope(Dispatchers.IO).launch {
val setupState = setupStateCached ?: NativeLibrary.areSystemTitlesInstalled().also {
setupStateCached = it
}
withContext(Dispatchers.Main) {
progressDialog?.dismiss()
inputBinding.editTextInput.setText(textInputValue)
inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
textInputValue = text.toString()
}
val buttonGroup = context?.let { it1 -> RadioGroup(it1) }!!
val buttonO3ds = context?.let { it1 ->
RadioButton(it1).apply {
text = context.getString(R.string.setup_system_files_o3ds)
isChecked = false
}
}!!
val buttonN3ds = context?.let { it1 ->
RadioButton(it1).apply {
text = context.getString(R.string.setup_system_files_n3ds)
isChecked = false
}
}!!
val textO3ds: String
val textN3ds: String
val colorO3ds: Int
val colorN3ds: Int
if (!setupStateCached!![0]) {
textO3ds = getString(R.string.setup_system_files_possible)
colorO3ds = R.color.citra_primary_blue
textN3ds = getString(R.string.setup_system_files_o3ds_needed)
colorN3ds = R.color.citra_primary_yellow
buttonN3ds.isEnabled = false
} else {
textO3ds = getString(R.string.setup_system_files_completed)
colorO3ds = R.color.citra_primary_green
if (!setupStateCached!![1]) {
textN3ds = getString(R.string.setup_system_files_possible)
colorN3ds = R.color.citra_primary_blue
} else {
textN3ds = getString(R.string.setup_system_files_completed)
colorN3ds = R.color.citra_primary_green
}
}
val tooltipO3ds = context?.let { it1 ->
MaterialTextView(it1).apply {
text = textO3ds
textSize = 12f
setTextColor(ContextCompat.getColor(requireContext(), colorO3ds))
}
}
val tooltipN3ds = context?.let { it1 ->
MaterialTextView(it1).apply {
text = textN3ds
textSize = 12f
setTextColor(ContextCompat.getColor(requireContext(), colorN3ds))
}
}
buttonGroup.apply {
addView(buttonO3ds)
addView(tooltipO3ds)
addView(buttonN3ds)
addView(tooltipN3ds)
}
inputBinding.root.apply {
addView(buttonGroup)
}
val dialog = context?.let {
MaterialAlertDialogBuilder(it)
.setView(inputBinding.root)
.setTitle(getString(R.string.setup_system_files_enter_address))
.setPositiveButton(android.R.string.ok) { diag, _ ->
if (textInputValue.isNotEmpty() && !(!buttonO3ds.isChecked && !buttonN3ds.isChecked)) {
preferences.edit()
.putString("last_artic_base_addr", textInputValue)
.apply()
val menu = Game(
title = getString(R.string.artic_base),
path = if (buttonO3ds.isChecked) {
"articinio://$textInputValue"
} else {
"articinin://$textInputValue"
},
filename = ""
)
val progressDialog2 = showProgressDialog(
getText(R.string.setup_system_files),
getString(
R.string.setup_system_files_preparing
)
)
CoroutineScope(Dispatchers.IO).launch {
NativeLibrary.uninstallSystemFiles(buttonO3ds.isChecked)
withContext(Dispatchers.Main) {
setupStateCached = null
progressDialog2?.dismiss()
val action =
HomeNavigationDirections.actionGlobalEmulationActivity(
menu
)
binding.root.findNavController().navigate(action)
}
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
}
if (!NativeLibrary.areKeysAvailable()) {
binding.apply {
systemType.isEnabled = false
systemRegion.isEnabled = false
buttonDownloadHomeMenu.isEnabled = false
textKeysMissing.visibility = View.VISIBLE
textKeysMissingHelp.visibility = View.VISIBLE
textKeysMissingHelp.text =
Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY)
textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance()
}
} else {
populateDownloadOptions()
}
binding.buttonDownloadHomeMenu.setOnClickListener {
val titleIds = NativeLibrary.getSystemTitleIds(
systemTypeDropdown.getValue(resources),
systemRegionDropdown.getValue(resources)
)
DownloadSystemFilesDialogFragment.newInstance(titleIds).show(
childFragmentManager,
DownloadSystemFilesDialogFragment.TAG
)
}
populateHomeMenuOptions()
@ -326,6 +216,51 @@ class SystemFilesFragment : Fragment() {
}
}
private fun populateDropdown(
dropdown: MaterialAutoCompleteTextView,
valuesId: Int,
dropdownItem: DropdownItem
) {
val valuesAdapter = ArrayAdapter.createFromResource(
requireContext(),
valuesId,
R.layout.support_simple_spinner_dropdown_item
)
dropdown.setAdapter(valuesAdapter)
dropdown.onItemClickListener = dropdownItem
}
private fun setDropdownSelection(
dropdown: MaterialAutoCompleteTextView,
dropdownItem: DropdownItem,
selection: Int
) {
if (dropdown.adapter != null) {
dropdown.setText(dropdown.adapter.getItem(selection).toString(), false)
}
dropdownItem.position = selection
}
private fun populateDownloadOptions() {
populateDropdown(binding.dropdownSystemType, R.array.systemFileTypes, systemTypeDropdown)
populateDropdown(
binding.dropdownSystemRegion,
R.array.systemFileRegions,
systemRegionDropdown
)
setDropdownSelection(
binding.dropdownSystemType,
systemTypeDropdown,
systemTypeDropdown.position
)
setDropdownSelection(
binding.dropdownSystemRegion,
systemRegionDropdown,
systemRegionDropdown.position
)
}
private fun populateHomeMenuOptions() {
regionValues = resources.getIntArray(R.array.systemFileRegionValues)
val regionEntries = resources.getStringArray(R.array.systemFileRegions)
@ -350,4 +285,30 @@ class SystemFilesFragment : Fragment() {
binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false)
}
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val mlpAppBar = binding.toolbarSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
mlpAppBar.leftMargin = leftInsets
mlpAppBar.rightMargin = rightInsets
binding.toolbarSystemFiles.layoutParams = mlpAppBar
val mlpScrollSystemFiles =
binding.scrollSystemFiles.layoutParams as ViewGroup.MarginLayoutParams
mlpScrollSystemFiles.leftMargin = leftInsets
mlpScrollSystemFiles.rightMargin = rightInsets
binding.scrollSystemFiles.layoutParams = mlpScrollSystemFiles
binding.scrollSystemFiles.updatePadding(bottom = barInsets.bottom)
windowInsets
}
}

View File

@ -63,7 +63,7 @@ class Game(
val allExtensions: Set<String> get() = extensions + badExtensions
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(

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);
}
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() {
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_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back"
app:title="@string/setup_system_files" />
app:title="@string/system_files" />
</com.google.android.material.appbar.AppBarLayout>
@ -39,48 +39,77 @@
android:paddingBottom="16dp">
<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"
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_set_up_system_files"
android:id="@+id/button_download_home_menu"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_tool_connect" />
<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" />
android:layout_marginTop="16dp"
android:text="@string/download" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:id="@+id/text_keys_missing"
android:layout_width="match_parent"
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:textAlignment="viewStart" />

View File

@ -297,6 +297,17 @@
<item>6</item>
</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">
<item>@string/mono</item>
<item>@string/stereo</item>

View File

@ -2,7 +2,7 @@
<resources>
<!-- 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_notification_channel_name" translatable="false">Azahar</string>
<string name="app_notification_channel_id" translatable="false">Azahar</string>
@ -148,12 +148,15 @@
<!-- System files strings -->
<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="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_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="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="download_system_files">Download System Files</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_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_preparing">Preparing setup, please wait...</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="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_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) -->
<string name="generic_buttons">Buttons</string>

View File

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

View File

@ -217,16 +217,6 @@
</widget>
</item>
<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">
<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>
@ -236,14 +226,14 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="3" column="0">
<widget class="QCheckBox" name="toggle_renderer_debug">
<property name="text">
<string>Enable debug renderer</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="4" column="0">
<widget class="QCheckBox" name="toggle_dump_command_buffers">
<property name="text">
<string>Dump command buffers</string>
@ -253,33 +243,43 @@
</layout>
</widget>
</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>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Miscellaneous</string>
<widget class="QLabel" name="label_cpu_clock_info">
<property name="text">
<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>
<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>

View File

@ -238,16 +238,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
&ConfigureSystem::RefreshConsoleID);
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
connect(ui->button_linked_console, &QPushButton::clicked, this,
&ConfigureSystem::UnlinkConsole);
connect(ui->combo_country, qOverload<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_start_download, &QPushButton::clicked, this,
&ConfigureSystem::DownloadFromNUS);
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
ui->button_secure_info->setEnabled(false);
@ -255,7 +247,11 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
this, tr("Select SecureInfo_A/B"), QString(),
tr("SecureInfo_A/B (SecureInfo_A SecureInfo_B);;All Files (*.*)"));
ui->button_secure_info->setEnabled(true);
#ifdef todotodo
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetSecureInfoAPath());
#else
InstallSecureData(file_path_qtstr.toStdString(), cfg->GetSecureInfoAPath());
#endif
});
connect(ui->button_friend_code_seed, &QPushButton::clicked, this, [this] {
ui->button_friend_code_seed->setEnabled(false);
@ -264,6 +260,7 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
tr("LocalFriendCodeSeed_A/B (LocalFriendCodeSeed_A "
"LocalFriendCodeSeed_B);;All Files (*.*)"));
ui->button_friend_code_seed->setEnabled(true);
#ifdef todotodo
InstallSecureData(file_path_qtstr.toStdString(),
HW::UniqueData::GetLocalFriendCodeSeedBPath());
});
@ -281,6 +278,16 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
this, tr("Select movable.sed"), QString(), tr("Sed file (*.sed);;All Files (*.*)"));
ui->button_movable->setEnabled(true);
InstallSecureData(file_path_qtstr.toStdString(), HW::UniqueData::GetMovablePath());
#else
InstallSecureData(file_path_qtstr.toStdString(), cfg->GetLocalFriendCodeSeedBPath());
});
connect(ui->button_ct_cert, &QPushButton::clicked, this, [this] {
ui->button_ct_cert->setEnabled(false);
const QString file_path_qtstr = QFileDialog::getOpenFileName(
this, tr("Select CTCert"), QString(), tr("CTCert.bin (*.bin);;All Files (*.*)"));
ui->button_ct_cert->setEnabled(true);
InstallCTCert(file_path_qtstr.toStdString());
#endif
});
for (u8 i = 0; i < country_names.size(); i++) {
@ -288,10 +295,36 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
ui->combo_country->addItem(tr(country_names.at(i)), i);
}
}
ui->label_country_invalid->setVisible(false);
ui->label_country_invalid->setStyleSheet(QStringLiteral("QLabel { color: #ff3333; }"));
SetupPerGameUI();
ui->combo_download_set->setCurrentIndex(0); // set to Minimal
ui->combo_download_region->setCurrentIndex(0); // set to the base region
HW::AES::InitKeys(true);
bool keys_available = HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) &&
HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure2);
for (u8 i = 0; i < HW::AES::MaxCommonKeySlot && keys_available; i++) {
HW::AES::SelectCommonKeyIndex(i);
if (!HW::AES::IsNormalKeyAvailable(HW::AES::KeySlotID::TicketCommonKey)) {
keys_available = false;
break;
}
}
if (keys_available) {
ui->button_start_download->setEnabled(true);
ui->combo_download_set->setEnabled(true);
ui->combo_download_region->setEnabled(true);
ui->label_nus_download->setText(tr("Download System Files from Nintendo servers"));
} else {
ui->button_start_download->setEnabled(false);
ui->combo_download_set->setEnabled(false);
ui->combo_download_region->setEnabled(false);
ui->label_nus_download->setTextInteractionFlags(Qt::TextBrowserInteraction);
ui->label_nus_download->setOpenExternalLinks(true);
ui->label_nus_download->setText(tr("Azahar is missing keys to download system files."));
}
ConfigureTime();
}
@ -300,19 +333,6 @@ ConfigureSystem::~ConfigureSystem() = default;
void ConfigureSystem::SetConfiguration() {
enabled = !system.IsPoweredOn();
if (!Settings::IsConfiguringGlobal()) {
ConfigurationShared::SetHighlight(ui->region_label,
!Settings::values.region_value.UsingGlobal());
const bool is_region_global = Settings::values.region_value.UsingGlobal();
ui->region_combobox->setCurrentIndex(
is_region_global ? ConfigurationShared::USE_GLOBAL_INDEX
: static_cast<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()));
QDateTime date_time;
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
@ -374,7 +394,6 @@ void ConfigureSystem::ReadSystemSettings() {
// set the country code
country_code = cfg->GetCountryCode();
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
CheckCountryValid(country_code);
// set whether system setup is needed
system_setup = cfg->IsSystemSetupNeeded();
@ -391,16 +410,15 @@ void ConfigureSystem::ReadSystemSettings() {
play_coin = Service::PTM::Module::GetPlayCoins();
ui->spinBox_play_coins->setValue(play_coin);
// set firmware download region
ui->combo_download_region->setCurrentIndex(static_cast<int>(cfg->GetRegionValue()));
// Refresh secure data status
RefreshSecureDataStatus();
}
void ConfigureSystem::ApplyConfiguration() {
if (enabled) {
ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value,
ui->region_combobox,
[](s32 index) { return index - 1; });
bool modified = false;
// apply username
@ -591,51 +609,6 @@ void ConfigureSystem::RefreshMAC() {
ui->label_mac->setText(tr("MAC: %1").arg(QString::fromStdString(mac_address)));
}
void ConfigureSystem::UnlinkConsole() {
QMessageBox::StandardButton reply;
QString warning_text =
tr("This action will unlink your real console from Azahar, with the following "
"consequences:<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) {
std::string from =
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
@ -646,9 +619,24 @@ void ConfigureSystem::InstallSecureData(const std::string& from_path, const std:
FileUtil::CreateFullPath(to);
FileUtil::Copy(from, to);
HW::UniqueData::InvalidateSecureData();
cfg->InvalidateSecureData();
RefreshSecureDataStatus();
}
void ConfigureSystem::InstallCTCert(const std::string& from_path) {
std::string from =
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
std::string to = FileUtil::SanitizePath(Service::AM::Module::GetCTCertPath(),
FileUtil::DirectorySeparator::PlatformDefault);
if (from.empty() || from == to) {
return;
}
FileUtil::Copy(from, to);
RefreshSecureDataStatus();
}
// todotodo
#ifdef todotodo
void ConfigureSystem::RefreshSecureDataStatus() {
auto status_to_str = [](HW::UniqueData::SecureDataLoadStatus status) {
switch (status) {
@ -676,16 +664,38 @@ void ConfigureSystem::RefreshSecureDataStatus() {
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str()));
ui->label_movable_status->setText(
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadMovable())).c_str()));
if (HW::UniqueData::IsFullConsoleLinked()) {
ui->linked_console->setVisible(true);
ui->button_otp->setEnabled(false);
ui->button_secure_info->setEnabled(false);
ui->button_friend_code_seed->setEnabled(false);
} else {
ui->linked_console->setVisible(false);
}
}
#endif
//--
void ConfigureSystem::RefreshSecureDataStatus() {
auto status_to_str = [](Service::CFG::SecureDataLoadStatus status) {
switch (status) {
case Service::CFG::SecureDataLoadStatus::Loaded:
return "Loaded";
case Service::CFG::SecureDataLoadStatus::NotFound:
return "Not Found";
case Service::CFG::SecureDataLoadStatus::Invalid:
return "Invalid";
case Service::CFG::SecureDataLoadStatus::IOError:
return "IO Error";
default:
return "";
}
};
Service::AM::CTCert ct_cert;
ui->label_secure_info_status->setText(
tr((std::string("Status: ") + status_to_str(cfg->LoadSecureInfoAFile())).c_str()));
ui->label_friend_code_seed_status->setText(
tr((std::string("Status: ") + status_to_str(cfg->LoadLocalFriendCodeSeedBFile())).c_str()));
ui->label_ct_cert_status->setText(
tr((std::string("Status: ") + status_to_str(static_cast<Service::CFG::SecureDataLoadStatus>(
Service::AM::Module::LoadCTCertFile(ct_cert))))
.c_str()));
}
//--
void ConfigureSystem::RetranslateUI() {
ui->retranslateUi(this);
@ -698,7 +708,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
ui->enable_required_online_lle_modules->setEnabled(
Settings::values.enable_required_online_lle_modules.UsingGlobal());
ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
return;
}
@ -710,7 +719,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_init_ticks_type->setVisible(false);
ui->label_init_ticks_value->setVisible(false);
ui->label_console_id->setVisible(false);
ui->label_mac->setVisible(false);
ui->label_sound->setVisible(false);
ui->label_language->setVisible(false);
ui->label_country->setVisible(false);
@ -732,7 +740,6 @@ void ConfigureSystem::SetupPerGameUI() {
ui->edit_init_ticks_value->setVisible(false);
ui->toggle_system_setup->setVisible(false);
ui->button_regenerate_console_id->setVisible(false);
ui->button_regenerate_mac->setVisible(false);
// Apps can change the state of the plugin loader, so plugins load
// to a chainloaded app with specific parameters. Don't allow
// the plugin loader state to be configured per-game as it may
@ -740,7 +747,9 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_plugin_loader->setVisible(false);
ui->plugin_loader->setVisible(false);
ui->allow_plugin_loader->setVisible(false);
ui->group_real_console_unique_data->setVisible(false);
// Disable the system firmware downloader.
ui->label_nus_download->setVisible(false);
ui->body_nus_download->setVisible(false);
ConfigurationShared::SetColoredTristate(ui->toggle_new_3ds, Settings::values.is_new_3ds,
is_new_3ds);
@ -749,7 +758,45 @@ void ConfigureSystem::SetupPerGameUI() {
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
Settings::values.enable_required_online_lle_modules,
required_online_lle_modules);
ConfigurationShared::SetColoredComboBox(
ui->region_combobox, ui->region_label,
static_cast<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 InstallSecureData(const std::string& from_path, const std::string& to_path);
void InstallCTCert(const std::string& from_path);
void RefreshSecureDataStatus();
void SetupPerGameUI();
void DownloadFromNUS();
private:
std::unique_ptr<Ui::ConfigureSystem> ui;
Core::System& system;

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>653</width>
<width>535</width>
<height>619</height>
</rect>
</property>
@ -14,26 +14,11 @@
<string>Form</string>
</property>
<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>
<widget class="QScrollArea" name="scrollArea">
<property name="minimumSize">
<size>
<width>660</width>
<width>0</width>
<height>480</height>
</size>
</property>
@ -64,83 +49,21 @@
<string>System Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<item row="1" column="0">
<widget class="QCheckBox" name="toggle_new_3ds">
<property name="text">
<string>Enable New 3DS mode</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QCheckBox" name="toggle_lle_applets">
<property name="text">
<string>Use LLE applets (if installed)</string>
</property>
</widget>
</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">
<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">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@ -153,21 +76,21 @@ online features (if installed)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_username">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="5" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_birthday">
<property name="text">
<string>Birthday</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_birthday2">
<item>
<widget class="QComboBox" name="combo_birthmonth">
@ -238,14 +161,14 @@ online features (if installed)</string>
</item>
</layout>
</item>
<item row="6" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_language">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="5" column="1">
<widget class="QComboBox" name="combo_language">
<property name="toolTip">
<string>Note: this can be overridden when region setting is auto-select</string>
@ -312,14 +235,14 @@ online features (if installed)</string>
</item>
</widget>
</item>
<item row="7" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label_sound">
<property name="text">
<string>Sound output mode</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="6" column="1">
<widget class="QComboBox" name="combo_sound">
<item>
<property name="text">
@ -338,31 +261,24 @@ online features (if installed)</string>
</item>
</widget>
</item>
<item row="8" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_country">
<property name="text">
<string>Country</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="7" column="1">
<widget class="QComboBox" name="combo_country"/>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_country_invalid">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="10" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_init_clock">
<property name="text">
<string>Clock</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="8" column="1">
<widget class="QComboBox" name="combo_init_clock">
<item>
<property name="text">
@ -376,28 +292,28 @@ online features (if installed)</string>
</item>
</widget>
</item>
<item row="11" column="0">
<item row="9" column="0">
<widget class="QLabel" name="label_init_time">
<property name="text">
<string>Startup time</string>
</property>
</widget>
</item>
<item row="11" column="1">
<item row="9" column="1">
<widget class="QDateTimeEdit" name="edit_init_time">
<property name="displayFormat">
<string>yyyy-MM-ddTHH:mm:ss</string>
</property>
</widget>
</item>
<item row="12" column="0">
<item row="9" column="0">
<widget class="QLabel" name="label_init_time_offset">
<property name="text">
<string>Offset time</string>
</property>
</widget>
</item>
<item row="12" column="1">
<item row="9" column="1">
<layout class="QGridLayout" name="edit_init_time_offset_grid">
<item row="0" column="0">
<widget class="QSpinBox" name="edit_init_time_offset_days">
@ -421,14 +337,14 @@ online features (if installed)</string>
</item>
</layout>
</item>
<item row="13" column="0">
<item row="10" column="0">
<widget class="QLabel" name="label_init_ticks_type">
<property name="text">
<string>Initial System Ticks</string>
</property>
</widget>
</item>
<item row="13" column="1">
<item row="10" column="1">
<widget class="QComboBox" name="combo_init_ticks_type">
<item>
<property name="text">
@ -442,14 +358,14 @@ online features (if installed)</string>
</item>
</widget>
</item>
<item row="14" column="0">
<item row="11" column="0">
<widget class="QLabel" name="label_init_ticks_value">
<property name="text">
<string>Initial System Ticks Override</string>
</property>
</widget>
</item>
<item row="14" column="1">
<item row="11" column="1">
<widget class="QLineEdit" name="edit_init_ticks_value">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@ -462,21 +378,21 @@ online features (if installed)</string>
</property>
</widget>
</item>
<item row="15" column="0">
<item row="12" column="0">
<widget class="QLabel" name="label_play_coins">
<property name="text">
<string>Play Coins</string>
</property>
</widget>
</item>
<item row="15" column="1">
<item row="12" column="1">
<widget class="QSpinBox" name="spinBox_play_coins">
<property name="maximum">
<number>300</number>
</property>
</widget>
</item>
<item row="16" column="0">
<item row="13" column="0">
<widget class="QLabel" name="label_steps_per_hour">
<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>
@ -486,28 +402,28 @@ online features (if installed)</string>
</property>
</widget>
</item>
<item row="16" column="1">
<item row="13" column="1">
<widget class="QSpinBox" name="spinBox_steps_per_hour">
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
<item row="17" column="1">
<item row="14" column="1">
<widget class="QCheckBox" name="toggle_system_setup">
<property name="text">
<string>Run System Setup when Home Menu is launched</string>
</property>
</widget>
</item>
<item row="18" column="0">
<item row="15" column="0">
<widget class="QLabel" name="label_console_id">
<property name="text">
<string>Console ID:</string>
</property>
</widget>
</item>
<item row="18" column="1">
<item row="15" column="1">
<widget class="QPushButton" name="button_regenerate_console_id">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
@ -523,50 +439,114 @@ online features (if installed)</string>
</property>
</widget>
</item>
<item row="19" 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">
<item row="16" column="0">
<widget class="QLabel" name="label_plugin_loader">
<property name="text">
<string>3GX Plugin Loader:</string>
</property>
</widget>
</item>
<item row="20" column="1">
<item row="16" column="1">
<widget class="QCheckBox" name="plugin_loader">
<property name="text">
<string>Enable 3GX plugin loader</string>
</property>
</widget>
</item>
<item row="21" column="1">
<item row="17" column="1">
<widget class="QCheckBox" name="allow_plugin_loader">
<property name="text">
<string>Allow applications to change plugin loader state</string>
</property>
</widget>
</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>
</widget>
</item>
@ -576,168 +556,97 @@ online features (if installed)</string>
<string>Real Console Unique Data</string>
</property>
<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">
<widget class="QLabel" name="label_movable">
<widget class="QLabel" name="label_secure_info">
<property name="text">
<string>movable.sed</string>
<string>SecureInfo_A/B</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QWidget" name="movable">
<layout class="QHBoxLayout" name="horizontalLayout_movable">
<widget class="QWidget" name="secure_info">
<layout class="QHBoxLayout" name="horizontalLayout_secure_info">
<item>
<widget class="QLabel" name="label_movable_status">
<widget class="QLabel" name="label_secure_info_status">
<property name="text">
<string/>
</property>
</widget>
</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">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -802,7 +711,6 @@ online features (if installed)</string>
<tabstop>spinBox_play_coins</tabstop>
<tabstop>spinBox_steps_per_hour</tabstop>
<tabstop>button_regenerate_console_id</tabstop>
<tabstop>button_regenerate_mac</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -1039,7 +1039,7 @@ void GameList::LoadInterfaceLayout() {
}
const QStringList GameList::supported_file_extensions = {
QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
QStringLiteral("3ds"), QStringLiteral("3dsx"), QStringLiteral("elf"), QStringLiteral("axf"),
QStringLiteral("cci"), QStringLiteral("cxi"), QStringLiteral("app")};
void GameList::RefreshGameDirectory() {

View File

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

View File

@ -94,7 +94,7 @@ struct Values {
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_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
Settings::Setting<bool> show_compat_column{true, "show_compat_column"};

View File

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

View File

@ -386,7 +386,15 @@ public:
[[nodiscard]] size_t ReadSpan(std::span<T> data) {
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
#ifdef todotodo
return ReadImpl(data.data(), data.size(), sizeof(T));
#else
if (!IsOpen()) {
return 0;
}
return std::fread(data.data(), sizeof(T), data.size(), m_file);
#endif
}
/**
@ -408,7 +416,15 @@ public:
[[nodiscard]] size_t WriteSpan(std::span<const T> data) {
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
#ifdef todotodo
return WriteImpl(data.data(), data.size(), sizeof(T));
#else
if (!IsOpen()) {
return 0;
}
return std::fwrite(data.data(), sizeof(T), data.size(), m_file);
#endif
}
[[nodiscard]] bool IsOpen() const {

View File

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

View File

@ -137,7 +137,9 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
return Loader::ResultStatus::Success;
}
#ifdef todotodo
for (int i = 0; i < 2; i++) {
#endif
if (!file->IsOpen()) {
return Loader::ResultStatus::Error;
}
@ -164,6 +166,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
// Verify we are loading the correct file type...
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
#ifdef todotodo
// We may be loading a crypto file, try again
if (i == 0) {
file.reset();
@ -172,8 +175,13 @@ Loader::ResultStatus NCCHContainer::LoadHeader() {
} else {
return Loader::ResultStatus::ErrorInvalidFormat;
}
#else
return Loader::ResultStatus::ErrorInvalidFormat;
#endif
}
#ifdef todotodo
}
#endif
if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
@ -192,7 +200,9 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->IsOpen()) {
size_t file_size;
#ifdef todotodo
for (int i = 0; i < 2; i++) {
#endif
file_size = file->GetSize();
// Reset read pointer in case this file has been read before.
@ -215,6 +225,7 @@ Loader::ResultStatus NCCHContainer::Load() {
// Verify we are loading the correct file type...
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
#ifdef todotodo
// We may be loading a crypto file, try again
if (i == 0) {
file = HW::UniqueData::OpenUniqueCryptoFile(
@ -222,14 +233,146 @@ Loader::ResultStatus NCCHContainer::Load() {
} else {
return Loader::ResultStatus::ErrorInvalidFormat;
}
#else
return Loader::ResultStatus::ErrorInvalidFormat;
#endif
}
#ifdef todotodo
}
#endif
if (file->IsCrypto()) {
LOG_DEBUG(Service_FS, "NCCH file has console unique crypto");
}
has_header = true;
bool failed_to_decrypt = false;
if (!ncch_header.no_crypto) {
is_encrypted = true;
// Find primary and secondary keys
if (ncch_header.fixed_key) {
LOG_DEBUG(Service_FS, "Fixed-key crypto");
primary_key.fill(0);
secondary_key.fill(0);
} else {
using namespace HW::AES;
InitKeys();
std::array<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) {
// The NCCH is a proto version, which does not use media size units
@ -237,10 +380,12 @@ Loader::ResultStatus NCCHContainer::Load() {
block_size = 1;
}
#ifdef todotodo
if (!ncch_header.no_crypto) {
// Encrypted NCCH are not supported
return Loader::ResultStatus::ErrorEncrypted;
}
#endif
// System archives and DLC don't have an extended header but have RomFS
// Proto apps don't have an ext header size
@ -254,6 +399,26 @@ Loader::ResultStatus NCCHContainer::Load() {
return Loader::ResultStatus::Error;
}
if (is_encrypted) {
// This ID check is masked to low 32-bit as a toleration to ill-formed ROM created
// by merging games and its updates.
if ((exheader_header.system_info.jump_id & 0xFFFFFFFF) ==
(ncch_header.program_id & 0xFFFFFFFF)) {
LOG_WARNING(Service_FS, "NCCH is marked as encrypted but with decrypted "
"exheader. Force no crypto scheme.");
is_encrypted = false;
} else {
if (failed_to_decrypt) {
LOG_ERROR(Service_FS, "Failed to decrypt");
return Loader::ResultStatus::ErrorEncrypted;
}
CryptoPP::byte* data = reinterpret_cast<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 =
fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
GetModId(ncch_header.program_id));
@ -323,6 +488,7 @@ Loader::ResultStatus NCCHContainer::Load() {
if (file->ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header))
return Loader::ResultStatus::Error;
#ifdef todotodo
if (file->IsCrypto()) {
exefs_file = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
@ -330,6 +496,16 @@ Loader::ResultStatus NCCHContainer::Load() {
exefs_file = std::make_unique<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;
}
@ -457,6 +633,17 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
: (section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset);
exefs_file->Seek(section_offset, SEEK_SET);
std::array<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;
if (strcmp(section.name, ".code") == 0 && is_compressed) {
@ -466,6 +653,10 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
temp_buffer.size())
return Loader::ResultStatus::Error;
if (is_encrypted) {
dec.ProcessData(&temp_buffer[0], &temp_buffer[0], section.size);
}
// Decompress .code section...
buffer.resize(LZSS_GetDecompressedSize(temp_buffer));
if (!LZSS_Decompress(temp_buffer, buffer)) {
@ -476,6 +667,9 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
buffer.resize(section_size);
if (exefs_file->ReadBytes(buffer.data(), section_size) != section_size)
return Loader::ResultStatus::Error;
if (is_encrypted) {
dec.ProcessData(buffer.data(), buffer.data(), section.size);
}
}
return Loader::ResultStatus::Success;
@ -607,18 +801,34 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<RomFSReader>& romf
// We reopen the file, to allow its position to be independent from file's
std::unique_ptr<FileUtil::IOFile> romfs_file_inner;
#ifdef todotodo
if (file->IsCrypto()) {
romfs_file_inner = HW::UniqueData::OpenUniqueCryptoFile(
filepath, "rb", HW::UniqueData::UniqueCryptoFileID::NCCH);
} else {
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
}
#else
romfs_file_inner = std::make_unique<FileUtil::IOFile>(filepath, "rb");
#endif
if (!romfs_file_inner->IsOpen())
return Loader::ResultStatus::Error;
#ifdef todotodo
std::shared_ptr<RomFSReader> direct_romfs =
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 =
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) {
#ifdef todotodo
if (file->IsCrypto())
return Loader::ResultStatus::ErrorEncrypted;
#endif
std::shared_ptr<RomFSReader> direct_romfs;
Loader::ResultStatus result = ReadRomFS(direct_romfs, false);

View File

@ -348,6 +348,14 @@ private:
bool is_loaded = false;
bool is_compressed = false;
bool is_encrypted = false;
// for decrypting exheader, exefs header and icon/banner section
std::array<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 exefs_offset = 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
if (segments.size() == 1 && segments[0].second > cache_line_size) {
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);
return length;
}
@ -43,6 +48,11 @@ std::size_t DirectRomFSReader::ReadFile(std::size_t offset, std::size_t length,
if (!cache_entry.first) {
// If not found, read from disk and cache the data
read_size = file->ReadAtBytes(cache_entry.second.data(), read_size, file_offset + page);
if (is_encrypted && read_size) {
CryptoPP::CTR_Mode<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,
(seg.first - page));
} else {

View File

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

View File

@ -4,6 +4,7 @@
#pragma once
#include "common/assert.h"
#include "common/common_types.h"
#include "common/logging/log.h"
@ -36,4 +37,4 @@ inline u32 GetSignatureSize(u32 signature_type) {
LOG_ERROR(Common_Filesystem, "Bad signature {}", signature_type);
return 0;
}
} // namespace FileSys
} // namespace FileSys

View File

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

View File

@ -43,8 +43,9 @@ public:
u8 audit;
INSERT_PADDING_BYTES(0x42);
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)
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) {
#ifdef todotodo
memcpy(&param, parameter.buffer.data(), std::min(parameter.buffer.size(), sizeof(param)));
// Do something here, like showing error codes, or prompting for EULA agreement.
if (param.type == DisplayType::Agree) {
param.result = 1;
}
#else
startup_param = parameter.buffer;
#endif
//--
// TODO(Subv): Set the expected fields in the response buffer before resending it to the
// application.
// TODO(Subv): Reverse the parameter format for the ErrEula applet
//--
// Let the application know that we're closing.
Finalize();
@ -57,8 +67,13 @@ Result ErrEula::Start(const Service::APT::MessageParameter& parameter) {
}
Result ErrEula::Finalize() {
#ifdef todotodo
std::vector<u8> buffer(sizeof(param));
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);
return ResultSuccess;
}

View File

@ -54,7 +54,8 @@ private:
std::shared_ptr<Kernel::SharedMemory> framebuffer_memory;
/// Parameter received by the applet on start.
ErrEulaParam param{};
// ErrEulaParam param{};
std::vector<u8> startup_param;
};
} // 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);
// Map the guard pages and mapped pages at once.
#ifdef todotodo
auto target_address_result = dst_process->vm_manager.MapBackingMemoryToBase(
Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE, buffer,
static_cast<u32>(buffer->GetSize()), Kernel::MemoryState::Shared);
ASSERT_MSG(target_address_result.Succeeded(), "Failed to map target address");
target_address = target_address_result.Unwrap();
#else
target_address =
dst_process->vm_manager
.MapBackingMemoryToBase(Memory::IPC_MAPPING_VADDR, Memory::IPC_MAPPING_SIZE,
buffer, static_cast<u32>(buffer->GetSize()),
Kernel::MemoryState::Shared)
.Unwrap();
#endif
// Change the permissions and state of the guard pages.
const VAddr low_guard_address = target_address;

View File

@ -47,6 +47,9 @@ union BatteryState {
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 {
Off = 0,
Poor = 1,

View File

@ -106,10 +106,26 @@ void Module::Interface::GetStatus(Kernel::HLERequestContext& ctx) {
void Module::Interface::GetWifiStatus(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
/* todotodo
*/
//--
bool can_reach_internet = false;
std::shared_ptr<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);
rb.Push(ResultSuccess);
#ifdef todotodo
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) {

View File

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

View File

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

View File

@ -89,12 +89,42 @@ struct TicketInfo {
static_assert(sizeof(TicketInfo) == 0x18, "Ticket info structure size is wrong");
bool CTCert::IsValid() const {
constexpr std::string_view expected_issuer_prod = "Nintendo CA - G3_NintendoCTR2prod";
constexpr std::string_view expected_issuer_dev = "Nintendo CA - G3_NintendoCTR2dev";
constexpr u32 expected_signature_type = 0x010005;
return signature_type == expected_signature_type &&
(std::string(issuer.data()) == expected_issuer_prod ||
std::string(issuer.data()) == expected_issuer_dev);
return false;
}
u32 CTCert::GetDeviceID() const {
constexpr std::string_view key_id_prefix = "CT";
const std::string key_id_str(key_id.data());
if (key_id_str.starts_with(key_id_prefix)) {
const std::string device_id =
key_id_str.substr(key_id_prefix.size(), key_id_str.find('-') - key_id_prefix.size());
char* end_ptr;
const u32 device_id_value = std::strtoul(device_id.c_str(), &end_ptr, 16);
if (*end_ptr == '\0') {
return device_id_value;
}
}
// Error
return 0;
}
class CIAFile::DecryptionState {
public:
std::vector<CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption> content;
};
NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_content) {
#ifdef todotodo
if (encrypted_content) {
// A console unique crypto file is used to store the decrypted NCCH file. This is done
// to prevent Azahar being used as a tool to download easy shareable decrypted contents
@ -108,15 +138,20 @@ NCCHCryptoFile::NCCHCryptoFile(const std::string& out_file, bool encrypted_conte
if (!file->IsOpen()) {
is_error = true;
}
#else
file = std::make_unique<FileUtil::IOFile>(out_file, "wb");
#endif
}
void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
if (is_error)
return;
#ifdef todotodo
if (is_not_ncch) {
file->WriteBytes(buffer, length);
}
#endif
const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
@ -130,10 +165,14 @@ void NCCHCryptoFile::Write(const u8* buffer, std::size_t length) {
if (!header_parsed && header_size == sizeof(NCCH_Header)) {
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) {
#ifdef todotodo
// Most likely DS contents, store without additional operations
is_not_ncch = true;
file->WriteBytes(&ncch_header, sizeof(ncch_header));
file->WriteBytes(buffer, length);
#else
is_error = true;
#endif
return;
}
@ -420,7 +459,7 @@ void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ct
}
CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn_)
: system(system_), from_cdn(from_cdn_), decryption_authorized(false), media_type(media_type),
: system(system_), from_cdn(from_cdn_), decryption_authorized(true), media_type(media_type),
decryption_state(std::make_unique<DecryptionState>()) {
// If data is being installing from CDN, provide a fake header to the container so that
// it's not uninitialized.
@ -457,6 +496,7 @@ Result CIAFile::WriteTicket() {
ErrorLevel::Permanent};
}
#ifdef todotodo
const auto& ticket = container.GetTicket();
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
@ -471,6 +511,7 @@ Result CIAFile::WriteTicket() {
// TODO: Correct result code.
return FileSys::ResultFileNotFound;
}
#endif
install_state = CIAInstallState::TicketLoaded;
return ResultSuccess;
@ -519,12 +560,25 @@ Result CIAFile::WriteTitleMetadata(std::span<const u8> tmd_data, std::size_t off
auto content_count = container.GetTitleMetadata().GetContentCount();
content_written.resize(content_count);
#ifdef todotodo
current_content_file.reset();
current_content_index = -1;
content_file_paths.clear();
#else
content_files.clear();
#endif
for (std::size_t i = 0; i < content_count; i++) {
auto path = GetTitleContentPath(media_type, tmd.GetTitleID(), i, is_update);
#ifdef todotodo
content_file_paths.emplace_back(path);
#else
auto& file = content_files.emplace_back(path, "wb");
if (!file.IsOpen()) {
LOG_ERROR(Service_AM, "Could not open output file '{}' for content {}.", path, i);
// TODO: Correct error code.
return FileSys::ResultFileNotFound;
}
#endif
}
if (container.GetTitleMetadata().HasEncryptedContent()) {
@ -561,6 +615,7 @@ ResultVal<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
// contents or only part of a larger .app's contents.
const u64 offset_max = offset + length;
bool success = true;
for (std::size_t i = 0; i < content_written.size(); i++) {
if (content_written[i] < container.GetContentSize(i)) {
// The size, minimum unwritten offset, and maximum unwritten offset of this content
@ -579,6 +634,7 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
// Since the incoming TMD has already been written, we can use GetTitleContentPath
// to get the content paths to write to.
#ifdef todotodo
const FileSys::TitleMetadata& tmd = container.GetTitleMetadata();
if (i != current_content_index) {
current_content_index = static_cast<u16>(i);
@ -588,6 +644,10 @@ ResultVal<std::size_t> CIAFile::WriteContentData(u64 offset, std::size_t length,
}
auto& file = *current_content_file;
#else
FileSys::TitleMetadata tmd = container.GetTitleMetadata();
auto& file = content_files[i];
#endif
std::vector<u8> temp(buffer + (range_min - offset),
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());
}
file.Write(temp.data(), temp.size());
if (file.IsError()) {
// This can never happen in real HW
return Result(ErrCodes::InvalidImportState, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent);
}
file.WriteBytes(temp.data(), temp.size());
// Keep tabs on how much of this content ID has been written so new range_min
// values can be calculated.
@ -610,7 +665,7 @@ ResultVal<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,
@ -732,17 +787,13 @@ ResultVal<std::size_t> CIAFile::WriteContentDataIndexed(u16 content_index, u64 o
}
file.Write(temp.data(), temp.size());
if (file.IsError()) {
// This can never happen in real HW
return Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent);
}
bool success = !file.IsError();
content_written[content_index] += temp.size();
LOG_DEBUG(Service_AM, "Wrote {} to content {}, total {}", temp.size(), content_index,
content_written[content_index]);
return temp.size();
return success ? temp.size() : 0;
}
u64 CIAFile::GetSize() const {
@ -848,6 +899,12 @@ bool TicketFile::SetSize(u64 size) const {
}
bool TicketFile::Close() {
FileSys::Ticket ticket;
if (ticket.Load(data, 0) == Loader::ResultStatus::Success) {
LOG_WARNING(Service_AM, "Discarding ticket for {:#016X}.", ticket.GetTitleID());
} else {
LOG_ERROR(Service_AM, "Invalid ticket provided to TicketFile.");
}
return true;
}
@ -865,6 +922,11 @@ Result TicketFile::Commit() {
ticket_id = ticket.GetTicketID();
const auto ticket_path = GetTicketPath(ticket.GetTitleID(), ticket.GetTicketID());
// Create ticket folder if it does not exist
std::string ticket_folder;
Common::SplitPath(ticket_path, &ticket_folder, nullptr, nullptr);
FileUtil::CreateFullPath(ticket_folder);
// Save ticket
if (ticket.Save(ticket_path) != Loader::ResultStatus::Success) {
LOG_ERROR(Service_AM, "Failed to install ticket provided to TicketFile.");
@ -964,8 +1026,10 @@ InstallStatus InstallCIA(const std::string& path,
Core::System::GetInstance(),
Service::AM::GetTitleMediaType(container.GetTitleMetadata().GetTitleID()));
if (container.GetTitleMetadata().HasEncryptedContent()) {
LOG_ERROR(Service_AM, "File {} is encrypted! Aborting...", path);
bool title_key_available = container.GetTicket().GetTitleKey().has_value();
if (!title_key_available && container.GetTitleMetadata().HasEncryptedContent()) {
LOG_ERROR(Service_AM, "File {} is encrypted and no title key is available! Aborting...",
path);
return InstallStatus::ErrorEncrypted;
}
@ -975,8 +1039,12 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorFailedToOpenFile;
}
#ifdef todotodo
std::vector<u8> buffer;
buffer.resize(0x10000);
#else
std::array<u8, 0x10000> buffer;
#endif
auto file_size = file.GetSize();
std::size_t total_bytes_read = 0;
while (total_bytes_read != file_size) {
@ -1035,6 +1103,96 @@ InstallStatus InstallCIA(const std::string& path,
return InstallStatus::ErrorInvalid;
}
InstallStatus InstallFromNus(u64 title_id, int version) {
LOG_DEBUG(Service_AM, "Downloading {:X}", title_id);
CIAFile install_file{Core::System::GetInstance(), GetTitleMediaType(title_id)};
std::string path = fmt::format("/ccs/download/{:016X}/tmd", title_id);
if (version != -1) {
path += fmt::format(".{}", version);
}
auto tmd_response = Core::NUS::Download(path);
if (!tmd_response) {
LOG_ERROR(Service_AM, "Failed to download tmd for {:016X}", title_id);
return InstallStatus::ErrorFileNotFound;
}
FileSys::TitleMetadata tmd;
tmd.Load(*tmd_response);
path = fmt::format("/ccs/download/{:016X}/cetk", title_id);
auto cetk_response = Core::NUS::Download(path);
if (!cetk_response) {
LOG_ERROR(Service_AM, "Failed to download cetk for {:016X}", title_id);
return InstallStatus::ErrorFileNotFound;
}
std::vector<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) {
// Real services seem to just discard and replace the whole high word.
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,
title_count * sizeof(u64));
async_data->title_info_out = &rp.PopMappedBuffer();
#ifdef todotodo
// nim checks if the current importing title already exists during installation.
// Normally, since the program wouldn't be commited, getting the title info returns not
// found. However, since GetTitleInfoFromList does not care if the program was commited and
// only checks for the tmd, it will detect the title and return information while it
// shouldn't. To prevent this, we check if the importing context is present and not
// committed. If that's the case, return not found
Result result = ResultSuccess;
for (auto tid : title_id_list) {
for (auto& import_ctx : am->import_title_contexts) {
if (import_ctx.first == tid &&
(import_ctx.second.state == ImportTitleContextState::WAITING_FOR_IMPORT ||
import_ctx.second.state == ImportTitleContextState::WAITING_FOR_COMMIT ||
import_ctx.second.state == ImportTitleContextState::RESUMABLE)) {
LOG_DEBUG(Service_AM, "title pending commit title_id={:016X}", tid);
result = Result(ErrorDescription::NotFound, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent);
}
}
}
if (result.IsSuccess())
result = GetTitleInfoFromList(title_id_list, media_type, title_info_out);
#else
ctx.RunAsync(
[this, async_data](Kernel::HLERequestContext& ctx) {
// nim checks if the current importing title already exists during installation.
// Normally, since the program wouldn't be commited, getting the title info returns
// not found. However, since GetTitleInfoFromList does not care if the program was
// commited and only checks for the tmd, it will detect the title and return
// information while it shouldn't. To prevent this, we check if the importing
// context is present and not committed. If that's the case, return not found
for (auto tid : async_data->title_id_list) {
for (auto& import_ctx : am->import_title_contexts) {
if (import_ctx.first == tid &&
(import_ctx.second.state ==
ImportTitleContextState::WAITING_FOR_IMPORT ||
import_ctx.second.state ==
ImportTitleContextState::WAITING_FOR_COMMIT ||
import_ctx.second.state == ImportTitleContextState::RESUMABLE)) {
LOG_DEBUG(Service_AM, "title pending commit title_id={:016X}", tid);
async_data->res =
Result(ErrorDescription::NotFound, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent);
}
}
}
if (async_data->res.IsSuccess()) {
async_data->res = GetTitleInfoFromList(async_data->title_id_list,
[async_data](Kernel::HLERequestContext& ctx) {
async_data->res = GetTitleInfoFromList(async_data->title_id_list,
async_data->media_type, async_data->out);
}
return 0;
},
[async_data](Kernel::HLERequestContext& ctx) {
@ -1873,6 +2030,7 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool
rb.PushMappedBuffer(*async_data->title_info_out);
},
true);
#endif
}
}
@ -2416,6 +2574,7 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
std::scoped_lock lock(am->am_lists_mutex);
#ifdef todotodo
auto range = am->am_ticket_list.equal_range(title_id);
if (range.first == range.second) {
rb.Push(Result(ErrorDescription::AlreadyDone, ErrorModule::AM, ErrorSummary::Success,
@ -2429,8 +2588,10 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
}
am->am_ticket_list.erase(range.first, range.second);
#endif
rb.Push(ResultSuccess);
LOG_WARNING(Service_AM, "(STUBBED) called title_id=0x{:016x}", title_id);
}
void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
@ -2439,11 +2600,19 @@ void Module::Interface::GetNumTickets(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "");
std::scoped_lock lock(am->am_lists_mutex);
#ifdef todotodo
u32 ticket_count = static_cast<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);
rb.Push(ResultSuccess);
rb.Push(ticket_count);
LOG_WARNING(Service_AM, "(STUBBED) called ticket_count=0x{:08x}", ticket_count);
}
void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
@ -2456,6 +2625,7 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
u32 tickets_written = 0;
std::scoped_lock lock(am->am_lists_mutex);
#ifdef todotodo
auto it = am->am_ticket_list.begin();
std::advance(it, std::min(static_cast<size_t>(ticket_index), am->am_ticket_list.size()));
@ -2463,11 +2633,22 @@ void Module::Interface::GetTicketList(Kernel::HLERequestContext& ctx) {
it++, tickets_written++) {
ticket_tids_out.Write(&it->first, tickets_written * sizeof(u64), sizeof(u64));
}
#else
for (const auto& title_list : am->am_title_list) {
const auto tickets_to_write =
std::min(static_cast<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);
rb.Push(ResultSuccess);
rb.Push(tickets_written);
rb.PushMappedBuffer(ticket_tids_out);
LOG_WARNING(Service_AM, "(STUBBED) ticket_list_count=0x{:08x}, ticket_index=0x{:08x}",
ticket_list_count, ticket_index);
}
void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
@ -2475,6 +2656,7 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "");
#ifdef todotodo
const auto& otp = HW::UniqueData::GetOTP();
if (!otp.Valid()) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -2490,6 +2672,13 @@ void Module::Interface::GetDeviceID(Kernel::HLERequestContext& ctx) {
if (am->force_old_device_id) {
deviceID &= ~0x80000000;
}
#else
const u32 deviceID = am->ct_cert.IsValid() ? am->ct_cert.GetDeviceID() : 0;
if (deviceID == 0) {
LOG_ERROR(Service_AM, "Invalid or missing CTCert");
}
#endif
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
rb.Push(ResultSuccess);
@ -2505,6 +2694,7 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
rb.Push(ResultSuccess);
#ifdef todotodo
u32 count = 0;
for (auto it = am->import_title_contexts.begin(); it != am->import_title_contexts.end(); it++) {
if ((include_installing &&
@ -2517,6 +2707,9 @@ void Module::Interface::GetNumImportTitleContextsImpl(IPC::RequestParser& rp,
}
rb.Push<u32>(count);
#else
rb.Push<u32>(static_cast<u32>(am->import_title_contexts.size()));
#endif
}
void Module::Interface::GetImportTitleContextListImpl(IPC::RequestParser& rp,
@ -2717,6 +2910,7 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "(STUBBED) media_type=0x{:02x}", media_type);
#ifdef todotodo
bool needs_cleanup = false;
for (auto& import_ctx : am->import_title_contexts) {
if (import_ctx.second.state == ImportTitleContextState::NEEDS_CLEANUP) {
@ -2732,10 +2926,15 @@ void Module::Interface::NeedsCleanup(Kernel::HLERequestContext& ctx) {
}
}
}
#endif
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess);
#ifdef todotodo
rb.Push<bool>(needs_cleanup);
#else
rb.Push<bool>(false);
#endif
}
void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
@ -2744,6 +2943,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_AM, "(STUBBED) called, media_type={:#02x}", media_type);
#ifdef todotodo
for (auto it = am->import_content_contexts.begin(); it != am->import_content_contexts.end();) {
if (it->second.state == ImportTitleContextState::NEEDS_CLEANUP) {
it = am->import_content_contexts.erase(it);
@ -2759,6 +2959,7 @@ void Module::Interface::DoCleanup(Kernel::HLERequestContext& ctx) {
it++;
}
}
#endif
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
@ -2775,6 +2976,7 @@ void Module::Interface::QueryAvailableTitleDatabase(Kernel::HLERequestContext& c
LOG_WARNING(Service_AM, "(STUBBED) media_type={}", media_type);
}
#ifdef todotodo
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
@ -2859,6 +3061,48 @@ void Module::Interface::GetImportTitleContextListFiltered(Kernel::HLERequestCont
LOG_WARNING(Service_AM, "(STUBBED) called, media_type={}, filter={}", media_type, filter);
}
#else
void Module::Interface::GetPersonalizedTicketInfoList(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 ticket_count = rp.Pop<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) {
IPC::RequestParser rp(ctx);
@ -2899,8 +3143,13 @@ void Module::Interface::BeginImportProgram(Kernel::HLERequestContext& ctx) {
if (am->cia_installing) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
#ifdef todotodo
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent));
#else
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent));
#endif
return;
}
@ -2924,8 +3173,13 @@ void Module::Interface::BeginImportProgramTemporarily(Kernel::HLERequestContext&
if (am->cia_installing) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
#ifdef todotodo
rb.Push(Result(ErrCodes::InvalidImportState, ErrorModule::AM, ErrorSummary::InvalidState,
ErrorLevel::Permanent));
#else
rb.Push(Result(ErrCodes::CIACurrentlyInstalling, ErrorModule::AM,
ErrorSummary::InvalidState, ErrorLevel::Permanent));
#endif
return;
}
@ -2975,7 +3229,25 @@ void Module::Interface::EndImportProgramWithoutCommit(Kernel::HLERequestContext&
}
void Module::Interface::CommitImportPrograms(Kernel::HLERequestContext& ctx) {
#ifdef todotodo
CommitImportTitlesImpl(ctx, false, false);
#else
IPC::RequestParser rp(ctx);
[[maybe_unused]] const auto media_type = static_cast<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.
@ -3452,45 +3724,39 @@ void Module::Interface::BeginImportTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
rb.Push(ResultSuccess); // No error
rb.PushCopyObjects(file->Connect());
LOG_WARNING(Service_AM, "(STUBBED) called");
}
#ifdef todotodo
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
const auto ticket = rp.PopObject<Kernel::ClientSession>();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket);
if (ticket_file.Succeeded()) {
struct AsyncData {
Service::AM::TicketFile* ticket_file;
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);
rb.Push(ticket_file.Unwrap()->Commit());
am->am_ticket_list.insert(std::make_pair(ticket_file.Unwrap()->GetTitleID(),
ticket_file.Unwrap()->GetTicketID()));
} else {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ticket_file.Code());
}
LOG_DEBUG(Service_AM, "title_id={:016X} ticket_id={:016X}", ticket_file.Unwrap()->GetTitleID(),
ticket_file.Unwrap()->GetTicketID());
}
#else
void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] const auto ticket = rp.PopObject<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) {
IPC::RequestParser rp(ctx);
@ -3621,6 +3887,7 @@ void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) {
}
am->importing_title->cia_file.SetDone();
am->ScanForTitles(am->importing_title->media_type);
am->importing_title.reset();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -3994,10 +4261,12 @@ void Module::serialize(Archive& ar, const unsigned int) {
ar & cia_installing;
ar & force_old_device_id;
ar & force_new_device_id;
ar & am_title_list;
ar & system_updater_mutex;
}
SERIALIZE_IMPL(Module)
#ifdef todotodo
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 size = rp.Pop<u32>();
@ -4021,6 +4290,52 @@ void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
rb.Push(0);
rb.PushMappedBuffer(buffer);
}
#else
void Module::Interface::GetDeviceCert(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 size = rp.Pop<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) {
CommitImportTitlesImpl(ctx, true, true);
@ -4051,7 +4366,11 @@ void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) {
auto path = GetTicketPath(title_id, ticket_id);
FileUtil::Delete(path);
#ifdef todotodo
am->am_ticket_list.erase(it);
#else
am->ScanForTickets();
#endif
rb.Push(ResultSuccess);
}
@ -4233,6 +4552,7 @@ void Module::Interface::ExportTicketWrapped(Kernel::HLERequestContext& ctx) {
Module::Module(Core::System& _system) : system(_system) {
FileUtil::CreateFullPath(GetTicketDirectory());
ScanForAllTitles();
LoadCTCertFile(ct_cert);
system_updater_mutex = system.Kernel().CreateMutex(false, "AM::SystemUpdaterMutex");
}

View File

@ -51,6 +51,7 @@ namespace Service::AM {
namespace ErrCodes {
enum {
InvalidImportState = 4,
CIACurrentlyInstalling = 4,
InvalidTID = 31,
EmptyCIA = 32,
TryingToUninstallSystemApp = 44,
@ -87,6 +88,13 @@ enum class ImportTitleContextState : u8 {
NEEDS_CLEANUP = 6,
};
enum class CTCertLoadStatus {
Loaded,
NotFound,
Invalid,
IOError,
};
struct ImportTitleContext {
u64 title_id;
u16 version;
@ -105,6 +113,24 @@ struct ImportContentContext {
};
static_assert(sizeof(ImportContentContext) == 0x18, "Invalid ImportContentContext size");
struct CTCert {
u32_be signature_type{};
std::array<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
constexpr std::size_t TITLE_ID_VALID_LENGTH = 16;
@ -126,7 +152,7 @@ private:
friend class CIAFile;
std::unique_ptr<FileUtil::IOFile> file;
bool is_error = false;
bool is_not_ncch = false;
// bool is_not_ncch = false;
bool decryption_authorized = false;
std::size_t written = 0;
@ -223,6 +249,7 @@ private:
std::vector<std::string> content_file_paths;
u16 current_content_index = -1;
std::unique_ptr<NCCHCryptoFile> current_content_file;
std::vector<FileUtil::IOFile> content_files;
Service::FS::MediaType media_type;
class DecryptionState;
@ -334,6 +361,13 @@ private:
InstallStatus InstallCIA(const std::string& path,
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
* @param titleId the title ID
@ -1022,6 +1056,18 @@ public:
force_new_device_id = true;
}
/**
* Gets the CTCert.bin path in the host filesystem
* @returns std::string CTCert.bin path in the host filesystem
*/
static std::string GetCTCertPath();
/**
* Loads the CTCert.bin file from the filesystem.
* @returns CTCertLoadStatus indicating the file load status.
*/
static CTCertLoadStatus LoadCTCertFile(CTCert& output);
private:
void ScanForTickets();
@ -1054,6 +1100,7 @@ private:
std::multimap<u64, u64> am_ticket_list;
std::shared_ptr<Kernel::Mutex> system_updater_mutex;
CTCert ct_cert{};
std::shared_ptr<CurrentImportingTitle> importing_title;
std::map<u64, ImportTitleContext> import_title_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) {
IPC::RequestParser rp(ctx);
#ifdef todotodo
const auto& secure_info_a = HW::UniqueData::GetSecureInfoA();
const auto& local_friend_code_seed_b = HW::UniqueData::GetLocalFriendCodeSeedB();
@ -465,12 +466,19 @@ void Module::Interface::SecureInfoGetByte101(Kernel::HLERequestContext& ctx) {
}
u8 ret = secure_info_a.body.unknown;
#else
u8 ret = 0;
if (cfg->secure_info_a_loaded) {
ret = cfg->secure_info_a.unknown;
}
#endif
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess);
rb.Push<u8>(ret);
}
#ifdef todotodo
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
@ -500,6 +508,32 @@ void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
rb.Push(ResultSuccess);
rb.PushMappedBuffer(out_buffer);
}
#else
void Module::Interface::SecureInfoGetSerialNo(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop<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) {
IPC::RequestParser rp(ctx);
@ -645,6 +679,7 @@ void Module::Interface::UpdateConfigNANDSavegame(Kernel::HLERequestContext& ctx)
rb.Push(cfg->UpdateConfigNANDSavegame());
}
#ifdef todotodo
void Module::Interface::GetLocalFriendCodeSeedData(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
[[maybe_unused]] u32 out_size = rp.Pop<u32>();
@ -688,6 +723,44 @@ void Module::Interface::GetLocalFriendCodeSeed(Kernel::HLERequestContext& ctx) {
rb.Push(ResultSuccess);
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) {
IPC::RequestParser rp(ctx);
@ -863,6 +936,14 @@ Result Module::UpdateConfigNANDSavegame() {
return ResultSuccess;
}
std::string Module::GetLocalFriendCodeSeedBPath() {
return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/LocalFriendCodeSeed_B";
}
std::string Module::GetSecureInfoAPath() {
return FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "rw/sys/SecureInfo_A";
}
Result Module::FormatConfig() {
Result res = DeleteConfigNANDSaveFile();
// The delete command fails if the file doesn't exist, so we have to check that too
@ -938,6 +1019,55 @@ Result Module::LoadConfigNANDSaveFile() {
return FormatConfig();
}
void Module::InvalidateSecureData() {
secure_info_a_loaded = local_friend_code_seed_b_loaded = false;
}
SecureDataLoadStatus Module::LoadSecureInfoAFile() {
if (secure_info_a_loaded) {
return SecureDataLoadStatus::Loaded;
}
std::string file_path = GetSecureInfoAPath();
if (!FileUtil::Exists(file_path)) {
return SecureDataLoadStatus::NotFound;
}
FileUtil::IOFile file(file_path, "rb");
if (!file.IsOpen()) {
return SecureDataLoadStatus::IOError;
}
if (file.GetSize() != sizeof(SecureInfoA)) {
return SecureDataLoadStatus::Invalid;
}
if (file.ReadBytes(&secure_info_a, sizeof(SecureInfoA)) != sizeof(SecureInfoA)) {
return SecureDataLoadStatus::IOError;
}
secure_info_a_loaded = true;
return SecureDataLoadStatus::Loaded;
}
SecureDataLoadStatus Module::LoadLocalFriendCodeSeedBFile() {
if (local_friend_code_seed_b_loaded) {
return SecureDataLoadStatus::Loaded;
}
std::string file_path = GetLocalFriendCodeSeedBPath();
if (!FileUtil::Exists(file_path)) {
return SecureDataLoadStatus::NotFound;
}
FileUtil::IOFile file(file_path, "rb");
if (!file.IsOpen()) {
return SecureDataLoadStatus::IOError;
}
if (file.GetSize() != sizeof(LocalFriendCodeSeedB)) {
return SecureDataLoadStatus::Invalid;
}
if (file.ReadBytes(&local_friend_code_seed_b, sizeof(LocalFriendCodeSeedB)) !=
sizeof(LocalFriendCodeSeedB)) {
return SecureDataLoadStatus::IOError;
}
local_friend_code_seed_b_loaded = true;
return SecureDataLoadStatus::Loaded;
}
void Module::LoadMCUConfig() {
FileUtil::IOFile mcu_data_file(
fmt::format("{}/mcu.dat", FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir)), "rb");
@ -976,6 +1106,8 @@ Module::Module(Core::System& system_) : system(system_) {
SetEULAVersion(default_version);
UpdateConfigNANDSavegame();
}
LoadSecureInfoAFile();
LoadLocalFriendCodeSeedBFile();
}
Module::~Module() = default;

View File

@ -181,6 +181,28 @@ enum class AccessFlag : u16 {
};
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 {
public:
Module(Core::System& system_);
@ -634,6 +656,35 @@ public:
*/
void SaveMacAddress();
/**
* Invalidates the loaded secure data so that it is loaded again.
*/
void InvalidateSecureData();
/**
* Loads the LocalFriendCodeSeed_B file from NAND.
* @returns LocalFriendCodeSeedBLoadStatus indicating the file load status.
*/
SecureDataLoadStatus LoadSecureInfoAFile();
/**
* Loads the LocalFriendCodeSeed_B file from NAND.
* @returns LocalFriendCodeSeedBLoadStatus indicating the file load status.
*/
SecureDataLoadStatus LoadLocalFriendCodeSeedBFile();
/**
* Gets the SecureInfo_A path in the host filesystem
* @returns std::string SecureInfo_A path in the host filesystem
*/
std::string GetSecureInfoAPath();
/**
* Gets the LocalFriendCodeSeed_B path in the host filesystem
* @returns std::string LocalFriendCodeSeed_B path in the host filesystem
*/
std::string GetLocalFriendCodeSeedBPath();
private:
void UpdatePreferredRegionCode();
SystemLanguage GetRawSystemLanguage();
@ -644,6 +695,10 @@ private:
std::array<u8, CONFIG_SAVEFILE_SIZE> cfg_config_file_buffer;
std::unique_ptr<FileSys::ArchiveBackend> cfg_system_save_data_archive;
u32 preferred_region_code = 0;
bool secure_info_a_loaded = false;
SecureInfoA secure_info_a;
bool local_friend_code_seed_b_loaded = false;
LocalFriendCodeSeedB local_friend_code_seed_b;
bool preferred_region_chosen = false;
MCUData mcu_data{};
std::string mac_address{};

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
// get dumped. Generated normal keys are also not accessible on a 3DS. The used formula for
// calculating the constant is a software implementation of what the hardware generator does.
AESKey generator_constant;
//AESKey generator_constant;
constexpr AESKey generator_constant = {{0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45,
0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A}};
AESKey HexToKey(const std::string& hex) {
if (hex.size() < 32) {
@ -143,6 +146,78 @@ struct KeyDesc {
bool same_as_before;
};
void LoadBootromKeys() {
constexpr std::array<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() {
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
@ -305,6 +486,8 @@ void InitKeys(bool force) {
return;
}
initialized = true;
HW::RSA::InitSlots();
LoadBootromKeys();
LoadPresetKeys();
movable_key.SetKeyX(key_slots[0x35].x);
movable_cmac.SetKeyX(key_slots[0x35].x);

View File

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

View File

@ -21,6 +21,19 @@
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;
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() {
static bool initialized = false;
if (initialized)
@ -193,6 +223,42 @@ void InitSlots() {
}
}
}
#endif
//--
void InitSlots() {
static bool initialized = false;
if (initialized)
return;
initialized = true;
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
FileUtil::IOFile file(filepath, "rb");
if (!file) {
return;
}
const std::size_t length = file.GetSize();
if (length != 65536) {
LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length);
return;
}
constexpr std::size_t RSA_MODULUS_POS = 0xB3E0;
file.Seek(RSA_MODULUS_POS, SEEK_SET);
std::vector<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;
const RsaSlot& GetSlot(std::size_t slot_id) {
@ -201,6 +267,31 @@ const RsaSlot& GetSlot(std::size_t slot_id) {
return rsa_slots[slot_id];
}
std::vector<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() {
return ticket_wrap_slot;
}

View File

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

View File

@ -26,6 +26,23 @@ public:
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port,
ArticInitMode init_mode);
Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port)
: AppLoader(system_, FileUtil::IOFile()) {
client = std::make_shared<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;
/**

View File

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