From 55031755a840dee101697bc25edcf9bb9efa5916 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 2 Aug 2018 22:14:58 -0500 Subject: [PATCH] Add ability to create NSP files from SD card contents --- hactoolnet/CA00000003_XS00000020 | Bin 0 -> 1792 bytes hactoolnet/CliParser.cs | 1 + hactoolnet/Options.cs | 1 + hactoolnet/Program.cs | 54 +++++++++ hactoolnet/hactoolnet.csproj | 5 + libhac/Pfs0Builder.cs | 77 ++++++++++++ libhac/Ticket.cs | 123 +++++++++++++++++--- libhac/Util.cs | 12 ++ libhac/XTSSharp/RandomAccessSectorStream.cs | 2 +- 9 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 hactoolnet/CA00000003_XS00000020 create mode 100644 libhac/Pfs0Builder.cs diff --git a/hactoolnet/CA00000003_XS00000020 b/hactoolnet/CA00000003_XS00000020 new file mode 100644 index 0000000000000000000000000000000000000000..1a8427bf9690092505e9ec8bdd1c228a758e4df5 GIT binary patch literal 1792 zcmbu7>pK$)9LLSQu;iAES#+e5VUtK$iQH-~xtuvOCYQ8~gSkz!B@;sKLo>Id!bpW8 z$^CK5y-l1hn7M31q(jc>>FEzR`hH*hUVXkVe$Ph$ARvTCoZ>%kVqDE3vH*HGU~ z6n(n&2Qpfg!?@jI4}LnPU22{}LCRU)XVxyhN;NJRh2A~ZT`|FT?GRMFz`72J*Bc&W zvF6RC(!7)-b0?_h+VfSlwTO8xM0+g-Twsh+r8U9Yz&s}W8|2V?>&!u!LchwwgKOB% zQs=k_&;6(khrKC-iTR{JODjd#0W#d$CQcV-qoJdRWeqJ{vT4W?taVtgGPCHQp=0h? zEAsPR7EO)GN+a+x+hp4og@L-ZmW{xf<56QG5OEJz+j$qSZM5RJRuw1?*2Ax8L1cL}#;sv}*v;64q5 z?v|VO_7wp%;nXUVjx~Z=;eoYcvU9)b5Gn+1pOG41=hr5!mjQqUQM59z}G2;JrW8 zF~4?c-SA6_SfUMKiJpn6)+fh40bYI>ykDL{z#B*Ed^6(ml`)Mf*%o1sjQo0qYjPY= z-F}}`K1?8|S)Apd887ySJ!&xSFm_#KyoJ76avz*>#c?EPGU{f-r~3Xs^_M?8iC`J? z^@iAYd0YTMHP;F2sfmYhd!-&;xFILyqrsryj_g;3_)S>`%ca>5_;-NHfSW9^<#l_aO{PC_ah3 z4Ed}CM1|cA7eDU|((OQ(w9 zR3OmCf0O(VsDgA28v5+0>kUo$Z2~1XN#D+)=ALOK{l%JjTU>-@a&>mB0<%kN#;&4a zDF(ESOixk!f?ixLS<|TUSdg!S3PNkI_i}aZDrM!4Hh24axbk-Mwx7&*|4;myzyMdr zANuR*{9OJ2cSyx}Hd@-BEnI+hl;#Aa#whZ-Bw*2&>K_eVOl^Iv#3rEPdauuXp7&7L zYrTRH7+3oG>SI+Xlc7eExnHE9;6~g4GkdON$l(Vc??Znva*H`Y??Jh|;vHb0ZVk)v z?y-#X^;IkI)r&=J(Q5vdyC>-`UsW&Bp3(_=pWx$t!cQj==?|~x6X@a7ldF5B2GX~f z%?#ag3VjTbLAT`n*1z~gZWJXkJL4M--dPMh8V_Ef6)&Y_SHC&s@D7;J!W1&GrC literal 0 HcmV?d00001 diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index 00926911..d229ed92 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -30,6 +30,7 @@ namespace hactoolnet new CliOption("romfsdir", 1, (o, a) => o.RomfsOutDir = a[0]), new CliOption("debugoutdir", 1, (o, a) => o.DebugOutDir = 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]), new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), new CliOption("basenca", 1, (o, a) => o.BaseNca = a[0]), diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index bb56ca4a..57c6e375 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -21,6 +21,7 @@ namespace hactoolnet public string DebugOutDir; public string OutDir; public string SdSeed; + public string NspOut; public string SdPath; public string BaseNca; public bool ListApps; diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index ad3c8c62..b07d1f4e 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using libhac; using libhac.Savefile; @@ -176,6 +177,11 @@ namespace hactoolnet { SaveTitle(ctx, switchFs); } + + if (ctx.Options.NspOut != null) + { + CreateNsp(ctx, switchFs); + } } private static void OpenKeyset(Context ctx) @@ -293,6 +299,53 @@ namespace hactoolnet } } + private static void CreateNsp(Context ctx, SdFs switchFs) + { + var id = ctx.Options.TitleId; + if (id == 0) + { + ctx.Logger.LogMessage("Title ID must be specified to save title"); + return; + } + + if (!switchFs.Titles.TryGetValue(id, out var title)) + { + ctx.Logger.LogMessage($"Could not find title {id:X16}"); + return; + } + + var builder = new Pfs0Builder(); + + foreach (var nca in title.Ncas) + { + builder.AddFile(nca.Filename, nca.Stream); + } + + var ticket = new Ticket + { + SignatureType = TicketSigType.Rsa2048Sha256, + Signature = new byte[0x200], + Issuer = "Root-CA00000003-XS00000020", + FormatVersion = 2, + RightsId = title.MainNca.Header.RightsId, + TitleKeyBlock = title.MainNca.TitleKey, + CryptoType = title.MainNca.Header.CryptoType2, + SectHeaderOffset = 0x2C0 + }; + var ticketBytes = ticket.GetBytes(); + builder.AddFile($"{ticket.RightsId.ToHexString()}.tik", new MemoryStream(ticketBytes)); + + var thisAssembly = Assembly.GetExecutingAssembly(); + var cert = thisAssembly.GetManifestResourceStream("hactoolnet.CA00000003_XS00000020"); + builder.AddFile($"{ticket.RightsId.ToHexString()}.cert", cert); + + + using (var outStream = new FileStream(ctx.Options.NspOut, FileMode.Create, FileAccess.ReadWrite)) + { + builder.Build(outStream, ctx.Logger); + } + } + static void ListTitles(SdFs sdfs) { foreach (var title in sdfs.Titles.Values.OrderBy(x => x.Id)) @@ -357,3 +410,4 @@ namespace hactoolnet } } } + diff --git a/hactoolnet/hactoolnet.csproj b/hactoolnet/hactoolnet.csproj index 48fc5f7e..df0aed13 100644 --- a/hactoolnet/hactoolnet.csproj +++ b/hactoolnet/hactoolnet.csproj @@ -6,6 +6,11 @@ 7.3 + + + + + diff --git a/libhac/Pfs0Builder.cs b/libhac/Pfs0Builder.cs new file mode 100644 index 00000000..2a592201 --- /dev/null +++ b/libhac/Pfs0Builder.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace libhac +{ + public class Pfs0Builder + { + private List Entries { get; } = new List(); + private long DataLength { get; set; } + + public void AddFile(string filename, Stream stream) + { + var entry = new Entry + { + Name = filename, + Stream = stream, + Length = stream.Length, + Offset = DataLength + }; + + DataLength += entry.Length; + + Entries.Add(entry); + } + + public void Build(Stream output, IProgressReport logger = null) + { + var strings = new MemoryStream(); + var stringWriter = new BinaryWriter(strings); + var writer = new BinaryWriter(output); + + foreach (var entry in Entries) + { + entry.StringOffset = (int)strings.Length; + stringWriter.WriteUTF8Z(entry.Name); + } + + strings.Position = Util.GetNextMultiple(strings.Length, 0x10); + var stringTable = strings.ToArray(); + + output.Position = 0; + writer.WriteUTF8("PFS0"); + writer.Write(Entries.Count); + writer.Write(stringTable.Length); + writer.Write(0); + + foreach (var entry in Entries) + { + writer.Write(entry.Offset); + writer.Write(entry.Length); + writer.Write(entry.StringOffset); + writer.Write(0); + } + + writer.Write(stringTable); + + foreach (var entry in Entries) + { + logger?.LogMessage(entry.Name); + entry.Stream.Position = 0; + entry.Stream.CopyStream(output, entry.Length, logger); + } + logger?.SetTotal(0); + } + + private class Entry + { + public string Name; + public Stream Stream; + public long Length; + public long Offset; + public int StringOffset; + } + } +} diff --git a/libhac/Ticket.cs b/libhac/Ticket.cs index e3fb9c2d..8a12a09d 100644 --- a/libhac/Ticket.cs +++ b/libhac/Ticket.cs @@ -8,17 +8,25 @@ namespace libhac { public class Ticket { - public TicketSigType SignatureType { get; } - public byte[] Signature { get; } - public string Issuer { get; } - public byte[] TitleKeyBlock { get; } - public TitleKeyType TitleKeyType { get; } - public byte CryptoType { get; } - public ulong TicketId { get; } - public ulong DeviceId { get; } - public byte[] RightsId { get; } - public uint AccountId { get; } - public int Length { get; } // Not completely sure about this one + public TicketSigType SignatureType { get; set; } + public byte[] Signature { get; set; } + public string Issuer { get; set; } + public byte[] TitleKeyBlock { get; set; } + public byte FormatVersion { get; set; } + public TitleKeyType TitleKeyType { get; set; } + public LicenseType LicenseType { get; set; } + public ushort TicketVersion { get; set; } + public byte CryptoType { get; set; } + public PropertyFlags PropertyMask { get; set; } + public ulong TicketId { get; set; } + public ulong DeviceId { get; set; } + public byte[] RightsId { get; set; } + public uint AccountId { get; set; } + public int SectTotalSize { get; set; } + public int SectHeaderOffset { get; set; } + public short SectNum { get; set; } + public short SectEntrySize { get; set; } + public byte[] File { get; } internal static readonly byte[] LabelHash = @@ -27,6 +35,8 @@ namespace libhac 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55 }; + public Ticket() { } + public Ticket(BinaryReader reader) { var fileStart = reader.BaseStream.Position; @@ -55,23 +65,84 @@ namespace libhac var dataStart = reader.BaseStream.Position; - Issuer = reader.ReadUtf8Z(); + Issuer = reader.ReadUtf8Z(0x40); reader.BaseStream.Position = dataStart + 0x40; TitleKeyBlock = reader.ReadBytes(0x100); - reader.BaseStream.Position = dataStart + 0x141; + FormatVersion = reader.ReadByte(); TitleKeyType = (TitleKeyType)reader.ReadByte(); - reader.BaseStream.Position = dataStart + 0x145; + TicketVersion = reader.ReadUInt16(); + LicenseType = (LicenseType)reader.ReadByte(); CryptoType = reader.ReadByte(); + PropertyMask = (PropertyFlags)reader.ReadUInt32(); reader.BaseStream.Position = dataStart + 0x150; TicketId = reader.ReadUInt64(); DeviceId = reader.ReadUInt64(); RightsId = reader.ReadBytes(0x10); AccountId = reader.ReadUInt32(); - reader.BaseStream.Position = dataStart + 0x178; - Length = reader.ReadInt32(); + SectTotalSize = reader.ReadInt32(); + SectHeaderOffset = reader.ReadInt32(); + SectNum = reader.ReadInt16(); + SectEntrySize = reader.ReadInt16(); reader.BaseStream.Position = fileStart; - File = reader.ReadBytes(Length); + File = reader.ReadBytes(SectHeaderOffset); + } + + public byte[] GetBytes() + { + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + int sigLength; + + switch (SignatureType) + { + case TicketSigType.Rsa4096Sha1: + case TicketSigType.Rsa4096Sha256: + sigLength = 0x200; + break; + case TicketSigType.Rsa2048Sha1: + case TicketSigType.Rsa2048Sha256: + sigLength = 0x100; + break; + case TicketSigType.EcdsaSha1: + case TicketSigType.EcdsaSha256: + sigLength = 0x3c; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var bodyStart = Util.GetNextMultiple(4 + sigLength, 0x40); + + writer.Write((int)SignatureType); + + if (Signature?.Length == sigLength) + { + writer.Write(Signature); + } + + stream.Position = bodyStart; + if (Issuer != null) writer.WriteUTF8(Issuer); + stream.Position = bodyStart + 0x40; + if (TitleKeyBlock?.Length <= 0x100) writer.Write(TitleKeyBlock); + stream.Position = bodyStart + 0x140; + writer.Write((byte)FormatVersion); + writer.Write((byte)TitleKeyType); + writer.Write(TicketVersion); + writer.Write((byte)LicenseType); + writer.Write(CryptoType); + writer.Write((uint)PropertyMask); + stream.Position = bodyStart + 0x150; + writer.Write(TicketId); + writer.Write(DeviceId); + if (RightsId?.Length <= 0x10) writer.Write(RightsId); + writer.Write(AccountId); + writer.Write(SectTotalSize); + writer.Write(SectHeaderOffset); + writer.Write(SectNum); + writer.Write(SectEntrySize); + + return stream.ToArray(); } public static Ticket[] SearchTickets(Stream file, IProgressReport logger = null) @@ -141,4 +212,22 @@ namespace libhac Common, Personalized } + + public enum LicenseType + { + Permanent, + Demo, + Trial, + Rental, + Subscription, + Service + } + + [Flags] + public enum PropertyFlags + { + PreInstall = 1 << 0, + SharedTitle = 1 << 1, + AllowAllContent = 1 << 2 + } } diff --git a/libhac/Util.cs b/libhac/Util.cs index c5e58297..12ad805e 100644 --- a/libhac/Util.cs +++ b/libhac/Util.cs @@ -112,6 +112,18 @@ namespace libhac return text; } + public static void WriteUTF8(this BinaryWriter writer, string value) + { + byte[] text = Encoding.UTF8.GetBytes(value); + writer.Write(text); + } + + public static void WriteUTF8Z(this BinaryWriter writer, string value) + { + writer.WriteUTF8(value); + writer.Write((byte)0); + } + public static string ReadAscii(this BinaryReader reader, int size) { return Encoding.ASCII.GetString(reader.ReadBytes(size), 0, size); diff --git a/libhac/XTSSharp/RandomAccessSectorStream.cs b/libhac/XTSSharp/RandomAccessSectorStream.cs index 63cef1bc..8384266a 100644 --- a/libhac/XTSSharp/RandomAccessSectorStream.cs +++ b/libhac/XTSSharp/RandomAccessSectorStream.cs @@ -119,7 +119,7 @@ namespace libhac.XTSSharp /// A long value representing the length of the stream in bytes. public override long Length { - get { return _s.Length + _bufferPos; } + get { return _s.Length; } } ///