mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2024-11-14 10:49:41 +01:00
Read save files from SD card
This commit is contained in:
parent
01f5ed7ccf
commit
49a1f0b5a4
6 changed files with 89 additions and 24 deletions
|
@ -21,11 +21,10 @@ namespace LibHac
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
KeepOpen = keepOpen;
|
KeepOpen = keepOpen;
|
||||||
ReadHeader(stream);
|
ReadHeader(stream);
|
||||||
DeriveKeys(keyset, sdPath);
|
DeriveKeys(keyset, sdPath, stream);
|
||||||
ValidateKeys(keyset, stream);
|
|
||||||
|
|
||||||
stream.Position = 0x4000;
|
stream.Position = 0x4000;
|
||||||
var xts = XtsAes128.Create(Keys[0], Keys[1]);
|
Xts xts = XtsAes128.Create(Keys[0], Keys[1]);
|
||||||
Stream = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x4000, 0x4000), keepOpen);
|
Stream = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x4000, 0x4000), keepOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,38 +43,45 @@ namespace LibHac
|
||||||
Length = reader.ReadInt64();
|
Length = reader.ReadInt64();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeriveKeys(Keyset keyset, string sdPath)
|
private void DeriveKeys(Keyset keyset, string sdPath, Stream stream)
|
||||||
{
|
{
|
||||||
|
stream.Position = 0x20;
|
||||||
|
var validationHashKey = new byte[0x60];
|
||||||
|
stream.Read(validationHashKey, 0, 0x60);
|
||||||
|
|
||||||
|
// Try both the NCA and save key sources and pick the one that works
|
||||||
for (int k = 0; k < 2; k++)
|
for (int k = 0; k < 2; k++)
|
||||||
{
|
{
|
||||||
var naxSpecificKeys = Util.CreateJaggedArray<byte[][]>(2, 0x10);
|
var naxSpecificKeys = Util.CreateJaggedArray<byte[][]>(2, 0x10);
|
||||||
var hashKey = new byte[0x10];
|
var hashKey = new byte[0x10];
|
||||||
Array.Copy(keyset.sd_card_keys[k], hashKey, 0x10);
|
Array.Copy(keyset.sd_card_keys[k], hashKey, 0x10);
|
||||||
|
|
||||||
|
// Use the sd path to generate the kek for this NAX0
|
||||||
var hash = new HMACSHA256(hashKey);
|
var hash = new HMACSHA256(hashKey);
|
||||||
var sdPathBytes = Encoding.ASCII.GetBytes(sdPath);
|
byte[] sdPathBytes = Encoding.ASCII.GetBytes(sdPath);
|
||||||
var checksum = hash.ComputeHash(sdPathBytes, 0, sdPathBytes.Length);
|
byte[] checksum = hash.ComputeHash(sdPathBytes, 0, sdPathBytes.Length);
|
||||||
Array.Copy(checksum, 0, naxSpecificKeys[0], 0, 0x10);
|
Array.Copy(checksum, 0, naxSpecificKeys[0], 0, 0x10);
|
||||||
Array.Copy(checksum, 0x10, naxSpecificKeys[1], 0, 0x10);
|
Array.Copy(checksum, 0x10, naxSpecificKeys[1], 0, 0x10);
|
||||||
|
|
||||||
|
// Decrypt this NAX0's keys
|
||||||
Crypto.DecryptEcb(naxSpecificKeys[0], EncKeys[0], Keys[0], 0x10);
|
Crypto.DecryptEcb(naxSpecificKeys[0], EncKeys[0], Keys[0], 0x10);
|
||||||
Crypto.DecryptEcb(naxSpecificKeys[1], EncKeys[1], Keys[1], 0x10);
|
Crypto.DecryptEcb(naxSpecificKeys[1], EncKeys[1], Keys[1], 0x10);
|
||||||
|
|
||||||
|
// Copy the decrypted keys into the NAX0 header and use that for the HMAC key
|
||||||
|
// for validating that the keys are correct
|
||||||
|
Array.Copy(Keys[0], 0, validationHashKey, 8, 0x10);
|
||||||
|
Array.Copy(Keys[1], 0, validationHashKey, 0x18, 0x10);
|
||||||
|
|
||||||
|
var validationHash = new HMACSHA256(validationHashKey);
|
||||||
|
byte[] validationMac = validationHash.ComputeHash(keyset.sd_card_keys[k], 0x10, 0x10);
|
||||||
|
|
||||||
|
if (Util.ArraysEqual(Hmac, validationMac))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void ValidateKeys(Keyset keyset, Stream stream)
|
throw new ArgumentException("NAX0 key derivation failed.");
|
||||||
{
|
|
||||||
stream.Position = 0x20;
|
|
||||||
var hashKey = new byte[0x60];
|
|
||||||
stream.Read(hashKey, 0, 0x60);
|
|
||||||
Array.Copy(Keys[0], 0, hashKey, 8, 0x10);
|
|
||||||
Array.Copy(Keys[1], 0, hashKey, 0x18, 0x10);
|
|
||||||
|
|
||||||
var hash = new HMACSHA256(hashKey);
|
|
||||||
var validationMac = hash.ComputeHash(keyset.sd_card_keys[1], 0x10, 0x10);
|
|
||||||
var isValid = Util.ArraysEqual(Hmac, validationMac);
|
|
||||||
|
|
||||||
if (!isValid) throw new ArgumentException("NAX0 key derivation failed.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -13,8 +13,10 @@ namespace LibHac
|
||||||
public Keyset Keyset { get; }
|
public Keyset Keyset { get; }
|
||||||
public IFileSystem Fs { get; }
|
public IFileSystem Fs { get; }
|
||||||
public string ContentsDir { get; }
|
public string ContentsDir { get; }
|
||||||
|
public string SaveDir { get; }
|
||||||
|
|
||||||
public Dictionary<string, Nca> Ncas { get; } = new Dictionary<string, Nca>(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, Nca> Ncas { get; } = new Dictionary<string, Nca>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public Dictionary<string, Savefile.Savefile> Saves { get; } = new Dictionary<string, Savefile.Savefile>(StringComparer.OrdinalIgnoreCase);
|
||||||
public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>();
|
public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>();
|
||||||
public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>();
|
public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>();
|
||||||
|
|
||||||
|
@ -26,10 +28,19 @@ namespace LibHac
|
||||||
if (fs.DirectoryExists("Nintendo"))
|
if (fs.DirectoryExists("Nintendo"))
|
||||||
{
|
{
|
||||||
ContentsDir = fs.GetFullPath(Path.Combine("Nintendo", "Contents"));
|
ContentsDir = fs.GetFullPath(Path.Combine("Nintendo", "Contents"));
|
||||||
|
SaveDir = fs.GetFullPath(Path.Combine("Nintendo", "save"));
|
||||||
}
|
}
|
||||||
else if (fs.DirectoryExists("Contents"))
|
else
|
||||||
{
|
{
|
||||||
ContentsDir = fs.GetFullPath("Contents");
|
if (fs.DirectoryExists("Contents"))
|
||||||
|
{
|
||||||
|
ContentsDir = fs.GetFullPath("Contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.DirectoryExists("save"))
|
||||||
|
{
|
||||||
|
SaveDir = fs.GetFullPath("save");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ContentsDir == null)
|
if (ContentsDir == null)
|
||||||
|
@ -37,6 +48,7 @@ namespace LibHac
|
||||||
throw new DirectoryNotFoundException("Could not find \"Contents\" directory");
|
throw new DirectoryNotFoundException("Could not find \"Contents\" directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OpenAllSaves();
|
||||||
OpenAllNcas();
|
OpenAllNcas();
|
||||||
ReadTitles();
|
ReadTitles();
|
||||||
ReadControls();
|
ReadControls();
|
||||||
|
@ -87,6 +99,35 @@ namespace LibHac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenAllSaves()
|
||||||
|
{
|
||||||
|
string[] files = Fs.GetFileSystemEntries(SaveDir, "*");
|
||||||
|
|
||||||
|
foreach (string file in files)
|
||||||
|
{
|
||||||
|
Savefile.Savefile save = null;
|
||||||
|
string saveName = Path.GetFileNameWithoutExtension(file);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Stream stream = Fs.OpenFile(file, FileMode.Open);
|
||||||
|
|
||||||
|
string sdPath = "/" + Util.GetRelativePath(file, SaveDir).Replace('\\', '/');
|
||||||
|
var nax0 = new Nax0(Keyset, stream, sdPath, false);
|
||||||
|
save = new Savefile.Savefile(nax0.Stream);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{ex.Message} {file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save != null && saveName != null)
|
||||||
|
{
|
||||||
|
Saves[saveName] = save;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ReadTitles()
|
private void ReadTitles()
|
||||||
{
|
{
|
||||||
foreach (var nca in Ncas.Values.Where(x => x.Header.ContentType == ContentType.Meta))
|
foreach (var nca in Ncas.Values.Where(x => x.Header.ContentType == ContentType.Meta))
|
||||||
|
@ -126,7 +167,7 @@ namespace LibHac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Titles.Add(title.Id, title);
|
Titles[title.Id] = title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +177,7 @@ namespace LibHac
|
||||||
{
|
{
|
||||||
var romfs = new Romfs(title.ControlNca.OpenSection(0, false));
|
var romfs = new Romfs(title.ControlNca.OpenSection(0, false));
|
||||||
var control = romfs.GetFile("/control.nacp");
|
var control = romfs.GetFile("/control.nacp");
|
||||||
|
|
||||||
var reader = new BinaryReader(new MemoryStream(control));
|
var reader = new BinaryReader(new MemoryStream(control));
|
||||||
title.Control = new Nacp(reader);
|
title.Control = new Nacp(reader);
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ Switch FS options:
|
||||||
--exefsdir <dir> Specify ExeFS directory path. (--title must be specified)
|
--exefsdir <dir> Specify ExeFS directory path. (--title must be specified)
|
||||||
--romfs <file> Specify RomFS directory path. (--title must be specified)
|
--romfs <file> Specify RomFS directory path. (--title must be specified)
|
||||||
--romfsdir <dir> Specify RomFS directory path. (--title must be specified)
|
--romfsdir <dir> Specify RomFS directory path. (--title must be specified)
|
||||||
|
--savedir <dir> Specify save file directory path.
|
||||||
Savefile options:
|
Savefile options:
|
||||||
--outdir <dir> Specify directory path to save contents to.
|
--outdir <dir> Specify directory path to save contents to.
|
||||||
--debugoutdir <dir> Specify directory path to save intermediate data to for debugging.
|
--debugoutdir <dir> Specify directory path to save intermediate data to for debugging.
|
||||||
|
|
|
@ -29,6 +29,7 @@ namespace hactoolnet
|
||||||
new CliOption("romfs", 1, (o, a) => o.RomfsOut = a[0]),
|
new CliOption("romfs", 1, (o, a) => o.RomfsOut = a[0]),
|
||||||
new CliOption("romfsdir", 1, (o, a) => o.RomfsOutDir = a[0]),
|
new CliOption("romfsdir", 1, (o, a) => o.RomfsOutDir = a[0]),
|
||||||
new CliOption("debugoutdir", 1, (o, a) => o.DebugOutDir = a[0]),
|
new CliOption("debugoutdir", 1, (o, a) => o.DebugOutDir = a[0]),
|
||||||
|
new CliOption("savedir", 1, (o, a) => o.SaveOutDir = a[0]),
|
||||||
new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]),
|
new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]),
|
||||||
new CliOption("nspout", 1, (o, a) => o.NspOut = a[0]),
|
new CliOption("nspout", 1, (o, a) => o.NspOut = a[0]),
|
||||||
new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]),
|
new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]),
|
||||||
|
@ -184,6 +185,7 @@ namespace hactoolnet
|
||||||
sb.AppendLine(" --exefsdir <dir> Specify ExeFS directory path. (--title must be specified)");
|
sb.AppendLine(" --exefsdir <dir> Specify ExeFS directory path. (--title must be specified)");
|
||||||
sb.AppendLine(" --romfs <file> Specify RomFS directory path. (--title must be specified)");
|
sb.AppendLine(" --romfs <file> Specify RomFS directory path. (--title must be specified)");
|
||||||
sb.AppendLine(" --romfsdir <dir> Specify RomFS directory path. (--title must be specified)");
|
sb.AppendLine(" --romfsdir <dir> Specify RomFS directory path. (--title must be specified)");
|
||||||
|
sb.AppendLine(" --savedir <dir> Specify save file directory path.");
|
||||||
sb.AppendLine("Savefile options:");
|
sb.AppendLine("Savefile options:");
|
||||||
sb.AppendLine(" --outdir <dir> Specify directory path to save contents to.");
|
sb.AppendLine(" --outdir <dir> Specify directory path to save contents to.");
|
||||||
sb.AppendLine(" --debugoutdir <dir> Specify directory path to save intermediate data to for debugging.");
|
sb.AppendLine(" --debugoutdir <dir> Specify directory path to save intermediate data to for debugging.");
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace hactoolnet
|
||||||
public string RomfsOut;
|
public string RomfsOut;
|
||||||
public string RomfsOutDir;
|
public string RomfsOutDir;
|
||||||
public string DebugOutDir;
|
public string DebugOutDir;
|
||||||
|
public string SaveOutDir;
|
||||||
public string OutDir;
|
public string OutDir;
|
||||||
public string SdSeed;
|
public string SdSeed;
|
||||||
public string NspOut;
|
public string NspOut;
|
||||||
|
|
|
@ -256,6 +256,11 @@ namespace hactoolnet
|
||||||
{
|
{
|
||||||
CreateNsp(ctx, switchFs);
|
CreateNsp(ctx, switchFs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx.Options.SaveOutDir != null)
|
||||||
|
{
|
||||||
|
ExportSdSaves(ctx, switchFs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ProcessXci(Context ctx)
|
private static void ProcessXci(Context ctx)
|
||||||
|
@ -650,6 +655,15 @@ namespace hactoolnet
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ExportSdSaves(Context ctx, SwitchFs switchFs)
|
||||||
|
{
|
||||||
|
foreach (var save in switchFs.Saves)
|
||||||
|
{
|
||||||
|
var outDir = Path.Combine(ctx.Options.SaveOutDir, save.Key);
|
||||||
|
save.Value.Extract(outDir, ctx.Logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue