mirror of
https://github.com/Thealexbarney/LibHac.git
synced 2025-02-09 13:14:46 +01:00
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:
parent
7804a919d1
commit
72915c0425
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
8
src/LibHac/IO/Messages.cs
Normal file
8
src/LibHac/IO/Messages.cs
Normal 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.";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
89
src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoIvfc.cs
Normal file
89
src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoIvfc.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
71
src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoSha256.cs
Normal file
71
src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoSha256.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
54
src/LibHac/IO/NcaUtils/NcaFsPatchInfo.cs
Normal file
54
src/LibHac/IO/NcaUtils/NcaFsPatchInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
272
src/LibHac/IO/NcaUtils/NcaHeader.cs
Normal file
272
src/LibHac/IO/NcaUtils/NcaHeader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
11
src/LibHac/IO/NcaUtils/NcaKeyType.cs
Normal file
11
src/LibHac/IO/NcaUtils/NcaKeyType.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace LibHac.IO.NcaUtils
|
||||
{
|
||||
internal enum NcaKeyType
|
||||
{
|
||||
AesXts0,
|
||||
AesXts1,
|
||||
AesCtr,
|
||||
Type3,
|
||||
Type4
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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[]>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user