Jelajahi Sumber

Added Tool for installing keys (#233)

#232 

![image](https://github.com/user-attachments/assets/5ae6118d-3857-4005-8392-5398c8fa91d5)
Nicola 1 tahun lalu
induk
melakukan
346dfe9542

+ 134 - 0
src/Ryujinx.HLE/FileSystem/ContentManager.cs

@@ -21,6 +21,7 @@ using System.IO;
 using System.IO.Compression;
 using System.Linq;
 using System.Text;
+using System.Text.RegularExpressions;
 using Path = System.IO.Path;
 
 namespace Ryujinx.HLE.FileSystem
@@ -474,6 +475,74 @@ namespace Ryujinx.HLE.FileSystem
             FinishInstallation(temporaryDirectory, registeredDirectory);
         }
 
+        public void InstallKeys(string keysSource, string installDirectory)
+        {
+            if (Directory.Exists(keysSource))
+            {
+                foreach (var filePath in Directory.EnumerateFiles(keysSource, "*.keys"))
+                {
+                    VerifyKeysFile(filePath);
+                    File.Copy(filePath, Path.Combine(installDirectory, Path.GetFileName(filePath)), true);
+                }
+
+                return;
+            }
+
+            if (!File.Exists(keysSource))
+            {
+                throw new FileNotFoundException("Keys file does not exist.");
+            }
+
+            FileInfo info = new(keysSource);
+
+            using FileStream file = File.OpenRead(keysSource);
+
+            switch (info.Extension)
+            {
+                case ".zip":
+                    using (ZipArchive archive = ZipFile.OpenRead(keysSource))
+                    {
+                        InstallKeysFromZip(archive, installDirectory);
+                    }
+                    break;
+                case ".keys":
+                    VerifyKeysFile(keysSource);
+                    File.Copy(keysSource, Path.Combine(installDirectory, info.Name), true);
+                    break;
+                default:
+                    throw new InvalidFirmwarePackageException("Input file is not a valid key package");
+            }
+        }
+
+        private void InstallKeysFromZip(ZipArchive archive, string installDirectory)
+        {
+            string temporaryDirectory = Path.Combine(installDirectory, "temp");
+            if (Directory.Exists(temporaryDirectory))
+            {
+                Directory.Delete(temporaryDirectory, true);
+            }
+            Directory.CreateDirectory(temporaryDirectory);
+            foreach (var entry in archive.Entries)
+            {
+                if (Path.GetExtension(entry.FullName).Equals(".keys", StringComparison.OrdinalIgnoreCase))
+                {
+                    string extractDestination = Path.Combine(temporaryDirectory, entry.Name);
+                    entry.ExtractToFile(extractDestination, overwrite: true);
+                    try
+                    {
+                        VerifyKeysFile(extractDestination);
+                        File.Move(extractDestination, Path.Combine(installDirectory, entry.Name), true);
+                    }
+                    catch (Exception)
+                    {
+                        Directory.Delete(temporaryDirectory, true);
+                        throw;
+                    }
+                }
+            }
+            Directory.Delete(temporaryDirectory, true);
+        }
+
         private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
         {
             if (Directory.Exists(registeredDirectory))
@@ -947,5 +1016,70 @@ namespace Ryujinx.HLE.FileSystem
 
             return null;
         }
+
+        public void VerifyKeysFile(string filePath)
+        {
+            // Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md
+            string genericPattern = @"^[a-z0-9_]+ = [a-z0-9]+$";
+            string titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$";
+
+            if (File.Exists(filePath))
+            {
+                // Read all lines from the file
+                string fileName = Path.GetFileName(filePath);
+                string[] lines = File.ReadAllLines(filePath);
+
+                bool verified = false;
+                switch (fileName)
+                {
+                    case "prod.keys":
+                        verified = verifyKeys(lines, genericPattern);
+                        break;
+                    case "title.keys":
+                        verified = verifyKeys(lines, titlePattern);
+                        break;
+                    case "console.keys":
+                        verified = verifyKeys(lines, genericPattern);
+                        break;
+                    case "dev.keys":
+                        verified = verifyKeys(lines, genericPattern);
+                        break;
+                    default:
+                        throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.");
+                }
+                if (!verified)
+                {
+                    throw new FormatException($"Invalid \"{filePath}\" file format.");
+                }
+            } else
+            {
+                throw new FileNotFoundException($"Keys file not found at \"{filePath}\".");
+            }
+        }
+
+        private bool verifyKeys(string[] lines, string regex)
+        {
+            foreach (string line in lines)
+            {
+                if (!Regex.IsMatch(line, regex))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public bool AreKeysAlredyPresent(string pathToCheck)
+        {
+            string[] fileNames = { "prod.keys", "title.keys", "console.keys", "dev.keys" };
+            foreach (var file in fileNames)
+            {
+                if (File.Exists(Path.Combine(pathToCheck, file)))
+                {
+                    return true;                    
+                }
+            }
+            return false;
+        }
     }
 }

+ 12 - 5
src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs

@@ -223,9 +223,10 @@ namespace Ryujinx.HLE.FileSystem
         {
             KeySet ??= KeySet.CreateDefaultKeySet();
 
-            string keyFile = null;
+            string prodKeyFile = null;
             string titleKeyFile = null;
             string consoleKeyFile = null;
+            string devKeyFile = null;
 
             if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
             {
@@ -236,13 +237,14 @@ namespace Ryujinx.HLE.FileSystem
 
             void LoadSetAtPath(string basePath)
             {
-                string localKeyFile = Path.Combine(basePath, "prod.keys");
+                string localProdKeyFile = Path.Combine(basePath, "prod.keys");
                 string localTitleKeyFile = Path.Combine(basePath, "title.keys");
                 string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
+                string localDevKeyFile = Path.Combine(basePath, "dev.keys");
 
-                if (File.Exists(localKeyFile))
+                if (File.Exists(localProdKeyFile))
                 {
-                    keyFile = localKeyFile;
+                    prodKeyFile = localProdKeyFile;
                 }
 
                 if (File.Exists(localTitleKeyFile))
@@ -254,9 +256,14 @@ namespace Ryujinx.HLE.FileSystem
                 {
                     consoleKeyFile = localConsoleKeyFile;
                 }
+
+                if (File.Exists(localDevKeyFile))
+                {
+                    devKeyFile = localDevKeyFile;
+                }
             }
 
-            ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null);
+            ExternalKeyReader.ReadKeyFile(KeySet, prodKeyFile, devKeyFile, titleKeyFile, consoleKeyFile, null);
         }
 
         public void ImportTickets(IFileSystem fs)

+ 10 - 0
src/Ryujinx/Assets/Locales/ar_SA.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت",
   "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "تثبيت برنامج ثابت من مجلد",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "إدارة أنواع الملفات",
   "MenuBarToolsInstallFileTypes": "تثبيت أنواع الملفات",
   "MenuBarToolsUninstallFileTypes": "إزالة أنواع الملفات",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\nهل تريد المتابعة؟",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "تثبيت البرنامج الثابت...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "تم تثبيت إصدار النظام {0} بنجاح.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد",
   "DialogUserProfileDeletionConfirmMessage": "هل تريد حذف الملف الشخصي المحدد",
   "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير محفوظة",

+ 10 - 0
src/Ryujinx/Assets/Locales/de_DE.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Firmware installieren",
   "MenuBarFileToolsInstallFirmwareFromFile": "Firmware von einer XCI- oder einer ZIP-Datei installieren",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Firmware aus einem Verzeichnis installieren",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Dateitypen verwalten",
   "MenuBarToolsInstallFileTypes": "Dateitypen installieren",
   "MenuBarToolsUninstallFileTypes": "Dateitypen deinstallieren",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nMöchtest du fortfahren?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware wird installiert...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Systemversion {0} wurde erfolgreich installiert.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.",
   "DialogUserProfileDeletionConfirmMessage": "Möchtest du das ausgewählte Profil löschen?",
   "DialogUserProfileUnsavedChangesTitle": "Warnung - Nicht gespeicherte Änderungen",

+ 10 - 0
src/Ryujinx/Assets/Locales/el_GR.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware",
   "MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Εγκατάσταση Firmware από τοποθεσία",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Διαχείριση τύπων αρχείων",
   "MenuBarToolsInstallFileTypes": "Εγκαταστήσετε τύπους αρχείων.",
   "MenuBarToolsUninstallFileTypes": "Απεγκαταστήσετε τύπους αρχείων",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nΘέλετε να συνεχίσετε;",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Εγκατάσταση Firmware...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Η έκδοση συστήματος {0} εγκαταστάθηκε με επιτυχία.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο",
   "DialogUserProfileDeletionConfirmMessage": "Θέλετε να διαγράψετε το επιλεγμένο προφίλ",
   "DialogUserProfileUnsavedChangesTitle": "Προσοχή - Μην Αποθηκευμένες Αλλαγές.",

+ 10 - 0
src/Ryujinx/Assets/Locales/en_US.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Install Firmware",
   "MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Install a firmware from a directory",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Manage file types",
   "MenuBarToolsInstallFileTypes": "Install file types",
   "MenuBarToolsUninstallFileTypes": "Uninstall file types",
@@ -518,6 +521,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDo you want to continue?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installing firmware...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted",
   "DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile",
   "DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes",

+ 10 - 0
src/Ryujinx/Assets/Locales/es_ES.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Instalar firmware",
   "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware desde una carpeta",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Administrar tipos de archivo",
   "MenuBarToolsInstallFileTypes": "Instalar tipos de archivo",
   "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n¿Continuar?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versión de sistema {0} instalada con éxito.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Si eliminas el perfil seleccionado no quedará ningún otro perfil",
   "DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?",
   "DialogUserProfileUnsavedChangesTitle": "Advertencia - Cambios sin guardar",

+ 10 - 0
src/Ryujinx/Assets/Locales/fr_FR.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Installer un firmware",
   "MenuBarFileToolsInstallFirmwareFromFile": "Installer un firmware depuis un fichier XCI ou ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Installer un firmware depuis un dossier",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Gérer les types de fichiers",
   "MenuBarToolsInstallFileTypes": "Installer les types de fichiers",
   "MenuBarToolsUninstallFileTypes": "Désinstaller les types de fichiers",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nVoulez-vous continuer ?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installation du firmware...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Version du système {0} installée avec succès.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Il n'y aurait aucun autre profil à ouvrir si le profil sélectionné est supprimé",
   "DialogUserProfileDeletionConfirmMessage": "Voulez-vous supprimer le profil sélectionné ?",
   "DialogUserProfileUnsavedChangesTitle": "Avertissement - Modifications non enregistrées",

+ 10 - 0
src/Ryujinx/Assets/Locales/he_IL.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "התקן קושחה",
   "MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "התקן קושחה מתוך תקייה",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "ניהול סוגי קבצים",
   "MenuBarToolsInstallFileTypes": "סוגי קבצי התקנה",
   "MenuBarToolsUninstallFileTypes": "סוגי קבצי הסרה",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nהאם ברצונך להמשיך?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "מתקין קושחה...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "גרסת המערכת {0} הותקנה בהצלחה.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "לא יהיו פרופילים אחרים שייפתחו אם הפרופיל שנבחר יימחק",
   "DialogUserProfileDeletionConfirmMessage": "האם ברצונך למחוק את הפרופיל שנבחר",
   "DialogUserProfileUnsavedChangesTitle": "אזהרה - שינויים לא שמורים",

+ 10 - 0
src/Ryujinx/Assets/Locales/it_IT.json

@@ -28,6 +28,9 @@
   "MenuBarToolsInstallFirmware": "Installa firmware",
   "MenuBarFileToolsInstallFirmwareFromFile": "Installa un firmware da file XCI o ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Installa un firmare da una cartella",
+  "MenuBarToolsInstallKeys": "Installa Chiavi",
+  "MenuBarFileToolsInstallKeysFromFile": "Installa Chiavi da file KEYS o ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Installa Chiavi da una Cartella",
   "MenuBarToolsManageFileTypes": "Gestisci i tipi di file",
   "MenuBarToolsInstallFileTypes": "Installa i tipi di file",
   "MenuBarToolsUninstallFileTypes": "Disinstalla i tipi di file",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nVuoi continuare?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installazione del firmware...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "La versione del sistema {0} è stata installata.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "E' stato trovato un file di chiavi invalido ' {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Installa Chavi",
+  "DialogKeysInstallerKeysInstallMessage": "Un nuovo file di Chiavi sarà intallato.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nQuesto potrebbe sovrascrivere alcune delle Chiavi già installate.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nVuoi continuare?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installando le chiavi...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "Nuovo file di chiavi installato con successo.",
   "DialogUserProfileDeletionWarningMessage": "Non ci sarebbero altri profili da aprire se il profilo selezionato viene cancellato",
   "DialogUserProfileDeletionConfirmMessage": "Vuoi eliminare il profilo selezionato?",
   "DialogUserProfileUnsavedChangesTitle": "Attenzione - Modifiche Non Salvate",

+ 10 - 0
src/Ryujinx/Assets/Locales/ja_JP.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "ファームウェアをインストール",
   "MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "ディレクトリからファームウェアをインストール",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "ファイル形式を管理",
   "MenuBarToolsInstallFileTypes": "ファイル形式をインストール",
   "MenuBarToolsUninstallFileTypes": "ファイル形式をアンインストール",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n続けてよろしいですか?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "ファームウェアをインストール中...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "システムバージョン {0} が正常にインストールされました.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "選択されたプロファイルを削除すると,プロファイルがひとつも存在しなくなります",
   "DialogUserProfileDeletionConfirmMessage": "選択されたプロファイルを削除しますか",
   "DialogUserProfileUnsavedChangesTitle": "警告 - 保存されていない変更",

+ 10 - 0
src/Ryujinx/Assets/Locales/ko_KR.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "펌웨어 설치",
   "MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP으로 펌웨어 설치",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "디렉터리에서 펌웨어 설치",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "파일 형식 관리",
   "MenuBarToolsInstallFileTypes": "파일 형식 설치",
   "MenuBarToolsUninstallFileTypes": "파일 형식 제거",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n계속하시겠습니까?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "펌웨어 설치 중...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "시스템 버전 {0}이(가) 설치되었습니다.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "선택한 프로필을 삭제하면 다른 프로필을 열 수 없음",
   "DialogUserProfileDeletionConfirmMessage": "선택한 프로필을 삭제하시겠습니까?",
   "DialogUserProfileUnsavedChangesTitle": "경고 - 저장되지 않은 변경 사항",

+ 10 - 0
src/Ryujinx/Assets/Locales/pl_PL.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Zainstaluj oprogramowanie",
   "MenuBarFileToolsInstallFirmwareFromFile": "Zainstaluj oprogramowanie z XCI lub ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Zainstaluj oprogramowanie z katalogu",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Zarządzaj rodzajami plików",
   "MenuBarToolsInstallFileTypes": "Typy plików instalacyjnych",
   "MenuBarToolsUninstallFileTypes": "Typy plików dezinstalacyjnych",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nCzy chcesz kontynuować?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalowanie firmware'u...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Wersja systemu {0} została pomyślnie zainstalowana.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Nie będzie innych profili do otwarcia, jeśli wybrany profil zostanie usunięty",
   "DialogUserProfileDeletionConfirmMessage": "Czy chcesz usunąć wybrany profil",
   "DialogUserProfileUnsavedChangesTitle": "Uwaga - Niezapisane zmiany",

+ 10 - 0
src/Ryujinx/Assets/Locales/pt_BR.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "_Instalar firmware",
   "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware a partir de um arquivo ZIP/XCI",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware a partir de um diretório",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Gerenciar tipos de arquivo",
   "MenuBarToolsInstallFileTypes": "Instalar tipos de arquivo",
   "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de arquivos",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDeseja continuar?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versão do sistema {0} instalada com sucesso.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Não haveria nenhum perfil selecionado se o perfil atual fosse deletado",
   "DialogUserProfileDeletionConfirmMessage": "Deseja deletar o perfil selecionado",
   "DialogUserProfileUnsavedChangesTitle": "Alerta - Alterações não salvas",

+ 10 - 0
src/Ryujinx/Assets/Locales/ru_RU.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Установка прошивки",
   "MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Установить прошивку из папки",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Управление типами файлов",
   "MenuBarToolsInstallFileTypes": "Установить типы файлов",
   "MenuBarToolsUninstallFileTypes": "Удалить типы файлов",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nПродолжить?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Установка прошивки...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Прошивка версии {0} успешно установлена.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Если выбранный профиль будет удален, другие профили не будут открываться.",
   "DialogUserProfileDeletionConfirmMessage": "Удалить выбранный профиль?",
   "DialogUserProfileUnsavedChangesTitle": "Внимание - Несохраненные изменения",

+ 10 - 0
src/Ryujinx/Assets/Locales/th_TH.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์",
   "MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "ติดตั้งเฟิร์มแวร์จากไดเร็กทอรี",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "จัดการประเภทไฟล์",
   "MenuBarToolsInstallFileTypes": "ติดตั้งประเภทไฟล์",
   "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งประเภทไฟล์",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nคุณต้องการดำเนินการต่อหรือไม่?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "กำลังติดตั้งเฟิร์มแวร์...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "ระบบเวอร์ชั่น {0} ติดตั้งเรียบร้อยแล้ว",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "จะไม่มีโปรไฟล์อื่นให้เปิดหากโปรไฟล์ที่เลือกถูกลบ",
   "DialogUserProfileDeletionConfirmMessage": "คุณต้องการลบโปรไฟล์ที่เลือกหรือไม่?",
   "DialogUserProfileUnsavedChangesTitle": "คำเตือน - มีการเปลี่ยนแปลงที่ไม่ได้บันทึก",

+ 10 - 0
src/Ryujinx/Assets/Locales/tr_TR.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Yazılım Yükle",
   "MenuBarFileToolsInstallFirmwareFromFile": "XCI veya ZIP'ten Yazılım Yükle",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Bir Dizin Üzerinden Yazılım Yükle",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Dosya uzantılarını yönet",
   "MenuBarToolsInstallFileTypes": "Dosya uzantılarını yükle",
   "MenuBarToolsUninstallFileTypes": "Dosya uzantılarını kaldır",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDevam etmek istiyor musunuz?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware yükleniyor...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Sistem sürümü {0} başarıyla yüklendi.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Seçilen profil silinirse kullanılabilen başka profil kalmayacak",
   "DialogUserProfileDeletionConfirmMessage": "Seçilen profili silmek istiyor musunuz",
   "DialogUserProfileUnsavedChangesTitle": "Uyarı - Kaydedilmemiş Değişiklikler",

+ 10 - 0
src/Ryujinx/Assets/Locales/uk_UA.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "Установити прошивку",
   "MenuBarFileToolsInstallFirmwareFromFile": "Установити прошивку з XCI або ZIP",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "Установити прошивку з теки",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "Керувати типами файлів",
   "MenuBarToolsInstallFileTypes": "Установити типи файлів",
   "MenuBarToolsUninstallFileTypes": "Видалити типи файлів",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nВи хочете продовжити?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Встановлення прошивки...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Версію системи {0} успішно встановлено.",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться",
   "DialogUserProfileDeletionConfirmMessage": "Ви хочете видалити вибраний профіль",
   "DialogUserProfileUnsavedChangesTitle": "Увага — Незбережені зміни",

+ 10 - 0
src/Ryujinx/Assets/Locales/zh_CN.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "安装系统固件",
   "MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹中安装系统固件",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "管理文件扩展名",
   "MenuBarToolsInstallFileTypes": "关联文件扩展名",
   "MenuBarToolsUninstallFileTypes": "取消关联扩展名",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n是否继续?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "安装系统固件中...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安装系统固件版本 {0} 。",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "删除后将没有可用的账户",
   "DialogUserProfileDeletionConfirmMessage": "是否删除所选账户",
   "DialogUserProfileUnsavedChangesTitle": "警告 - 有未保存的更改",

+ 10 - 0
src/Ryujinx/Assets/Locales/zh_TW.json

@@ -31,6 +31,9 @@
   "MenuBarToolsInstallFirmware": "安裝韌體",
   "MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
   "MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體",
+  "MenuBarToolsInstallKeys": "Install Keys",
+  "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
+  "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
   "MenuBarToolsManageFileTypes": "管理檔案類型",
   "MenuBarToolsInstallFileTypes": "安裝檔案類型",
   "MenuBarToolsUninstallFileTypes": "移除檔案類型",
@@ -506,6 +509,13 @@
   "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n您確定要繼續嗎?",
   "DialogFirmwareInstallerFirmwareInstallWaitMessage": "正在安裝韌體...",
   "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本 {0}。",
+  "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
+  "DialogKeysInstallerKeysInstallTitle": "Install Keys",
+  "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
+  "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
+  "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
+  "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
+  "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
   "DialogUserProfileDeletionWarningMessage": "如果刪除選取的設定檔,將無法開啟其他設定檔",
   "DialogUserProfileDeletionConfirmMessage": "您是否要刪除所選設定檔",
   "DialogUserProfileUnsavedChangesTitle": "警告 - 未儲存的變更",

+ 149 - 0
src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs

@@ -1271,6 +1271,108 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
+        private async Task HandleKeysInstallation(string filename)
+        {
+            try
+            {
+                string systemDirectory = AppDataManager.KeysDirPath;
+                if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && Directory.Exists(AppDataManager.KeysDirPathUser))
+                {
+                    systemDirectory = AppDataManager.KeysDirPathUser;
+                }
+
+                string dialogTitle = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallTitle);
+                string dialogMessage = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
+
+                bool alreadyKesyInstalled = ContentManager.AreKeysAlredyPresent(systemDirectory);
+                if (alreadyKesyInstalled)
+                {
+                    dialogMessage += LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallSubMessage);
+                }
+
+                dialogMessage += LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallConfirmMessage];
+
+                UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
+                    dialogTitle,
+                    dialogMessage,
+                    LocaleManager.Instance[LocaleKeys.InputDialogYes],
+                    LocaleManager.Instance[LocaleKeys.InputDialogNo],
+                    LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
+
+                UpdateWaitWindow waitingDialog = new(dialogTitle, LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallWaitMessage]);
+
+                if (result == UserResult.Yes)
+                {
+                    Logger.Info?.Print(LogClass.Application, $"Installing Keys");
+
+                    Thread thread = new(() =>
+                    {
+                        Dispatcher.UIThread.InvokeAsync(delegate
+                        {
+                            waitingDialog.Show();
+                        });
+
+                        try
+                        {
+                            ContentManager.InstallKeys(filename, systemDirectory);
+
+                            Dispatcher.UIThread.InvokeAsync(async delegate
+                            {
+                                waitingDialog.Close();
+
+                                string message = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallSuccessMessage);
+
+                                await ContentDialogHelper.CreateInfoDialog(
+                                    dialogTitle,
+                                    message,
+                                    LocaleManager.Instance[LocaleKeys.InputDialogOk],
+                                    string.Empty,
+                                    LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
+
+                                Logger.Info?.Print(LogClass.Application, message);
+                            });
+                        }
+                        catch (Exception ex)
+                        {
+                            Dispatcher.UIThread.InvokeAsync(async () =>
+                            {
+                                waitingDialog.Close();
+
+                                string message = ex.Message;
+                                if(ex is FormatException)
+                                {
+                                    message = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, filename);
+                                }
+
+                                await ContentDialogHelper.CreateErrorDialog(message);
+                            });
+                        }
+                        finally
+                        {
+                            VirtualFileSystem.ReloadKeySet();
+                        }
+                    })
+                    {
+                        Name = "GUI.KeysInstallerThread",
+                    };
+
+                    thread.Start();
+                }
+            }
+            catch (MissingKeyException ex)
+            {
+                if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
+                {
+                    Logger.Error?.Print(LogClass.Application, ex.ToString());
+
+                    await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys);
+                }
+            }
+            catch (Exception ex)
+            {
+                await ContentDialogHelper.CreateErrorDialog(ex.Message);
+            }
+        }
         private void ProgressHandler<T>(T state, int current, int total) where T : Enum
         {
             Dispatcher.UIThread.Post(() =>
@@ -1559,6 +1661,53 @@ namespace Ryujinx.Ava.UI.ViewModels
             }
         }
 
+        public async Task InstallKeysFromFile()
+        {
+            var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+            {
+                AllowMultiple = false,
+                FileTypeFilter = new List<FilePickerFileType>
+                {
+                    new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes])
+                    {
+                        Patterns = new[] { "*.keys", "*.zip" },
+                        AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci", "public.zip-archive" },
+                        MimeTypes = new[] { "application/keys", "application/zip" },
+                    },
+                    new("KEYS")
+                    {
+                        Patterns = new[] { "*.keys" },
+                        AppleUniformTypeIdentifiers = new[] { "com.ryujinx.xci" },
+                        MimeTypes = new[] { "application/keys" },
+                    },
+                    new("ZIP")
+                    {
+                        Patterns = new[] { "*.zip" },
+                        AppleUniformTypeIdentifiers = new[] { "public.zip-archive" },
+                        MimeTypes = new[] { "application/zip" },
+                    },
+                },
+            });
+
+            if (result.Count > 0)
+            {
+                await HandleKeysInstallation(result[0].Path.LocalPath);
+            }
+        }
+
+        public async Task InstallKeysFromFolder()
+        {
+            var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+            {
+                AllowMultiple = false,
+            });
+
+            if (result.Count > 0)
+            {
+                await HandleKeysInstallation(result[0].Path.LocalPath);
+            }
+        }
+
         public void OpenRyujinxFolder()
         {
             OpenHelper.OpenFolder(AppDataManager.BaseDirPath);

+ 4 - 0
src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml

@@ -260,6 +260,10 @@
                     IsEnabled="{Binding IsGameRunning}" />
             </MenuItem>
             <MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarTools}">
+                <MenuItem Header="{ext:Locale MenuBarToolsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}" IsEnabled="{Binding EnableNonGameRunningControls}">
+                    <MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBarFileToolsInstallKeysFromFile}" Icon="{ext:Icon mdi-file-cog}" />
+                    <MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallKeysFromFolder}" Icon="{ext:Icon mdi-folder-cog}" />
+                </MenuItem>
                 <MenuItem Header="{ext:Locale MenuBarToolsInstallFirmware}" Icon="{ext:Icon fa-solid fa-download}" IsEnabled="{Binding EnableNonGameRunningControls}">
                     <MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromFile}" Icon="{ext:Icon mdi-file-cog}" />
                     <MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />