diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 93376717..4a33ab8c 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using libhac; namespace hactoolnet @@ -22,7 +23,7 @@ namespace hactoolnet using (var output = new FileStream(args[4], FileMode.Create)) using (var progress = new ProgressBar()) { - progress.LogMessage($"Title ID: {nca.TitleId:X8}"); + progress.LogMessage($"Title ID: {nca.Header.TitleId:X16}"); progress.LogMessage($"Writing {args[4]}"); nax0.Stream.CopyStream(output, nax0.Stream.Length, progress); } @@ -33,11 +34,11 @@ namespace hactoolnet var keyset = ExternalKeys.ReadKeyFile(args[0]); keyset.SetSdSeed(args[1].ToBytes()); var sdfs = new SdFs(keyset, args[2]); - var ncas = sdfs.ReadAllNca(); + var ncas = sdfs.ReadAllNca().ToArray(); foreach (var nca in ncas) { - Console.WriteLine($"{nca.TitleId:X8} {nca.ContentType} {nca.Name}"); + Console.WriteLine($"{nca.Header.TitleId:X16} {nca.Header.ContentType} {nca.Name}"); } } } diff --git a/hactoolnet/hactoolnet.csproj b/hactoolnet/hactoolnet.csproj index e15691c0..e70e51cf 100644 --- a/hactoolnet/hactoolnet.csproj +++ b/hactoolnet/hactoolnet.csproj @@ -1,8 +1,9 @@ - + Exe netcoreapp2.1 + 7.3 diff --git a/libhac/Keyset.cs b/libhac/Keyset.cs index bd34a105..99b9ca72 100644 --- a/libhac/Keyset.cs +++ b/libhac/Keyset.cs @@ -31,6 +31,7 @@ namespace libhac public byte[] header_kek_source { get; set; } = new byte[0x10]; public byte[] sd_card_kek_source { get; set; } = new byte[0x10]; public byte[][] sd_card_key_sources { get; set; } = Util.CreateJaggedArray(2, 0x20); + public byte[][] sd_card_key_sources_specific { get; set; } = Util.CreateJaggedArray(2, 0x20); public byte[] encrypted_header_key { get; set; } = new byte[0x20]; public byte[] header_key { get; set; } = new byte[0x20]; public byte[][] titlekeks { get; set; } = Util.CreateJaggedArray(0x20, 0x10); @@ -42,13 +43,14 @@ namespace libhac public void SetSdSeed(byte[] sdseed) { - foreach (byte[] key in sd_card_key_sources) + for (int k = 0; k < sd_card_key_sources.Length; k++) { for (int i = 0; i < 0x20; i++) { - key[i] ^= sdseed[i & 0xF]; + sd_card_key_sources_specific[k][i] = (byte)(sd_card_key_sources[k][i] ^ sdseed[i & 0xF]); } } + DeriveKeys(); } @@ -65,9 +67,9 @@ namespace libhac var sdKek = new byte[0x10]; Crypto.GenerateKek(sdKek, sd_card_kek_source, master_keys[0], aes_kek_generation_source, aes_key_generation_source); - for (int k = 0; k < sd_card_key_sources.Length; k++) + for (int k = 0; k < sd_card_key_sources_specific.Length; k++) { - Crypto.DecryptEcb(sdKek, sd_card_key_sources[k], sd_card_keys[k], 0x20); + Crypto.DecryptEcb(sdKek, sd_card_key_sources_specific[k], sd_card_keys[k], 0x20); } } } @@ -76,7 +78,7 @@ namespace libhac { private static readonly Dictionary KeyDict = CreateKeyDict(); - public static Keyset ReadKeyFile(string filename) + public static Keyset ReadKeyFile(string filename, IProgressReport progress = null) { var keyset = new Keyset(); using (var reader = new StreamReader(new FileStream(filename, FileMode.Open))) @@ -92,14 +94,14 @@ namespace libhac if (!KeyDict.TryGetValue(key, out var kv)) { - Console.WriteLine($"Failed to match key {key}"); + progress?.LogMessage($"Failed to match key {key}"); continue; } var value = valueStr.ToBytes(); if (value.Length != kv.Size) { - Console.WriteLine($"Key {key} had incorrect size {value.Length}. (Expected {kv.Size})"); + progress?.LogMessage($"Key {key} had incorrect size {value.Length}. (Expected {kv.Size})"); continue; } diff --git a/libhac/Nca.cs b/libhac/Nca.cs index b6df9b50..b365038d 100644 --- a/libhac/Nca.cs +++ b/libhac/Nca.cs @@ -1,54 +1,325 @@ -using System.IO; +using System; +using System.IO; using libhac.XTSSharp; namespace libhac { public class Nca { - public byte[] Signature1 { get; set; } // RSA-PSS signature over header with fixed key. - public byte[] Signature2 { get; set; } // RSA-PSS signature over header with key in NPDM. - public string Magic { get; set; } - public byte Distribution { get; set; } // System vs gamecard. - public byte ContentType { get; set; } - public byte CryptoType { get; set; } // Which keyblob (field 1) - public byte KaekInd { get; set; } // Which kaek index? - public ulong NcaSize { get; set; } // Entire archive size. - public ulong TitleId { get; set; } - public uint SdkVersion { get; set; } // What SDK was this built with? - public byte CryptoType2 { get; set; } // Which keyblob (field 2) - public byte[] RightsId { get; set; } + public NcaHeader Header { get; private set; } public string Name { get; set; } + public bool HasRightsId { get; private set; } + public int CryptoType { get; private set; } + public byte[][] DecryptedKeys { get; } = Util.CreateJaggedArray(4, 0x10); public Nca(Keyset keyset, Stream stream) { ReadHeader(keyset, stream); + + CryptoType = Math.Max(Header.CryptoType, Header.CryptoType2); + if (CryptoType > 0) CryptoType--; + + HasRightsId = !Header.RightsId.IsEmpty(); + + if (!HasRightsId) + { + DecryptKeyArea(keyset); + } } private void ReadHeader(Keyset keyset, Stream stream) { stream.Position = 0; var xts = XtsAes128.Create(keyset.header_key); - var header = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x200)); - var reader = new BinaryReader(header); + var headerDec = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x200)); + var reader = new BinaryReader(headerDec); - Signature1 = reader.ReadBytes(0x100); - Signature2 = reader.ReadBytes(0x100); - Magic = reader.ReadAscii(4); - if (Magic != "NCA3") throw new InvalidDataException("Not an NCA3 file"); - Distribution = reader.ReadByte(); - ContentType = reader.ReadByte(); - CryptoType = reader.ReadByte(); - KaekInd = reader.ReadByte(); - NcaSize = reader.ReadUInt64(); - TitleId = reader.ReadUInt64(); - header.Position += 4; + Header = NcaHeader.Read(reader); - SdkVersion = reader.ReadUInt32(); - CryptoType2 = reader.ReadByte(); - header.Position += 0xF; + headerDec.Close(); + } - RightsId = reader.ReadBytes(0x10); - header.Close(); + private void DecryptKeyArea(Keyset keyset) + { + for (int i = 0; i < 4; i++) + { + Crypto.DecryptEcb(keyset.key_area_keys[CryptoType][Header.KaekInd], Header.EncryptedKeys[i], + DecryptedKeys[i], 0x10); + } } } + + public class NcaHeader + { + public byte[] Signature1; // RSA-PSS signature over header with fixed key. + public byte[] Signature2; // RSA-PSS signature over header with key in NPDM. + public string Magic; + public byte Distribution; // System vs gamecard. + public ContentType ContentType; + public byte CryptoType; // Which keyblob (field 1) + public byte KaekInd; // Which kaek index? + public ulong NcaSize; // Entire archive size. + public ulong TitleId; + public uint SdkVersion; // What SDK was this built with? + public byte CryptoType2; // Which keyblob (field 2) + public byte[] RightsId; + public string Name; + + public NcaSectionEntry[] SectionEntries = new NcaSectionEntry[4]; + public byte[][] SectionHashes = new byte[4][]; + public byte[][] EncryptedKeys = new byte[4][]; + + public NcaFsHeader[] FsHeaders = new NcaFsHeader[4]; + + public static NcaHeader Read(BinaryReader reader) + { + var head = new NcaHeader(); + + head.Signature1 = reader.ReadBytes(0x100); + head.Signature2 = reader.ReadBytes(0x100); + head.Magic = reader.ReadAscii(4); + head.Distribution = reader.ReadByte(); + head.ContentType = (ContentType)reader.ReadByte(); + head.CryptoType = reader.ReadByte(); + head.KaekInd = reader.ReadByte(); + head.NcaSize = reader.ReadUInt64(); + head.TitleId = reader.ReadUInt64(); + reader.BaseStream.Position += 4; + + head.SdkVersion = reader.ReadUInt32(); + head.CryptoType2 = reader.ReadByte(); + reader.BaseStream.Position += 0xF; + + head.RightsId = reader.ReadBytes(0x10); + + for (int i = 0; i < 4; i++) + { + head.SectionEntries[i] = new NcaSectionEntry(reader); + } + + for (int i = 0; i < 4; i++) + { + head.SectionHashes[i] = reader.ReadBytes(0x20); + } + + for (int i = 0; i < 4; i++) + { + head.EncryptedKeys[i] = reader.ReadBytes(0x10); + } + + reader.BaseStream.Position += 0xC0; + + for (int i = 0; i < 4; i++) + { + head.FsHeaders[i] = new NcaFsHeader(reader); + } + return head; + } + } + + public class NcaSectionEntry + { + public uint MediaStartOffset; + public uint MediaEndOffset; + + public NcaSectionEntry(BinaryReader reader) + { + MediaStartOffset = reader.ReadUInt32(); + MediaEndOffset = reader.ReadUInt32(); + reader.BaseStream.Position += 8; + } + } + + public class NcaFsHeader + { + public byte Field0; + public byte Field1; + public SectionPartitionType PartitionType; + public SectionFsType FsType; + public SectionCryptType CryptType; + public SectionType Type; + + public Pfs0Superblock Pfs0; + public RomfsSuperblock Romfs; + public BktrSuperblock Bktr; + + public NcaFsHeader(BinaryReader reader) + { + Field0 = reader.ReadByte(); + Field1 = reader.ReadByte(); + PartitionType = (SectionPartitionType)reader.ReadByte(); + FsType = (SectionFsType)reader.ReadByte(); + CryptType = (SectionCryptType)reader.ReadByte(); + reader.BaseStream.Position += 3; + + if (PartitionType == SectionPartitionType.Pfs0 && FsType == SectionFsType.Pfs0) + { + Type = SectionType.Pfs0; + Pfs0 = new Pfs0Superblock(reader); + } + else if (PartitionType == SectionPartitionType.Romfs && FsType == SectionFsType.Romfs) + { + if (CryptType == SectionCryptType.BKTR) + { + Type = SectionType.Bktr; + Bktr = new BktrSuperblock(reader); + } + else + { + Type = SectionType.Romfs; + Romfs = new RomfsSuperblock(reader); + } + } + } + } + + public class Pfs0Superblock + { + public byte[] MasterHash; /* SHA-256 hash of the hash table. */ + public uint BlockSize; /* In bytes. */ + public uint Always2; + public ulong HashTableOffset; /* Normally zero. */ + public ulong HashTableSize; + public ulong Pfs0Offset; + public ulong Pfs0Size; + + public Pfs0Superblock(BinaryReader reader) + { + MasterHash = reader.ReadBytes(0x20); + BlockSize = reader.ReadUInt32(); + Always2 = reader.ReadUInt32(); + HashTableOffset = reader.ReadUInt64(); + HashTableSize = reader.ReadUInt64(); + Pfs0Offset = reader.ReadUInt64(); + Pfs0Size = reader.ReadUInt64(); + reader.BaseStream.Position += 0xF0; + } + } + + public class RomfsSuperblock + { + public IvfcHeader IvfcHeader; + + public RomfsSuperblock(BinaryReader reader) + { + IvfcHeader = new IvfcHeader(reader); + reader.BaseStream.Position += 0x58; + } + } + + public class BktrSuperblock + { + public IvfcHeader IvfcHeader; + public BktrHeader RelocationHeader; + public BktrHeader SubsectionHeader; + + public BktrSuperblock(BinaryReader reader) + { + IvfcHeader = new IvfcHeader(reader); + reader.BaseStream.Position += 0x18; + RelocationHeader = new BktrHeader(reader); + SubsectionHeader = new BktrHeader(reader); + } + } + + public class IvfcHeader + { + public string Magic; + public uint Id; + public uint MasterHashSize; + public uint NumLevels; + public IvfcLevelHeader[] LevelHeaders = new IvfcLevelHeader[6]; + public byte[] MasterHash; + + + public IvfcHeader(BinaryReader reader) + { + Magic = reader.ReadAscii(4); + Id = reader.ReadUInt32(); + MasterHashSize = reader.ReadUInt32(); + NumLevels = reader.ReadUInt32(); + + for (int i = 0; i < LevelHeaders.Length; i++) + { + LevelHeaders[i] = new IvfcLevelHeader(reader); + } + + reader.BaseStream.Position += 0x20; + MasterHash = reader.ReadBytes(0x20); + } + } + + public class IvfcLevelHeader + { + public ulong LogicalOffset; + public ulong HashDataSize; + public uint BlockSize; + public uint Reserved; + + public IvfcLevelHeader(BinaryReader reader) + { + LogicalOffset = reader.ReadUInt64(); + HashDataSize = reader.ReadUInt64(); + BlockSize = reader.ReadUInt32(); + Reserved = reader.ReadUInt32(); + } + } + + public class BktrHeader + { + public ulong Offset; + public ulong Size; + public uint Magic; + public uint Field14; + public uint NumEntries; + public uint Field1C; + + public BktrHeader(BinaryReader reader) + { + Offset = reader.ReadUInt64(); + Size = reader.ReadUInt64(); + Magic = reader.ReadUInt32(); + Field14 = reader.ReadUInt32(); + NumEntries = reader.ReadUInt32(); + Field1C = reader.ReadUInt32(); + } + } + + public enum ContentType + { + Program, + Meta, + Control, + Manual, + Data, + Unknown + } + + public enum SectionCryptType + { + None = 1, + XTS, + CTR, + BKTR + } + + public enum SectionFsType + { + Pfs0 = 2, + Romfs + } + + public enum SectionPartitionType + { + Romfs, + Pfs0 + } + + public enum SectionType + { + Invalid, + Pfs0, + Romfs, + Bktr + } } diff --git a/libhac/SdFs.cs b/libhac/SdFs.cs index 50f6a16f..f2110dda 100644 --- a/libhac/SdFs.cs +++ b/libhac/SdFs.cs @@ -23,19 +23,17 @@ namespace libhac Files = Directory.GetFiles(ContentsDir, "00", SearchOption.AllDirectories).Select(Path.GetDirectoryName).ToArray(); } - public List ReadAllNca() + public IEnumerable ReadAllNca() { - List ncas = new List(); foreach (var file in Files) { var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/'); var nax0 = new Nax0(Keyset, file, sdPath); var nca = new Nca(Keyset, nax0.Stream); - ncas.Add(nca); nca.Name = Path.GetFileName(file); + yield return nca; } - return ncas; } } } diff --git a/libhac/Util.cs b/libhac/Util.cs index 74012cf1..69cf2f35 100644 --- a/libhac/Util.cs +++ b/libhac/Util.cs @@ -6,6 +6,8 @@ namespace libhac { public static class Util { + private const int MediaSize = 0x200; + public static T CreateJaggedArray(params int[] lengths) { return (T)InitializeJaggedArray(typeof(T).GetElementType(), 0, lengths); @@ -49,7 +51,7 @@ namespace libhac for (int i = 0; i < array.Length; i++) { - if (i != 0) + if (array[i] != 0) { return false; } @@ -159,5 +161,10 @@ namespace libhac } return result; } + + internal static ulong MediaToReal(ulong media) + { + return MediaSize * media; + } } }