From 322721811441e5735c64a8821e4771d6872a0fb7 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sat, 8 Sep 2018 12:33:27 -0600 Subject: [PATCH] Add XCI, NSP and NCA loading support (#404) * Add XCI and NCA loading support * Code style changes * Add NSP loading * Changes from code review * Read XCIs with patches. Code style * Add KEYS.md file * Make file extension matching case-insensitive --- KEYS.md | 104 +++++++++++++++ Ryujinx.HLE/HOS/Horizon.cs | 215 +++++++++++++++++++++++++++++++ Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + Ryujinx.HLE/Switch.cs | 15 +++ Ryujinx.HLE/VirtualFileSystem.cs | 6 + Ryujinx/Ui/Program.cs | 22 +++- 6 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 KEYS.md diff --git a/KEYS.md b/KEYS.md new file mode 100644 index 00000000..a2867ddc --- /dev/null +++ b/KEYS.md @@ -0,0 +1,104 @@ +# Keys + +Keys are required for decrypting most of the file formats used by the Nintendo Switch. + +Keysets are stored as text files. These 3 filenames are automatically read: +`prod.keys` - Contains common keys usedy by all Switch devices. +`console.keys` - Contains console-unique keys. +`title.keys` - Contains game-specific keys. + +Ryujinx will first look for keys in `RyuFS/system`, and if it doesn't find any there it will look in `$HOME/.switch`. + +A guide to assist with dumping your own keys can be found [here](https://gist.github.com/roblabla/d8358ab058bbe3b00614740dcba4f208). + +## Common keys + +Here is a template for a key file containing the main keys Ryujinx uses to read content files. +Both `prod.keys` and `console.keys` use this format. + +``` +master_key_00 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +master_key_01 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +master_key_02 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +master_key_03 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +master_key_04 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +master_key_05 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +titlekek_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +key_area_key_application_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +key_area_key_ocean_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +key_area_key_system_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +aes_kek_generation_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +aes_key_generation_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +header_kek_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +header_key_source = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +## Title keys + +Title keys are stored in the format `rights_id,key`. + +For example: + +``` +01000000000100000000000000000003,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +01000000000108000000000000000003,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +01000000000108000000000000000004,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +## Complete key list +Below is a complete list of keys that are currently recognized. +\## represents a hexadecimal number between 00 and 1F +@@ represents a hexadecimal number between 00 and 03 + +### Common keys + +``` +master_key_source +keyblob_mac_key_source +package2_key_source +aes_kek_generation_source +aes_key_generation_source +key_area_key_application_source +key_area_key_ocean_source +key_area_key_system_source +titlekek_source +header_kek_source +header_key_source +sd_card_kek_source +sd_card_nca_key_source +sd_card_save_key_source +retail_specific_aes_key_source +per_console_key_source +bis_kek_source +bis_key_source_@@ + +header_key +xci_header_key +eticket_rsa_kek + +master_key_## +package1_key_## +package2_key_## +titlekek_## +key_area_key_application_## +key_area_key_ocean_## +key_area_key_system_## +keyblob_key_source_## +keyblob_## +``` + +### Console-unique keys + +``` +secure_boot_key +tsec_key +device_key +bis_key_@@ + +keyblob_key_## +keyblob_mac_key_## +encrypted_keyblob_## + +sd_seed +``` diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index a5579675..92a87661 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -1,3 +1,4 @@ +using LibHac; using Ryujinx.HLE.HOS.Font; using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.SystemState; @@ -7,6 +8,7 @@ using Ryujinx.HLE.Logging; using System; using System.Collections.Concurrent; using System.IO; +using System.Linq; namespace Ryujinx.HLE.HOS { @@ -30,6 +32,8 @@ namespace Ryujinx.HLE.HOS internal KEvent VsyncEvent { get; private set; } + internal Keyset KeySet { get; private set; } + public Horizon(Switch Device) { this.Device = Device; @@ -52,6 +56,8 @@ namespace Ryujinx.HLE.HOS Font = new SharedFontManager(Device, FontSharedMem.PA); VsyncEvent = new KEvent(); + + LoadKeySet(); } public void LoadCart(string ExeFsDir, string RomFsFile = null) @@ -119,6 +125,179 @@ namespace Ryujinx.HLE.HOS MainProcess.Run(); } + public void LoadXci(string XciFile) + { + FileStream File = new FileStream(XciFile, FileMode.Open, FileAccess.Read); + + Xci Xci = new Xci(KeySet, File); + + Nca Nca = GetXciMainNca(Xci); + + if (Nca == null) + { + Device.Log.PrintError(LogClass.Loader, "Unable to load XCI"); + + return; + } + + LoadNca(Nca); + } + + private Nca GetXciMainNca(Xci Xci) + { + if (Xci.SecurePartition == null) + { + throw new InvalidDataException("Could not find XCI secure partition"); + } + + Nca MainNca = null; + Nca PatchNca = null; + + foreach (PfsFileEntry FileEntry in Xci.SecurePartition.Files.Where(x => x.Name.EndsWith(".nca"))) + { + Stream NcaStream = Xci.SecurePartition.OpenFile(FileEntry); + + Nca Nca = new Nca(KeySet, NcaStream, true); + + if (Nca.Header.ContentType == ContentType.Program) + { + if (Nca.Sections.Any(x => x?.Type == SectionType.Romfs)) + { + MainNca = Nca; + } + else if (Nca.Sections.Any(x => x?.Type == SectionType.Bktr)) + { + PatchNca = Nca; + } + } + } + + if (MainNca == null) + { + Device.Log.PrintError(LogClass.Loader, "Could not find an Application NCA in the provided XCI file"); + } + + MainNca.SetBaseNca(PatchNca); + + return MainNca; + } + + public void LoadNca(string NcaFile) + { + FileStream File = new FileStream(NcaFile, FileMode.Open, FileAccess.Read); + + Nca Nca = new Nca(KeySet, File, true); + + LoadNca(Nca); + } + + public void LoadNsp(string NspFile) + { + FileStream File = new FileStream(NspFile, FileMode.Open, FileAccess.Read); + + Pfs Nsp = new Pfs(File); + + PfsFileEntry TicketFile = Nsp.Files.FirstOrDefault(x => x.Name.EndsWith(".tik")); + + // Load title key from the NSP's ticket in case the user doesn't have a title key file + if (TicketFile != null) + { + // todo Change when Ticket(Stream) overload is added + Ticket Ticket = new Ticket(new BinaryReader(Nsp.OpenFile(TicketFile))); + + KeySet.TitleKeys[Ticket.RightsId] = Ticket.GetTitleKey(KeySet); + } + + foreach (PfsFileEntry NcaFile in Nsp.Files.Where(x => x.Name.EndsWith(".nca"))) + { + Nca Nca = new Nca(KeySet, Nsp.OpenFile(NcaFile), true); + + if (Nca.Header.ContentType == ContentType.Program) + { + LoadNca(Nca); + + return; + } + } + + Device.Log.PrintError(LogClass.Loader, "Could not find an Application NCA in the provided NSP file"); + } + + public void LoadNca(Nca Nca) + { + NcaSection RomfsSection = Nca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs); + NcaSection ExefsSection = Nca.Sections.FirstOrDefault(x => x?.IsExefs == true); + + if (ExefsSection == null) + { + Device.Log.PrintError(LogClass.Loader, "No ExeFS found in NCA"); + + return; + } + + if (RomfsSection == null) + { + Device.Log.PrintError(LogClass.Loader, "No RomFS found in NCA"); + + return; + } + + Stream RomfsStream = Nca.OpenSection(RomfsSection.SectionNum, false); + Device.FileSystem.SetRomFs(RomfsStream); + + Stream ExefsStream = Nca.OpenSection(ExefsSection.SectionNum, false); + Pfs Exefs = new Pfs(ExefsStream); + + Npdm MetaData = null; + + if (Exefs.FileExists("main.npdm")) + { + Device.Log.PrintInfo(LogClass.Loader, "Loading main.npdm..."); + + MetaData = new Npdm(Exefs.OpenFile("main.npdm")); + } + else + { + Device.Log.PrintWarning(LogClass.Loader, $"NPDM file not found, using default values!"); + } + + Process MainProcess = MakeProcess(MetaData); + + void LoadNso(string Filename) + { + foreach (PfsFileEntry File in Exefs.Files.Where(x => x.Name.StartsWith(Filename))) + { + if (Path.GetExtension(File.Name) != string.Empty) + { + continue; + } + + Device.Log.PrintInfo(LogClass.Loader, $"Loading {Filename}..."); + + string Name = Path.GetFileNameWithoutExtension(File.Name); + + Nso Program = new Nso(Exefs.OpenFile(File), Name); + + MainProcess.LoadProgram(Program); + } + } + + if (!MainProcess.MetaData.Is64Bits) + { + throw new NotImplementedException("32-bit titles are unsupported!"); + } + + LoadNso("rtld"); + + MainProcess.SetEmptyArgs(); + + LoadNso("main"); + LoadNso("subsdk"); + LoadNso("sdk"); + + MainProcess.Run(); + } + public void LoadProgram(string FilePath) { bool IsNro = Path.GetExtension(FilePath).ToLower() == ".nro"; @@ -156,6 +335,42 @@ namespace Ryujinx.HLE.HOS MainProcess.Run(IsNro); } + public void LoadKeySet() + { + string KeyFile = null; + string TitleKeyFile = null; + string ConsoleKeyFile = null; + + string Home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + LoadSetAtPath(Path.Combine(Home, ".switch")); + LoadSetAtPath(Device.FileSystem.GetSystemPath()); + + KeySet = ExternalKeys.ReadKeyFile(KeyFile, TitleKeyFile, ConsoleKeyFile); + + void LoadSetAtPath(string BasePath) + { + string LocalKeyFile = Path.Combine(BasePath, "prod.keys"); + string LocalTitleKeyFile = Path.Combine(BasePath, "title.keys"); + string LocalConsoleKeyFile = Path.Combine(BasePath, "console.keys"); + + if (File.Exists(LocalKeyFile)) + { + KeyFile = LocalKeyFile; + } + + if (File.Exists(LocalTitleKeyFile)) + { + TitleKeyFile = LocalTitleKeyFile; + } + + if (File.Exists(LocalConsoleKeyFile)) + { + ConsoleKeyFile = LocalConsoleKeyFile; + } + } + } + public void SignalVsync() => VsyncEvent.WaitEvent.Set(); private Process MakeProcess(Npdm MetaData = null) diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj index fa4c254e..cd1bb034 100644 --- a/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -25,6 +25,7 @@ + diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index 32d5bef6..090aae11 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -61,6 +61,21 @@ namespace Ryujinx.HLE System.LoadCart(ExeFsDir, RomFsFile); } + public void LoadXci(string XciFile) + { + System.LoadXci(XciFile); + } + + public void LoadNca(string NcaFile) + { + System.LoadNca(NcaFile); + } + + public void LoadNsp(string NspFile) + { + System.LoadNsp(NspFile); + } + public void LoadProgram(string FileName) { System.LoadProgram(FileName); diff --git a/Ryujinx.HLE/VirtualFileSystem.cs b/Ryujinx.HLE/VirtualFileSystem.cs index 31b8e184..133538f9 100644 --- a/Ryujinx.HLE/VirtualFileSystem.cs +++ b/Ryujinx.HLE/VirtualFileSystem.cs @@ -17,6 +17,12 @@ namespace Ryujinx.HLE RomFs = new FileStream(FileName, FileMode.Open, FileAccess.Read); } + public void SetRomFs(Stream RomfsStream) + { + RomFs?.Close(); + RomFs = RomfsStream; + } + public string GetFullPath(string BasePath, string FileName) { if (FileName.StartsWith("//")) diff --git a/Ryujinx/Ui/Program.cs b/Ryujinx/Ui/Program.cs index fdbc59a5..053cf1be 100644 --- a/Ryujinx/Ui/Program.cs +++ b/Ryujinx/Ui/Program.cs @@ -50,9 +50,25 @@ namespace Ryujinx } else if (File.Exists(args[0])) { - Console.WriteLine("Loading as homebrew."); - - Device.LoadProgram(args[0]); + switch (Path.GetExtension(args[0]).ToLowerInvariant()) + { + case ".xci": + Console.WriteLine("Loading as XCI."); + Device.LoadXci(args[0]); + break; + case ".nca": + Console.WriteLine("Loading as NCA."); + Device.LoadNca(args[0]); + break; + case ".nsp": + Console.WriteLine("Loading as NSP."); + Device.LoadNsp(args[0]); + break; + default: + Console.WriteLine("Loading as homebrew."); + Device.LoadProgram(args[0]); + break; + } } } else