diff --git a/LibHac/Nax0.cs b/LibHac/Nax0.cs index aa9154ea..f561d196 100644 --- a/LibHac/Nax0.cs +++ b/LibHac/Nax0.cs @@ -21,11 +21,10 @@ namespace LibHac stream.Position = 0; KeepOpen = keepOpen; ReadHeader(stream); - DeriveKeys(keyset, sdPath); - ValidateKeys(keyset, stream); + DeriveKeys(keyset, sdPath, stream); 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); } @@ -44,38 +43,45 @@ namespace LibHac 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++) { var naxSpecificKeys = Util.CreateJaggedArray(2, 0x10); var hashKey = new byte[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 sdPathBytes = Encoding.ASCII.GetBytes(sdPath); - var checksum = hash.ComputeHash(sdPathBytes, 0, sdPathBytes.Length); + byte[] sdPathBytes = Encoding.ASCII.GetBytes(sdPath); + byte[] checksum = hash.ComputeHash(sdPathBytes, 0, sdPathBytes.Length); Array.Copy(checksum, 0, naxSpecificKeys[0], 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[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) - { - 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."); + throw new ArgumentException("NAX0 key derivation failed."); } public void Dispose() diff --git a/LibHac/SwitchFs.cs b/LibHac/SwitchFs.cs index 931518e5..4b35d3cf 100644 --- a/LibHac/SwitchFs.cs +++ b/LibHac/SwitchFs.cs @@ -13,8 +13,10 @@ namespace LibHac public Keyset Keyset { get; } public IFileSystem Fs { get; } public string ContentsDir { get; } + public string SaveDir { get; } public Dictionary Ncas { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Saves { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Titles { get; } = new Dictionary(); public Dictionary Applications { get; } = new Dictionary(); @@ -26,10 +28,19 @@ namespace LibHac if (fs.DirectoryExists("Nintendo")) { 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) @@ -37,6 +48,7 @@ namespace LibHac throw new DirectoryNotFoundException("Could not find \"Contents\" directory"); } + OpenAllSaves(); OpenAllNcas(); ReadTitles(); 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() { 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 control = romfs.GetFile("/control.nacp"); - + var reader = new BinaryReader(new MemoryStream(control)); title.Control = new Nacp(reader); diff --git a/README.md b/README.md index 5ad4d0c6..d612cf2d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Switch FS options: --exefsdir Specify ExeFS directory path. (--title must be specified) --romfs Specify RomFS directory path. (--title must be specified) --romfsdir Specify RomFS directory path. (--title must be specified) + --savedir Specify save file directory path. Savefile options: --outdir Specify directory path to save contents to. --debugoutdir Specify directory path to save intermediate data to for debugging. diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index 75b327b8..15654aaf 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -29,6 +29,7 @@ namespace hactoolnet new CliOption("romfs", 1, (o, a) => o.RomfsOut = a[0]), new CliOption("romfsdir", 1, (o, a) => o.RomfsOutDir = 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("nspout", 1, (o, a) => o.NspOut = a[0]), new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), @@ -184,6 +185,7 @@ namespace hactoolnet sb.AppendLine(" --exefsdir Specify ExeFS directory path. (--title must be specified)"); sb.AppendLine(" --romfs Specify RomFS directory path. (--title must be specified)"); sb.AppendLine(" --romfsdir Specify RomFS directory path. (--title must be specified)"); + sb.AppendLine(" --savedir Specify save file directory path."); sb.AppendLine("Savefile options:"); sb.AppendLine(" --outdir Specify directory path to save contents to."); sb.AppendLine(" --debugoutdir Specify directory path to save intermediate data to for debugging."); diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index e34f498e..f3ba3f67 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -19,6 +19,7 @@ namespace hactoolnet public string RomfsOut; public string RomfsOutDir; public string DebugOutDir; + public string SaveOutDir; public string OutDir; public string SdSeed; public string NspOut; diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 567c376c..72cfaa60 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -256,6 +256,11 @@ namespace hactoolnet { CreateNsp(ctx, switchFs); } + + if (ctx.Options.SaveOutDir != null) + { + ExportSdSaves(ctx, switchFs); + } } private static void ProcessXci(Context ctx) @@ -650,6 +655,15 @@ namespace hactoolnet 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); + } + } } }