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;