2018-06-21 23:03:58 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
using System.Security.Cryptography;
|
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-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-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))
|
|
|
|
|
{
|
|
|
|
|
Crypto.DecryptEcb(keyset.titlekeks[CryptoType], titleKey, DecryptedKeys[2], 0x10);
|
|
|
|
|
}
|
|
|
|
|
}
|
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-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
|
|
|
|
|
|
|
|
|
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-06-28 22:02:23 +02:00
|
|
|
|
switch (sect.Header.FsType)
|
|
|
|
|
{
|
|
|
|
|
case SectionFsType.Pfs0:
|
|
|
|
|
offset = sect.Offset + sect.Pfs0.Pfs0Offset;
|
|
|
|
|
size = sect.Pfs0.Pfs0Size;
|
|
|
|
|
break;
|
|
|
|
|
case SectionFsType.Romfs:
|
|
|
|
|
offset = sect.Offset + (long)sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1]
|
|
|
|
|
.LogicalOffset;
|
|
|
|
|
size = (long)sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize;
|
|
|
|
|
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:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
return Stream;
|
2018-06-21 16:25:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
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-06-26 00:26:47 +02:00
|
|
|
|
sect.Pfs0 = header.Pfs0;
|
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-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];
|
|
|
|
|
var stream = OpenSection(index, true);
|
|
|
|
|
|
|
|
|
|
byte[] expected = null;
|
|
|
|
|
byte[] actual;
|
|
|
|
|
long offset = 0;
|
|
|
|
|
long size = 0;
|
|
|
|
|
|
|
|
|
|
switch (sect.Type)
|
|
|
|
|
{
|
|
|
|
|
case SectionType.Invalid:
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Pfs0:
|
|
|
|
|
var pfs0 = sect.Header.Pfs0;
|
|
|
|
|
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;
|
|
|
|
|
offset = (long)ivfc.LevelHeaders[0].LogicalOffset;
|
|
|
|
|
size = 1 << (int)ivfc.LevelHeaders[0].BlockSize;
|
2018-06-28 22:02:23 +02:00
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sect.SuperblockHashValidity = Util.ArraysEqual(expected, actual) ? Validity.Valid : Validity.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
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-06-26 00:26:47 +02:00
|
|
|
|
public Pfs0Superblock Pfs0 { get; 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)
|
|
|
|
|
{
|
|
|
|
|
if(index < 0 || index > 3) throw new IndexOutOfRangeException();
|
|
|
|
|
if (nca.Sections[index] == null) return;
|
|
|
|
|
|
|
|
|
|
var section = nca.OpenSection(index, raw);
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(filename));
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
if(index < 0 || index > 3) throw new IndexOutOfRangeException();
|
|
|
|
|
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:
|
|
|
|
|
var pfs0 = new Pfs0(stream);
|
|
|
|
|
pfs0.Extract(outputDir, logger);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Romfs:
|
|
|
|
|
var romfs = new Romfs(stream);
|
|
|
|
|
romfs.Extract(outputDir, logger);
|
|
|
|
|
break;
|
|
|
|
|
case SectionType.Bktr:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-21 16:25:20 +02:00
|
|
|
|
}
|