Refactor NCA-related classes (#56)

* Begin refactoring NCA classes

* Section opening

* Add NcaNewExtensions

* hactoolnet: Move most nca code to the new api

* Verify nca signatures

* hactoolnet: process patched ncas

* Add NCA encryption counter generation method

From what I can tell, this is the current method used to generate the counter. It's possible that may change in the future.

* Verify NCAs

* Use NcaNew in SwitchFs

* Remove old NCA code

* Rename new NCA classes

* Move struct constants around

* hactoolnet: verify patched nca sections

* Misc
This commit is contained in:
Alex Barney 2019-05-06 13:41:51 -05:00 committed by GitHub
parent 7804a919d1
commit 72915c0425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1302 additions and 698 deletions

View File

@ -12,10 +12,19 @@ namespace LibHac.IO
private readonly object _locker = new object();
public Aes128CtrExStorage(IStorage baseStorage, IStorage bucketTreeHeader, IStorage bucketTreeData, byte[] key, long counterOffset, byte[] ctrHi, bool leaveOpen)
public Aes128CtrExStorage(IStorage baseStorage, IStorage bucketTreeData, byte[] key, long counterOffset, byte[] ctrHi, bool leaveOpen)
: base(baseStorage, key, counterOffset, ctrHi, leaveOpen)
{
BucketTree = new BucketTree<AesSubsectionEntry>(bucketTreeHeader, bucketTreeData);
BucketTree = new BucketTree<AesSubsectionEntry>(bucketTreeData);
SubsectionEntries = BucketTree.GetEntryList();
SubsectionOffsets = SubsectionEntries.Select(x => x.Offset).ToList();
}
public Aes128CtrExStorage(IStorage baseStorage, IStorage bucketTreeData, byte[] key, byte[] counter, bool leaveOpen)
: base(baseStorage, key, counter, leaveOpen)
{
BucketTree = new BucketTree<AesSubsectionEntry>(bucketTreeData);
SubsectionEntries = BucketTree.GetEntryList();
SubsectionOffsets = SubsectionEntries.Select(x => x.Offset).ToList();

View File

@ -1,5 +1,6 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
namespace LibHac.IO
{
@ -103,5 +104,15 @@ namespace LibHac.IO
// of byte 8 need to have their original value preserved
Counter[8] = (byte)((Counter[8] & 0xF0) | (int)(off & 0x0F));
}
public static byte[] CreateCounter(ulong hiBytes, long offset)
{
var counter = new byte[0x10];
BinaryPrimitives.WriteUInt64BigEndian(counter, hiBytes);
BinaryPrimitives.WriteInt64BigEndian(counter.AsSpan(8), offset / 0x10);
return counter;
}
}
}

View File

