Read save files from SD card

This commit is contained in:
Alex Barney 2018-09-10 22:09:35 -05:00
parent 01f5ed7ccf
commit 49a1f0b5a4
6 changed files with 89 additions and 24 deletions

View file

@ -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()

View file

@ -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;
} }
} }

View file

@ -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.

View file

@ -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.");

View file

@ -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;

View file

@ -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);
}
}
} }
} }