Add ability to create NSP files from SD card contents

This commit is contained in:
Alex Barney 2018-08-02 22:14:58 -05:00
parent d84c406655
commit 55031755a8
9 changed files with 257 additions and 18 deletions

Binary file not shown.

View File

@ -30,6 +30,7 @@ namespace hactoolnet
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("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("sdseed", 1, (o, a) => o.SdSeed = a[0]), new CliOption("sdseed", 1, (o, a) => o.SdSeed = a[0]),
new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]), new CliOption("sdpath", 1, (o, a) => o.SdPath = a[0]),
new CliOption("basenca", 1, (o, a) => o.BaseNca = a[0]), new CliOption("basenca", 1, (o, a) => o.BaseNca = a[0]),

View File

@ -21,6 +21,7 @@ namespace hactoolnet
public string DebugOutDir; public string DebugOutDir;
public string OutDir; public string OutDir;
public string SdSeed; public string SdSeed;
public string NspOut;
public string SdPath; public string SdPath;
public string BaseNca; public string BaseNca;
public bool ListApps; public bool ListApps;

View File

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using libhac; using libhac;
using libhac.Savefile; using libhac.Savefile;
@ -176,6 +177,11 @@ namespace hactoolnet
{ {
SaveTitle(ctx, switchFs); SaveTitle(ctx, switchFs);
} }
if (ctx.Options.NspOut != null)
{
CreateNsp(ctx, switchFs);
}
} }
private static void OpenKeyset(Context ctx) 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) static void ListTitles(SdFs sdfs)
{ {
foreach (var title in sdfs.Titles.Values.OrderBy(x => x.Id)) foreach (var title in sdfs.Titles.Values.OrderBy(x => x.Id))
@ -357,3 +410,4 @@ namespace hactoolnet
} }
} }
} }

View File

@ -6,6 +6,11 @@
<LangVersion>7.3</LangVersion> <LangVersion>7.3</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="CA00000003_XS00000020" />
<EmbeddedResource Include="CA00000003_XS00000020" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\libhac\libhac.csproj" /> <ProjectReference Include="..\libhac\libhac.csproj" />
</ItemGroup> </ItemGroup>

77
libhac/Pfs0Builder.cs Normal file
View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace libhac
{
public class Pfs0Builder
{
private List<Entry> Entries { get; } = new List<Entry>();
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;
}
}
}

View File

@ -8,17 +8,25 @@ namespace libhac
{ {
public class Ticket public class Ticket
{ {
public TicketSigType SignatureType { get; } public TicketSigType SignatureType { get; set; }
public byte[] Signature { get; } public byte[] Signature { get; set; }
public string Issuer { get; } public string Issuer { get; set; }
public byte[] TitleKeyBlock { get; } public byte[] TitleKeyBlock { get; set; }
public TitleKeyType TitleKeyType { get; } public byte FormatVersion { get; set; }
public byte CryptoType { get; } public TitleKeyType TitleKeyType { get; set; }
public ulong TicketId { get; } public LicenseType LicenseType { get; set; }
public ulong DeviceId { get; } public ushort TicketVersion { get; set; }
public byte[] RightsId { get; } public byte CryptoType { get; set; }
public uint AccountId { get; } public PropertyFlags PropertyMask { get; set; }
public int Length { get; } // Not completely sure about this one 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; } public byte[] File { get; }
internal static readonly byte[] LabelHash = 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 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55
}; };
public Ticket() { }
public Ticket(BinaryReader reader) public Ticket(BinaryReader reader)
{ {
var fileStart = reader.BaseStream.Position; var fileStart = reader.BaseStream.Position;
@ -55,23 +65,84 @@ namespace libhac
var dataStart = reader.BaseStream.Position; var dataStart = reader.BaseStream.Position;
Issuer = reader.ReadUtf8Z(); Issuer = reader.ReadUtf8Z(0x40);
reader.BaseStream.Position = dataStart + 0x40; reader.BaseStream.Position = dataStart + 0x40;
TitleKeyBlock = reader.ReadBytes(0x100); TitleKeyBlock = reader.ReadBytes(0x100);
reader.BaseStream.Position = dataStart + 0x141; FormatVersion = reader.ReadByte();
TitleKeyType = (TitleKeyType)reader.ReadByte(); TitleKeyType = (TitleKeyType)reader.ReadByte();
reader.BaseStream.Position = dataStart + 0x145; TicketVersion = reader.ReadUInt16();
LicenseType = (LicenseType)reader.ReadByte();
CryptoType = reader.ReadByte(); CryptoType = reader.ReadByte();
PropertyMask = (PropertyFlags)reader.ReadUInt32();
reader.BaseStream.Position = dataStart + 0x150; reader.BaseStream.Position = dataStart + 0x150;
TicketId = reader.ReadUInt64(); TicketId = reader.ReadUInt64();
DeviceId = reader.ReadUInt64(); DeviceId = reader.ReadUInt64();
RightsId = reader.ReadBytes(0x10); RightsId = reader.ReadBytes(0x10);
AccountId = reader.ReadUInt32(); AccountId = reader.ReadUInt32();
reader.BaseStream.Position = dataStart + 0x178; SectTotalSize = reader.ReadInt32();
Length = reader.ReadInt32(); SectHeaderOffset = reader.ReadInt32();
SectNum = reader.ReadInt16();
SectEntrySize = reader.ReadInt16();
reader.BaseStream.Position = fileStart; 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) public static Ticket[] SearchTickets(Stream file, IProgressReport logger = null)
@ -141,4 +212,22 @@ namespace libhac
Common, Common,
Personalized Personalized
} }
public enum LicenseType
{
Permanent,
Demo,
Trial,
Rental,
Subscription,
Service
}
[Flags]
public enum PropertyFlags
{
PreInstall = 1 << 0,
SharedTitle = 1 << 1,
AllowAllContent = 1 << 2
}
} }

View File

@ -112,6 +112,18 @@ namespace libhac
return text; 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) public static string ReadAscii(this BinaryReader reader, int size)
{ {
return Encoding.ASCII.GetString(reader.ReadBytes(size), 0, size); return Encoding.ASCII.GetString(reader.ReadBytes(size), 0, size);

View File

@ -119,7 +119,7 @@ namespace libhac.XTSSharp
/// <returns>A long value representing the length of the stream in bytes.</returns> /// <returns>A long value representing the length of the stream in bytes.</returns>
public override long Length public override long Length
{ {
get { return _s.Length + _bufferPos; } get { return _s.Length; }
} }
/// <summary> /// <summary>