@ -7,13 +7,11 @@ namespace LibHac.IO
public class BucketTree<T> where T : BucketTreeEntry<T>, new()
{
private const int BucketAlignment = 0x4000;
public BucketTreeHeader Header { get; }
public BucketTreeBucket<OffsetEntry> BucketOffsets { get; }
public BucketTreeBucket<T>[] Buckets { get; }
public BucketTree(IStorage header, IStorage data)
public BucketTree(IStorage data)
{
Header = new BucketTreeHeader(header);
var reader = new BinaryReader(data.AsStream());
BucketOffsets = new BucketTreeBucket<OffsetEntry>(reader);
@ -43,24 +41,6 @@ namespace LibHac.IO
}
}
public class BucketTreeHeader
{
public string Magic;
public int Version;
public int NumEntries;
public int FieldC;
public BucketTreeHeader(IStorage storage)
{
var reader = new BinaryReader(storage.AsStream());
Magic = reader.ReadAscii(4);
Version = reader.ReadInt32();
NumEntries = reader.ReadInt32();
FieldC = reader.ReadInt32();
}
}
public class BucketTreeBucket<T> where T : BucketTreeEntry<T>, new()
{
public int Index;

View File

@ -13,13 +13,13 @@ namespace LibHac.IO
private BucketTree<RelocationEntry> BucketTree { get; }
private long _length;
public IndirectStorage(IStorage bucketTreeHeader, IStorage bucketTreeData, bool leaveOpen, params IStorage[] sources)
public IndirectStorage(IStorage bucketTreeData, bool leaveOpen, params IStorage[] sources)
{
Sources.AddRange(sources);
if (!leaveOpen) ToDispose.AddRange(sources);
BucketTree = new BucketTree<RelocationEntry>(bucketTreeHeader, bucketTreeData);
BucketTree = new BucketTree<RelocationEntry>(bucketTreeData);
RelocationEntries = BucketTree.GetEntryList();
RelocationOffsets = RelocationEntries.Select(x => x.Offset).ToList();

View File

@ -0,0 +1,8 @@
namespace LibHac.IO
{
internal static class Messages
{
public static string DestSpanTooSmall => "Destination array is not long enough to hold the requested data.";
public static string NcaSectionMissing => "NCA section does not exist.";
}
}

View File

@ -1,110 +1,105 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using LibHac.IO.RomFs;
namespace LibHac.IO.NcaUtils
{
public class Nca : IDisposable
public class Nca
{
private const int HeaderSize = 0xc00;
private const int HeaderSectorSize = 0x200;
private Keyset Keyset { get; }
public IStorage BaseStorage { get; }
public NcaHeader Header { get; }
public string NcaId { get; set; }
public string Filename { get; set; }
public bool HasRightsId { get; }
public int CryptoType { get; }
public byte[][] DecryptedKeys { get; } = Util.CreateJaggedArray<byte[][]>(4, 0x10);
public byte[] TitleKey { get; }
public byte[] TitleKeyDec { get; } = new byte[0x10];
private bool LeaveOpen { get; }
private Nca BaseNca { get; set; }
private IStorage BaseStorage { get; }
private Keyset Keyset { get; }
public Npdm.NpdmBinary Npdm { get; private set; }
private bool IsMissingTitleKey { get; set; }
private string MissingKeyName { get; set; }
public NcaSection[] Sections { get; } = new NcaSection[4];
public Nca(Keyset keyset, IStorage storage, bool leaveOpen)
public Nca(Keyset keyset, IStorage storage)
{
LeaveOpen = leaveOpen;
BaseStorage = storage;
Keyset = keyset;
Header = DecryptHeader();
CryptoType = Math.Max(Header.CryptoType, Header.CryptoType2);
if (CryptoType > 0) CryptoType--;
HasRightsId = !Header.RightsId.IsEmpty();
if (!HasRightsId)
{
DecryptKeyArea(keyset);
}
else if (keyset.TitleKeys.TryGetValue(Header.RightsId, out byte[] titleKey))
{
if (keyset.TitleKeks[CryptoType].IsEmpty())
{
MissingKeyName = $"titlekek_{CryptoType:x2}";
}
TitleKey = titleKey;
Crypto.DecryptEcb(keyset.TitleKeks[CryptoType], titleKey, TitleKeyDec, 0x10);
DecryptedKeys[2] = TitleKeyDec;
}
else
{
IsMissingTitleKey = true;
}
for (int i = 0; i < 4; i++)
{
NcaSection section = ParseSection(i);
if (section == null) continue;
Sections[i] = section;
}
BaseStorage = storage;
Header = new NcaHeader(keyset, storage);
}
/// <summary>
/// Opens the <see cref="IStorage"/> of the underlying NCA file.
/// </summary>
/// <returns>The <see cref="IStorage"/> that provides access to the entire raw NCA file.</returns>
public IStorage GetStorage()
{
return BaseStorage.AsReadOnly();
}
public bool CanOpenSection(int index)
public byte[] GetDecryptedKey(int index)
{
if (index < 0 || index > 3) throw new ArgumentOutOfRangeException(nameof(index));
NcaSection sect = Sections[index];
if (sect == null) return false;
int keyRevision = Util.GetMasterKeyRevision(Header.KeyGeneration);
byte[] keyAreaKey = Keyset.KeyAreaKeys[keyRevision][Header.KeyAreaKeyIndex];
return sect.Header.EncryptionType == NcaEncryptionType.None || !IsMissingTitleKey && string.IsNullOrWhiteSpace(MissingKeyName);
if (keyAreaKey.IsEmpty())
{
string keyName = $"key_area_key_{Keyset.KakNames[Header.KeyAreaKeyIndex]}_{keyRevision:x2}";
throw new MissingKeyException("Unable to decrypt NCA section.", keyName, KeyType.Common);
}
byte[] encryptedKey = Header.GetEncryptedKey(index).ToArray();
var decryptedKey = new byte[Crypto.Aes128Size];
Crypto.DecryptEcb(keyAreaKey, encryptedKey, decryptedKey, Crypto.Aes128Size);
return decryptedKey;
}
public bool CanOpenSection(NcaSectionType type)
public byte[] GetDecryptedTitleKey()
{
return CanOpenSection(GetSectionIndexFromType(type));
int keyRevision = Util.GetMasterKeyRevision(Header.KeyGeneration);
byte[] titleKek = Keyset.TitleKeks[keyRevision];
if (!Keyset.TitleKeys.TryGetValue(Header.RightsId.ToArray(), out byte[] encryptedKey))
{
throw new MissingKeyException("Missing NCA title key.", Header.RightsId.ToHexString(), KeyType.Title);
}
if (titleKek.IsEmpty())
{
string keyName = $"titlekek_{keyRevision:x2}";
throw new MissingKeyException("Unable to decrypt title key.", keyName, KeyType.Common);
}
var decryptedKey = new byte[Crypto.Aes128Size];
Crypto.DecryptEcb(titleKek, encryptedKey, decryptedKey, Crypto.Aes128Size);
return decryptedKey;
}
internal byte[] GetContentKey(NcaKeyType type)
{
return Header.HasRightsId ? GetDecryptedTitleKey() : GetDecryptedKey((int)type);
}
public bool CanOpenSection(NcaSectionType type) => CanOpenSection(GetSectionIndexFromType(type));
public bool CanOpenSection(int index)
{
if (!SectionExists(index)) return false;
if (Header.GetFsHeader(index).EncryptionType == NcaEncryptionType.None) return true;
int keyRevision = Util.GetMasterKeyRevision(Header.KeyGeneration);
if (Header.HasRightsId)
{
return Keyset.TitleKeys.ContainsKey(Header.RightsId.ToArray()) &&
!Keyset.TitleKeks[keyRevision].IsEmpty();
}
return !Keyset.KeyAreaKeys[keyRevision][Header.KeyAreaKeyIndex].IsEmpty();
}
public bool SectionExists(NcaSectionType type) => SectionExists(GetSectionIndexFromType(type));
public bool SectionExists(int index)
{
return Header.IsSectionEnabled(index);
}
private IStorage OpenEncryptedStorage(int index)
{
if (index < 0 || index > 3) throw new ArgumentOutOfRangeException(nameof(index));
if (!SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing);
NcaSection sect = Sections[index];
if (sect == null) throw new ArgumentOutOfRangeException(nameof(index), "Section is empty");
long offset = sect.Offset;
long size = sect.Size;
long offset = Header.GetSectionStartOffset(index);
long size = Header.GetSectionSize(index);
if (!Util.IsSubRange(offset, size, BaseStorage.GetSize()))
{
@ -115,105 +110,153 @@ namespace LibHac.IO.NcaUtils
return BaseStorage.Slice(offset, size);
}
private IStorage OpenDecryptedStorage(IStorage baseStorage, NcaSection sect)
private IStorage OpenDecryptedStorage(IStorage baseStorage, int index)
{
if (sect.Header.EncryptionType != NcaEncryptionType.None)
{
if (IsMissingTitleKey)
{
throw new MissingKeyException("Unable to decrypt NCA section.", Header.RightsId.ToHexString(), KeyType.Title);
}
NcaFsHeader header = Header.GetFsHeader(index);
if (!string.IsNullOrWhiteSpace(MissingKeyName))
{
throw new MissingKeyException("Unable to decrypt NCA section.", MissingKeyName, KeyType.Common);
}
}
switch (sect.Header.EncryptionType)
switch (header.EncryptionType)
{
case NcaEncryptionType.None:
return baseStorage;
case NcaEncryptionType.XTS:
throw new NotImplementedException("NCA sections using XTS are not supported");
return OpenAesXtsStorage(baseStorage, index);
case NcaEncryptionType.AesCtr:
return new CachedStorage(new Aes128CtrStorage(baseStorage, DecryptedKeys[2], sect.Offset, sect.Header.Ctr, true), 0x4000, 4, true);
return OpenAesCtrStorage(baseStorage, index);
case NcaEncryptionType.AesCtrEx:
BktrPatchInfo info = sect.Header.BktrInfo;
long bktrOffset = info.RelocationHeader.Offset;
long bktrSize = sect.Size - bktrOffset;
long dataSize = info.RelocationHeader.Offset;
IStorage bucketTreeHeader = new MemoryStorage(sect.Header.BktrInfo.EncryptionHeader.Header);
IStorage bucketTreeData = new CachedStorage(new Aes128CtrStorage(baseStorage.Slice(bktrOffset, bktrSize), DecryptedKeys[2], bktrOffset + sect.Offset, sect.Header.Ctr, true), 4, true);
IStorage encryptionBucketTreeData = bucketTreeData.Slice(info.EncryptionHeader.Offset - bktrOffset);
IStorage decStorage = new Aes128CtrExStorage(baseStorage.Slice(0, dataSize), bucketTreeHeader, encryptionBucketTreeData, DecryptedKeys[2], sect.Offset, sect.Header.Ctr, true);
decStorage = new CachedStorage(decStorage, 0x4000, 4, true);
return new ConcatenationStorage(new[] { decStorage, bucketTreeData }, true);
return OpenAesCtrExStorage(baseStorage, index);
default:
throw new ArgumentOutOfRangeException();
}
}
// ReSharper disable once UnusedParameter.Local
private IStorage OpenAesXtsStorage(IStorage baseStorage, int index)
{
throw new NotImplementedException("NCA sections using XTS are not supported yet.");
}
private IStorage OpenAesCtrStorage(IStorage baseStorage, int index)
{
NcaFsHeader fsHeader = Header.GetFsHeader(index);
byte[] key = GetContentKey(NcaKeyType.AesCtr);
byte[] counter = Aes128CtrStorage.CreateCounter(fsHeader.Counter, Header.GetSectionStartOffset(index));
var aesStorage = new Aes128CtrStorage(baseStorage, key, Header.GetSectionStartOffset(index), counter, true);
return new CachedStorage(aesStorage, 0x4000, 4, true);
}
private IStorage OpenAesCtrExStorage(IStorage baseStorage, int index)
{
NcaFsHeader fsHeader = Header.GetFsHeader(index);
NcaFsPatchInfo info = fsHeader.GetPatchInfo();
long sectionOffset = Header.GetSectionStartOffset(index);
long sectionSize = Header.GetSectionSize(index);
long bktrOffset = info.RelocationTreeOffset;
long bktrSize = sectionSize - bktrOffset;
long dataSize = info.RelocationTreeOffset;
byte[] key = GetContentKey(NcaKeyType.AesCtr);
byte[] counter = Aes128CtrStorage.CreateCounter(fsHeader.Counter, bktrOffset + sectionOffset);
byte[] counterEx = Aes128CtrStorage.CreateCounter(fsHeader.Counter, sectionOffset);
IStorage bucketTreeData = new CachedStorage(new Aes128CtrStorage(baseStorage.Slice(bktrOffset, bktrSize), key, counter, true), 4, true);
IStorage encryptionBucketTreeData = bucketTreeData.Slice(info.EncryptionTreeOffset - bktrOffset);
IStorage decStorage = new Aes128CtrExStorage(baseStorage.Slice(0, dataSize), encryptionBucketTreeData, key, counterEx, true);
decStorage = new CachedStorage(decStorage, 0x4000, 4, true);
return new ConcatenationStorage(new[] { decStorage, bucketTreeData }, true);
}
public IStorage OpenRawStorage(int index)
{
IStorage encryptedStorage = OpenEncryptedStorage(index);
IStorage decryptedStorage = OpenDecryptedStorage(encryptedStorage, Sections[index]);
IStorage decryptedStorage = OpenDecryptedStorage(encryptedStorage, index);
return decryptedStorage;
}
public IStorage OpenRawStorage(NcaSectionType type)
public IStorage OpenRawStorageWithPatch(Nca patchNca, int index)
{
return OpenRawStorage(GetSectionIndexFromType(type));
IStorage patchStorage = patchNca.OpenRawStorage(index);
IStorage baseStorage = OpenRawStorage(index);
NcaFsHeader header = patchNca.Header.GetFsHeader(index);
NcaFsPatchInfo patchInfo = header.GetPatchInfo();
if (patchInfo.RelocationTreeSize == 0)
{
return patchStorage;
}
IStorage relocationTableStorage = patchStorage.Slice(patchInfo.RelocationTreeOffset, patchInfo.RelocationTreeSize);
return new IndirectStorage(relocationTableStorage, true, baseStorage, patchStorage);
}
public IStorage OpenStorage(int index, IntegrityCheckLevel integrityCheckLevel)
{
IStorage rawStorage = OpenRawStorage(index);
NcaFsHeader header = Header.GetFsHeader(index);
NcaSection sect = Sections[index];
NcaFsHeader header = sect.Header;
// todo don't assume that ctr ex means it's a patch
if (header.EncryptionType == NcaEncryptionType.AesCtrEx)
{
return rawStorage.Slice(0, header.BktrInfo.RelocationHeader.Offset);
return rawStorage.Slice(0, header.GetPatchInfo().RelocationTreeOffset);
}
switch (header.HashType)
{
case NcaHashType.Sha256:
return InitIvfcForPartitionfs(header.Sha256Info, rawStorage, integrityCheckLevel, true);
return InitIvfcForPartitionFs(header.GetIntegrityInfoSha256(), rawStorage, integrityCheckLevel, true);
case NcaHashType.Ivfc:
return new HierarchicalIntegrityVerificationStorage(header.IvfcInfo, new MemoryStorage(header.IvfcInfo.MasterHash), rawStorage,
IntegrityStorageType.RomFs, integrityCheckLevel, true);
return InitIvfcForRomFs(header.GetIntegrityInfoIvfc(), rawStorage, integrityCheckLevel, true);
default:
throw new ArgumentOutOfRangeException();
}
}
public IStorage OpenStorage(NcaSectionType type, IntegrityCheckLevel integrityCheckLevel)
public IStorage OpenStorageWithPatch(Nca patchNca, int index, IntegrityCheckLevel integrityCheckLevel)
{
return OpenStorage(GetSectionIndexFromType(type), integrityCheckLevel);
IStorage rawStorage = OpenRawStorageWithPatch(patchNca, index);
NcaFsHeader header = patchNca.Header.GetFsHeader(index);
switch (header.HashType)
{
case NcaHashType.Sha256:
return InitIvfcForPartitionFs(header.GetIntegrityInfoSha256(), rawStorage, integrityCheckLevel, true);
case NcaHashType.Ivfc:
return InitIvfcForRomFs(header.GetIntegrityInfoIvfc(), rawStorage, integrityCheckLevel, true);
default:
throw new ArgumentOutOfRangeException();
}
}
public IFileSystem OpenFileSystem(int index, IntegrityCheckLevel integrityCheckLevel)
{
IStorage storage = OpenStorage(index, integrityCheckLevel);
NcaFsHeader header = Header.GetFsHeader(index);
switch (Sections[index].Header.Type)
return OpenFileSystem(storage, header);
}
public IFileSystem OpenFileSystemWithPatch(Nca patchNca, int index, IntegrityCheckLevel integrityCheckLevel)
{
IStorage storage = OpenStorageWithPatch(patchNca, index, integrityCheckLevel);
NcaFsHeader header = Header.GetFsHeader(index);
return OpenFileSystem(storage, header);
}
private IFileSystem OpenFileSystem(IStorage storage, NcaFsHeader header)
{
switch (header.FormatType)
{
case SectionType.Pfs0:
case NcaFormatType.Pfs0:
return new PartitionFileSystem(storage);
case SectionType.Romfs:
case NcaFormatType.Romfs:
return new RomFsFileSystem(storage);
case SectionType.Bktr:
// todo Possibly check if a patch completely replaces the original
throw new InvalidOperationException("Cannot open a patched section without the original");
default:
throw new ArgumentOutOfRangeException();
}
@ -224,10 +267,54 @@ namespace LibHac.IO.NcaUtils
return OpenFileSystem(GetSectionIndexFromType(type), integrityCheckLevel);
}
public IFileSystem OpenFileSystemWithPatch(Nca patchNca, NcaSectionType type, IntegrityCheckLevel integrityCheckLevel)
{
return OpenFileSystemWithPatch(patchNca, GetSectionIndexFromType(type), integrityCheckLevel);
}
public IStorage OpenRawStorage(NcaSectionType type)
{
return OpenRawStorage(GetSectionIndexFromType(type));
}
public IStorage OpenRawStorageWithPatch(Nca patchNca, NcaSectionType type)
{
return OpenRawStorageWithPatch(patchNca, GetSectionIndexFromType(type));
}
public IStorage OpenStorage(NcaSectionType type, IntegrityCheckLevel integrityCheckLevel)
{
return OpenStorage(GetSectionIndexFromType(type), integrityCheckLevel);
}
public IStorage OpenStorageWithPatch(Nca patchNca, NcaSectionType type, IntegrityCheckLevel integrityCheckLevel)
{
return OpenStorageWithPatch(patchNca, GetSectionIndexFromType(type), integrityCheckLevel);
}
public IStorage OpenDecryptedNca()
{
var builder = new ConcatenationStorageBuilder();
builder.Add(OpenDecryptedHeaderStorage(), 0);
for (int i = 0; i < NcaHeader.SectionCount; i++)
{
if (Header.IsSectionEnabled(i))
{
builder.Add(OpenRawStorage(i), Header.GetSectionStartOffset(i));
}
}
return builder.Build();
}
private int GetSectionIndexFromType(NcaSectionType type)
{
ContentType contentType = Header.ContentType;
return SectionIndexFromType(type, Header.ContentType);
}
public static int SectionIndexFromType(NcaSectionType type, ContentType contentType)
{
switch (type)
{
case NcaSectionType.Code when contentType == ContentType.Program: return 0;
@ -238,11 +325,25 @@ namespace LibHac.IO.NcaUtils
}
}
private static HierarchicalIntegrityVerificationStorage InitIvfcForPartitionfs(Sha256Info sb,
public static NcaSectionType SectionTypeFromIndex(int index, ContentType contentType)
{
switch (index)
{
case 0 when contentType == ContentType.Program: return NcaSectionType.Code;
case 1 when contentType == ContentType.Program: return NcaSectionType.Data;
case 2 when contentType == ContentType.Program: return NcaSectionType.Logo;
case 0: return NcaSectionType.Data;
default: throw new ArgumentOutOfRangeException(nameof(index), "NCA type does not contain this index.");
}
}
private static HierarchicalIntegrityVerificationStorage InitIvfcForPartitionFs(NcaFsIntegrityInfoSha256 info,
IStorage pfsStorage, IntegrityCheckLevel integrityCheckLevel, bool leaveOpen)
{
IStorage hashStorage = pfsStorage.Slice(sb.HashTableOffset, sb.HashTableSize, leaveOpen);
IStorage dataStorage = pfsStorage.Slice(sb.DataOffset, sb.DataSize, leaveOpen);
Debug.Assert(info.LevelCount == 2);
IStorage hashStorage = pfsStorage.Slice(info.GetLevelOffset(0), info.GetLevelSize(0), leaveOpen);
IStorage dataStorage = pfsStorage.Slice(info.GetLevelOffset(1), info.GetLevelSize(1), leaveOpen);
var initInfo = new IntegrityVerificationInfo[3];
@ -250,7 +351,7 @@ namespace LibHac.IO.NcaUtils
initInfo[0] = new IntegrityVerificationInfo
{
// todo Get hash directly from header
Data = new MemoryStorage(sb.MasterHash),
Data = new MemoryStorage(info.MasterHash.ToArray()),
BlockSize = 0,
Type = IntegrityStorageType.PartitionFs
@ -259,104 +360,67 @@ namespace LibHac.IO.NcaUtils
initInfo[1] = new IntegrityVerificationInfo
{
Data = hashStorage,
BlockSize = (int)sb.HashTableSize,
BlockSize = (int)info.GetLevelSize(0),
Type = IntegrityStorageType.PartitionFs
};
initInfo[2] = new IntegrityVerificationInfo
{
Data = dataStorage,
BlockSize = sb.BlockSize,
BlockSize = info.BlockSize,
Type = IntegrityStorageType.PartitionFs
};
return new HierarchicalIntegrityVerificationStorage(initInfo, integrityCheckLevel, leaveOpen);
}
public bool SectionExists(int index)
private static HierarchicalIntegrityVerificationStorage InitIvfcForRomFs(NcaFsIntegrityInfoIvfc ivfc,
IStorage dataStorage, IntegrityCheckLevel integrityCheckLevel, bool leaveOpen)
{
if (index < 0 || index > 3) return false;
var initInfo = new IntegrityVerificationInfo[ivfc.LevelCount];
return Sections[index] != null;
}
public bool SectionExists(NcaSectionType type)
{
return SectionExists(GetSectionIndexFromType(type));
}
/// <summary>
/// Sets a base <see cref="Nca"/> to use when reading patches.
/// </summary>
/// <param name="baseNca">The base <see cref="Nca"/></param>
public void SetBaseNca(Nca baseNca) => BaseNca = baseNca;
/// <summary>
/// Validates the master hash and store the result in <see cref="NcaSection.MasterHashValidity"/> for each <see cref="NcaSection"/>.
/// </summary>
public void ValidateMasterHashes()
{
for (int i = 0; i < 4; i++)
initInfo[0] = new IntegrityVerificationInfo
{
if (Sections[i] == null) continue;
ValidateMasterHash(i);
}
}
Data = new MemoryStorage(ivfc.MasterHash.ToArray()),
BlockSize = 0
};
public void ParseNpdm()
{
if (Header.ContentType != ContentType.Program) return;
IFileSystem pfs = OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid);
if (!pfs.FileExists("main.npdm")) return;
IFile npdmStorage = pfs.OpenFile("main.npdm", OpenMode.Read);
Npdm = new Npdm.NpdmBinary(npdmStorage.AsStream(), Keyset);
Header.ValidateNpdmSignature(Npdm.AciD.Rsa2048Modulus);
}
public IStorage OpenDecryptedNca()
{
var builder = new ConcatenationStorageBuilder();
builder.Add(OpenHeaderStorage(), 0);
foreach (NcaSection section in Sections.Where(x => x != null))
for (int i = 1; i < ivfc.LevelCount; i++)
{
builder.Add(OpenRawStorage(section.SectionNum), section.Offset);
initInfo[i] = new IntegrityVerificationInfo
{
Data = dataStorage.Slice(ivfc.GetLevelOffset(i - 1), ivfc.GetLevelSize(i - 1)),
BlockSize = 1 << ivfc.GetLevelBlockSize(i - 1),
Type = IntegrityStorageType.RomFs
};
}
return builder.Build();
return new HierarchicalIntegrityVerificationStorage(initInfo, integrityCheckLevel, leaveOpen);
}
private NcaHeader DecryptHeader()
public IStorage OpenDecryptedHeaderStorage()
{
if (Keyset.HeaderKey.IsEmpty())
{
throw new MissingKeyException("Unable to decrypt NCA header.", "header_key", KeyType.Common);
}
return new NcaHeader(new BinaryReader(OpenHeaderStorage().AsStream()), Keyset);
}
public IStorage OpenHeaderStorage()
{
long size = HeaderSize;
long firstSectionOffset = long.MaxValue;
bool hasEnabledSection = false;
// Encrypted portion continues until the first section
if (Sections.Any(x => x != null))
for (int i = 0; i < NcaHeader.SectionCount; i++)
{
size = Sections.Where(x => x != null).Min(x => x.Offset);
if (Header.IsSectionEnabled(i))
{
hasEnabledSection = true;
firstSectionOffset = Math.Min(firstSectionOffset, Header.GetSectionStartOffset(i));
}
}
IStorage header = new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(0, size), Keyset.HeaderKey, HeaderSectorSize, true), 1, true);
long headerSize = hasEnabledSection ? NcaHeader.HeaderSize : firstSectionOffset;
IStorage header = new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(0, headerSize), Keyset.HeaderKey, NcaHeader.HeaderSectorSize, true), 1, true);
int version = ReadHeaderVersion(header);
if (version == 2)
{
header = OpenNca2Header(size);
header = OpenNca2Header(headerSize);
}
return header;
@ -364,182 +428,74 @@ namespace LibHac.IO.NcaUtils
private int ReadHeaderVersion(IStorage header)
{
if (Header != null)
{
return Header.Version;
}
else
{
Span<byte> buf = stackalloc byte[1];
header.Read(buf, 0x203);
return buf[0] - '0';
}
Span<byte> buf = stackalloc byte[1];
header.Read(buf, 0x203);
return buf[0] - '0';
}
private IStorage OpenNca2Header(long size)
{
var sources = new List<IStorage>();
sources.Add(new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(0, 0x400), Keyset.HeaderKey, HeaderSectorSize, true), 1, true));
const int sectorSize = NcaHeader.HeaderSectorSize;
for (int i = 0x400; i < size; i += HeaderSectorSize)
var sources = new List<IStorage>();
sources.Add(new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(0, 0x400), Keyset.HeaderKey, sectorSize, true), 1, true));
for (int i = 0x400; i < size; i += sectorSize)
{
sources.Add(new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(i, HeaderSectorSize), Keyset.HeaderKey, HeaderSectorSize, true), 1, true));
sources.Add(new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(i, sectorSize), Keyset.HeaderKey, sectorSize, true), 1, true));
}
return new ConcatenationStorage(sources, true);
}
private void DecryptKeyArea(Keyset keyset)
public Validity VerifyHeaderSignature()
{
if (keyset.KeyAreaKeys[CryptoType][Header.KaekInd].IsEmpty())
{
MissingKeyName = $"key_area_key_{Keyset.KakNames[Header.KaekInd]}_{CryptoType:x2}";
return;
}
for (int i = 0; i < 4; i++)
{
Crypto.DecryptEcb(keyset.KeyAreaKeys[CryptoType][Header.KaekInd], Header.EncryptedKeys[i],
DecryptedKeys[i], 0x10);
}
return Header.VerifySignature1(Keyset.NcaHdrFixedKeyModulus);
}
private NcaSection ParseSection(int index)
internal void GenerateAesCounter(int sectionIndex, CnmtContentType type, int minorVersion)
{
NcaSectionEntry entry = Header.SectionEntries[index];
NcaFsHeader header = Header.FsHeaders[index];
if (entry.MediaStartOffset == 0) return null;
int counterType;
int counterVersion;
var sect = new NcaSection();
NcaFsHeader header = Header.GetFsHeader(sectionIndex);
if (header.EncryptionType != NcaEncryptionType.AesCtr &&
header.EncryptionType != NcaEncryptionType.AesCtrEx) return;
sect.SectionNum = index;
sect.Offset = Util.MediaToReal(entry.MediaStartOffset);
sect.Size = Util.MediaToReal(entry.MediaEndOffset) - sect.Offset;
sect.Header = header;
sect.Type = header.Type;
return sect;
}
private void CheckBktrKey(NcaSection sect)
{
// The encryption subsection table in the bktr partition contains the length of the entire partition.
// The encryption table is always located immediately following the partition data
// Decrypt this value and compare it to the encryption table offset found in the NCA header
long offset = sect.Header.BktrInfo.EncryptionHeader.Offset;
using (var streamDec = new CachedStorage(new Aes128CtrStorage(GetStorage().Slice(sect.Offset, sect.Size), DecryptedKeys[2], sect.Offset, sect.Header.Ctr, true), 0x4000, 4, false))
switch (type)
{
var reader = new BinaryReader(streamDec.AsStream());
reader.BaseStream.Position = offset + 8;
long size = reader.ReadInt64();
if (size != offset)
{
sect.MasterHashValidity = Validity.Invalid;
}
}
}
private void ValidateMasterHash(int index)
{
if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
NcaSection sect = Sections[index];
if (!CanOpenSection(index))
{
sect.MasterHashValidity = Validity.MissingKey;
return;
}
byte[] expected = sect.GetMasterHash();
long offset = 0;
long size = 0;
switch (sect.Header.HashType)
{
case NcaHashType.Sha256:
offset = sect.Header.Sha256Info.HashTableOffset;
size = sect.Header.Sha256Info.HashTableSize;
case CnmtContentType.Program:
counterType = sectionIndex + 1;
break;
case NcaHashType.Ivfc when sect.Header.EncryptionType == NcaEncryptionType.AesCtrEx:
CheckBktrKey(sect);
return;
case NcaHashType.Ivfc:
offset = sect.Header.IvfcInfo.LevelHeaders[0].Offset;
size = 1 << sect.Header.IvfcInfo.LevelHeaders[0].BlockSizePower;
case CnmtContentType.HtmlDocument:
counterType = (int)CnmtContentType.HtmlDocument;
break;
case CnmtContentType.LegalInformation:
counterType = (int)CnmtContentType.LegalInformation;
break;
default:
counterType = 0;
break;
}
IStorage storage = OpenRawStorage(index);
var hashTable = new byte[size];
storage.Read(hashTable, offset);
sect.MasterHashValidity = Crypto.CheckMemoryHashTable(hashTable, expected, 0, hashTable.Length);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
// Version of firmware NCAs appears to always be 0
// Haven't checked delta fragment NCAs
switch (Header.ContentType)
{
BaseStorage?.Flush();
BaseNca?.BaseStorage?.Flush();
if (!LeaveOpen)
{
BaseStorage?.Dispose();
BaseNca?.Dispose();
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
public class NcaSection
{
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; }
public Validity MasterHashValidity
{
get
{
if (Header.HashType == NcaHashType.Ivfc) return Header.IvfcInfo.LevelHeaders[0].HashValidity;
if (Header.HashType == NcaHashType.Sha256) return Header.Sha256Info.MasterHashValidity;
return Validity.Unchecked;
}
set
{
if (Header.HashType == NcaHashType.Ivfc) Header.IvfcInfo.LevelHeaders[0].HashValidity = value;
if (Header.HashType == NcaHashType.Sha256) Header.Sha256Info.MasterHashValidity = value;
}
}
public byte[] GetMasterHash()
{
var hash = new byte[Crypto.Sha256DigestSize];
switch (Header.HashType)
{
case NcaHashType.Sha256:
Array.Copy(Header.Sha256Info.MasterHash, hash, Crypto.Sha256DigestSize);
case ContentType.Program:
case ContentType.Manual:
counterVersion = Math.Max(minorVersion - 1, 0);
break;
case NcaHashType.Ivfc:
Array.Copy(Header.IvfcInfo.MasterHash, hash, Crypto.Sha256DigestSize);
case ContentType.PublicData:
counterVersion = minorVersion << 16;
break;
default:
counterVersion = 0;
break;
}
return hash;
header.CounterType = counterType;
header.CounterVersion = counterVersion;
}
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics;
namespace LibHac.IO.NcaUtils
{
@ -46,11 +48,87 @@ namespace LibHac.IO.NcaUtils
fs.Extract(outputDir, logger);
}
public static Validity ValidateSectionMasterHash(this Nca nca, int index)
{
if (!nca.SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing);
if (!nca.CanOpenSection(index)) return Validity.MissingKey;
NcaFsHeader header = nca.Header.GetFsHeader(index);
// The base data is needed to validate the hash, so use a trick involving the AES-CTR extended
// encryption table to check if the decryption is invalid.
// todo: If the patch replaces the data checked by the master hash, use that directly
if (header.IsPatchSection())
{
if (header.EncryptionType != NcaEncryptionType.AesCtrEx) return Validity.Unchecked;
Validity ctrExValidity = ValidateCtrExDecryption(nca, index);
return ctrExValidity == Validity.Invalid ? Validity.Invalid : Validity.Unchecked;
}
byte[] expectedHash;
long offset;
long size;
switch (header.HashType)
{
case NcaHashType.Ivfc:
NcaFsIntegrityInfoIvfc ivfcInfo = header.GetIntegrityInfoIvfc();
expectedHash = ivfcInfo.MasterHash.ToArray();
offset = ivfcInfo.GetLevelOffset(0);
size = 1 << ivfcInfo.GetLevelBlockSize(0);
break;
case NcaHashType.Sha256:
NcaFsIntegrityInfoSha256 sha256Info = header.GetIntegrityInfoSha256();
expectedHash = sha256Info.MasterHash.ToArray();
offset = sha256Info.GetLevelOffset(0);
size = sha256Info.GetLevelSize(0);
break;
default:
return Validity.Unchecked;
}
IStorage storage = nca.OpenRawStorage(index);
var data = new byte[size];
storage.Read(data, offset);
byte[] actualHash = Crypto.ComputeSha256(data, 0, data.Length);
if (Util.ArraysEqual(expectedHash, actualHash)) return Validity.Valid;
return Validity.Invalid;
}
private static Validity ValidateCtrExDecryption(Nca nca, int index)
{
// The encryption subsection table in an AesCtrEx-encrypted partition contains the length of the entire partition.
// The encryption table is always located immediately following the partition data, so the offset value of the encryption
// table located in the NCA header should be the same as the size read from the encryption table.
Debug.Assert(nca.CanOpenSection(index));
NcaFsPatchInfo header = nca.Header.GetFsHeader(index).GetPatchInfo();
IStorage decryptedStorage = nca.OpenRawStorage(index);
Span<byte> buffer = stackalloc byte[sizeof(long)];
decryptedStorage.Read(buffer, header.EncryptionTreeOffset + 8);
long readDataSize = BinaryPrimitives.ReadInt64LittleEndian(buffer);
if (header.EncryptionTreeOffset != readDataSize) return Validity.Invalid;
return Validity.Valid;
}
public static Validity VerifyNca(this Nca nca, IProgressReport logger = null, bool quiet = false)
{
for (int i = 0; i < 3; i++)
{
if (nca.Sections[i] != null)
if (nca.CanOpenSection(i))
{
Validity sectionValidity = VerifySection(nca, i, logger, quiet);
@ -63,28 +141,48 @@ namespace LibHac.IO.NcaUtils
public static Validity VerifySection(this Nca nca, int index, IProgressReport logger = null, bool quiet = false)
{
if (nca.Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
NcaSection sect = nca.Sections[index];
NcaHashType hashType = sect.Header.HashType;
NcaFsHeader sect = nca.Header.GetFsHeader(index);
NcaHashType hashType = sect.HashType;
if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked;
var stream = nca.OpenStorage(index, IntegrityCheckLevel.IgnoreOnInvalid, false)
var stream = nca.OpenStorage(index, IntegrityCheckLevel.IgnoreOnInvalid)
as HierarchicalIntegrityVerificationStorage;
if (stream == null) return Validity.Unchecked;
if (!quiet) logger?.LogMessage($"Verifying section {index}...");
Validity validity = stream.Validate(true, logger);
if (hashType == NcaHashType.Ivfc)
return validity;
}
public static Validity VerifyNca(this Nca nca, Nca patchNca, IProgressReport logger = null, bool quiet = false)
{
for (int i = 0; i < 3; i++)
{
stream.SetLevelValidities(sect.Header.IvfcInfo);
}
else if (hashType == NcaHashType.Sha256)
{
sect.Header.Sha256Info.HashValidity = validity;
if (patchNca.CanOpenSection(i))
{
Validity sectionValidity = VerifySection(nca, patchNca, i, logger, quiet);
if (sectionValidity == Validity.Invalid) return Validity.Invalid;
}
}
return Validity.Valid;
}
public static Validity VerifySection(this Nca nca, Nca patchNca, int index, IProgressReport logger = null, bool quiet = false)
{
NcaFsHeader sect = nca.Header.GetFsHeader(index);
NcaHashType hashType = sect.HashType;
if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked;
var stream = nca.OpenStorageWithPatch(patchNca, index, IntegrityCheckLevel.IgnoreOnInvalid)
as HierarchicalIntegrityVerificationStorage;
if (stream == null) return Validity.Unchecked;
if (!quiet) logger?.LogMessage($"Verifying section {index}...");
Validity validity = stream.Validate(true, logger);
return validity;
}
}

View File

@ -1,71 +1,97 @@
using System.IO;
using System.Linq;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.IO.NcaUtils
{
public class NcaFsHeader
public struct NcaFsHeader
{
public short Version;
public NcaFormatType FormatType;
public NcaHashType HashType;
public NcaEncryptionType EncryptionType;
public SectionType Type;
private readonly Memory<byte> _header;
public IvfcHeader IvfcInfo;
public Sha256Info Sha256Info;
public BktrPatchInfo BktrInfo;
public byte[] Ctr;
public NcaFsHeader(BinaryReader reader)
public NcaFsHeader(Memory<byte> headerData)
{
long start = reader.BaseStream.Position;
Version = reader.ReadInt16();
FormatType = (NcaFormatType)reader.ReadByte();
HashType = (NcaHashType)reader.ReadByte();
EncryptionType = (NcaEncryptionType)reader.ReadByte();
reader.BaseStream.Position += 3;
_header = headerData;
}
switch (HashType)
{
case NcaHashType.Sha256:
Sha256Info = new Sha256Info(reader);
break;
case NcaHashType.Ivfc:
IvfcInfo = new IvfcHeader(reader);
break;
}
private ref FsHeaderStruct Header => ref Unsafe.As<byte, FsHeaderStruct>(ref _header.Span[0]);
if (EncryptionType == NcaEncryptionType.AesCtrEx)
{
BktrInfo = new BktrPatchInfo();
public short Version
{
get => Header.Version;
set => Header.Version = value;
}
reader.BaseStream.Position = start + 0x100;
public NcaFormatType FormatType
{
get => (NcaFormatType)Header.FormatType;
set => Header.FormatType = (byte)value;
}
BktrInfo.RelocationHeader = new BktrHeader(reader);
BktrInfo.EncryptionHeader = new BktrHeader(reader);
}
public NcaHashType HashType
{
get => (NcaHashType)Header.HashType;
set => Header.HashType = (byte)value;
}
if (FormatType == NcaFormatType.Pfs0)
{
Type = SectionType.Pfs0;
}
else if (FormatType == NcaFormatType.Romfs)
{
if (EncryptionType == NcaEncryptionType.AesCtrEx)
{
Type = SectionType.Bktr;
}
else
{
Type = SectionType.Romfs;
}
}
public NcaEncryptionType EncryptionType
{
get => (NcaEncryptionType)Header.EncryptionType;
set => Header.EncryptionType = (byte)value;
}
reader.BaseStream.Position = start + 0x140;
Ctr = reader.ReadBytes(8).Reverse().ToArray();
public NcaFsIntegrityInfoIvfc GetIntegrityInfoIvfc()
{
return new NcaFsIntegrityInfoIvfc(_header.Slice(FsHeaderStruct.IntegrityInfoOffset, FsHeaderStruct.IntegrityInfoSize));
}
reader.BaseStream.Position = start + 512;
public NcaFsIntegrityInfoSha256 GetIntegrityInfoSha256()
{
return new NcaFsIntegrityInfoSha256(_header.Slice(FsHeaderStruct.IntegrityInfoOffset, FsHeaderStruct.IntegrityInfoSize));
}
public NcaFsPatchInfo GetPatchInfo()
{
return new NcaFsPatchInfo(_header.Slice(FsHeaderStruct.PatchInfoOffset, FsHeaderStruct.PatchInfoSize));
}
public bool IsPatchSection()
{
return GetPatchInfo().RelocationTreeSize != 0;
}
public ulong Counter
{
get => Header.UpperCounter;
set => Header.UpperCounter = value;
}
public int CounterType
{
get => Header.CounterType;
set => Header.CounterType = value;
}
public int CounterVersion
{
get => Header.CounterVersion;
set => Header.CounterVersion = value;
}
[StructLayout(LayoutKind.Explicit)]
private struct FsHeaderStruct
{
public const int IntegrityInfoOffset = 8;
public const int IntegrityInfoSize = 0xF8;
public const int PatchInfoOffset = 0x100;
public const int PatchInfoSize = 0x40;
[FieldOffset(0)] public short Version;
[FieldOffset(2)] public byte FormatType;
[FieldOffset(3)] public byte HashType;
[FieldOffset(4)] public byte EncryptionType;
[FieldOffset(0x140)] public ulong UpperCounter;
[FieldOffset(0x140)] public int CounterType;
[FieldOffset(0x144)] public int CounterVersion;
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.IO.NcaUtils
{
public struct NcaFsIntegrityInfoIvfc
{
private readonly Memory<byte> _data;
public NcaFsIntegrityInfoIvfc(Memory<byte> data)
{
_data = data;
}
private ref IvfcStruct Data => ref Unsafe.As<byte, IvfcStruct>(ref _data.Span[0]);
private ref IvfcLevel GetLevelInfo(int index)
{
ValidateLevelIndex(index);
int offset = IvfcStruct.IvfcLevelsOffset + IvfcLevel.IvfcLevelSize * index;
return ref Unsafe.As<byte, IvfcLevel>(ref _data.Span[offset]);
}
public uint Magic
{
get => Data.Magic;
set => Data.Magic = value;
}
public int Version
{
get => Data.Version;
set => Data.Version = value;
}
public int MasterHashSize
{
get => Data.MasterHashSize;
set => Data.MasterHashSize = value;
}
public int LevelCount
{
get => Data.LevelCount;
set => Data.LevelCount = value;
}
public Span<byte> SaltSource => _data.Span.Slice(IvfcStruct.SaltSourceOffset, IvfcStruct.SaltSourceSize);
public Span<byte> MasterHash => _data.Span.Slice(IvfcStruct.MasterHashOffset, MasterHashSize);
public ref long GetLevelOffset(int index) => ref GetLevelInfo(index).Offset;
public ref long GetLevelSize(int index) => ref GetLevelInfo(index).Size;
public ref int GetLevelBlockSize(int index) => ref GetLevelInfo(index).BlockSize;
private static void ValidateLevelIndex(int index)
{
if (index < 0 || index > 6)
{
throw new ArgumentOutOfRangeException($"IVFC level index must be between 0 and 6. Actual: {index}");
}
}
[StructLayout(LayoutKind.Explicit)]
private struct IvfcStruct
{
public const int IvfcLevelsOffset = 0x10;
public const int SaltSourceOffset = 0xA0;
public const int SaltSourceSize = 0x20;
public const int MasterHashOffset = 0xC0;
[FieldOffset(0)] public uint Magic;
[FieldOffset(4)] public int Version;
[FieldOffset(8)] public int MasterHashSize;
[FieldOffset(12)] public int LevelCount;
}
[StructLayout(LayoutKind.Explicit, Size = IvfcLevelSize)]
private struct IvfcLevel
{
public const int IvfcLevelSize = 0x18;
[FieldOffset(0)] public long Offset;
[FieldOffset(8)] public long Size;
[FieldOffset(0x10)] public int BlockSize;
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.IO.NcaUtils
{
public struct NcaFsIntegrityInfoSha256
{
private readonly Memory<byte> _data;
public NcaFsIntegrityInfoSha256(Memory<byte> data)
{
_data = data;
}
private ref Sha256Struct Data => ref Unsafe.As<byte, Sha256Struct>(ref _data.Span[0]);
private ref Sha256Level GetLevelInfo(int index)
{
ValidateLevelIndex(index);
int offset = Sha256Struct.Sha256LevelOffset + Sha256Level.Sha256LevelSize * index;
return ref Unsafe.As<byte, Sha256Level>(ref _data.Span[offset]);
}
public int BlockSize
{
get => Data.BlockSize;
set => Data.BlockSize = value;
}
public int LevelCount
{
get => Data.LevelCount;
set => Data.LevelCount = value;
}
public Span<byte> MasterHash => _data.Span.Slice(Sha256Struct.MasterHashOffset, Sha256Struct.MasterHashSize);
public ref long GetLevelOffset(int index) => ref GetLevelInfo(index).Offset;
public ref long GetLevelSize(int index) => ref GetLevelInfo(index).Size;
private static void ValidateLevelIndex(int index)
{
if (index < 0 || index > 5)
{
throw new ArgumentOutOfRangeException($"IVFC level index must be between 0 and 5. Actual: {index}");
}
}
[StructLayout(LayoutKind.Explicit)]
private struct Sha256Struct
{
public const int MasterHashOffset = 0;
public const int MasterHashSize = 0x20;
public const int Sha256LevelOffset = 0x28;
[FieldOffset(0x20)] public int BlockSize;
[FieldOffset(0x24)] public int LevelCount;
}
[StructLayout(LayoutKind.Explicit)]
private struct Sha256Level
{
public const int Sha256LevelSize = 0x10;
[FieldOffset(0)] public long Offset;
[FieldOffset(8)] public long Size;
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.IO.NcaUtils
{
public struct NcaFsPatchInfo
{
private readonly Memory<byte> _data;
public NcaFsPatchInfo(Memory<byte> data)
{
_data = data;
}
private ref PatchInfoStruct Data => ref Unsafe.As<byte, PatchInfoStruct>(ref _data.Span[0]);
public long RelocationTreeOffset
{
get => Data.RelocationTreeOffset;
set => Data.RelocationTreeOffset = value;
}
public long RelocationTreeSize
{
get => Data.RelocationTreeSize;
set => Data.RelocationTreeSize = value;
}
public long EncryptionTreeOffset
{
get => Data.EncryptionTreeOffset;
set => Data.EncryptionTreeOffset = value;
}
public long EncryptionTreeSize
{
get => Data.EncryptionTreeSize;
set => Data.EncryptionTreeSize = value;
}
public Span<byte> RelocationTreeHeader => _data.Span.Slice(0x10, 0x10);
public Span<byte> EncryptionTreeHeader => _data.Span.Slice(0x30, 0x10);
[StructLayout(LayoutKind.Explicit)]
private struct PatchInfoStruct
{
[FieldOffset(0x00)] public long RelocationTreeOffset;
[FieldOffset(0x08)] public long RelocationTreeSize;
[FieldOffset(0x20)] public long EncryptionTreeOffset;
[FieldOffset(0x28)] public long EncryptionTreeSize;
}
}
}

View File

@ -0,0 +1,272 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.IO.NcaUtils
{
public struct NcaHeader
{
internal const int HeaderSize = 0xC00;
internal const int HeaderSectorSize = 0x200;
internal const int BlockSize = 0x200;
internal const int SectionCount = 4;
private readonly Memory<byte> _header;
public NcaHeader(IStorage headerStorage)
{
_header = new byte[HeaderSize];
headerStorage.Read(_header.Span, 0);
}
public NcaHeader(Keyset keyset, IStorage headerStorage)
{
_header = DecryptHeader(keyset, headerStorage);
}
private ref NcaHeaderStruct Header => ref Unsafe.As<byte, NcaHeaderStruct>(ref _header.Span[0]);
public Span<byte> Signature1 => _header.Span.Slice(0, 0x100);
public Span<byte> Signature2 => _header.Span.Slice(0x100, 0x100);
public uint Magic
{
get => Header.Magic;
set => Header.Magic = value;
}
public int Version => _header.Span[0x203] - '0';
public DistributionType DistributionType
{
get => (DistributionType)Header.DistributionType;
set => Header.DistributionType = (byte)value;
}
public ContentType ContentType
{
get => (ContentType)Header.ContentType;
set => Header.ContentType = (byte)value;
}
public byte KeyGeneration
{
get => Math.Max(Header.KeyGeneration1, Header.KeyGeneration2);
set
{
if (value > 2)
{
Header.KeyGeneration1 = 2;
Header.KeyGeneration2 = value;
}
else
{
Header.KeyGeneration1 = value;
Header.KeyGeneration2 = 0;
}
}
}
public byte KeyAreaKeyIndex
{
get => Header.KeyAreaKeyIndex;
set => Header.KeyAreaKeyIndex = value;
}
public long NcaSize
{
get => Header.NcaSize;
set => Header.NcaSize = value;
}
public ulong TitleId
{
get => Header.TitleId;
set => Header.TitleId = value;
}
public int ContentIndex
{
get => Header.ContentIndex;
set => Header.ContentIndex = value;
}
public TitleVersion SdkVersion
{
get => new TitleVersion(Header.SdkVersion);
set => Header.SdkVersion = value.Version;
}
public Span<byte> RightsId => _header.Span.Slice(NcaHeaderStruct.RightsIdOffset, NcaHeaderStruct.RightsIdSize);
public bool HasRightsId => !Util.IsEmpty(RightsId);
private ref NcaSectionEntryStruct GetSectionEntry(int index)
{
ValidateSectionIndex(index);
int offset = NcaHeaderStruct.SectionEntriesOffset + NcaSectionEntryStruct.SectionEntrySize * index;
return ref Unsafe.As<byte, NcaSectionEntryStruct>(ref _header.Span[offset]);
}
public long GetSectionStartOffset(int index)
{
return BlockToOffset(GetSectionEntry(index).StartBlock);
}
public long GetSectionEndOffset(int index)
{
return BlockToOffset(GetSectionEntry(index).EndBlock);
}
public long GetSectionSize(int index)
{
ref NcaSectionEntryStruct info = ref GetSectionEntry(index);
return BlockToOffset(info.EndBlock - info.StartBlock);
}
public bool IsSectionEnabled(int index)
{
return GetSectionEntry(index).IsEnabled;
}
public Span<byte> GetFsHeaderHash(int index)
{
ValidateSectionIndex(index);
int offset = NcaHeaderStruct.FsHeaderHashOffset + NcaHeaderStruct.FsHeaderHashSize * index;
return _header.Span.Slice(offset, NcaHeaderStruct.FsHeaderHashSize);
}
public Span<byte> GetEncryptedKey(int index)
{
if (index < 0 || index >= SectionCount)
{
throw new ArgumentOutOfRangeException($"Key index must be between 0 and 3. Actual: {index}");
}
int offset = NcaHeaderStruct.KeyAreaOffset + Crypto.Aes128Size * index;
return _header.Span.Slice(offset, Crypto.Aes128Size);
}
public NcaFsHeader GetFsHeader(int index)
{
Span<byte> expectedHash = GetFsHeaderHash(index);
int offset = NcaHeaderStruct.FsHeadersOffset + NcaHeaderStruct.FsHeaderSize * index;
Memory<byte> headerData = _header.Slice(offset, NcaHeaderStruct.FsHeaderSize);
byte[] actualHash = Crypto.ComputeSha256(headerData.ToArray(), 0, NcaHeaderStruct.FsHeaderSize);
if (!Util.SpansEqual(expectedHash, actualHash))
{
throw new InvalidDataException("FS header hash is invalid.");
}
return new NcaFsHeader(headerData);
}
private static void ValidateSectionIndex(int index)
{
if (index < 0 || index >= SectionCount)
{
throw new ArgumentOutOfRangeException($"NCA section index must be between 0 and 3. Actual: {index}");
}
}
private static long BlockToOffset(int blockIndex)
{
return (long)blockIndex * BlockSize;
}
public static byte[] DecryptHeader(Keyset keyset, IStorage storage)
{
var buf = new byte[HeaderSize];
storage.Read(buf, 0);
byte[] key1 = keyset.HeaderKey.AsSpan(0, 0x10).ToArray();
byte[] key2 = keyset.HeaderKey.AsSpan(0x10, 0x10).ToArray();
var transform = new Aes128XtsTransform(key1, key2, true);
transform.TransformBlock(buf, HeaderSectorSize * 0, HeaderSectorSize, 0);
transform.TransformBlock(buf, HeaderSectorSize * 1, HeaderSectorSize, 1);
if (buf[0x200] != 'N' || buf[0x201] != 'C' || buf[0x202] != 'A')
{
throw new InvalidDataException(
"Unable to decrypt NCA header. The file is not an NCA file or the header key is incorrect.");
}
int version = buf[0x203] - '0';
if (version == 3)
{
for (int sector = 2; sector < HeaderSize / HeaderSectorSize; sector++)
{
transform.TransformBlock(buf, sector * HeaderSectorSize, HeaderSectorSize, (ulong)sector);
}
}
else if (version == 2)
{
for (int i = 0x400; i < HeaderSize; i += HeaderSectorSize)
{
transform.TransformBlock(buf, i, HeaderSectorSize, 1);
}
}
else
{
throw new NotSupportedException($"NCA version {version} is not supported.");
}
return buf;
}
public Validity VerifySignature1(byte[] modulus)
{
return Crypto.Rsa2048PssVerify(_header.Span.Slice(0x200, 0x200).ToArray(), Signature1.ToArray(), modulus);
}
public Validity VerifySignature2(byte[] modulus)
{
return Crypto.Rsa2048PssVerify(_header.Span.Slice(0x200, 0x200).ToArray(), Signature2.ToArray(), modulus);
}
[StructLayout(LayoutKind.Explicit, Size = 0xC00)]
private struct NcaHeaderStruct
{
public const int RightsIdOffset = 0x230;
public const int RightsIdSize = 0x10;
public const int SectionEntriesOffset = 0x240;
public const int FsHeaderHashOffset = 0x280;
public const int FsHeaderHashSize = 0x20;
public const int KeyAreaOffset = 0x300;
public const int FsHeadersOffset = 0x400;
public const int FsHeaderSize = 0x200;
[FieldOffset(0x000)] public byte Signature1;
[FieldOffset(0x100)] public byte Signature2;
[FieldOffset(0x200)] public uint Magic;
[FieldOffset(0x204)] public byte DistributionType;
[FieldOffset(0x205)] public byte ContentType;
[FieldOffset(0x206)] public byte KeyGeneration1;
[FieldOffset(0x207)] public byte KeyAreaKeyIndex;
[FieldOffset(0x208)] public long NcaSize;
[FieldOffset(0x210)] public ulong TitleId;
[FieldOffset(0x218)] public int ContentIndex;
[FieldOffset(0x21C)] public uint SdkVersion;
[FieldOffset(0x220)] public byte KeyGeneration2;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = SectionEntrySize)]
private struct NcaSectionEntryStruct
{
public const int SectionEntrySize = 0x10;
public int StartBlock;
public int EndBlock;
public bool IsEnabled;
}
}
}

View File

@ -0,0 +1,11 @@
namespace LibHac.IO.NcaUtils
{
internal enum NcaKeyType
{
AesXts0,
AesXts1,
AesCtr,
Type3,
Type4
}
}

View File

@ -1,164 +1,5 @@
using System;
using System.IO;
namespace LibHac.IO.NcaUtils
namespace LibHac.IO.NcaUtils
{
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 int Version;
public DistributionType Distribution; // System vs gamecard.
public ContentType ContentType;
public byte CryptoType; // Which keyblob (field 1)
public byte KaekInd; // Which kaek index?
public long NcaSize; // Entire archive size.
public ulong TitleId;
public TitleVersion 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];
private byte[] SignatureData { get; }
public Validity FixedSigValidity { get; }
public Validity NpdmSigValidity { get; private set; }
public NcaHeader(BinaryReader reader, Keyset keyset)
{
Signature1 = reader.ReadBytes(0x100);
Signature2 = reader.ReadBytes(0x100);
Magic = reader.ReadAscii(4);
if (!Magic.StartsWith("NCA") || Magic[3] < '0' || Magic[3] > '9')
throw new InvalidDataException("Unable to decrypt NCA header, or the file is not an NCA file.");
Version = Magic[3] - '0';
if (Version != 2 && Version != 3) throw new NotSupportedException($"NCA version {Version} is not supported.");
reader.BaseStream.Position -= 4;
SignatureData = reader.ReadBytes(0x200);
FixedSigValidity = Crypto.Rsa2048PssVerify(SignatureData, Signature1, keyset.NcaHdrFixedKeyModulus);
reader.BaseStream.Position -= 0x200 - 4;
Distribution = (DistributionType)reader.ReadByte();
ContentType = (ContentType)reader.ReadByte();
CryptoType = reader.ReadByte();
KaekInd = reader.ReadByte();
NcaSize = reader.ReadInt64();
TitleId = reader.ReadUInt64();
reader.BaseStream.Position += 4;
SdkVersion = new TitleVersion(reader.ReadUInt32());
CryptoType2 = reader.ReadByte();
reader.BaseStream.Position += 0xF;
RightsId = reader.ReadBytes(0x10);
for (int i = 0; i < 4; i++)
{
SectionEntries[i] = new NcaSectionEntry(reader);
}
for (int i = 0; i < 4; i++)
{
SectionHashes[i] = reader.ReadBytes(0x20);
}
for (int i = 0; i < 4; i++)
{
EncryptedKeys[i] = reader.ReadBytes(0x10);
}
reader.BaseStream.Position += 0xC0;
for (int i = 0; i < 4; i++)
{
FsHeaders[i] = new NcaFsHeader(reader);
}
}
internal void ValidateNpdmSignature(byte[] modulus)
{
NpdmSigValidity = Crypto.Rsa2048PssVerify(SignatureData, Signature2, modulus);
}
}
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 BktrPatchInfo
{
public BktrHeader RelocationHeader;
public BktrHeader EncryptionHeader;
}
public class Sha256Info
{
public byte[] MasterHash;
public int BlockSize; // In bytes
public uint LevelCount;
public long HashTableOffset;
public long HashTableSize;
public long DataOffset;
public long DataSize;
public Validity MasterHashValidity = Validity.Unchecked;
public Validity HashValidity = Validity.Unchecked;
public Sha256Info(BinaryReader reader)
{
MasterHash = reader.ReadBytes(0x20);
BlockSize = reader.ReadInt32();
LevelCount = reader.ReadUInt32();
HashTableOffset = reader.ReadInt64();
HashTableSize = reader.ReadInt64();
DataOffset = reader.ReadInt64();
DataSize = reader.ReadInt64();
}
}
public class BktrHeader
{
public long Offset;
public long Size;
public uint Magic;
public uint Version;
public uint NumEntries;
public uint Field1C;
public byte[] Header;
public BktrHeader(BinaryReader reader)
{
Offset = reader.ReadInt64();
Size = reader.ReadInt64();
Magic = reader.ReadUInt32();
Version = reader.ReadUInt32();
NumEntries = reader.ReadUInt32();
Field1C = reader.ReadUInt32();
reader.BaseStream.Position -= 0x10;
Header = reader.ReadBytes(0x10);
}
}
public class TitleVersion
{
public uint Version { get; }
@ -238,12 +79,4 @@ namespace LibHac.IO.NcaUtils
Romfs,
Pfs0
}
public enum SectionType
{
Invalid,
Pfs0,
Romfs,
Bktr
}
}

View File

@ -351,6 +351,13 @@ namespace LibHac
}
internal static readonly string[] KakNames = { "application", "ocean", "system" };
public static int GetMasterKeyRevisionFromKeyGeneration(int keyGeneration)
{
if (keyGeneration == 0) return 0;
return keyGeneration - 1;
}
}
public static class ExternalKeys

View File

@ -21,6 +21,8 @@ namespace LibHac.Npdm
public Validity SignatureValidity { get; }
public Acid(Stream stream, int offset) : this(stream, offset, null) { }
public Acid(Stream stream, int offset, Keyset keyset)
{
stream.Seek(offset, SeekOrigin.Begin);
@ -38,9 +40,12 @@ namespace LibHac.Npdm
Size = reader.ReadInt32();
reader.BaseStream.Position = offset + 0x100;
byte[] signatureData = reader.ReadBytes(Size);
SignatureValidity = Crypto.Rsa2048PssVerify(signatureData, Rsa2048Signature, keyset.AcidFixedKeyModulus);
if (keyset != null)
{
reader.BaseStream.Position = offset + 0x100;
byte[] signatureData = reader.ReadBytes(Size);
SignatureValidity = Crypto.Rsa2048PssVerify(signatureData, Rsa2048Signature, keyset.AcidFixedKeyModulus);
}
reader.BaseStream.Position = offset + 0x208;
reader.ReadInt32();

View File

@ -24,6 +24,8 @@ namespace LibHac.Npdm
public Aci0 Aci0 { get; }
public Acid AciD { get; }
public NpdmBinary(Stream stream) : this(stream, null) { }
public NpdmBinary(Stream stream, Keyset keyset)
{
var reader = new BinaryReader(stream);

View File

@ -15,7 +15,7 @@ namespace LibHac
public IFileSystem ContentFs { get; }
public IFileSystem SaveFs { get; }
public Dictionary<string, Nca> Ncas { get; } = new Dictionary<string, Nca>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, SwitchFsNca> Ncas { get; } = new Dictionary<string, SwitchFsNca>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, SaveDataFileSystem> Saves { get; } = new Dictionary<string, SaveDataFileSystem>(StringComparer.OrdinalIgnoreCase);
public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>();
public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>();
@ -68,15 +68,15 @@ namespace LibHac
foreach (DirectoryEntry fileEntry in files)
{
Nca nca = null;
SwitchFsNca nca = null;
try
{
IStorage storage = ContentFs.OpenFile(fileEntry.FullPath, OpenMode.Read).AsStorage();
nca = new Nca(Keyset, storage, false);
nca = new SwitchFsNca(new Nca(Keyset, storage));
nca.NcaId = Path.GetFileNameWithoutExtension(fileEntry.Name);
string extension = nca.Header.ContentType == ContentType.Meta ? ".cnmt.nca" : ".nca";
string extension = nca.Nca.Header.ContentType == ContentType.Meta ? ".cnmt.nca" : ".nca";
nca.Filename = nca.NcaId + extension;
}
catch (MissingKeyException ex)
@ -126,7 +126,7 @@ namespace LibHac
private void ReadTitles()
{
foreach (Nca nca in Ncas.Values.Where(x => x.Header.ContentType == ContentType.Meta))
foreach (SwitchFsNca nca in Ncas.Values.Where(x => x.Nca.Header.ContentType == ContentType.Meta))
{
var title = new Title();
@ -146,7 +146,7 @@ namespace LibHac
{
string ncaId = content.NcaId.ToHexString();
if (Ncas.TryGetValue(ncaId, out Nca contentNca))
if (Ncas.TryGetValue(ncaId, out SwitchFsNca contentNca))
{
title.Ncas.Add(contentNca);
}
@ -205,22 +205,22 @@ namespace LibHac
foreach (Application app in Applications.Values)
{
Nca main = app.Main?.MainNca;
Nca patch = app.Patch?.MainNca;
SwitchFsNca main = app.Main?.MainNca;
SwitchFsNca patch = app.Patch?.MainNca;
if (main != null)
if (main != null && patch != null)
{
patch?.SetBaseNca(main);
patch.BaseNca = main.Nca;
}
}
}
private void DisposeNcas()
{
foreach (Nca nca in Ncas.Values)
{
nca.Dispose();
}
//foreach (SwitchFsNca nca in Ncas.Values)
//{
// nca.Dispose();
//}
Ncas.Clear();
Titles.Clear();
}
@ -231,23 +231,72 @@ namespace LibHac
}
}
public class SwitchFsNca
{
public Nca Nca { get; set; }
public Nca BaseNca { get; set; }
public string NcaId { get; set; }
public string Filename { get; set; }
public SwitchFsNca(Nca nca)
{
Nca = nca;
}
public IStorage OpenStorage(int index, IntegrityCheckLevel integrityCheckLevel)
{
if (BaseNca != null) return BaseNca.OpenStorageWithPatch(Nca, index, integrityCheckLevel);
return Nca.OpenStorage(index, integrityCheckLevel);
}
public IFileSystem OpenFileSystem(int index, IntegrityCheckLevel integrityCheckLevel)
{
if (BaseNca != null) return BaseNca.OpenFileSystemWithPatch(Nca, index, integrityCheckLevel);
return Nca.OpenFileSystem(index, integrityCheckLevel);
}
public IStorage OpenStorage(NcaSectionType type, IntegrityCheckLevel integrityCheckLevel)
{
return OpenStorage(Nca.SectionIndexFromType(type, Nca.Header.ContentType), integrityCheckLevel);
}
public IFileSystem OpenFileSystem(NcaSectionType type, IntegrityCheckLevel integrityCheckLevel)
{
return OpenFileSystem(Nca.SectionIndexFromType(type, Nca.Header.ContentType), integrityCheckLevel);
}
public Validity VerifyNca(IProgressReport logger = null, bool quiet = false)
{
if (BaseNca != null)
{
return BaseNca.VerifyNca(Nca, logger, quiet);
}
else
{
return Nca.VerifyNca(logger, quiet);
}
}
}
[DebuggerDisplay("{" + nameof(Name) + "}")]
public class Title
{
public ulong Id { get; internal set; }
public TitleVersion Version { get; internal set; }
public List<Nca> Ncas { get; } = new List<Nca>();
public List<SwitchFsNca> Ncas { get; } = new List<SwitchFsNca>();
public Cnmt Metadata { get; internal set; }
public string Name { get; internal set; }
public Nacp Control { get; internal set; }
public Nca MetaNca { get; internal set; }
public Nca MainNca { get; internal set; }
public Nca ControlNca { get; internal set; }
public SwitchFsNca MetaNca { get; internal set; }
public SwitchFsNca MainNca { get; internal set; }
public SwitchFsNca ControlNca { get; internal set; }
public long GetSize()
{
return Ncas.Sum(x => x.Header.NcaSize);
return Ncas.Sum(x => x.Nca.Header.NcaSize);
}
}

View File

@ -352,7 +352,9 @@ namespace LibHac
return result;
}
public static string ToHexString(this byte[] bytes)
public static string ToHexString(this byte[] bytes) => ToHexString(bytes.AsSpan());
public static string ToHexString(this Span<byte> bytes)
{
uint[] lookup32 = Lookup32;
var result = new char[bytes.Length * 2];
@ -509,6 +511,13 @@ namespace LibHac
((uintVal << 8) & 0x00ff0000) |
((uintVal << 24) & 0xff000000));
}
public static int GetMasterKeyRevision(int keyGeneration)
{
if (keyGeneration == 0) return 0;
return keyGeneration - 1;
}
}
public class ByteArray128BitComparer : EqualityComparer<byte[]>

View File

@ -1,7 +1,9 @@
using System;
using System.Buffers.Binary;
using System.Text;
using LibHac;
using LibHac.IO;
using LibHac.IO.NcaUtils;
namespace hactoolnet
{
@ -65,5 +67,49 @@ namespace hactoolnet
PrintItem(sb, colLen, $"{prefix2}Hash BlockSize:", $"0x{1 << level.BlockSizePower:x8}");
}
}
public static void PrintIvfcHashNew(StringBuilder sb, int colLen, int indentSize, NcaFsIntegrityInfoIvfc ivfcInfo, IntegrityStorageType type, Validity masterHashValidity)
{
string prefix = new string(' ', indentSize);
string prefix2 = new string(' ', indentSize + 4);
if (type == IntegrityStorageType.RomFs)
PrintItem(sb, colLen, $"{prefix}Master Hash{masterHashValidity.GetValidityString()}:", ivfcInfo.MasterHash.ToArray());
PrintItem(sb, colLen, $"{prefix}Magic:", MagicToString(ivfcInfo.Magic));
PrintItem(sb, colLen, $"{prefix}Version:", ivfcInfo.Version >> 16);
if (type == IntegrityStorageType.Save)
PrintItem(sb, colLen, $"{prefix}Salt Seed:", ivfcInfo.SaltSource.ToArray());
int levelCount = Math.Max(ivfcInfo.LevelCount - 1, 0);
if (type == IntegrityStorageType.Save) levelCount = 4;
int offsetLen = type == IntegrityStorageType.Save ? 16 : 12;
for (int i = 0; i < levelCount; i++)
{
long hashOffset = 0;
if (i != 0)
{
hashOffset = ivfcInfo.GetLevelOffset(i - 1);
}
sb.AppendLine($"{prefix}Level {i}:");
PrintItem(sb, colLen, $"{prefix2}Data Offset:", $"0x{ivfcInfo.GetLevelOffset(i).ToString($"x{offsetLen}")}");
PrintItem(sb, colLen, $"{prefix2}Data Size:", $"0x{ivfcInfo.GetLevelSize(i).ToString($"x{offsetLen}")}");
PrintItem(sb, colLen, $"{prefix2}Hash Offset:", $"0x{hashOffset.ToString($"x{offsetLen}")}");
PrintItem(sb, colLen, $"{prefix2}Hash BlockSize:", $"0x{1 << ivfcInfo.GetLevelBlockSize(i):x8}");
}
}
public static string MagicToString(uint value)
{
var buf = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(buf, value);
return Encoding.ASCII.GetString(buf);
}
}
}

View File

@ -25,7 +25,7 @@ namespace hactoolnet
{
try
{
var nca = new Nca(ctx.Keyset, deltaStorage, true);
var nca = new Nca(ctx.Keyset, deltaStorage);
IFileSystem fs = nca.OpenFileSystem(0, IntegrityCheckLevel.ErrorOnInvalid);
if (!fs.FileExists(FragmentFileName))

View File

@ -3,6 +3,7 @@ using System.Text;
using LibHac;
using LibHac.IO;
using LibHac.IO.NcaUtils;
using LibHac.Npdm;
using static hactoolnet.Print;
namespace hactoolnet
@ -13,47 +14,55 @@ namespace hactoolnet
{
using (IStorage file = new LocalStorage(ctx.Options.InFile, FileAccess.Read))
{
var nca = new Nca(ctx.Keyset, file, false);
var nca = new Nca(ctx.Keyset, file);
Nca baseNca = null;
var ncaHolder = new NcaHolder { Nca = nca };
if (ctx.Options.HeaderOut != null)
{
using (var outHeader = new FileStream(ctx.Options.HeaderOut, FileMode.Create, FileAccess.ReadWrite))
{
nca.OpenHeaderStorage().Slice(0, 0xc00).CopyToStream(outHeader);
nca.OpenDecryptedHeaderStorage().Slice(0, 0xc00).CopyToStream(outHeader);
}
}
nca.ValidateMasterHashes();
nca.ParseNpdm();
if (ctx.Options.BaseNca != null)
{
IStorage baseFile = new LocalStorage(ctx.Options.BaseNca, FileAccess.Read);
var baseNca = new Nca(ctx.Keyset, baseFile, false);
nca.SetBaseNca(baseNca);
baseNca = new Nca(ctx.Keyset, baseFile);
ncaHolder.BaseNca = baseNca;
}
for (int i = 0; i < 3; i++)
{
if (ctx.Options.SectionOut[i] != null)
{
nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
OpenStorage(i).WriteAllBytes(ctx.Options.SectionOut[i], ctx.Logger);
}
if (ctx.Options.SectionOutDir[i] != null)
{
nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Options.IntegrityLevel, ctx.Logger);
IFileSystem fs = OpenFileSystem(i);
fs.Extract(ctx.Options.SectionOutDir[i], ctx.Logger);
}
if (ctx.Options.Validate && nca.SectionExists(i))
{
nca.VerifySection(i, ctx.Logger);
if (nca.Header.GetFsHeader(i).IsPatchSection() && baseNca != null)
{
ncaHolder.Validities[i] = baseNca.VerifySection(nca, i, ctx.Logger);
}
else
{
ncaHolder.Validities[i] = nca.VerifySection(i, ctx.Logger);
}
}
}
if (ctx.Options.ListRomFs && nca.CanOpenSection(NcaSectionType.Data))
{
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, ctx.Options.IntegrityLevel);
IFileSystem romfs = OpenFileSystemByType(NcaSectionType.Data);
foreach (DirectoryEntry entry in romfs.EnumerateEntries())
{
@ -71,18 +80,19 @@ namespace hactoolnet
if (ctx.Options.RomfsOut != null)
{
nca.ExportSection(NcaSectionType.Data, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
OpenStorageByType(NcaSectionType.Data).WriteAllBytes(ctx.Options.RomfsOut, ctx.Logger);
}
if (ctx.Options.RomfsOutDir != null)
{
nca.ExtractSection(NcaSectionType.Data, ctx.Options.RomfsOutDir, ctx.Options.IntegrityLevel, ctx.Logger);
IFileSystem fs = OpenFileSystemByType(NcaSectionType.Data);
fs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
if (ctx.Options.ReadBench)
{
long bytesToRead = 1024L * 1024 * 1024 * 5;
IStorage storage = nca.OpenStorage(NcaSectionType.Data, ctx.Options.IntegrityLevel);
IStorage storage = OpenStorageByType(NcaSectionType.Data);
var dest = new NullStorage(storage.GetSize());
int iterations = (int)(bytesToRead / storage.GetSize()) + 1;
@ -117,12 +127,13 @@ namespace hactoolnet
if (ctx.Options.ExefsOut != null)
{
nca.ExportSection(NcaSectionType.Code, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
OpenStorageByType(NcaSectionType.Code).WriteAllBytes(ctx.Options.ExefsOut, ctx.Logger);
}
if (ctx.Options.ExefsOutDir != null)
{
nca.ExtractSection(NcaSectionType.Code, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger);
IFileSystem fs = OpenFileSystemByType(NcaSectionType.Code);
fs.Extract(ctx.Options.ExefsOutDir, ctx.Logger);
}
}
@ -131,45 +142,92 @@ namespace hactoolnet
nca.OpenDecryptedNca().WriteAllBytes(ctx.Options.PlaintextOut, ctx.Logger);
}
if (!ctx.Options.ReadBench) ctx.Logger.LogMessage(nca.Print());
if (!ctx.Options.ReadBench) ctx.Logger.LogMessage(ncaHolder.Print());
IStorage OpenStorage(int index)
{
if (ctx.Options.Raw)
{
if (baseNca != null) return baseNca.OpenRawStorageWithPatch(nca, index);
return nca.OpenRawStorage(index);
}
if (baseNca != null) return baseNca.OpenStorageWithPatch(nca, index, ctx.Options.IntegrityLevel);
return nca.OpenStorage(index, ctx.Options.IntegrityLevel);
}
IFileSystem OpenFileSystem(int index)
{
if (baseNca != null) return baseNca.OpenFileSystemWithPatch(nca, index, ctx.Options.IntegrityLevel);
return nca.OpenFileSystem(index, ctx.Options.IntegrityLevel);
}
IStorage OpenStorageByType(NcaSectionType type)
{
return OpenStorage(Nca.SectionIndexFromType(type, nca.Header.ContentType));
}
IFileSystem OpenFileSystemByType(NcaSectionType type)
{
return OpenFileSystem(Nca.SectionIndexFromType(type, nca.Header.ContentType));
}
}
}
private static string Print(this Nca nca)
private static Validity VerifySignature2(this Nca nca)
{
if (nca.Header.ContentType != ContentType.Program) return Validity.Unchecked;
IFileSystem pfs = nca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid);
if (!pfs.FileExists("main.npdm")) return Validity.Unchecked;
IFile npdmStorage = pfs.OpenFile("main.npdm", OpenMode.Read);
var npdm = new NpdmBinary(npdmStorage.AsStream());
return nca.Header.VerifySignature2(npdm.AciD.Rsa2048Modulus);
}
private static string Print(this NcaHolder ncaHolder)
{
Nca nca = ncaHolder.Nca;
int masterKey = Keyset.GetMasterKeyRevisionFromKeyGeneration(nca.Header.KeyGeneration);
int colLen = 36;
var sb = new StringBuilder();
sb.AppendLine();
sb.AppendLine("NCA:");
PrintItem(sb, colLen, "Magic:", nca.Header.Magic);
PrintItem(sb, colLen, $"Fixed-Key Signature{nca.Header.FixedSigValidity.GetValidityString()}:", nca.Header.Signature1);
PrintItem(sb, colLen, $"NPDM Signature{nca.Header.NpdmSigValidity.GetValidityString()}:", nca.Header.Signature2);
PrintItem(sb, colLen, "Magic:", MagicToString(nca.Header.Magic));
PrintItem(sb, colLen, $"Fixed-Key Signature{nca.VerifyHeaderSignature().GetValidityString()}:", nca.Header.Signature1.ToArray());
PrintItem(sb, colLen, $"NPDM Signature{nca.VerifySignature2().GetValidityString()}:", nca.Header.Signature2.ToArray());
PrintItem(sb, colLen, "Content Size:", $"0x{nca.Header.NcaSize:x12}");
PrintItem(sb, colLen, "TitleID:", $"{nca.Header.TitleId:X16}");
PrintItem(sb, colLen, "SDK Version:", nca.Header.SdkVersion);
PrintItem(sb, colLen, "Distribution type:", nca.Header.Distribution);
PrintItem(sb, colLen, "Distribution type:", nca.Header.DistributionType);
PrintItem(sb, colLen, "Content Type:", nca.Header.ContentType);
PrintItem(sb, colLen, "Master Key Revision:", $"{nca.CryptoType} ({Util.GetKeyRevisionSummary(nca.CryptoType)})");
PrintItem(sb, colLen, "Encryption Type:", $"{(nca.HasRightsId ? "Titlekey crypto" : "Standard crypto")}");
PrintItem(sb, colLen, "Master Key Revision:", $"{masterKey} ({Util.GetKeyRevisionSummary(masterKey)})");
PrintItem(sb, colLen, "Encryption Type:", $"{(nca.Header.HasRightsId ? "Titlekey crypto" : "Standard crypto")}");
if (nca.HasRightsId)
if (nca.Header.HasRightsId)
{
PrintItem(sb, colLen, "Rights ID:", nca.Header.RightsId);
PrintItem(sb, colLen, "Rights ID:", nca.Header.RightsId.ToArray());
}
else
{
PrintItem(sb, colLen, "Key Area Encryption Key:", nca.Header.KaekInd);
PrintItem(sb, colLen, "Key Area Encryption Key:", nca.Header.KeyAreaKeyIndex);
sb.AppendLine("Key Area (Encrypted):");
for (int i = 0; i < 4; i++)
{
PrintItem(sb, colLen, $" Key {i} (Encrypted):", nca.Header.EncryptedKeys[i]);
PrintItem(sb, colLen, $" Key {i} (Encrypted):", nca.Header.GetEncryptedKey(i).ToArray());
}
sb.AppendLine("Key Area (Decrypted):");
for (int i = 0; i < 4; i++)
{
PrintItem(sb, colLen, $" Key {i} (Decrypted):", nca.DecryptedKeys[i]);
PrintItem(sb, colLen, $" Key {i} (Decrypted):", nca.GetDecryptedKey(i));
}
}
@ -183,24 +241,27 @@ namespace hactoolnet
for (int i = 0; i < 4; i++)
{
NcaSection sect = nca.Sections[i];
if (sect == null) continue;
if (!nca.Header.IsSectionEnabled(i)) continue;
NcaFsHeader sectHeader = nca.Header.GetFsHeader(i);
bool isExefs = nca.Header.ContentType == ContentType.Program && i == 0;
sb.AppendLine($" Section {i}:");
PrintItem(sb, colLen, " Offset:", $"0x{sect.Offset:x12}");
PrintItem(sb, colLen, " Size:", $"0x{sect.Size:x12}");
PrintItem(sb, colLen, " Partition Type:", isExefs ? "ExeFS" : sect.Type.ToString());
PrintItem(sb, colLen, " Section CTR:", sect.Header.Ctr);
PrintItem(sb, colLen, " Offset:", $"0x{nca.Header.GetSectionStartOffset(i):x12}");
PrintItem(sb, colLen, " Size:", $"0x{nca.Header.GetSectionSize(i):x12}");
PrintItem(sb, colLen, " Partition Type:", (isExefs ? "ExeFS" : sectHeader.FormatType.ToString()) + (sectHeader.IsPatchSection() ? " patch" : ""));
PrintItem(sb, colLen, " Section CTR:", $"{sectHeader.Counter:x16}");
PrintItem(sb, colLen, " Section Validity:", $"{ncaHolder.Validities[i]}");
switch (sect.Header.HashType)
switch (sectHeader.HashType)
{
case NcaHashType.Sha256:
PrintSha256Hash(sect);
PrintSha256Hash(sectHeader, i);
break;
case NcaHashType.Ivfc:
PrintIvfcHash(sb, colLen, 8, sect.Header.IvfcInfo, IntegrityStorageType.RomFs);
Validity masterHashValidity = nca.ValidateSectionMasterHash(i);
PrintIvfcHashNew(sb, colLen, 8, sectHeader.GetIntegrityInfoIvfc(), IntegrityStorageType.RomFs, masterHashValidity);
break;
default:
sb.AppendLine(" Unknown/invalid superblock!");
@ -209,19 +270,26 @@ namespace hactoolnet
}
}
void PrintSha256Hash(NcaSection sect)
void PrintSha256Hash(NcaFsHeader sect, int index)
{
Sha256Info hashInfo = sect.Header.Sha256Info;
NcaFsIntegrityInfoSha256 hashInfo = sect.GetIntegrityInfoSha256();
PrintItem(sb, colLen, $" Master Hash{sect.MasterHashValidity.GetValidityString()}:", hashInfo.MasterHash);
sb.AppendLine($" Hash Table{sect.Header.Sha256Info.HashValidity.GetValidityString()}:");
PrintItem(sb, colLen, $" Master Hash{nca.ValidateSectionMasterHash(index).GetValidityString()}:", hashInfo.MasterHash.ToArray());
sb.AppendLine($" Hash Table:");
PrintItem(sb, colLen, " Offset:", $"0x{hashInfo.HashTableOffset:x12}");
PrintItem(sb, colLen, " Size:", $"0x{hashInfo.HashTableSize:x12}");
PrintItem(sb, colLen, " Offset:", $"0x{hashInfo.GetLevelOffset(0):x12}");
PrintItem(sb, colLen, " Size:", $"0x{hashInfo.GetLevelSize(0):x12}");
PrintItem(sb, colLen, " Block Size:", $"0x{hashInfo.BlockSize:x}");
PrintItem(sb, colLen, " PFS0 Offset:", $"0x{hashInfo.DataOffset:x12}");
PrintItem(sb, colLen, " PFS0 Size:", $"0x{hashInfo.DataSize:x12}");
PrintItem(sb, colLen, " PFS0 Offset:", $"0x{hashInfo.GetLevelOffset(1):x12}");
PrintItem(sb, colLen, " PFS0 Size:", $"0x{hashInfo.GetLevelSize(1):x12}");
}
}
private class NcaHolder
{
public Nca Nca;
public Nca BaseNca;
public Validity[] Validities = new Validity[4];
}
}
}

View File

@ -3,7 +3,6 @@ using System.Reflection;
using System.Text;
using LibHac;
using LibHac.IO;
using LibHac.IO.NcaUtils;
using static hactoolnet.Print;
namespace hactoolnet
@ -68,9 +67,9 @@ namespace hactoolnet
var builder = new PartitionFileSystemBuilder();
foreach (Nca nca in title.Ncas)
foreach (SwitchFsNca nca in title.Ncas)
{
builder.AddFile(nca.Filename, nca.GetStorage().AsFile(OpenMode.Read));
builder.AddFile(nca.Filename, nca.Nca.BaseStorage.AsFile(OpenMode.Read));
}
var ticket = new Ticket
@ -79,9 +78,9 @@ namespace hactoolnet
Signature = new byte[0x200],
Issuer = "Root-CA00000003-XS00000020",
FormatVersion = 2,
RightsId = title.MainNca.Header.RightsId,
TitleKeyBlock = title.MainNca.TitleKey,
CryptoType = title.MainNca.Header.CryptoType2,
RightsId = title.MainNca.Nca.Header.RightsId.ToArray(),
TitleKeyBlock = title.MainNca.Nca.GetDecryptedTitleKey(),
CryptoType = title.MainNca.Nca.Header.KeyGeneration,
SectHeaderOffset = 0x2C0
};
byte[] ticketBytes = ticket.GetBytes();

View File

@ -69,7 +69,7 @@ namespace hactoolnet
return;
}
if (!title.MainNca.SectionExists(NcaSectionType.Code))
if (!title.MainNca.Nca.SectionExists(NcaSectionType.Code))
{
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no ExeFS section");
return;
@ -77,12 +77,13 @@ namespace hactoolnet
if (ctx.Options.ExefsOutDir != null)
{
title.MainNca.ExtractSection(NcaSectionType.Code, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger);
IFileSystem fs = title.MainNca.OpenFileSystem(NcaSectionType.Code, ctx.Options.IntegrityLevel);
fs.Extract(ctx.Options.ExefsOutDir, ctx.Logger);
}
if (ctx.Options.ExefsOut != null)
{
title.MainNca.ExportSection(NcaSectionType.Code, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger);
title.MainNca.OpenStorage(NcaSectionType.Code, ctx.Options.IntegrityLevel).WriteAllBytes(ctx.Options.ExefsOut, ctx.Logger);
}
}
@ -107,13 +108,13 @@ namespace hactoolnet
return;
}
if (!title.MainNca.SectionExists(NcaSectionType.Data))
if (!title.MainNca.Nca.SectionExists(NcaSectionType.Data))
{
ctx.Logger.LogMessage($"Main NCA for title {id:X16} has no RomFS section");
return;
}
ProcessRomfs.Process(ctx, title.MainNca.OpenStorage(NcaSectionType.Data, ctx.Options.IntegrityLevel, false));
ProcessRomfs.Process(ctx, title.MainNca.OpenStorage(NcaSectionType.Data, ctx.Options.IntegrityLevel));
}
if (ctx.Options.OutDir != null)
@ -178,9 +179,9 @@ namespace hactoolnet
{
ctx.Logger.LogMessage($" {caption} {title.Id:x16}");
foreach (Nca nca in title.Ncas)
foreach (SwitchFsNca nca in title.Ncas)
{
ctx.Logger.LogMessage($" {nca.Header.ContentType.ToString()}");
ctx.Logger.LogMessage($" {nca.Nca.Header.ContentType.ToString()}");
Validity validity = nca.VerifyNca(ctx.Logger, true);
@ -211,9 +212,9 @@ namespace hactoolnet
string saveDir = Path.Combine(ctx.Options.OutDir, $"{title.Id:X16}v{title.Version.Version}");
Directory.CreateDirectory(saveDir);
foreach (Nca nca in title.Ncas)
foreach (SwitchFsNca nca in title.Ncas)
{
Stream stream = nca.GetStorage().AsStream();
Stream stream = nca.Nca.BaseStorage.AsStream();
string outFile = Path.Combine(saveDir, nca.Filename);
ctx.Logger.LogMessage(nca.Filename);
using (var outStream = new FileStream(outFile, FileMode.Create, FileAccess.ReadWrite))
@ -245,9 +246,9 @@ namespace hactoolnet
{
var table = new TableBuilder("NCA ID", "Type", "Title ID");
foreach (Nca nca in sdfs.Ncas.Values.OrderBy(x => x.NcaId))
foreach (SwitchFsNca nca in sdfs.Ncas.Values.OrderBy(x => x.NcaId))
{
table.AddRow(nca.NcaId, nca.Header.ContentType.ToString(), nca.Header.TitleId.ToString("X16"));
table.AddRow(nca.NcaId, nca.Nca.Header.ContentType.ToString(), nca.Nca.Header.TitleId.ToString("X16"));
}
return table.Print();

View File

@ -125,7 +125,7 @@ namespace hactoolnet
foreach (PartitionFileEntry fileEntry in partition.Files.Where(x => x.Name.EndsWith(".nca")))
{
IStorage ncaStorage = partition.OpenFile(fileEntry, OpenMode.Read).AsStorage();
var nca = new Nca(ctx.Keyset, ncaStorage, true);
var nca = new Nca(ctx.Keyset, ncaStorage);
if (nca.Header.ContentType == ContentType.Program)
{