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
This commit is contained in:
parent
ce1d5be212
commit
3227218114
6 changed files with 360 additions and 3 deletions
104
KEYS.md
Normal file
104
KEYS.md
Normal file
|
@ -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
|
||||||
|
```
|
|
@ -1,3 +1,4 @@
|
||||||
|
using LibHac;
|
||||||
using Ryujinx.HLE.HOS.Font;
|
using Ryujinx.HLE.HOS.Font;
|
||||||
using Ryujinx.HLE.HOS.Kernel;
|
using Ryujinx.HLE.HOS.Kernel;
|
||||||
using Ryujinx.HLE.HOS.SystemState;
|
using Ryujinx.HLE.HOS.SystemState;
|
||||||
|
@ -7,6 +8,7 @@ using Ryujinx.HLE.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS
|
namespace Ryujinx.HLE.HOS
|
||||||
{
|
{
|
||||||
|
@ -30,6 +32,8 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
internal KEvent VsyncEvent { get; private set; }
|
internal KEvent VsyncEvent { get; private set; }
|
||||||
|
|
||||||
|
internal Keyset KeySet { get; private set; }
|
||||||
|
|
||||||
public Horizon(Switch Device)
|
public Horizon(Switch Device)
|
||||||
{
|
{
|
||||||
this.Device = Device;
|
this.Device = Device;
|
||||||
|
@ -52,6 +56,8 @@ namespace Ryujinx.HLE.HOS
|
||||||
Font = new SharedFontManager(Device, FontSharedMem.PA);
|
Font = new SharedFontManager(Device, FontSharedMem.PA);
|
||||||
|
|
||||||
VsyncEvent = new KEvent();
|
VsyncEvent = new KEvent();
|
||||||
|
|
||||||
|
LoadKeySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadCart(string ExeFsDir, string RomFsFile = null)
|
public void LoadCart(string ExeFsDir, string RomFsFile = null)
|
||||||
|
@ -119,6 +125,179 @@ namespace Ryujinx.HLE.HOS
|
||||||
MainProcess.Run();
|
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)
|
public void LoadProgram(string FilePath)
|
||||||
{
|
{
|
||||||
bool IsNro = Path.GetExtension(FilePath).ToLower() == ".nro";
|
bool IsNro = Path.GetExtension(FilePath).ToLower() == ".nro";
|
||||||
|
@ -156,6 +335,42 @@ namespace Ryujinx.HLE.HOS
|
||||||
MainProcess.Run(IsNro);
|
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();
|
public void SignalVsync() => VsyncEvent.WaitEvent.Set();
|
||||||
|
|
||||||
private Process MakeProcess(Npdm MetaData = null)
|
private Process MakeProcess(Npdm MetaData = null)
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
<ProjectReference Include="..\ChocolArm64\ChocolArm64.csproj" />
|
<ProjectReference Include="..\ChocolArm64\ChocolArm64.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Graphics\Ryujinx.Graphics.csproj" />
|
<ProjectReference Include="..\Ryujinx.Graphics\Ryujinx.Graphics.csproj" />
|
||||||
|
<PackageReference Include="LibHac" Version="0.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -61,6 +61,21 @@ namespace Ryujinx.HLE
|
||||||
System.LoadCart(ExeFsDir, RomFsFile);
|
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)
|
public void LoadProgram(string FileName)
|
||||||
{
|
{
|
||||||
System.LoadProgram(FileName);
|
System.LoadProgram(FileName);
|
||||||
|
|
|
@ -17,6 +17,12 @@ namespace Ryujinx.HLE
|
||||||
RomFs = new FileStream(FileName, FileMode.Open, FileAccess.Read);
|
RomFs = new FileStream(FileName, FileMode.Open, FileAccess.Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetRomFs(Stream RomfsStream)
|
||||||
|
{
|
||||||
|
RomFs?.Close();
|
||||||
|
RomFs = RomfsStream;
|
||||||
|
}
|
||||||
|
|
||||||
public string GetFullPath(string BasePath, string FileName)
|
public string GetFullPath(string BasePath, string FileName)
|
||||||
{
|
{
|
||||||
if (FileName.StartsWith("//"))
|
if (FileName.StartsWith("//"))
|
||||||
|
|
|
@ -50,9 +50,25 @@ namespace Ryujinx
|
||||||
}
|
}
|
||||||
else if (File.Exists(args[0]))
|
else if (File.Exists(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.");
|
Console.WriteLine("Loading as homebrew.");
|
||||||
|
|
||||||
Device.LoadProgram(args[0]);
|
Device.LoadProgram(args[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
Reference in a new issue