From 300fa877e4d0e37b49151ed6d69a664d1439a656 Mon Sep 17 00:00:00 2001 From: "[mRg]" <221695+emargee@users.noreply.github.com> Date: Thu, 16 Nov 2023 02:22:17 +0000 Subject: [PATCH] Allow user to supply a titlekey on the command-line when processing NCAs (#290) * feat: Enable user to supply a single titlekey when processing NCAs * chore: update README * feat: add `basetitlekey` support * chore: README * fix: check hasnt already been added via title.keys * fix: Update logic so commandline supplied keys override existing keys in file * hactoolnet: Parse title keys when reading CLI arguments --- README.md | 2 ++ src/hactoolnet/CliParser.cs | 18 +++++++++++++++++ src/hactoolnet/Options.cs | 2 ++ src/hactoolnet/ProcessNca.cs | 38 +++++++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fd499c0..df8a7387 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ NCA options: --romfsdir Specify RomFS directory path. --listromfs List files in RomFS. --basenca Set Base NCA to use with update partitions. + --basetitlekey Specify single (encrypted) titlekey for the base NCA. + --titlekey Specify single (encrypted) titlekey. KIP1 options: --uncompressed Specify file path for saving uncompressed KIP1. RomFS options: diff --git a/src/hactoolnet/CliParser.cs b/src/hactoolnet/CliParser.cs index 525ca19f..b18a7c9a 100644 --- a/src/hactoolnet/CliParser.cs +++ b/src/hactoolnet/CliParser.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using System.Text; +using LibHac.Util; namespace hactoolnet; @@ -49,6 +50,8 @@ internal static class CliParser new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]), new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), new CliOption("basenca", 1, (o, a) => o.BaseNca = a[0]), + new CliOption("basetitlekey", 1, (o, a) => o.BaseTitleKey = ParseTitleKey(o, a[0])), + new CliOption("titlekey", 1, (o, a) => o.TitleKey = ParseTitleKey(o, a[0])), new CliOption("basefile", 1, (o, a) => o.BaseFile = a[0]), new CliOption("rootdir", 1, (o, a) => o.RootDir = a[0]), new CliOption("updatedir", 1, (o, a) => o.UpdateDir = a[0]), @@ -214,6 +217,19 @@ internal static class CliParser return id; } + private static byte[] ParseTitleKey(Options options, string input) + { + byte[] key = new byte[32]; + + if (input.Length != 32 || !StringUtils.TryFromHexString(input, key)) + { + options.ParseErrorMessage ??= "TitleKey must be 32 hex characters long"; + return default; + } + + return key; + } + private static double ParseDouble(Options options, string input) { if (!double.TryParse(input, out double value)) @@ -276,6 +292,8 @@ internal static class CliParser sb.AppendLine(" --romfsdir Specify RomFS directory path."); sb.AppendLine(" --listromfs List files in RomFS."); sb.AppendLine(" --basenca Set Base NCA to use with update partitions."); + sb.AppendLine(" --basetitlekey Specify single (encrypted) titlekey for the base NCA."); + sb.AppendLine(" --titlekey Specify single (encrypted) titlekey for the NCA."); sb.AppendLine("KIP1 options:"); sb.AppendLine(" --uncompressed Specify file path for saving uncompressed KIP1."); sb.AppendLine("RomFS options:"); diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index 77d1a458..b3f55b4d 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -62,6 +62,8 @@ internal class Options public bool BuildHfs; public bool ExtractIni1; public ulong TitleId; + public byte[] TitleKey; + public byte[] BaseTitleKey; public string BenchType; public double CpuFrequencyGhz; diff --git a/src/hactoolnet/ProcessNca.cs b/src/hactoolnet/ProcessNca.cs index ffebeff0..8f2494b9 100644 --- a/src/hactoolnet/ProcessNca.cs +++ b/src/hactoolnet/ProcessNca.cs @@ -1,15 +1,19 @@ -using System.IO; +using System; +using System.IO; using System.Text; using LibHac; using LibHac.Common; +using LibHac.Common.Keys; using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.Fs.Impl; using LibHac.FsSystem; +using LibHac.Spl; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.Npdm; +using LibHac.Util; using static hactoolnet.Print; using NcaFsHeader = LibHac.Tools.FsSystem.NcaUtils.NcaFsHeader; @@ -24,6 +28,15 @@ internal static class ProcessNca var nca = new Nca(ctx.KeySet, file); Nca baseNca = null; + if (ctx.Options.TitleKey != null && nca.Header.HasRightsId) + { + if (!TryAddTitleKey(ctx.KeySet, ctx.Options.TitleKey, nca.Header.RightsId)) + { + ctx.Logger.LogMessage($"Invalid title key \"{ctx.Options.TitleKey}\""); + return; + } + } + var ncaHolder = new NcaHolder { Nca = nca }; if (ctx.Options.HeaderOut != null) @@ -38,6 +51,15 @@ internal static class ProcessNca { IStorage baseFile = new LocalStorage(ctx.Options.BaseNca, FileAccess.Read); baseNca = new Nca(ctx.KeySet, baseFile); + + if (ctx.Options.BaseTitleKey != null && baseNca.Header.HasRightsId) + { + if (!TryAddTitleKey(ctx.KeySet, ctx.Options.BaseTitleKey, baseNca.Header.RightsId)) + { + ctx.Logger.LogMessage($"Invalid base title key \"{ctx.Options.BaseTitleKey}\""); + return; + } + } } for (int i = 0; i < 4; i++) @@ -252,6 +274,20 @@ internal static class ProcessNca } } + private static bool TryAddTitleKey(KeySet keySet, ReadOnlySpan key, ReadOnlySpan rightsId) + { + if (key.Length != 32) + return false; + + var titleKey = new AccessKey(key); + var rId = new RightsId(rightsId); + + keySet.ExternalKeySet.Remove(rId); + keySet.ExternalKeySet.Add(rId, titleKey); + + return true; + } + private static Validity VerifySignature2(this Nca nca) { if (nca.Header.ContentType != NcaContentType.Program) return Validity.Unchecked;