diff --git a/LibHac.sln b/LibHac.sln index f6dfb9cd..1fcc39f3 100644 --- a/LibHac.sln +++ b/LibHac.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NandReader", "NandReader\Na EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NandReaderGui", "NandReaderGui\NandReaderGui.csproj", "{3CBD38B0-6575-4768-8E94-A8AF2D2C9F43}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net", "Net\Net.csproj", "{3398A7B0-E962-4D3D-9B64-0D5F90E56F16}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,6 +39,11 @@ Global {9889C467-284F-4061-B4DB-EC94051C29C0}.Release|Any CPU.Build.0 = Release|Any CPU {3CBD38B0-6575-4768-8E94-A8AF2D2C9F43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3CBD38B0-6575-4768-8E94-A8AF2D2C9F43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CBD38B0-6575-4768-8E94-A8AF2D2C9F43}.Release|Any CPU.Build.0 = Release|Any CPU + {3398A7B0-E962-4D3D-9B64-0D5F90E56F16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3398A7B0-E962-4D3D-9B64-0D5F90E56F16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3398A7B0-E962-4D3D-9B64-0D5F90E56F16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3398A7B0-E962-4D3D-9B64-0D5F90E56F16}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Net/CliParser.cs b/Net/CliParser.cs new file mode 100644 index 00000000..eaeb9440 --- /dev/null +++ b/Net/CliParser.cs @@ -0,0 +1,130 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Net +{ + internal static class CliParser + { + private static readonly CliOption[] CliOptions = + { + new CliOption("keyset", 'k', 1, (o, a) => o.Keyfile = a[0]), + new CliOption("titlekeys", 1, (o, a) => o.TitleKeyFile = a[0]), + new CliOption("consolekeys", 1, (o, a) => o.ConsoleKeyFile = a[0]), + new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])), + new CliOption("version", 1, (o, a) => o.Version = ParseVersion(a[0])), + new CliOption("did", 1, (o, a) => o.DeviceId = ParseTitleId(a[0])), + new CliOption("cert", 1, (o, a) => o.CertFile = a[0]) + }; + + public static Options Parse(string[] args) + { + var options = new Options(); + + for (int i = 0; i < args.Length; i++) + { + string arg; + + if (args[i].Length == 2 && (args[i][0] == '-' || args[i][0] == '/')) + { + arg = args[i][1].ToString().ToLower(); + } + else if (args[i].Length > 2 && args[i].Substring(0, 2) == "--") + { + arg = args[i].Substring(2).ToLower(); + } + else + { + PrintWithUsage($"Unable to parse option {args[i]}"); + return null; + } + + var option = CliOptions.FirstOrDefault(x => x.Long == arg || x.Short == arg); + if (option == null) + { + PrintWithUsage($"Unknown option {args[i]}"); + return null; + } + + if (i + option.ArgsNeeded >= args.Length) + { + PrintWithUsage($"Need {option.ArgsNeeded} parameter{(option.ArgsNeeded == 1 ? "" : "s")} after {args[i]}"); + return null; + } + + var optionArgs = new string[option.ArgsNeeded]; + Array.Copy(args, i + 1, optionArgs, 0, option.ArgsNeeded); + + option.Assigner(options, optionArgs); + i += option.ArgsNeeded; + } + + + return options; + } + + private static ulong ParseTitleId(string input) + { + if (input.Length != 16) + { + PrintWithUsage("Title ID must be 16 hex characters long"); + } + + if (!ulong.TryParse(input, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var id)) + { + PrintWithUsage("Could not parse title ID"); + } + + return id; + } + + private static int ParseVersion(string input) + { + if (!int.TryParse(input, out var version)) + { + PrintWithUsage("Could not parse version"); + } + + return version; + } + + internal static void PrintWithUsage(string toPrint) + { + Console.WriteLine(toPrint); + Console.WriteLine(GetUsage()); + // PrintUsage(); + } + + private static string GetUsage() + { + var sb = new StringBuilder(); + + sb.AppendLine("Usage: Don't"); + + return sb.ToString(); + } + + private class CliOption + { + public CliOption(string longName, char shortName, int argsNeeded, Action assigner) + { + Long = longName; + Short = shortName.ToString(); + ArgsNeeded = argsNeeded; + Assigner = assigner; + } + public CliOption(string longName, int argsNeeded, Action assigner) + { + Long = longName; + ArgsNeeded = argsNeeded; + Assigner = assigner; + } + + public string Long { get; } + public string Short { get; } + public int ArgsNeeded { get; } + public Action Assigner { get; } + } + } +} diff --git a/Net/Net.csproj b/Net/Net.csproj new file mode 100644 index 00000000..48fc5f7e --- /dev/null +++ b/Net/Net.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp2.1;net45 + 7.3 + + + + + + + diff --git a/Net/NetContext.cs b/Net/NetContext.cs new file mode 100644 index 00000000..79596183 --- /dev/null +++ b/Net/NetContext.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using libhac; + +namespace Net +{ + internal class NetContext + { + private X509Certificate2 Certificate { get; set; } + private string Eid { get; } = "lp1"; + private ulong Did { get; } + private string Firmware { get; } = "5.1.0-3.0"; + private string CachePath { get; } = "titles"; + private Context ToolCtx { get; } + + public NetContext(Context ctx) + { + ToolCtx = ctx; + Did = ctx.Options.DeviceId; + if (ctx.Options.CertFile != null) + { + SetCertificate(ctx.Options.CertFile); + } + } + + public void SetCertificate(string filename) + { + Certificate = new X509Certificate2(filename, "switch"); + } + + public Cnmt GetCnmt(ulong titleId, int version) + { + using (var stream = GetCnmtFile(titleId, version)) + { + var nca = new Nca(ToolCtx.Keyset, stream, true); + Stream sect = nca.OpenSection(0, false); + var pfs0 = new Pfs0(sect); + var file = pfs0.GetFile(0); + + var cnmt = new Cnmt(new MemoryStream(file)); + return cnmt; + } + } + + public Stream GetCnmtFile(ulong titleId, int version) + { + var cnmt = GetCnmtFileFromCache(titleId, version); + if (cnmt != null) return cnmt; + + if (Certificate == null) return null; + + DownloadCnmt(titleId, version); + return GetCnmtFileFromCache(titleId, version); + } + + public Stream GetCnmtFileFromCache(ulong titleId, int version) + { + string titleDir = GetTitleDir(titleId, version); + var cnmtFiles = Directory.GetFiles(titleDir, "*.cnmt.nca").ToArray(); + + if (cnmtFiles.Length == 1) + { + return new FileStream(cnmtFiles[0], FileMode.Open, FileAccess.Read, FileShare.Read); + } + + if (cnmtFiles.Length > 1) + { + throw new FileNotFoundException($"More than cnmt file exists for {titleId:x16}v{version}"); + } + + return null; + } + + private void DownloadCnmt(ulong titleId, int version) + { + var titleDir = GetTitleDir(titleId, version); + + var ncaId = GetMetadataNcaId(titleId, version); + var filename = $"{ncaId}.cnmt.nca"; + var filePath = Path.Combine(titleDir, filename); + DownloadFile(GetContentUrl(ncaId), filePath); + } + + public void DownloadFile(string url, string filePath) + { + var response = Request("GET", url); + using (var responseStream = response.GetResponseStream()) + using (var outStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite)) + { + var dir = Path.GetDirectoryName(filePath) ?? throw new DirectoryNotFoundException(); + Directory.CreateDirectory(dir); + responseStream.CopyStream(outStream, response.ContentLength, ToolCtx.Logger); + } + } + + private string GetTitleDir(ulong titleId, int version) + { + var titleDir = Path.Combine(CachePath, $"{titleId:x16}", $"{version}"); + Directory.CreateDirectory(titleDir); + return titleDir; + } + + public string GetContentUrl(string ncaId) + { + string url = $"{GetAtumUrl()}/c/a/{ncaId}"; + return url; + } + + public string GetMetadataNcaId(ulong titleId, int version) + { + string url = $"{GetAtumUrl()}/t/a/{titleId:x16}/{version}?deviceid={Did}"; + + using (WebResponse response = Request("HEAD", url)) + { + return response.Headers.Get("X-Nintendo-Content-ID"); + } + } + + private string GetAtumUrl() + { + return $"https://atum.hac.{Eid}.d4c.nintendo.net"; + } + + public WebResponse Request(string method, string url) + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.ClientCertificates.Add(Certificate); + request.UserAgent = string.Format("NintendoSDK Firmware/{0} (platform:NX; did:{1}; eid:{2})", Firmware, Did, Eid); + request.Method = method; + ServicePointManager.ServerCertificateValidationCallback = ((sender, certificate, chain, sslPolicyErrors) => true); + if (((HttpWebResponse)request.GetResponse()).StatusCode != HttpStatusCode.OK) { Console.WriteLine("http error"); return null; } + return request.GetResponse(); + } + } +} diff --git a/Net/Options.cs b/Net/Options.cs new file mode 100644 index 00000000..1ba2ce3d --- /dev/null +++ b/Net/Options.cs @@ -0,0 +1,22 @@ +using libhac; + +namespace Net +{ + internal class Options + { + public string Keyfile; + public string TitleKeyFile; + public string ConsoleKeyFile; + public ulong TitleId; + public int Version; + public ulong DeviceId; + public string CertFile; + } + + internal class Context + { + public Options Options; + public Keyset Keyset; + public IProgressReport Logger; + } +} diff --git a/Net/Program.cs b/Net/Program.cs new file mode 100644 index 00000000..a950e335 --- /dev/null +++ b/Net/Program.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Text; +using libhac; + +namespace Net +{ + public static class Program + { + public static void Main(string[] args) + { + Console.OutputEncoding = Encoding.UTF8; + var ctx = new Context(); + ctx.Options = CliParser.Parse(args); + if (ctx.Options == null) return; + + using (var logger = new ProgressBar()) + { + ctx.Logger = logger; + OpenKeyset(ctx); + ProcessNet(ctx); + } + } + + private static void ProcessNet(Context ctx) + { + if (ctx.Options.DeviceId == 0) + { + CliParser.PrintWithUsage("A non-zero Device ID must be set."); + return; + } + + var net = new NetContext(ctx); + var cnmt = net.GetCnmt(ctx.Options.TitleId, ctx.Options.Version); + foreach (var entry in cnmt.ContentEntries) + { + Console.WriteLine($"{entry.NcaId.ToHexString()} {entry.Type}"); + } + } + + private static void OpenKeyset(Context ctx) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var homeKeyFile = Path.Combine(home, ".switch", "prod.keys"); + var homeTitleKeyFile = Path.Combine(home, ".switch", "title.keys"); + var homeConsoleKeyFile = Path.Combine(home, ".switch", "console.keys"); + var keyFile = ctx.Options.Keyfile; + var titleKeyFile = ctx.Options.TitleKeyFile; + var consoleKeyFile = ctx.Options.ConsoleKeyFile; + + if (keyFile == null && File.Exists(homeKeyFile)) + { + keyFile = homeKeyFile; + } + + if (titleKeyFile == null && File.Exists(homeTitleKeyFile)) + { + titleKeyFile = homeTitleKeyFile; + } + + if (consoleKeyFile == null && File.Exists(homeConsoleKeyFile)) + { + consoleKeyFile = homeConsoleKeyFile; + } + + ctx.Keyset = ExternalKeys.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile, ctx.Logger); + } + } +}