diff --git a/src/LibHac/IO/Aes128CtrExStorage.cs b/src/LibHac/IO/Aes128CtrExStorage.cs index 6cefd4e8..8fc0a371 100644 --- a/src/LibHac/IO/Aes128CtrExStorage.cs +++ b/src/LibHac/IO/Aes128CtrExStorage.cs @@ -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(bucketTreeHeader, bucketTreeData); + BucketTree = new BucketTree(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(bucketTreeData); SubsectionEntries = BucketTree.GetEntryList(); SubsectionOffsets = SubsectionEntries.Select(x => x.Offset).ToList(); diff --git a/src/LibHac/IO/Aes128CtrStorage.cs b/src/LibHac/IO/Aes128CtrStorage.cs index 1f80cc30..f3c1f987 100644 --- a/src/LibHac/IO/Aes128CtrStorage.cs +++ b/src/LibHac/IO/Aes128CtrStorage.cs @@ -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; + } } } diff --git a/src/LibHac/IO/BucketTree.cs b/src/LibHac/IO/BucketTree.cs index 9e533284..c8ee17a0 100644 --- a/src/LibHac/IO/BucketTree.cs +++ b/src/LibHac/IO/BucketTree.cs @@ -7,13 +7,11 @@ namespace LibHac.IO public class BucketTree where T : BucketTreeEntry, new() { private const int BucketAlignment = 0x4000; - public BucketTreeHeader Header { get; } public BucketTreeBucket BucketOffsets { get; } public BucketTreeBucket[] 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(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 where T : BucketTreeEntry, new() { public int Index; diff --git a/src/LibHac/IO/IndirectStorage.cs b/src/LibHac/IO/IndirectStorage.cs index 80568989..b864797e 100644 --- a/src/LibHac/IO/IndirectStorage.cs +++ b/src/LibHac/IO/IndirectStorage.cs @@ -13,13 +13,13 @@ namespace LibHac.IO private BucketTree 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(bucketTreeHeader, bucketTreeData); + BucketTree = new BucketTree(bucketTreeData); RelocationEntries = BucketTree.GetEntryList(); RelocationOffsets = RelocationEntries.Select(x => x.Offset).ToList(); diff --git a/src/LibHac/IO/Messages.cs b/src/LibHac/IO/Messages.cs new file mode 100644 index 00000000..57db491b --- /dev/null +++ b/src/LibHac/IO/Messages.cs @@ -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."; + } +} diff --git a/src/LibHac/IO/NcaUtils/Nca.cs b/src/LibHac/IO/NcaUtils/Nca.cs index 3a31d2cc..fd591328 100644 --- a/src/LibHac/IO/NcaUtils/Nca.cs +++ b/src/LibHac/IO/NcaUtils/Nca.cs @@ -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(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); } - /// - /// Opens the of the underlying NCA file. - /// - /// The that provides access to the entire raw NCA file. - 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)); - } - - /// - /// Sets a base to use when reading patches. - /// - /// The base - public void SetBaseNca(Nca baseNca) => BaseNca = baseNca; - - /// - /// Validates the master hash and store the result in for each . - /// - 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 buf = stackalloc byte[1]; - header.Read(buf, 0x203); - return buf[0] - '0'; - } + Span buf = stackalloc byte[1]; + header.Read(buf, 0x203); + return buf[0] - '0'; } private IStorage OpenNca2Header(long size) { - var sources = new List(); - 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(); + 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; } } } diff --git a/src/LibHac/IO/NcaUtils/NcaExtensions.cs b/src/LibHac/IO/NcaUtils/NcaExtensions.cs index c7c04bc2..eb3ddd15 100644 --- a/src/LibHac/IO/NcaUtils/NcaExtensions.cs +++ b/src/LibHac/IO/NcaUtils/NcaExtensions.cs @@ -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 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; } } diff --git a/src/LibHac/IO/NcaUtils/NcaFsHeader.cs b/src/LibHac/IO/NcaUtils/NcaFsHeader.cs index 23e7fddf..6fcf1ec7 100644 --- a/src/LibHac/IO/NcaUtils/NcaFsHeader.cs +++ b/src/LibHac/IO/NcaUtils/NcaFsHeader.cs @@ -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 _header; - public IvfcHeader IvfcInfo; - public Sha256Info Sha256Info; - public BktrPatchInfo BktrInfo; - - public byte[] Ctr; - - public NcaFsHeader(BinaryReader reader) + public NcaFsHeader(Memory 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(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; } } } diff --git a/src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoIvfc.cs b/src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoIvfc.cs new file mode 100644 index 00000000..039cc724 --- /dev/null +++ b/src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoIvfc.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.IO.NcaUtils +{ + public struct NcaFsIntegrityInfoIvfc + { + private readonly Memory _data; + + public NcaFsIntegrityInfoIvfc(Memory data) + { + _data = data; + } + + private ref IvfcStruct Data => ref Unsafe.As(ref _data.Span[0]); + + private ref IvfcLevel GetLevelInfo(int index) + { + ValidateLevelIndex(index); + + int offset = IvfcStruct.IvfcLevelsOffset + IvfcLevel.IvfcLevelSize * index; + return ref Unsafe.As(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 SaltSource => _data.Span.Slice(IvfcStruct.SaltSourceOffset, IvfcStruct.SaltSourceSize); + public Span 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; + } + } +} diff --git a/src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoSha256.cs b/src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoSha256.cs new file mode 100644 index 00000000..fb26aca4 --- /dev/null +++ b/src/LibHac/IO/NcaUtils/NcaFsIntegrityInfoSha256.cs @@ -0,0 +1,71 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.IO.NcaUtils +{ + public struct NcaFsIntegrityInfoSha256 + { + private readonly Memory _data; + + public NcaFsIntegrityInfoSha256(Memory data) + { + _data = data; + } + + private ref Sha256Struct Data => ref Unsafe.As(ref _data.Span[0]); + + private ref Sha256Level GetLevelInfo(int index) + { + ValidateLevelIndex(index); + + int offset = Sha256Struct.Sha256LevelOffset + Sha256Level.Sha256LevelSize * index; + return ref Unsafe.As(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 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; + } + } +} diff --git a/src/LibHac/IO/NcaUtils/NcaFsPatchInfo.cs b/src/LibHac/IO/NcaUtils/NcaFsPatchInfo.cs new file mode 100644 index 00000000..2af4433f --- /dev/null +++ b/src/LibHac/IO/NcaUtils/NcaFsPatchInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.IO.NcaUtils +{ + public struct NcaFsPatchInfo + { + private readonly Memory _data; + + public NcaFsPatchInfo(Memory data) + { + _data = data; + } + + private ref PatchInfoStruct Data => ref Unsafe.As(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 RelocationTreeHeader => _data.Span.Slice(0x10, 0x10); + public Span 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; + } + } +} diff --git a/src/LibHac/IO/NcaUtils/NcaHeader.cs b/src/LibHac/IO/NcaUtils/NcaHeader.cs new file mode 100644 index 00000000..831bf082 --- /dev/null +++ b/src/LibHac/IO/NcaUtils/NcaHeader.cs @@ -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 _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(ref _header.Span[0]); + + public Span Signature1 => _header.Span.Slice(0, 0x100); + public Span 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 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(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 GetFsHeaderHash(int index) + { + ValidateSectionIndex(index); + + int offset = NcaHeaderStruct.FsHeaderHashOffset + NcaHeaderStruct.FsHeaderHashSize * index; + return _header.Span.Slice(offset, NcaHeaderStruct.FsHeaderHashSize); + } + + public Span 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 expectedHash = GetFsHeaderHash(index); + + int offset = NcaHeaderStruct.FsHeadersOffset + NcaHeaderStruct.FsHeaderSize * index; + Memory 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; + } + } +} diff --git a/src/LibHac/IO/NcaUtils/NcaKeyType.cs b/src/LibHac/IO/NcaUtils/NcaKeyType.cs new file mode 100644 index 00000000..11c41a69 --- /dev/null +++ b/src/LibHac/IO/NcaUtils/NcaKeyType.cs @@ -0,0 +1,11 @@ +namespace LibHac.IO.NcaUtils +{ + internal enum NcaKeyType + { + AesXts0, + AesXts1, + AesCtr, + Type3, + Type4 + } +} diff --git a/src/LibHac/IO/NcaUtils/NcaStructs.cs b/src/LibHac/IO/NcaUtils/NcaStructs.cs index df570ee7..45710568 100644 --- a/src/LibHac/IO/NcaUtils/NcaStructs.cs +++ b/src/LibHac/IO/NcaUtils/NcaStructs.cs @@ -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 - } } diff --git a/src/LibHac/Keyset.cs b/src/LibHac/Keyset.cs index 94591f8b..9b21266f 100644 --- a/src/LibHac/Keyset.cs +++ b/src/LibHac/Keyset.cs @@ -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 diff --git a/src/LibHac/Npdm/Acid.cs b/src/LibHac/Npdm/Acid.cs index c869c5af..2e575031 100644 --- a/src/LibHac/Npdm/Acid.cs +++ b/src/LibHac/Npdm/Acid.cs @@ -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(); diff --git a/src/LibHac/Npdm/NpdmBinary.cs b/src/LibHac/Npdm/NpdmBinary.cs index 9816683e..df21ff3d 100644 --- a/src/LibHac/Npdm/NpdmBinary.cs +++ b/src/LibHac/Npdm/NpdmBinary.cs @@ -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); diff --git a/src/LibHac/SwitchFs.cs b/src/LibHac/SwitchFs.cs index e814e997..e63886f9 100644 --- a/src/LibHac/SwitchFs.cs +++ b/src/LibHac/SwitchFs.cs @@ -15,7 +15,7 @@ namespace LibHac public IFileSystem ContentFs { get; } public IFileSystem SaveFs { get; } - public Dictionary Ncas { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Ncas { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Saves { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Titles { get; } = new Dictionary(); public Dictionary Applications { get; } = new Dictionary(); @@ -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 Ncas { get; } = new List(); + public List Ncas { get; } = new List(); 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); } } diff --git a/src/LibHac/Util.cs b/src/LibHac/Util.cs index 71de4130..de08f97a 100644 --- a/src/LibHac/Util.cs +++ b/src/LibHac/Util.cs @@ -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 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 diff --git a/src/hactoolnet/Print.cs b/src/hactoolnet/Print.cs index 6cbaae67..68d0022a 100644 --- a/src/hactoolnet/Print.cs +++ b/src/hactoolnet/Print.cs @@ -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); + } } } diff --git a/src/hactoolnet/ProcessDelta.cs b/src/hactoolnet/ProcessDelta.cs index 6a5272ca..31ec547f 100644 --- a/src/hactoolnet/ProcessDelta.cs +++ b/src/hactoolnet/ProcessDelta.cs @@ -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)) diff --git a/src/hactoolnet/ProcessNca.cs b/src/hactoolnet/ProcessNca.cs index b4d142f5..8e65b5ce 100644 --- a/src/hactoolnet/ProcessNca.cs +++ b/src/hactoolnet/ProcessNca.cs @@ -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]; + } } } diff --git a/src/hactoolnet/ProcessPfs.cs b/src/hactoolnet/ProcessPfs.cs index 43b3730d..46f75321 100644 --- a/src/hactoolnet/ProcessPfs.cs +++ b/src/hactoolnet/ProcessPfs.cs @@ -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(); diff --git a/src/hactoolnet/ProcessSwitchFs.cs b/src/hactoolnet/ProcessSwitchFs.cs index ef8100ef..da329c32 100644 --- a/src/hactoolnet/ProcessSwitchFs.cs +++ b/src/hactoolnet/ProcessSwitchFs.cs @@ -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(); diff --git a/src/hactoolnet/ProcessXci.cs b/src/hactoolnet/ProcessXci.cs index b6928c4f..43e3638e 100644 --- a/src/hactoolnet/ProcessXci.cs +++ b/src/hactoolnet/ProcessXci.cs @@ -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) {