diff --git a/LibHac/HierarchicalIntegrityVerificationStream.cs b/LibHac/HierarchicalIntegrityVerificationStream.cs index 5a5adad8..c995d018 100644 --- a/LibHac/HierarchicalIntegrityVerificationStream.cs +++ b/LibHac/HierarchicalIntegrityVerificationStream.cs @@ -10,10 +10,19 @@ namespace LibHac public Stream DataLevel { get; } public IntegrityCheckLevel IntegrityCheckLevel { get; } + /// + /// 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]; IntegrityCheckLevel = integrityCheckLevel; + LevelValidities = new Validity[levelInfo.Length - 1][]; + IntegrityStreams = new IntegrityVerificationStream[levelInfo.Length - 1]; Levels[0] = levelInfo[0].Data; @@ -22,11 +31,55 @@ namespace LibHac 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 = 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 9057cad5..c8d72f46 100644 --- a/LibHac/IntegrityVerificationStream.cs +++ b/LibHac/IntegrityVerificationStream.cs @@ -11,6 +11,7 @@ namespace LibHac private Stream HashStream { get; } public IntegrityCheckLevel IntegrityCheckLevel { get; } + public Validity[] BlockValidities { get; } private byte[] Salt { get; } private IntegrityStreamType Type { get; } @@ -25,6 +26,8 @@ namespace LibHac 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 (IntegrityCheckLevel == IntegrityCheckLevel.None) 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!"); } @@ -150,9 +167,9 @@ namespace LibHac /// None, /// - /// + /// Invalid blocks will be marked as invalid when read, and will not cause an error. /// - WarnOnInvalid, + IgnoreOnInvalid, /// /// An will be thrown if an integrity check fails. /// diff --git a/LibHac/Nca.cs b/LibHac/Nca.cs index eab91d70..83ea1ab2 100644 --- a/LibHac/Nca.cs +++ b/LibHac/Nca.cs @@ -182,6 +182,18 @@ namespace LibHac } } + /// + /// 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. /// @@ -383,6 +395,7 @@ namespace LibHac stream.Read(hashTable, 0, hashTable.Length); sect.MasterHashValidity = Crypto.CheckMemoryHashTable(hashTable, expected, 0, hashTable.Length); + if (sect.Type == SectionType.Romfs) sect.Header.IvfcInfo.LevelHeaders[0].HashValidity = sect.MasterHashValidity; } public void Dispose() @@ -470,24 +483,27 @@ namespace LibHac if (nca.Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index)); NcaSection sect = nca.Sections[index]; - Stream stream = nca.OpenSection(index, false, IntegrityCheckLevel.WarnOnInvalid); + NcaHashType hashType = sect.Header.HashType; + if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return; + + HierarchicalIntegrityVerificationStream stream = nca.OpenHashedSection(index, IntegrityCheckLevel.IgnoreOnInvalid); + if (stream == null) return; + logger?.LogMessage($"Verifying section {index}..."); - switch (sect.Header.HashType) + for (int i = 0; i < stream.Levels.Length - 1; i++) { - case NcaHashType.Sha256: - case NcaHashType.Ivfc: - if (stream is HierarchicalIntegrityVerificationStream ivfc) - { - for (int i = 1; i < ivfc.Levels.Length; i++) - { - logger?.LogMessage($" Verifying IVFC Level {i}..."); - ivfc.Levels[i].CopyStream(Stream.Null, ivfc.Levels[i].Length, logger); - } - } - break; - default: - throw new ArgumentOutOfRangeException(); + logger?.LogMessage($" Verifying Hash Level {i}..."); + Validity result = stream.ValidateLevel(i, true, logger); + + if (hashType == NcaHashType.Ivfc) + { + sect.Header.IvfcInfo.LevelHeaders[i].HashValidity = result; + } + else if (hashType == NcaHashType.Sha256 && i == stream.Levels.Length - 2) + { + sect.Header.Sha256Info.HashValidity = result; + } } } } diff --git a/LibHac/NcaStructs.cs b/LibHac/NcaStructs.cs index 7d2a7df3..369f43cc 100644 --- a/LibHac/NcaStructs.cs +++ b/LibHac/NcaStructs.cs @@ -215,6 +215,8 @@ namespace LibHac public int BlockSizePower; public uint Reserved; + public Validity HashValidity = Validity.Unchecked; + public IvfcLevelHeader(BinaryReader reader) { LogicalOffset = reader.ReadInt64(); @@ -234,6 +236,8 @@ namespace LibHac public long DataOffset; public long DataSize; + public Validity HashValidity = Validity.Unchecked; + public Sha256Info(BinaryReader reader) { MasterHash = reader.ReadBytes(0x20); @@ -366,7 +370,7 @@ namespace LibHac Bktr } - public enum Validity + public enum Validity : byte { Unchecked, Invalid, 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/hactoolnet/ProcessNca.cs b/hactoolnet/ProcessNca.cs index 51eae229..c000021b 100644 --- a/hactoolnet/ProcessNca.cs +++ b/hactoolnet/ProcessNca.cs @@ -169,6 +169,7 @@ namespace hactoolnet PrintRomfs(sect); break; case SectionType.Bktr: + PrintRomfs(sect); break; default: sb.AppendLine(" Unknown/invalid superblock!"); @@ -181,9 +182,8 @@ namespace hactoolnet { Sha256Info hashInfo = sect.Header.Sha256Info; - PrintItem(sb, colLen, $" Superblock Hash{sect.MasterHashValidity.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}"); @@ -196,7 +196,7 @@ namespace hactoolnet { IvfcHeader ivfcInfo = sect.Header.IvfcInfo; - PrintItem(sb, colLen, $" Superblock Hash{sect.MasterHashValidity.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}"); @@ -210,8 +210,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}");