diff --git a/LibHac/Bktr.cs b/LibHac/Bktr.cs index edc5aec1..f3a289f4 100644 --- a/LibHac/Bktr.cs +++ b/LibHac/Bktr.cs @@ -19,7 +19,7 @@ namespace LibHac public Bktr(Stream patchRomfs, Stream baseRomfs, NcaSection section) { - if (section.Type != SectionType.Bktr) throw new ArgumentException("Section is not of type BKTR"); + if (section.Header.EncryptionType != NcaEncryptionType.AesCtrEx) throw new ArgumentException("Section is not of type BKTR"); Patch = patchRomfs ?? throw new NullReferenceException($"{nameof(patchRomfs)} cannot be null"); Base = baseRomfs ?? throw new NullReferenceException($"{nameof(baseRomfs)} cannot be null"); diff --git a/LibHac/BktrCryptoStream.cs b/LibHac/BktrCryptoStream.cs index 97153636..31ed4a96 100644 --- a/LibHac/BktrCryptoStream.cs +++ b/LibHac/BktrCryptoStream.cs @@ -87,6 +87,10 @@ namespace LibHac CurrentEntry = CurrentEntry.Next; UpdateCounterSubsection(CurrentEntry.Counter); } + else if (bytesRead == 0) + { + break; + } } return totalBytesRead; diff --git a/LibHac/HierarchicalIntegrityVerificationStream.cs b/LibHac/HierarchicalIntegrityVerificationStream.cs index bdb09c2b..c6b2d9d5 100644 --- a/LibHac/HierarchicalIntegrityVerificationStream.cs +++ b/LibHac/HierarchicalIntegrityVerificationStream.cs @@ -8,25 +8,78 @@ namespace LibHac { public Stream[] Levels { get; } public Stream DataLevel { get; } - public bool EnableIntegrityChecks { get; } + public IntegrityCheckLevel IntegrityCheckLevel { get; } - public HierarchicalIntegrityVerificationStream(IntegrityVerificationInfo[] levelInfo, bool enableIntegrityChecks) + /// + /// An array of the hash statuses of every block in each level. + /// + public Validity[][] LevelValidities { get; } + + private IntegrityVerificationStream[] IntegrityStreams { get; } + + public HierarchicalIntegrityVerificationStream(IntegrityVerificationInfo[] levelInfo, IntegrityCheckLevel integrityCheckLevel) { Levels = new Stream[levelInfo.Length]; - EnableIntegrityChecks = enableIntegrityChecks; + IntegrityCheckLevel = integrityCheckLevel; + LevelValidities = new Validity[levelInfo.Length - 1][]; + IntegrityStreams = new IntegrityVerificationStream[levelInfo.Length - 1]; Levels[0] = levelInfo[0].Data; for (int i = 1; i < Levels.Length; i++) { - var levelData = new IntegrityVerificationStream(levelInfo[i], Levels[i - 1], enableIntegrityChecks); + var levelData = new IntegrityVerificationStream(levelInfo[i], Levels[i - 1], integrityCheckLevel); Levels[i] = new RandomAccessSectorStream(levelData); + LevelValidities[i - 1] = levelData.BlockValidities; + IntegrityStreams[i - 1] = levelData; } DataLevel = Levels[Levels.Length - 1]; } + /// + /// Checks the hashes of any unchecked blocks and returns the of the hash level. + /// + /// The level of hierarchical hashes to check. + /// If , return as soon as an invalid block is found. + /// An optional for reporting progress. + /// The of the data of the specified hash level. + public Validity ValidateLevel(int level, bool returnOnError, IProgressReport logger = null) + { + Validity[] validities = LevelValidities[level]; + IntegrityVerificationStream levelStream = IntegrityStreams[level]; + + // The original position of the stream must be restored when we're done validating + long initialPosition = levelStream.Position; + + var buffer = new byte[levelStream.SectorSize]; + var result = Validity.Valid; + + logger?.SetTotal(levelStream.SectorCount); + + for (int i = 0; i < levelStream.SectorCount; i++) + { + if (validities[i] == Validity.Unchecked) + { + levelStream.Position = (long)levelStream.SectorSize * i; + levelStream.Read(buffer, 0, buffer.Length, IntegrityCheckLevel.IgnoreOnInvalid); + } + + if (validities[i] == Validity.Invalid) + { + result = Validity.Invalid; + if (returnOnError) break; + } + + logger?.ReportAdd(1); + } + + logger?.SetTotal(0); + levelStream.Position = initialPosition; + return result; + } + public override void Flush() { throw new NotImplementedException(); diff --git a/LibHac/IntegrityVerificationStream.cs b/LibHac/IntegrityVerificationStream.cs index 2c835aab..c8d72f46 100644 --- a/LibHac/IntegrityVerificationStream.cs +++ b/LibHac/IntegrityVerificationStream.cs @@ -10,7 +10,8 @@ namespace LibHac private const int DigestSize = 0x20; private Stream HashStream { get; } - public bool EnableIntegrityChecks { get; } + public IntegrityCheckLevel IntegrityCheckLevel { get; } + public Validity[] BlockValidities { get; } private byte[] Salt { get; } private IntegrityStreamType Type { get; } @@ -18,13 +19,15 @@ namespace LibHac private readonly byte[] _hashBuffer = new byte[DigestSize]; private readonly SHA256 _hash = SHA256.Create(); - public IntegrityVerificationStream(IntegrityVerificationInfo info, Stream hashStream, bool enableIntegrityChecks) + public IntegrityVerificationStream(IntegrityVerificationInfo info, Stream hashStream, IntegrityCheckLevel integrityCheckLevel) : base(info.Data, info.BlockSize) { HashStream = hashStream; - EnableIntegrityChecks = enableIntegrityChecks; + IntegrityCheckLevel = integrityCheckLevel; Salt = info.Salt; Type = info.Type; + + BlockValidities = new Validity[SectorCount]; } public override void Flush() @@ -55,12 +58,16 @@ namespace LibHac throw new NotImplementedException(); } - public override int Read(byte[] buffer, int offset, int count) + public override int Read(byte[] buffer, int offset, int count) => + Read(buffer, offset, count, IntegrityCheckLevel); + + public int Read(byte[] buffer, int offset, int count, IntegrityCheckLevel integrityCheckLevel) { - HashStream.Position = CurrentSector * DigestSize; + long blockNum = CurrentSector; + HashStream.Position = blockNum * DigestSize; HashStream.Read(_hashBuffer, 0, DigestSize); - int bytesRead = base.Read(buffer, 0, count); + int bytesRead = base.Read(buffer, offset, count); int bytesToHash = SectorSize; if (bytesRead == 0) return 0; @@ -68,14 +75,14 @@ namespace LibHac // If a hash is zero the data for the entire block is zero if (Type == IntegrityStreamType.Save && _hashBuffer.IsEmpty()) { - Array.Clear(buffer, 0, SectorSize); + Array.Clear(buffer, offset, SectorSize); return bytesRead; } if (bytesRead < SectorSize) { // Pad out unused portion of block - Array.Clear(buffer, bytesRead, SectorSize - bytesRead); + Array.Clear(buffer, offset + bytesRead, SectorSize - bytesRead); // Partition FS hashes don't pad out an incomplete block if (Type == IntegrityStreamType.PartitionFs) @@ -83,8 +90,15 @@ namespace LibHac bytesToHash = bytesRead; } } + + if (BlockValidities[blockNum] == Validity.Invalid && integrityCheckLevel == IntegrityCheckLevel.ErrorOnInvalid) + { + throw new InvalidDataException("Hash error!"); + } - if (!EnableIntegrityChecks) return bytesRead; + if (integrityCheckLevel == IntegrityCheckLevel.None) return bytesRead; + + if (BlockValidities[blockNum] != Validity.Unchecked) return bytesRead; _hash.Initialize(); @@ -93,7 +107,7 @@ namespace LibHac _hash.TransformBlock(Salt, 0, Salt.Length, null, 0); } - _hash.TransformBlock(buffer, 0, bytesToHash, null, 0); + _hash.TransformBlock(buffer, offset, bytesToHash, null, 0); _hash.TransformFinalBlock(buffer, 0, 0); byte[] hash = _hash.Hash; @@ -104,7 +118,10 @@ namespace LibHac hash[0x1F] |= 0x80; } - if (!Util.ArraysEqual(_hashBuffer, hash)) + Validity validity = Util.ArraysEqual(_hashBuffer, hash) ? Validity.Valid : Validity.Invalid; + BlockValidities[blockNum] = validity; + + if (validity == Validity.Invalid && integrityCheckLevel == IntegrityCheckLevel.ErrorOnInvalid) { throw new InvalidDataException("Hash error!"); } @@ -139,4 +156,23 @@ namespace LibHac RomFs, PartitionFs } + + /// + /// Represents the level of integrity checks to be performed. + /// + public enum IntegrityCheckLevel + { + /// + /// No integrity checks will be performed. + /// + None, + /// + /// Invalid blocks will be marked as invalid when read, and will not cause an error. + /// + IgnoreOnInvalid, + /// + /// An will be thrown if an integrity check fails. + /// + ErrorOnInvalid + } } diff --git a/LibHac/Keyset.cs b/LibHac/Keyset.cs index 10d7174e..9f25203f 100644 --- a/LibHac/Keyset.cs +++ b/LibHac/Keyset.cs @@ -246,7 +246,7 @@ namespace LibHac Crypto.DecryptEcb(headerKek, HeaderKeySource, HeaderKey, 0x20); } - private void DeriveSdCardKeys() + public void DeriveSdCardKeys() { var sdKek = new byte[0x10]; Crypto.GenerateKek(MasterKeys[0], SdCardKekSource, sdKek, AesKekGenerationSource, AesKeyGenerationSource); @@ -264,6 +264,8 @@ namespace LibHac Crypto.DecryptEcb(sdKek, SdCardKeySourcesSpecific[k], SdCardKeys[k], 0x20); } } + + internal static readonly string[] KakNames = {"application", "ocean", "system"}; } public static class ExternalKeys @@ -396,7 +398,7 @@ namespace LibHac public static string PrintKeys(Keyset keyset, Dictionary dict) { - if(dict.Count == 0) return string.Empty; + if (dict.Count == 0) return string.Empty; var sb = new StringBuilder(); int maxNameLength = dict.Values.Max(x => x.Name.Length); diff --git a/LibHac/Nca.cs b/LibHac/Nca.cs index 077c1ea7..26477910 100644 --- a/LibHac/Nca.cs +++ b/LibHac/Nca.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using LibHac.Streams; using LibHac.XTSSharp; @@ -20,6 +19,9 @@ namespace LibHac private bool KeepOpen { get; } private Nca BaseNca { get; set; } + private bool IsMissingTitleKey { get; set; } + private string MissingKeyName { get; set; } + public NcaSection[] Sections { get; } = new NcaSection[4]; public Nca(Keyset keyset, Stream stream, bool keepOpen) @@ -38,19 +40,20 @@ namespace LibHac { 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 { - if (keyset.TitleKeys.TryGetValue(Header.RightsId, out byte[] titleKey)) - { - TitleKey = titleKey; - Crypto.DecryptEcb(keyset.Titlekeks[CryptoType], titleKey, TitleKeyDec, 0x10); - DecryptedKeys[2] = TitleKeyDec; - } - else - { - // todo enable key check when opening a section - // throw new MissingKeyException("A required key is missing.", $"{Header.RightsId.ToHexString()}", KeyType.Title); - } + IsMissingTitleKey = true; } for (int i = 0; i < 4; i++) @@ -58,36 +61,57 @@ namespace LibHac NcaSection section = ParseSection(i); if (section == null) continue; Sections[i] = section; - ValidateSuperblockHash(i); - } - - foreach (NcaSection pfsSection in Sections.Where(x => x != null && x.Type == SectionType.Pfs0)) - { - Stream sectionStream = OpenSection(pfsSection.SectionNum, false, false); - if (sectionStream == null) continue; - - var pfs = new Pfs(sectionStream); - if (!pfs.FileExists("main.npdm")) continue; - - pfsSection.IsExefs = true; } } + /// + /// Opens a of the underlying NCA file. + /// + /// A that provides access to the entire raw NCA file. public Stream GetStream() { return StreamSource.CreateStream(); } + public bool CanOpenSection(int index) + { + if (index < 0 || index > 3) throw new ArgumentOutOfRangeException(nameof(index)); + + NcaSection sect = Sections[index]; + if (sect == null) return false; + + return sect.Header.EncryptionType == NcaEncryptionType.None || !IsMissingTitleKey && string.IsNullOrWhiteSpace(MissingKeyName); + } + private Stream OpenRawSection(int index) { - NcaSection sect = Sections[index]; - if (sect == null) throw new ArgumentOutOfRangeException(nameof(index)); + if (index < 0 || index > 3) throw new ArgumentOutOfRangeException(nameof(index)); - //if (sect.SuperblockHashValidity == Validity.Invalid) return null; + NcaSection sect = Sections[index]; + if (sect == null) return null; + + if (sect.Header.EncryptionType != NcaEncryptionType.None) + { + if (IsMissingTitleKey) + { + throw new MissingKeyException("Unable to decrypt NCA section.", Header.RightsId.ToHexString(), KeyType.Title); + } + + if (!string.IsNullOrWhiteSpace(MissingKeyName)) + { + throw new MissingKeyException("Unable to decrypt NCA section.", MissingKeyName, KeyType.Common); + } + } long offset = sect.Offset; long size = sect.Size; + if (!Util.IsSubRange(offset, size, StreamSource.Length)) + { + throw new InvalidDataException( + $"Section offset (0x{offset:x}) and length (0x{size:x}) fall outside the total NCA length (0x{StreamSource.Length:x})."); + } + Stream rawStream = StreamSource.CreateStream(offset, size); switch (sect.Header.EncryptionType) @@ -104,10 +128,8 @@ namespace LibHac false); if (BaseNca == null) return rawStream; - NcaSection baseSect = BaseNca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs); - if (baseSect == null) throw new InvalidDataException("Base NCA has no RomFS section"); + Stream baseStream = BaseNca.OpenSection(ProgramPartitionType.Data, true, IntegrityCheckLevel.None) ?? Stream.Null; - Stream baseStream = BaseNca.OpenSection(baseSect.SectionNum, true, false); return new Bktr(rawStream, baseStream, sect); default: @@ -115,27 +137,65 @@ namespace LibHac } } - public Stream OpenSection(int index, bool raw, bool enableIntegrityChecks) + /// + /// Opens one of the sections in the current . + /// + /// The index of the NCA section to open. Valid indexes are 0-3. + /// to open the raw section with hash metadata. + /// The level of integrity checks to be performed when reading the section. + /// Always if is . + /// A that provides access to the specified section. if the section does not exist. + /// The specified is outside the valid range. + public Stream OpenSection(int index, bool raw, IntegrityCheckLevel integrityCheckLevel) { Stream rawStream = OpenRawSection(index); - NcaSection sect = Sections[index]; if (raw || rawStream == null) return rawStream; - switch (sect.Header.Type) + NcaSection sect = Sections[index]; + NcaFsHeader header = sect.Header; + + // If it's a patch section without a base, return the raw section because it has no hash data + if (header.EncryptionType == NcaEncryptionType.AesCtrEx && BaseNca == null) return rawStream; + + switch (header.HashType) { - case SectionType.Pfs0: - return InitIvfcForPartitionfs(sect.Header.Sha256Info, new SharedStreamSource(rawStream), enableIntegrityChecks); - case SectionType.Romfs: - case SectionType.Bktr: - return InitIvfcForRomfs(sect.Header.IvfcInfo, new SharedStreamSource(rawStream), enableIntegrityChecks); + case NcaHashType.Sha256: + return InitIvfcForPartitionfs(header.Sha256Info, new SharedStreamSource(rawStream), integrityCheckLevel); + case NcaHashType.Ivfc: + return InitIvfcForRomfs(header.IvfcInfo, new SharedStreamSource(rawStream), integrityCheckLevel); + default: throw new ArgumentOutOfRangeException(); } } + /// + /// Opens one of the sections in the current as a + /// Only works with sections that have a of or . + /// + /// The index of the NCA section to open. Valid indexes are 0-3. + /// The level of integrity checks to be performed when reading the section. + /// A that provides access to the specified section. if the section does not exist, + /// or is has no hash metadata. + /// The specified is outside the valid range. + public HierarchicalIntegrityVerificationStream OpenHashedSection(int index, IntegrityCheckLevel integrityCheckLevel) => + OpenSection(index, false, integrityCheckLevel) as HierarchicalIntegrityVerificationStream; + + /// + /// Opens one of the sections in the current . For use with type NCAs. + /// + /// The type of section to open. + /// to open the raw section with hash metadata. + /// The level of integrity checks to be performed when reading the section. + /// Always if is . + /// A that provides access to the specified section. if the section does not exist. + /// The specified is outside the valid range. + public Stream OpenSection(ProgramPartitionType type, bool raw, IntegrityCheckLevel integrityCheckLevel) => + OpenSection((int)type, raw, integrityCheckLevel); + private static HierarchicalIntegrityVerificationStream InitIvfcForRomfs(IvfcHeader ivfc, - SharedStreamSource romfsStreamSource, bool enableIntegrityChecks) + SharedStreamSource romfsStreamSource, IntegrityCheckLevel integrityCheckLevel) { var initInfo = new IntegrityVerificationInfo[ivfc.NumLevels]; @@ -159,11 +219,11 @@ namespace LibHac }; } - return new HierarchicalIntegrityVerificationStream(initInfo, enableIntegrityChecks); + return new HierarchicalIntegrityVerificationStream(initInfo, integrityCheckLevel); } private static Stream InitIvfcForPartitionfs(Sha256Info sb, - SharedStreamSource pfsStreamSource, bool enableIntegrityChecks) + SharedStreamSource pfsStreamSource, IntegrityCheckLevel integrityCheckLevel) { SharedStream hashStream = pfsStreamSource.CreateStream(sb.HashTableOffset, sb.HashTableSize); SharedStream dataStream = pfsStreamSource.CreateStream(sb.DataOffset, sb.DataSize); @@ -192,13 +252,34 @@ namespace LibHac Type = IntegrityStreamType.PartitionFs }; - return new HierarchicalIntegrityVerificationStream(initInfo, enableIntegrityChecks); + return new HierarchicalIntegrityVerificationStream(initInfo, integrityCheckLevel); } + /// + /// 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++) + { + if (Sections[i] == null) continue; + ValidateMasterHash(i); + } + } + private void DecryptHeader(Keyset keyset, Stream stream) { + if (keyset.HeaderKey.IsEmpty()) + { + throw new MissingKeyException("Unable to decrypt NCA header.", "header_key", KeyType.Common); + } + var headerBytes = new byte[0xC00]; Xts xts = XtsAes128.Create(keyset.HeaderKey); using (var headerDec = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x200))) @@ -213,6 +294,12 @@ namespace LibHac private void DecryptKeyArea(Keyset keyset) { + 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], @@ -239,6 +326,10 @@ namespace LibHac 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 RandomAccessSectorStream(new Aes128CtrStream(GetStream(), DecryptedKeys[2], sect.Offset, sect.Size, sect.Offset, sect.Header.Ctr))) { @@ -248,75 +339,51 @@ namespace LibHac if (size != offset) { - sect.SuperblockHashValidity = Validity.Invalid; + sect.MasterHashValidity = Validity.Invalid; } } } - private void ValidateSuperblockHash(int index) + private void ValidateMasterHash(int index) { if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index)); NcaSection sect = Sections[index]; - byte[] expected = null; + if (!CanOpenSection(index)) + { + sect.MasterHashValidity = Validity.MissingKey; + return; + } + + byte[] expected = sect.GetMasterHash(); long offset = 0; long size = 0; - switch (sect.Type) + switch (sect.Header.HashType) { - case SectionType.Invalid: + case NcaHashType.Sha256: + offset = sect.Header.Sha256Info.HashTableOffset; + size = sect.Header.Sha256Info.HashTableSize; break; - case SectionType.Pfs0: - Sha256Info pfs0 = sect.Header.Sha256Info; - expected = pfs0.MasterHash; - offset = pfs0.HashTableOffset; - size = pfs0.HashTableSize; - break; - case SectionType.Romfs: - IvfcHeader ivfc = sect.Header.IvfcInfo; - expected = ivfc.MasterHash; - offset = ivfc.LevelHeaders[0].LogicalOffset; - size = 1 << ivfc.LevelHeaders[0].BlockSizePower; - break; - case SectionType.Bktr: + case NcaHashType.Ivfc when sect.Header.EncryptionType == NcaEncryptionType.AesCtrEx: CheckBktrKey(sect); return; + case NcaHashType.Ivfc: + offset = sect.Header.IvfcInfo.LevelHeaders[0].LogicalOffset; + size = 1 << sect.Header.IvfcInfo.LevelHeaders[0].BlockSizePower; + break; } - Stream stream = OpenSection(index, true, false); - if (stream == null) return; - if (expected == null) return; + Stream stream = OpenSection(index, true, IntegrityCheckLevel.None); var hashTable = new byte[size]; stream.Position = offset; stream.Read(hashTable, 0, hashTable.Length); - sect.SuperblockHashValidity = Crypto.CheckMemoryHashTable(hashTable, expected, 0, hashTable.Length); - // todo if (sect.Type == SectionType.Romfs) sect.Romfs.IvfcLevels[0].HashValidity = sect.SuperblockHashValidity; + sect.MasterHashValidity = Crypto.CheckMemoryHashTable(hashTable, expected, 0, hashTable.Length); + if (sect.Header.HashType == NcaHashType.Ivfc) sect.Header.IvfcInfo.LevelHeaders[0].HashValidity = sect.MasterHashValidity; } - public void VerifySection(int index, IProgressReport logger = null) - { - if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index)); - NcaSection sect = Sections[index]; - Stream stream = OpenSection(index, true, false); - logger?.LogMessage($"Verifying section {index}..."); - - switch (sect.Type) - { - case SectionType.Invalid: - break; - case SectionType.Pfs0: - // todo VerifyPfs0(stream, sect.Pfs0, logger); - break; - case SectionType.Romfs: - // todo VerifyIvfc(stream, sect.Romfs.IvfcLevels, logger); - break; - case SectionType.Bktr: - break; - } - } - public void Dispose() { if (!KeepOpen) @@ -333,19 +400,34 @@ namespace LibHac public int SectionNum { get; set; } public long Offset { get; set; } public long Size { get; set; } - public Validity SuperblockHashValidity { get; set; } + public Validity MasterHashValidity { get; set; } - public bool IsExefs { get; internal set; } + public byte[] GetMasterHash() + { + var hash = new byte[Crypto.Sha256DigestSize]; + + switch (Header.HashType) + { + case NcaHashType.Sha256: + Array.Copy(Header.Sha256Info.MasterHash, hash, Crypto.Sha256DigestSize); + break; + case NcaHashType.Ivfc: + Array.Copy(Header.IvfcInfo.MasterHash, hash, Crypto.Sha256DigestSize); + break; + } + + return hash; + } } public static class NcaExtensions { - public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, bool verify = false, IProgressReport logger = null) + public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, IntegrityCheckLevel integrityCheckLevel = IntegrityCheckLevel.None, IProgressReport logger = null) { if (index < 0 || index > 3) throw new IndexOutOfRangeException(); if (nca.Sections[index] == null) return; - Stream section = nca.OpenSection(index, raw, verify); + Stream section = nca.OpenSection(index, raw, integrityCheckLevel); string dir = Path.GetDirectoryName(filename); if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); @@ -355,13 +437,13 @@ namespace LibHac } } - public static void ExtractSection(this Nca nca, int index, string outputDir, bool verify = false, IProgressReport logger = null) + public static void ExtractSection(this Nca nca, int index, string outputDir, IntegrityCheckLevel integrityCheckLevel = IntegrityCheckLevel.None, IProgressReport logger = null) { if (index < 0 || index > 3) throw new IndexOutOfRangeException(); if (nca.Sections[index] == null) return; NcaSection section = nca.Sections[index]; - Stream stream = nca.OpenSection(index, false, verify); + Stream stream = nca.OpenSection(index, false, integrityCheckLevel); switch (section.Type) { @@ -379,5 +461,53 @@ namespace LibHac break; } } + + 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) + { + Validity sectionValidity = VerifySection(nca, i, logger, quiet); + + if (sectionValidity == Validity.Invalid) return Validity.Invalid; + } + } + + return Validity.Valid; + } + + 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; + if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked; + + HierarchicalIntegrityVerificationStream stream = nca.OpenHashedSection(index, IntegrityCheckLevel.IgnoreOnInvalid); + if (stream == null) return Validity.Unchecked; + + if (!quiet) logger?.LogMessage($"Verifying section {index}..."); + + for (int i = 0; i < stream.Levels.Length - 1; i++) + { + if (!quiet) logger?.LogMessage($" Verifying Hash Level {i}..."); + Validity levelValidity = stream.ValidateLevel(i, true, logger); + + if (hashType == NcaHashType.Ivfc) + { + sect.Header.IvfcInfo.LevelHeaders[i].HashValidity = levelValidity; + } + else if (hashType == NcaHashType.Sha256 && i == stream.Levels.Length - 2) + { + sect.Header.Sha256Info.HashValidity = levelValidity; + } + + if (levelValidity == Validity.Invalid) return Validity.Invalid; + } + + return Validity.Valid; + } } } diff --git a/LibHac/NcaStructs.cs b/LibHac/NcaStructs.cs index 5ad9ef55..c75f1c68 100644 --- a/LibHac/NcaStructs.cs +++ b/LibHac/NcaStructs.cs @@ -148,32 +148,6 @@ namespace LibHac } } - public class RomfsSuperblock - { - public IvfcHeader IvfcHeader; - - public RomfsSuperblock(BinaryReader reader) - { - IvfcHeader = new IvfcHeader(reader); - reader.BaseStream.Position += 0x58; - } - } - - public class BktrSuperblock - { - public IvfcHeader IvfcHeader; - public BktrHeader RelocationHeader; - public BktrHeader SubsectionHeader; - - public BktrSuperblock(BinaryReader reader) - { - IvfcHeader = new IvfcHeader(reader); - reader.BaseStream.Position += 0x18; - RelocationHeader = new BktrHeader(reader); - SubsectionHeader = new BktrHeader(reader); - } - } - public class BktrPatchInfo { public BktrHeader RelocationHeader; @@ -215,6 +189,8 @@ namespace LibHac public int BlockSizePower; public uint Reserved; + public Validity HashValidity = Validity.Unchecked; + public IvfcLevelHeader(BinaryReader reader) { LogicalOffset = reader.ReadInt64(); @@ -234,6 +210,8 @@ namespace LibHac public long DataOffset; public long DataSize; + public Validity HashValidity = Validity.Unchecked; + public Sha256Info(BinaryReader reader) { MasterHash = reader.ReadBytes(0x20); @@ -300,17 +278,12 @@ namespace LibHac } } - public class Pfs0Section + public enum ProgramPartitionType { - public PfsSuperblock Superblock { get; set; } - public Validity Validity { get; set; } - } - - public class RomfsSection - { - public RomfsSuperblock Superblock { get; set; } - public IvfcLevel[] IvfcLevels { get; set; } = new IvfcLevel[Romfs.IvfcMaxLevel]; - } + Code, + Data, + Logo + }; public enum ContentType { @@ -359,10 +332,11 @@ namespace LibHac Bktr } - public enum Validity + public enum Validity : byte { Unchecked, Invalid, - Valid + Valid, + MissingKey } } diff --git a/LibHac/Pfs.cs b/LibHac/Pfs.cs index a3d8ab1b..c809dca2 100644 --- a/LibHac/Pfs.cs +++ b/LibHac/Pfs.cs @@ -68,29 +68,6 @@ namespace LibHac Hfs0 } - public class PfsSuperblock - { - public byte[] MasterHash; /* SHA-256 hash of the hash table. */ - public int BlockSize; /* In bytes. */ - public uint Always2; - public long HashTableOffset; /* Normally zero. */ - public long HashTableSize; - public long Pfs0Offset; - public long Pfs0Size; - - public PfsSuperblock(BinaryReader reader) - { - MasterHash = reader.ReadBytes(0x20); - BlockSize = reader.ReadInt32(); - Always2 = reader.ReadUInt32(); - HashTableOffset = reader.ReadInt64(); - HashTableSize = reader.ReadInt64(); - Pfs0Offset = reader.ReadInt64(); - Pfs0Size = reader.ReadInt64(); - reader.BaseStream.Position += 0xF0; - } - } - public class PfsHeader { public string Magic; diff --git a/LibHac/Romfs.cs b/LibHac/Romfs.cs index be15a7a7..2b8bd7b2 100644 --- a/LibHac/Romfs.cs +++ b/LibHac/Romfs.cs @@ -143,19 +143,6 @@ namespace LibHac } } - - - public class IvfcLevel - { - public long DataOffset { get; set; } - public long DataSize { get; set; } - public long HashOffset { get; set; } - public long HashSize { get; set; } - public long HashBlockSize { get; set; } - public long HashBlockCount { get; set; } - public Validity HashValidity { get; set; } - } - public static class RomfsExtensions { public static void Extract(this Romfs romfs, string outDir, IProgressReport logger = null) diff --git a/LibHac/Savefile/Savefile.cs b/LibHac/Savefile/Savefile.cs index 015e47ac..cee44595 100644 --- a/LibHac/Savefile/Savefile.cs +++ b/LibHac/Savefile/Savefile.cs @@ -41,7 +41,7 @@ namespace LibHac.Savefile public DirectoryEntry[] Directories { get; private set; } private Dictionary FileDict { get; } - public Savefile(Keyset keyset, Stream file, bool enableIntegrityChecks) + public Savefile(Keyset keyset, Stream file, IntegrityCheckLevel integrityCheckLevel) { SavefileSource = new SharedStreamSource(file); @@ -106,7 +106,7 @@ namespace LibHac.Savefile JournalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); JournalStreamSource = new SharedStreamSource(JournalStream); - IvfcStream = InitIvfcStream(enableIntegrityChecks); + IvfcStream = InitIvfcStream(integrityCheckLevel); IvfcStreamSource = new SharedStreamSource(IvfcStream); ReadFileInfo(); @@ -120,7 +120,7 @@ namespace LibHac.Savefile } } - private HierarchicalIntegrityVerificationStream InitIvfcStream(bool enableIntegrityChecks) + private HierarchicalIntegrityVerificationStream InitIvfcStream(IntegrityCheckLevel integrityCheckLevel) { IvfcHeader ivfc = Header.Ivfc; @@ -151,7 +151,7 @@ namespace LibHac.Savefile }; } - return new HierarchicalIntegrityVerificationStream(initInfo, enableIntegrityChecks); + return new HierarchicalIntegrityVerificationStream(initInfo, integrityCheckLevel); } public Stream OpenFile(string filename) diff --git a/LibHac/Streams/CombinationStream.cs b/LibHac/Streams/CombinationStream.cs index e37a3883..9dd350e6 100644 --- a/LibHac/Streams/CombinationStream.cs +++ b/LibHac/Streams/CombinationStream.cs @@ -83,7 +83,7 @@ namespace LibHac.Streams break; } int idx = 0; - while (idx+1 < _streamsStartPos.Count) + while (idx + 1 < _streamsStartPos.Count) { if (_streamsStartPos[idx + 1] > pos) { @@ -122,10 +122,11 @@ namespace LibHac.Streams if (count > 0) { - if (_currentStreamIndex >= _streams.Count) + if (_currentStreamIndex + 1 >= _streams.Count) break; - _currentStream = _streams[_currentStreamIndex++]; + _currentStream = _streams[_currentStreamIndex + 1]; + _currentStreamIndex++; _currentStream.Position = 0; } } diff --git a/LibHac/Streams/SectorStream.cs b/LibHac/Streams/SectorStream.cs index 80ac740c..c69d42be 100644 --- a/LibHac/Streams/SectorStream.cs +++ b/LibHac/Streams/SectorStream.cs @@ -15,6 +15,11 @@ namespace LibHac.Streams /// public int SectorSize { get; } + /// + /// The number of sectors in the stream. + /// + public int SectorCount { get; } + /// /// The maximum number of sectors that can be read or written in a single operation. /// @@ -64,6 +69,8 @@ namespace LibHac.Streams _keepOpen = keepOpen; _maxBufferSize = MaxSectors * SectorSize; baseStream.Position = offset; + + SectorCount = (int)Util.DivideByRoundUp(_baseStream.Length - _offset, sectorSize); } public override void Flush() diff --git a/LibHac/SwitchFs.cs b/LibHac/SwitchFs.cs index a009ff83..967aa14e 100644 --- a/LibHac/SwitchFs.cs +++ b/LibHac/SwitchFs.cs @@ -126,7 +126,7 @@ namespace LibHac string sdPath = "/" + Util.GetRelativePath(file, SaveDir).Replace('\\', '/'); var nax0 = new Nax0(Keyset, stream, sdPath, false); - save = new Savefile.Savefile(Keyset, nax0.Stream, false); + save = new Savefile.Savefile(Keyset, nax0.Stream, IntegrityCheckLevel.None); } catch (Exception ex) { @@ -147,7 +147,7 @@ namespace LibHac var title = new Title(); // Meta contents always have 1 Partition FS section with 1 file in it - Stream sect = nca.OpenSection(0, false, true); + Stream sect = nca.OpenSection(0, false, IntegrityCheckLevel.ErrorOnInvalid); var pfs0 = new Pfs(sect); Stream file = pfs0.OpenFile(pfs0.Files[0]); @@ -187,7 +187,7 @@ namespace LibHac { foreach (Title title in Titles.Values.Where(x => x.ControlNca != null)) { - var romfs = new Romfs(title.ControlNca.OpenSection(0, false, true)); + var romfs = new Romfs(title.ControlNca.OpenSection(0, false, IntegrityCheckLevel.ErrorOnInvalid)); byte[] control = romfs.GetFile("/control.nacp"); var reader = new BinaryReader(new MemoryStream(control)); diff --git a/LibHac/Util.cs b/LibHac/Util.cs index 8eebbae7..2e7c5e2a 100644 --- a/LibHac/Util.cs +++ b/LibHac/Util.cs @@ -399,10 +399,16 @@ namespace LibHac case 2: return "3.0.1-3.0.2"; case 3: return "4.0.0-4.1.0"; case 4: return "5.0.0-5.1.0"; - case 5: return "6.0.0"; + case 5: return "6.0.0-6.0.1"; default: return "Unknown"; } } + + public static bool IsSubRange(long startIndex, long subLength, long length) + { + bool isOutOfRange = startIndex < 0 || startIndex > length || subLength < 0 || startIndex > length - subLength; + return !isOutOfRange; + } } public class ByteArray128BitComparer : EqualityComparer diff --git a/NandReader/Program.cs b/NandReader/Program.cs index 37c9d3bf..4718535d 100644 --- a/NandReader/Program.cs +++ b/NandReader/Program.cs @@ -102,7 +102,7 @@ namespace NandReader private static List ReadTickets(Keyset keyset, Stream savefile) { var tickets = new List(); - var save = new Savefile(keyset, savefile, false); + var save = new Savefile(keyset, savefile, IntegrityCheckLevel.None); var ticketList = new BinaryReader(save.OpenFile("/ticket_list.bin")); var ticketFile = new BinaryReader(save.OpenFile("/ticket.bin")); diff --git a/NandReaderGui/ViewModel/NandViewModel.cs b/NandReaderGui/ViewModel/NandViewModel.cs index 2f4a1504..feddd0ad 100644 --- a/NandReaderGui/ViewModel/NandViewModel.cs +++ b/NandReaderGui/ViewModel/NandViewModel.cs @@ -84,7 +84,7 @@ namespace NandReaderGui.ViewModel private static List ReadTickets(Keyset keyset, Stream savefile) { var tickets = new List(); - var save = new Savefile(keyset, savefile, false); + var save = new Savefile(keyset, savefile, IntegrityCheckLevel.None); var ticketList = new BinaryReader(save.OpenFile("/ticket_list.bin")); var ticketFile = new BinaryReader(save.OpenFile("/ticket.bin")); diff --git a/README.md b/README.md index 8c54efae..ffcefd72 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ hactoolnet is an example program that uses LibHac. It is used in a similar manne Usage: hactoolnet.exe [options...] Options: -r, --raw Keep raw data, don't unpack. - -y, --verify Verify hashes. + -y, --verify Verify all hashes in the input file. + -h, --enablehash Enable hash checks when reading the input file. -k, --keyset Load keys from an external file. - -t, --intype=type Specify input file type [nca, xci, romfs, pk11, pk21, switchfs, save, keygen] + -t, --intype=type Specify input file type [nca, xci, romfs, pk11, pk21, ini1, kip1, switchfs, save, keygen] --titlekeys Load title keys from an external file. NCA options: --section0 Specify Section 0 file path. @@ -53,6 +54,8 @@ Package1 options: --outdir Specify Package1 directory path. Package2 options: --outdir Specify Package2 directory path. +INI1 options: + --outdir Specify INI1 directory path. Switch FS options: --sdseed Set console unique seed for SD card NAX0 encryption. --listapps List application info. @@ -64,10 +67,13 @@ Switch FS options: --romfs Specify RomFS directory path. (--title must be specified) --romfsdir Specify RomFS directory path. (--title must be specified) --savedir Specify save file directory path. + -y, --verify Verify all titles, or verify a single title if --title is set. Savefile options: --outdir Specify directory path to save contents to. --debugoutdir Specify directory path to save intermediate data to for debugging. --sign Sign the save file. (Requires device_key in key file) +Keygen options: + --outdir Specify directory path to save key files to. ``` ## Examples diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index 476912f3..d2a434d4 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -198,6 +198,7 @@ namespace hactoolnet sb.AppendLine(" --romfs Specify RomFS directory path. (--title must be specified)"); sb.AppendLine(" --romfsdir Specify RomFS directory path. (--title must be specified)"); sb.AppendLine(" --savedir Specify save file directory path."); + sb.AppendLine(" -y, --verify Verify all titles, or verify a single title if --title is set."); sb.AppendLine("Savefile options:"); sb.AppendLine(" --outdir Specify directory path to save contents to."); sb.AppendLine(" --debugoutdir Specify directory path to save intermediate data to for debugging."); diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index 1437906b..69d4aa30 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -36,6 +36,9 @@ namespace hactoolnet public bool ListRomFs; public bool SignSave; public ulong TitleId; + + public IntegrityCheckLevel IntegrityLevel => + EnableHash ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; } internal enum FileType diff --git a/hactoolnet/ProcessNca.cs b/hactoolnet/ProcessNca.cs index 464852f8..1b87a1e8 100644 --- a/hactoolnet/ProcessNca.cs +++ b/hactoolnet/ProcessNca.cs @@ -13,6 +13,7 @@ namespace hactoolnet using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read)) { var nca = new Nca(ctx.Keyset, file, false); + nca.ValidateMasterHashes(); if (ctx.Options.BaseNca != null) { @@ -25,12 +26,12 @@ namespace hactoolnet { if (ctx.Options.SectionOut[i] != null) { - nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } if (ctx.Options.SectionOutDir[i] != null) { - nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Options.EnableHash, ctx.Logger); + nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Options.IntegrityLevel, ctx.Logger); } if (ctx.Options.Validate && nca.Sections[i] != null) @@ -41,7 +42,7 @@ namespace hactoolnet if (ctx.Options.ListRomFs && nca.Sections[1] != null) { - var romfs = new Romfs(nca.OpenSection(1, false, ctx.Options.EnableHash)); + var romfs = new Romfs(nca.OpenSection(1, false, ctx.Options.IntegrityLevel)); foreach (RomfsFile romfsFile in romfs.Files) { @@ -67,19 +68,25 @@ namespace hactoolnet if (ctx.Options.RomfsOut != null) { - nca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + nca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } if (ctx.Options.RomfsOutDir != null) { - var romfs = new Romfs(nca.OpenSection(section.SectionNum, false, ctx.Options.EnableHash)); + var romfs = new Romfs(nca.OpenSection(section.SectionNum, false, ctx.Options.IntegrityLevel)); romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger); } } if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null) { - NcaSection section = nca.Sections.FirstOrDefault(x => x?.IsExefs == true); + if (nca.Header.ContentType != ContentType.Program) + { + ctx.Logger.LogMessage("NCA's content type is not \"Program\""); + return; + } + + NcaSection section = nca.Sections[(int)ProgramPartitionType.Code]; if (section == null) { @@ -89,12 +96,12 @@ namespace hactoolnet if (ctx.Options.ExefsOut != null) { - nca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + nca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } if (ctx.Options.ExefsOutDir != null) { - nca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger); + nca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger); } } @@ -153,21 +160,21 @@ namespace hactoolnet NcaSection sect = nca.Sections[i]; if (sect == null) continue; + bool isExefs = nca.Header.ContentType == ContentType.Program && i == (int)ProgramPartitionType.Code; + 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:", sect.IsExefs ? "ExeFS" : sect.Type.ToString()); + PrintItem(sb, colLen, " Partition Type:", isExefs ? "ExeFS" : sect.Type.ToString()); PrintItem(sb, colLen, " Section CTR:", sect.Header.Ctr); - switch (sect.Type) + switch (sect.Header.HashType) { - case SectionType.Pfs0: - PrintPfs0(sect); + case NcaHashType.Sha256: + PrintSha256Hash(sect); break; - case SectionType.Romfs: - PrintRomfs(sect); - break; - case SectionType.Bktr: + case NcaHashType.Ivfc: + PrintIvfcHash(sect); break; default: sb.AppendLine(" Unknown/invalid superblock!"); @@ -176,13 +183,12 @@ namespace hactoolnet } } - void PrintPfs0(NcaSection sect) + void PrintSha256Hash(NcaSection sect) { Sha256Info hashInfo = sect.Header.Sha256Info; - PrintItem(sb, colLen, $" Superblock Hash{sect.SuperblockHashValidity.GetValidityString()}:", hashInfo.MasterHash); - // todo sb.AppendLine($" Hash Table{sect.Pfs0.Validity.GetValidityString()}:"); - sb.AppendLine($" Hash Table:"); + PrintItem(sb, colLen, $" Master Hash{sect.MasterHashValidity.GetValidityString()}:", hashInfo.MasterHash); + sb.AppendLine($" Hash Table{sect.Header.Sha256Info.HashValidity.GetValidityString()}:"); PrintItem(sb, colLen, " Offset:", $"0x{hashInfo.HashTableOffset:x12}"); PrintItem(sb, colLen, " Size:", $"0x{hashInfo.HashTableSize:x12}"); @@ -191,11 +197,11 @@ namespace hactoolnet PrintItem(sb, colLen, " PFS0 Size:", $"0x{hashInfo.DataSize:x12}"); } - void PrintRomfs(NcaSection sect) + void PrintIvfcHash(NcaSection sect) { IvfcHeader ivfcInfo = sect.Header.IvfcInfo; - PrintItem(sb, colLen, $" Superblock Hash{sect.SuperblockHashValidity.GetValidityString()}:", ivfcInfo.MasterHash); + PrintItem(sb, colLen, $" Master Hash{sect.MasterHashValidity.GetValidityString()}:", ivfcInfo.MasterHash); PrintItem(sb, colLen, " Magic:", ivfcInfo.Magic); PrintItem(sb, colLen, " Version:", $"{ivfcInfo.Version:x8}"); @@ -209,8 +215,7 @@ namespace hactoolnet hashOffset = ivfcInfo.LevelHeaders[i - 1].LogicalOffset; } - // todo sb.AppendLine($" Level {i}{level.HashValidity.GetValidityString()}:"); - sb.AppendLine($" Level {i}:"); + sb.AppendLine($" Level {i}{level.HashValidity.GetValidityString()}:"); PrintItem(sb, colLen, " Data Offset:", $"0x{level.LogicalOffset:x12}"); PrintItem(sb, colLen, " Data Size:", $"0x{level.HashDataSize:x12}"); PrintItem(sb, colLen, " Hash Offset:", $"0x{hashOffset:x12}"); diff --git a/hactoolnet/ProcessSave.cs b/hactoolnet/ProcessSave.cs index 91efb08c..060f3c3c 100644 --- a/hactoolnet/ProcessSave.cs +++ b/hactoolnet/ProcessSave.cs @@ -14,7 +14,7 @@ namespace hactoolnet { using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.ReadWrite)) { - var save = new Savefile(ctx.Keyset, file, ctx.Options.EnableHash); + var save = new Savefile(ctx.Keyset, file, ctx.Options.IntegrityLevel); if (ctx.Options.OutDir != null) { diff --git a/hactoolnet/ProcessSwitchFs.cs b/hactoolnet/ProcessSwitchFs.cs index b23bc75b..fe2e89cd 100644 --- a/hactoolnet/ProcessSwitchFs.cs +++ b/hactoolnet/ProcessSwitchFs.cs @@ -45,7 +45,7 @@ namespace hactoolnet return; } - NcaSection section = title.MainNca.Sections.FirstOrDefault(x => x.IsExefs); + NcaSection section = title.MainNca.Sections[(int)ProgramPartitionType.Code]; if (section == null) { @@ -55,12 +55,12 @@ namespace hactoolnet if (ctx.Options.ExefsOutDir != null) { - title.MainNca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger); + title.MainNca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger); } if (ctx.Options.ExefsOut != null) { - title.MainNca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + title.MainNca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } } @@ -95,13 +95,13 @@ namespace hactoolnet if (ctx.Options.RomfsOutDir != null) { - var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false, ctx.Options.EnableHash)); + var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false, ctx.Options.IntegrityLevel)); romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger); } if (ctx.Options.RomfsOut != null) { - title.MainNca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + title.MainNca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } } @@ -119,6 +119,67 @@ namespace hactoolnet { ExportSdSaves(ctx, switchFs); } + + if (ctx.Options.Validate) + { + ValidateSwitchFs(ctx, switchFs); + } + } + + private static void ValidateSwitchFs(Context ctx, SwitchFs switchFs) + { + if (ctx.Options.TitleId != 0) + { + ulong id = ctx.Options.TitleId; + + if (!switchFs.Titles.TryGetValue(id, out Title title)) + { + ctx.Logger.LogMessage($"Could not find title {id:X16}"); + return; + } + + ValidateTitle(ctx, title, ""); + + return; + } + + foreach (Application app in switchFs.Applications.Values) + { + ctx.Logger.LogMessage($"Checking {app.Name}..."); + + Title mainTitle = app.Patch ?? app.Main; + + if (mainTitle != null) + { + ValidateTitle(ctx, mainTitle, "Main title"); + } + + foreach (Title title in app.AddOnContent) + { + ValidateTitle(ctx, title, "Add-on content"); + } + } + } + + private static void ValidateTitle(Context ctx, Title title, string caption) + { + try + { + ctx.Logger.LogMessage($" {caption} {title.Id:x16}"); + + foreach (Nca nca in title.Ncas) + { + ctx.Logger.LogMessage($" {nca.Header.ContentType.ToString()}"); + + Validity validity = nca.VerifyNca(ctx.Logger, true); + + ctx.Logger.LogMessage($" {validity.ToString()}"); + } + } + catch (Exception ex) + { + ctx.Logger.LogMessage($"Error processing title {title.Id:x16}:\n{ex.Message}"); + } } private static void SaveTitle(Context ctx, SwitchFs switchFs) @@ -170,7 +231,7 @@ namespace hactoolnet foreach (NcaSection sect in nca.Sections.Where(x => x != null)) { - Console.WriteLine($" {sect.SectionNum} {sect.Type} {sect.Header.EncryptionType} {sect.SuperblockHashValidity}"); + Console.WriteLine($" {sect.SectionNum} {sect.Type} {sect.Header.EncryptionType} {sect.MasterHashValidity}"); } } diff --git a/hactoolnet/ProcessXci.cs b/hactoolnet/ProcessXci.cs index 8edb511a..1ac4037d 100644 --- a/hactoolnet/ProcessXci.cs +++ b/hactoolnet/ProcessXci.cs @@ -69,7 +69,7 @@ namespace hactoolnet return; } - NcaSection exefsSection = mainNca.Sections.FirstOrDefault(x => x.IsExefs); + NcaSection exefsSection = mainNca.Sections[(int)ProgramPartitionType.Code]; if (exefsSection == null) { @@ -79,12 +79,12 @@ namespace hactoolnet if (ctx.Options.ExefsOutDir != null) { - mainNca.ExtractSection(exefsSection.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger); + mainNca.ExtractSection(exefsSection.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.IntegrityLevel, ctx.Logger); } if (ctx.Options.ExefsOut != null) { - mainNca.ExportSection(exefsSection.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + mainNca.ExportSection(exefsSection.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } } @@ -108,13 +108,13 @@ namespace hactoolnet if (ctx.Options.RomfsOutDir != null) { - var romfs = new Romfs(mainNca.OpenSection(romfsSection.SectionNum, false, ctx.Options.EnableHash)); + var romfs = new Romfs(mainNca.OpenSection(romfsSection.SectionNum, false, ctx.Options.IntegrityLevel)); romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger); } if (ctx.Options.RomfsOut != null) { - mainNca.ExportSection(romfsSection.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger); + mainNca.ExportSection(romfsSection.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.IntegrityLevel, ctx.Logger); } } } diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 3476f4e4..1c12bafa 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -8,6 +8,27 @@ namespace hactoolnet public static class Program { public static void Main(string[] args) + { + try + { + Run(args); + } + catch (MissingKeyException ex) + { + string name = ex.Type == KeyType.Title ? $"Title key for rights ID {ex.Name}" : ex.Name; + Console.WriteLine($"\nERROR: {ex.Message}\nA required key is missing.\nKey name: {name}\n"); + } + catch (Exception ex) + { + Console.WriteLine($"\nERROR: {ex.Message}\n"); + + Console.WriteLine("Additional information:"); + Console.WriteLine(ex.GetType().FullName); + Console.WriteLine(ex.StackTrace); + } + } + + private static void Run(string[] args) { Console.OutputEncoding = Encoding.UTF8; var ctx = new Context(); @@ -25,47 +46,52 @@ namespace hactoolnet return; } - switch (ctx.Options.InFileType) - { - case FileType.Nca: - ProcessNca.Process(ctx); - break; - case FileType.Pfs0: - case FileType.Nsp: - ProcessNsp.Process(ctx); - break; - case FileType.Romfs: - ProcessRomfs.Process(ctx); - break; - case FileType.Nax0: - break; - case FileType.SwitchFs: - ProcessSwitchFs.Process(ctx); - break; - case FileType.Save: - ProcessSave.Process(ctx); - break; - case FileType.Xci: - ProcessXci.Process(ctx); - break; - case FileType.Keygen: - ProcessKeygen(ctx); - break; - case FileType.Pk11: - ProcessPackage.ProcessPk11(ctx); - break; - case FileType.Pk21: - ProcessPackage.ProcessPk21(ctx); - break; - case FileType.Kip1: - ProcessKip.ProcessKip1(ctx); - break; - case FileType.Ini1: - ProcessKip.ProcessIni1(ctx); - break; - default: - throw new ArgumentOutOfRangeException(); - } + RunTask(ctx); + } + } + + private static void RunTask(Context ctx) + { + switch (ctx.Options.InFileType) + { + case FileType.Nca: + ProcessNca.Process(ctx); + break; + case FileType.Pfs0: + case FileType.Nsp: + ProcessNsp.Process(ctx); + break; + case FileType.Romfs: + ProcessRomfs.Process(ctx); + break; + case FileType.Nax0: + break; + case FileType.SwitchFs: + ProcessSwitchFs.Process(ctx); + break; + case FileType.Save: + ProcessSave.Process(ctx); + break; + case FileType.Xci: + ProcessXci.Process(ctx); + break; + case FileType.Keygen: + ProcessKeygen(ctx); + break; + case FileType.Pk11: + ProcessPackage.ProcessPk11(ctx); + break; + case FileType.Pk21: + ProcessPackage.ProcessPk21(ctx); + break; + case FileType.Kip1: + ProcessKip.ProcessKip1(ctx); + break; + case FileType.Ini1: + ProcessKip.ProcessIni1(ctx); + break; + default: + throw new ArgumentOutOfRangeException(); } }