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();
}
}