2018-06-21 23:03:58 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
2018-07-07 22:45:06 +02:00
|
|
|
|
using System.Linq;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
using System.Security.Cryptography;
|
2018-07-05 23:37:30 +02:00
|
|
|
|
using System.Text;
|
2018-06-21 16:25:20 +02:00
|
|
|
|
using libhac.XTSSharp;
|
|
|
|
|
|
|
|
|
|
namespace libhac
|
|
|
|
|
{
|
2018-06-26 00:26:47 +02:00
|
|
|
|
public class Nca : IDisposable
|
2018-06-21 16:25:20 +02:00
|
|
|
|
{
|
2018-06-21 23:03:58 +02:00
|
|
|
|
public NcaHeader Header { get; private set; }
|
2018-06-27 02:10:21 +02:00
|
|
|
|
public string NcaId { get; set; }
|
2018-06-27 02:42:01 +02:00
|
|
|
|
public string Filename { get; set; }
|
2018-06-21 23:03:58 +02:00
|
|
|
|
public bool HasRightsId { get; private set; }
|
|
|
|
|
public int CryptoType { get; private set; }
|
|
|
|
|
public byte[][] DecryptedKeys { get; } = Util.CreateJaggedArray<byte[][]>(4, 0x10);
|
2018-07-05 23:37:30 +02:00
|
|
|
|
public byte[] TitleKey { get; }
|
|
|
|
|
public byte[] TitleKeyDec { get; } = new byte[0x10];
|
2018-06-22 21:05:29 +02:00
|
|
|
|
public Stream Stream { get; private set; }
|
2018-06-26 00:26:47 +02:00
|
|
|
|
private bool KeepOpen { get; }
|
2018-07-07 22:45:06 +02:00
|
|
|
|
private Nca BaseNca { get; set; }
|
2018-06-22 21:05:29 +02:00
|
|
|
|
|
2018-06-28 23:55:36 +02:00
|
|
|
|
public NcaSection[] Sections { get; } = new NcaSection[4];
|
2018-06-21 16:25:20 +02:00
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
public Nca(Keyset keyset, Stream stream, bool keepOpen)
|
2018-06-21 16:25:20 +02:00
|
|
|
|
{
|
2018-06-26 00:26:47 +02:00
|
|
|
|
stream.Position = 0;
|
|
|
|
|
KeepOpen = keepOpen;
|
2018-06-22 21:05:29 +02:00
|
|
|
|
Stream = stream;
|
2018-06-26 00:26:47 +02:00
|
|
|
|
DecryptHeader(keyset, stream);
|
2018-06-21 23:03:58 +02:00
|
|
|
|
|
|
|
|
|
CryptoType = Math.Max(Header.CryptoType, Header.CryptoType2);
|
|
|
|
|
if (CryptoType > 0) CryptoType--;
|
|
|
|
|
|
|
|
|
|
HasRightsId = !Header.RightsId.IsEmpty();
|
|
|
|
|
|
|
|
|
|
if (!HasRightsId)
|
|
|
|
|
{
|
|
|
|
|
DecryptKeyArea(keyset);
|
|
|
|
|
}
|
2018-06-28 22:02:23 +02:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (keyset.TitleKeys.TryGetValue(Header.RightsId, out var titleKey))
|
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
TitleKey = titleKey;
|
|
|
|
|
Crypto.DecryptEcb(keyset.titlekeks[CryptoType], titleKey, TitleKeyDec, 0x10);
|
|
|
|
|
DecryptedKeys[2] = TitleKeyDec;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-22 21:05:29 +02:00
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
|
{
|
2018-06-26 00:26:47 +02:00
|
|
|
|
var section = ParseSection(i);
|
2018-06-28 22:02:23 +02:00
|
|
|
|
if (section == null) continue;
|
2018-06-28 23:55:36 +02:00
|
|
|
|
Sections[i] = section;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
ValidateSuperblockHash(i);
|
2018-06-22 21:05:29 +02:00
|
|
|
|
}
|
2018-08-16 00:33:45 +02:00
|
|
|
|
|
|
|
|
|
foreach (var pfsSection in Sections.Where(x => x != null && x.Type == SectionType.Pfs0))
|
|
|
|
|
{
|
|
|
|
|
var sectionStream = OpenSection(pfsSection.SectionNum, false);
|
|
|
|
|
if (sectionStream == null) continue;
|
|
|
|
|
|
|
|
|
|
var pfs = new Pfs(sectionStream);
|
|
|
|
|
if (!pfs.FileExists("main.npdm")) continue;
|
|
|
|
|
|
|
|
|
|
pfsSection.IsExefs = true;
|
|
|
|
|
}
|
2018-06-22 21:05:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-28 22:02:23 +02:00
|
|
|
|
public Stream OpenSection(int index, bool raw)
|
2018-06-22 21:05:29 +02:00
|
|
|
|
{
|
2018-06-28 23:55:36 +02:00
|
|
|
|
if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
|
2018-06-22 21:05:29 +02:00
|
|
|
|
var sect = Sections[index];
|
2018-06-22 23:17:20 +02:00
|
|
|
|
|
2018-07-11 00:16:25 +02:00
|
|
|
|
if (sect.SuperblockHashValidity == Validity.Invalid) return null;
|
|
|
|
|
|
2018-06-22 23:17:20 +02:00
|
|
|
|
long offset = sect.Offset;
|
|
|
|
|
long size = sect.Size;
|
|
|
|
|
|
2018-06-28 22:02:23 +02:00
|
|
|
|
if (!raw)
|
2018-06-22 23:17:20 +02:00
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
switch (sect.Header.Type)
|
2018-06-28 22:02:23 +02:00
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
case SectionType.Pfs0:
|
|
|
|
|
offset = sect.Offset + sect.Pfs0.Superblock.Pfs0Offset;
|
|
|
|
|
size = sect.Pfs0.Superblock.Pfs0Size;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
break;
|
2018-07-05 23:37:30 +02:00
|
|
|
|
case SectionType.Romfs:
|
|
|
|
|
offset = sect.Offset + sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].LogicalOffset;
|
|
|
|
|
size = sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize;
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
2018-06-28 22:02:23 +02:00
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
|
}
|
2018-06-22 23:17:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Stream.Position = offset;
|
2018-06-22 21:05:29 +02:00
|
|
|
|
|
|
|
|
|
switch (sect.Header.CryptType)
|
|
|
|
|
{
|
|
|
|
|
case SectionCryptType.None:
|
2018-06-28 23:55:36 +02:00
|
|
|
|
return new SubStream(Stream, offset, size);
|
2018-06-22 21:05:29 +02:00
|
|
|
|
case SectionCryptType.XTS:
|
|
|
|
|
break;
|
|
|
|
|
case SectionCryptType.CTR:
|
2018-06-28 22:02:23 +02:00
|
|
|
|
return new RandomAccessSectorStream(new AesCtrStream(Stream, DecryptedKeys[2], offset, size, offset, sect.Header.Ctr), false);
|
2018-06-22 21:05:29 +02:00
|
|
|
|
case SectionCryptType.BKTR:
|
2018-07-07 22:45:06 +02:00
|
|
|
|
var patchStream = new RandomAccessSectorStream(
|
|
|
|
|
new BktrCryptoStream(Stream, DecryptedKeys[2], offset, size, offset, sect.Header.Ctr, sect),
|
|
|
|
|
false);
|
|
|
|
|
if (BaseNca == null)
|
|
|
|
|
{
|
|
|
|
|
return patchStream;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var dataLevel = sect.Header.Bktr.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1];
|
|
|
|
|
|
|
|
|
|
var baseSect = BaseNca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs);
|
|
|
|
|
if (baseSect == null) throw new InvalidDataException("Base NCA has no RomFS section");
|
|
|
|
|
|
|
|
|
|
var baseStream = BaseNca.OpenSection(baseSect.SectionNum, true);
|
|
|
|
|
var virtStreamRaw = new Bktr(patchStream, baseStream, sect);
|
|
|
|
|
|
|
|
|
|
if (raw) return virtStreamRaw;
|
|
|
|
|
var virtStream = new SubStream(virtStreamRaw, dataLevel.LogicalOffset, dataLevel.HashDataSize);
|
|
|
|
|
return virtStream;
|
|
|
|
|
}
|
2018-06-22 21:05:29 +02:00
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
return new SubStream(Stream, offset, size);
|
2018-06-21 16:25:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-07 22:45:06 +02:00
|
|
|
|
public void SetBaseNca(Nca baseNca) => BaseNca = baseNca;
|
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
private void DecryptHeader(Keyset keyset, Stream stream)
|
2018-06-21 16:25:20 +02:00
|
|
|
|
{
|
2018-06-26 00:26:47 +02:00
|
|
|
|
byte[] headerBytes = new byte[0xC00];
|
2018-06-21 16:25:20 +02:00
|
|
|
|
var xts = XtsAes128.Create(keyset.header_key);
|
2018-06-26 00:26:47 +02:00
|
|
|
|
using (var headerDec = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x200)))
|
|
|
|
|
{
|
|
|
|
|
headerDec.Read(headerBytes, 0, headerBytes.Length);
|
|
|
|
|
}
|
2018-06-21 23:03:58 +02:00
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
var reader = new BinaryReader(new MemoryStream(headerBytes));
|
2018-06-21 23:03:58 +02:00
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
Header = NcaHeader.Read(reader);
|
2018-06-21 23:03:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
private NcaSection ParseSection(int index)
|
2018-06-21 23:03:58 +02:00
|
|
|
|
{
|
2018-06-22 21:05:29 +02:00
|
|
|
|
var entry = Header.SectionEntries[index];
|
|
|
|
|
var header = Header.FsHeaders[index];
|
|
|
|
|
if (entry.MediaStartOffset == 0) return null;
|
2018-06-21 23:03:58 +02:00
|
|
|
|
|
2018-06-22 21:05:29 +02:00
|
|
|
|
var sect = new NcaSection();
|
2018-06-21 23:03:58 +02:00
|
|
|
|
|
2018-06-22 21:05:29 +02:00
|
|
|
|
sect.SectionNum = index;
|
|
|
|
|
sect.Offset = Util.MediaToReal(entry.MediaStartOffset);
|
|
|
|
|
sect.Size = Util.MediaToReal(entry.MediaEndOffset) - sect.Offset;
|
|
|
|
|
sect.Header = header;
|
|
|
|
|
sect.Type = header.Type;
|
2018-06-21 23:03:58 +02:00
|
|
|
|
|
2018-06-22 21:05:29 +02:00
|
|
|
|
if (sect.Type == SectionType.Pfs0)
|
2018-06-21 23:03:58 +02:00
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
sect.Pfs0 = new Pfs0Section();
|
2018-08-15 01:21:07 +02:00
|
|
|
|
sect.Pfs0.Superblock = header.Pfs;
|
2018-07-05 23:37:30 +02:00
|
|
|
|
}
|
|
|
|
|
else if (sect.Type == SectionType.Romfs)
|
|
|
|
|
{
|
|
|
|
|
ProcessIvfcSection(sect);
|
2018-06-21 23:03:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-22 21:05:29 +02:00
|
|
|
|
return sect;
|
2018-06-21 23:03:58 +02:00
|
|
|
|
}
|
2018-06-26 00:26:47 +02:00
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
private void ProcessIvfcSection(NcaSection sect)
|
|
|
|
|
{
|
|
|
|
|
sect.Romfs = new RomfsSection();
|
|
|
|
|
sect.Romfs.Superblock = sect.Header.Romfs;
|
|
|
|
|
var headers = sect.Romfs.Superblock.IvfcHeader.LevelHeaders;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < Romfs.IvfcMaxLevel; i++)
|
|
|
|
|
{
|
|
|
|
|
var level = new IvfcLevel();
|
|
|
|
|
sect.Romfs.IvfcLevels[i] = level;
|
|
|
|
|
var header = headers[i];
|
|
|
|
|
level.DataOffset = header.LogicalOffset;
|
|
|
|
|
level.DataSize = header.HashDataSize;
|
|
|
|
|
level.HashBlockSize = 1 << header.BlockSize;
|
|
|
|
|
level.HashBlockCount = Util.DivideByRoundUp(level.DataSize, level.HashBlockSize);
|
|
|
|
|
level.HashSize = level.HashBlockCount * 0x20;
|
|
|
|
|
|
|
|
|
|
if (i != 0)
|
|
|
|
|
{
|
|
|
|
|
level.HashOffset = sect.Romfs.IvfcLevels[i - 1].DataOffset;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-11 00:16:25 +02:00
|
|
|
|
private void CheckBktrKey(NcaSection sect)
|
|
|
|
|
{
|
|
|
|
|
var offset = sect.Header.Bktr.SubsectionHeader.Offset;
|
|
|
|
|
using (var streamDec = new RandomAccessSectorStream(new AesCtrStream(Stream, DecryptedKeys[2], sect.Offset, sect.Size, sect.Offset, sect.Header.Ctr)))
|
|
|
|
|
{
|
|
|
|
|
var reader = new BinaryReader(streamDec);
|
|
|
|
|
streamDec.Position = offset + 8;
|
|
|
|
|
var size = reader.ReadInt64();
|
|
|
|
|
|
|
|
|
|
if (size != offset)
|
|
|
|
|
{
|
|
|
|
|
sect.SuperblockHashValidity = Validity.Invalid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-28 22:02:23 +02:00
|
|
|
|
private void ValidateSuperblockHash(int index)
|
|
|
|
|
{
|
2018-06-28 23:55:36 +02:00
|
|
|
|
if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
|
2018-06-28 22:02:23 +02:00
|
|
|
|
var sect = Sections[index];
|
|
|
|
|
|
|
|
|
|
byte[] expected = null;
|
|
|
|
|
byte[] actual;
|
|
|
|
|
long offset = 0;
|
|
|
|
|
long size = 0;
|
|
|
|
|
|
|
|
|
|
switch (sect.Type)
|
|
|
|
|
{
|
|
|
|
|
case SectionType.Invalid:
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Pfs0:
|
2018-08-15 01:21:07 +02:00
|
|
|
|
var pfs0 = sect.Header.Pfs;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
expected = pfs0.MasterHash;
|
|
|
|
|
offset = pfs0.HashTableOffset;
|
|
|
|
|
size = pfs0.HashTableSize;
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Romfs:
|
2018-06-28 23:55:36 +02:00
|
|
|
|
var ivfc = sect.Header.Romfs.IvfcHeader;
|
|
|
|
|
expected = ivfc.MasterHash;
|
2018-07-05 23:37:30 +02:00
|
|
|
|
offset = ivfc.LevelHeaders[0].LogicalOffset;
|
|
|
|
|
size = 1 << ivfc.LevelHeaders[0].BlockSize;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
2018-07-11 00:16:25 +02:00
|
|
|
|
CheckBktrKey(sect);
|
|
|
|
|
return;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-11 00:16:25 +02:00
|
|
|
|
var stream = OpenSection(index, true);
|
|
|
|
|
if (stream == null) return;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
if (expected == null) return;
|
|
|
|
|
|
|
|
|
|
var hashTable = new byte[size];
|
|
|
|
|
stream.Position = offset;
|
|
|
|
|
stream.Read(hashTable, 0, hashTable.Length);
|
|
|
|
|
|
|
|
|
|
using (SHA256 hash = SHA256.Create())
|
|
|
|
|
{
|
|
|
|
|
actual = hash.ComputeHash(hashTable);
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
var validity = Util.ArraysEqual(expected, actual) ? Validity.Valid : Validity.Invalid;
|
|
|
|
|
|
|
|
|
|
sect.SuperblockHashValidity = validity;
|
|
|
|
|
if (sect.Type == SectionType.Romfs) sect.Romfs.IvfcLevels[0].HashValidity = validity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void VerifySection(int index, IProgressReport logger = null)
|
|
|
|
|
{
|
|
|
|
|
if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
|
var sect = Sections[index];
|
|
|
|
|
var stream = OpenSection(index, true);
|
|
|
|
|
logger?.LogMessage($"Verifying section {index}...");
|
|
|
|
|
|
|
|
|
|
switch (sect.Type)
|
|
|
|
|
{
|
|
|
|
|
case SectionType.Invalid:
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Pfs0:
|
|
|
|
|
VerifyPfs0(stream, sect.Pfs0, logger);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Romfs:
|
|
|
|
|
VerifyIvfc(stream, sect.Romfs.IvfcLevels, logger);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void VerifyPfs0(Stream section, Pfs0Section pfs0, IProgressReport logger = null)
|
|
|
|
|
{
|
|
|
|
|
var sb = pfs0.Superblock;
|
|
|
|
|
var table = new byte[sb.HashTableSize];
|
|
|
|
|
section.Position = sb.HashTableOffset;
|
|
|
|
|
section.Read(table, 0, table.Length);
|
|
|
|
|
|
|
|
|
|
pfs0.Validity = VerifyHashTable(section, table, sb.Pfs0Offset, sb.Pfs0Size, sb.BlockSize, false, logger);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void VerifyIvfc(Stream section, IvfcLevel[] levels, IProgressReport logger = null)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 1; i < levels.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
logger?.LogMessage($" Verifying IVFC Level {i}...");
|
|
|
|
|
var level = levels[i];
|
|
|
|
|
var table = new byte[level.HashSize];
|
|
|
|
|
section.Position = level.HashOffset;
|
|
|
|
|
section.Read(table, 0, table.Length);
|
|
|
|
|
level.HashValidity =
|
|
|
|
|
VerifyHashTable(section, table, level.DataOffset, level.DataSize, level.HashBlockSize, true, logger);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Validity VerifyHashTable(Stream section, byte[] hashTable, long dataOffset, long dataLen, long blockSize, bool isFinalBlockFull, IProgressReport logger = null)
|
|
|
|
|
{
|
|
|
|
|
const int hashSize = 0x20;
|
|
|
|
|
var currentBlock = new byte[blockSize];
|
|
|
|
|
var expectedHash = new byte[hashSize];
|
|
|
|
|
var blockCount = Util.DivideByRoundUp(dataLen, blockSize);
|
|
|
|
|
int curBlockSize = (int)blockSize;
|
|
|
|
|
section.Position = dataOffset;
|
|
|
|
|
logger?.SetTotal(blockCount);
|
|
|
|
|
|
|
|
|
|
using (SHA256 sha256 = SHA256.Create())
|
|
|
|
|
{
|
|
|
|
|
for (long i = 0; i < blockCount; i++)
|
|
|
|
|
{
|
|
|
|
|
var remaining = (dataLen - i * blockSize);
|
|
|
|
|
if (remaining < blockSize)
|
|
|
|
|
{
|
|
|
|
|
Array.Clear(currentBlock, 0, currentBlock.Length);
|
|
|
|
|
if (!isFinalBlockFull) curBlockSize = (int)remaining;
|
|
|
|
|
}
|
|
|
|
|
Array.Copy(hashTable, i * hashSize, expectedHash, 0, hashSize);
|
|
|
|
|
section.Read(currentBlock, 0, curBlockSize);
|
|
|
|
|
var actualHash = sha256.ComputeHash(currentBlock, 0, curBlockSize);
|
|
|
|
|
|
|
|
|
|
if (!Util.ArraysEqual(expectedHash, actualHash))
|
|
|
|
|
{
|
|
|
|
|
return Validity.Invalid;
|
|
|
|
|
}
|
|
|
|
|
logger?.ReportAdd(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Validity.Valid;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (!KeepOpen)
|
|
|
|
|
{
|
|
|
|
|
Stream?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-21 23:03:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-22 21:05:29 +02:00
|
|
|
|
public class NcaSection
|
2018-06-21 23:03:58 +02:00
|
|
|
|
{
|
2018-06-22 23:17:20 +02:00
|
|
|
|
public Stream Stream { get; set; }
|
2018-06-22 21:05:29 +02:00
|
|
|
|
public NcaFsHeader Header { get; set; }
|
|
|
|
|
public SectionType Type { get; set; }
|
|
|
|
|
public int SectionNum { get; set; }
|
|
|
|
|
public long Offset { get; set; }
|
|
|
|
|
public long Size { get; set; }
|
2018-06-28 22:02:23 +02:00
|
|
|
|
public Validity SuperblockHashValidity { get; set; }
|
2018-06-22 21:05:29 +02:00
|
|
|
|
|
2018-07-05 23:37:30 +02:00
|
|
|
|
public Pfs0Section Pfs0 { get; set; }
|
|
|
|
|
public RomfsSection Romfs { get; set; }
|
2018-08-16 00:33:45 +02:00
|
|
|
|
public bool IsExefs { get; internal set; }
|
2018-06-21 23:03:58 +02:00
|
|
|
|
}
|
2018-07-03 04:21:35 +02:00
|
|
|
|
|
|
|
|
|
public static class NcaExtensions
|
|
|
|
|
{
|
|
|
|
|
public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, IProgressReport logger = null)
|
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
if (index < 0 || index > 3) throw new IndexOutOfRangeException();
|
2018-07-03 04:21:35 +02:00
|
|
|
|
if (nca.Sections[index] == null) return;
|
|
|
|
|
|
|
|
|
|
var section = nca.OpenSection(index, raw);
|
2018-07-05 23:37:30 +02:00
|
|
|
|
var dir = Path.GetDirectoryName(filename);
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
2018-07-03 04:21:35 +02:00
|
|
|
|
|
|
|
|
|
using (var outFile = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
section.CopyStream(outFile, section.Length, logger);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void ExtractSection(this Nca nca, int index, string outputDir, IProgressReport logger = null)
|
|
|
|
|
{
|
2018-07-05 23:37:30 +02:00
|
|
|
|
if (index < 0 || index > 3) throw new IndexOutOfRangeException();
|
2018-07-03 04:21:35 +02:00
|
|
|
|
if (nca.Sections[index] == null) return;
|
|
|
|
|
|
|
|
|
|
var section = nca.Sections[index];
|
|
|
|
|
var stream = nca.OpenSection(index, false);
|
|
|
|
|
|
|
|
|
|
switch (section.Type)
|
|
|
|
|
{
|
|
|
|
|
case SectionType.Invalid:
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Pfs0:
|
2018-08-15 01:21:07 +02:00
|
|
|
|
var pfs0 = new Pfs(stream);
|
2018-07-03 04:21:35 +02:00
|
|
|
|
pfs0.Extract(outputDir, logger);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Romfs:
|
|
|
|
|
var romfs = new Romfs(stream);
|
|
|
|
|
romfs.Extract(outputDir, logger);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-05 23:37:30 +02:00
|
|
|
|
|
|
|
|
|
public static string Dump(this Nca nca)
|
|
|
|
|
{
|
|
|
|
|
int colLen = 36;
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
|
|
|
|
|
sb.AppendLine("NCA:");
|
|
|
|
|
PrintItem("Magic:", nca.Header.Magic);
|
|
|
|
|
PrintItem("Fixed-Key Signature:", nca.Header.Signature1);
|
|
|
|
|
PrintItem("NPDM Signature:", nca.Header.Signature2);
|
|
|
|
|
PrintItem("Content Size:", $"0x{nca.Header.NcaSize:x12}");
|
|
|
|
|
PrintItem("TitleID:", $"{nca.Header.TitleId:X16}");
|
|
|
|
|
PrintItem("SDK Version:", nca.Header.SdkVersion);
|
|
|
|
|
PrintItem("Distribution type:", nca.Header.Distribution);
|
|
|
|
|
PrintItem("Content Type:", nca.Header.ContentType);
|
|
|
|
|
PrintItem("Master Key Revision:", $"{nca.CryptoType} ({Util.GetKeyRevisionSummary(nca.CryptoType)})");
|
|
|
|
|
PrintItem("Encryption Type:", $"{(nca.HasRightsId ? "Titlekey crypto" : "Standard crypto")}");
|
|
|
|
|
|
|
|
|
|
if (nca.HasRightsId)
|
|
|
|
|
{
|
|
|
|
|
PrintItem("Rights ID:", nca.Header.RightsId);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
PrintItem("Key Area Encryption Key:", nca.Header.KaekInd);
|
|
|
|
|
sb.AppendLine("Key Area (Encrypted):");
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
|
{
|
|
|
|
|
PrintItem($" Key {i} (Encrypted):", nca.Header.EncryptedKeys[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb.AppendLine("Key Area (Decrypted):");
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
|
{
|
|
|
|
|
PrintItem($" Key {i} (Decrypted):", nca.DecryptedKeys[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PrintSections();
|
|
|
|
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
|
|
|
|
void PrintSections()
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine("Sections:");
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
|
{
|
|
|
|
|
NcaSection sect = nca.Sections[i];
|
|
|
|
|
if (sect == null) continue;
|
|
|
|
|
|
|
|
|
|
sb.AppendLine($" Section {i}:");
|
|
|
|
|
PrintItem(" Offset:", $"0x{sect.Offset:x12}");
|
|
|
|
|
PrintItem(" Size:", $"0x{sect.Size:x12}");
|
2018-08-16 00:33:45 +02:00
|
|
|
|
PrintItem(" Partition Type:", sect.IsExefs ? "ExeFS" : sect.Type.ToString());
|
2018-07-05 23:37:30 +02:00
|
|
|
|
PrintItem(" Section CTR:", sect.Header.Ctr);
|
|
|
|
|
|
|
|
|
|
switch (sect.Type)
|
|
|
|
|
{
|
|
|
|
|
case SectionType.Pfs0:
|
|
|
|
|
PrintPfs0(sect);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Romfs:
|
|
|
|
|
PrintRomfs(sect);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
sb.AppendLine(" Unknown/invalid superblock!");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PrintPfs0(NcaSection sect)
|
|
|
|
|
{
|
|
|
|
|
var sBlock = sect.Pfs0.Superblock;
|
|
|
|
|
PrintItem($" Superblock Hash{sect.SuperblockHashValidity.GetValidityString()}:", sBlock.MasterHash);
|
|
|
|
|
sb.AppendLine($" Hash Table{sect.Pfs0.Validity.GetValidityString()}:");
|
|
|
|
|
|
|
|
|
|
PrintItem(" Offset:", $"0x{sBlock.HashTableOffset:x12}");
|
|
|
|
|
PrintItem(" Size:", $"0x{sBlock.HashTableSize:x12}");
|
|
|
|
|
PrintItem(" Block Size:", $"0x{sBlock.BlockSize:x}");
|
|
|
|
|
PrintItem(" PFS0 Offset:", $"0x{sBlock.Pfs0Offset:x12}");
|
|
|
|
|
PrintItem(" PFS0 Size:", $"0x{sBlock.Pfs0Size:x12}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PrintRomfs(NcaSection sect)
|
|
|
|
|
{
|
|
|
|
|
var sBlock = sect.Romfs.Superblock;
|
|
|
|
|
var levels = sect.Romfs.IvfcLevels;
|
|
|
|
|
|
|
|
|
|
PrintItem($" Superblock Hash{sect.SuperblockHashValidity.GetValidityString()}:", sBlock.IvfcHeader.MasterHash);
|
|
|
|
|
PrintItem(" Magic:", sBlock.IvfcHeader.Magic);
|
|
|
|
|
PrintItem(" ID:", $"{sBlock.IvfcHeader.Id:x8}");
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < Romfs.IvfcMaxLevel; i++)
|
|
|
|
|
{
|
|
|
|
|
var level = levels[i];
|
|
|
|
|
sb.AppendLine($" Level {i}{level.HashValidity.GetValidityString()}:");
|
|
|
|
|
PrintItem(" Data Offset:", $"0x{level.DataOffset:x12}");
|
|
|
|
|
PrintItem(" Data Size:", $"0x{level.DataSize:x12}");
|
|
|
|
|
PrintItem(" Hash Offset:", $"0x{level.HashOffset:x12}");
|
|
|
|
|
PrintItem(" Hash BlockSize:", $"0x{level.HashBlockSize:x8}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PrintItem(string prefix, object data)
|
|
|
|
|
{
|
|
|
|
|
if (data is byte[] byteData)
|
|
|
|
|
{
|
|
|
|
|
sb.MemDump(prefix.PadRight(colLen), byteData);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine(prefix.PadRight(colLen) + data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GetValidityString(this Validity validity)
|
|
|
|
|
{
|
|
|
|
|
switch (validity)
|
|
|
|
|
{
|
|
|
|
|
case Validity.Invalid: return " (FAIL)";
|
|
|
|
|
case Validity.Valid: return " (GOOD)";
|
|
|
|
|
default: return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-03 04:21:35 +02:00
|
|
|
|
}
|
2018-06-21 16:25:20 +02:00
|
|
|
|
}
|