Add the option to enable IVFC integrity checks

This commit is contained in:
Alex Barney 2018-09-20 16:16:40 -05:00
parent f04fc33b07
commit ed6c3c2bed
10 changed files with 268 additions and 56 deletions

View File

@ -0,0 +1,79 @@
using System;
using System.IO;
using LibHac.Streams;
namespace LibHac
{
public class HierarchicalIntegrityVerificationStream : Stream
{
public Stream[] Levels { get; }
public Stream DataLevel { get; }
public bool EnableIntegrityChecks { get; }
public HierarchicalIntegrityVerificationStream(IntegrityVerificationInfo[] levelInfo, bool enableIntegrityChecks)
{
Levels = new Stream[levelInfo.Length];
EnableIntegrityChecks = enableIntegrityChecks;
Levels[0] = levelInfo[0].Data;
for (int i = 1; i < Levels.Length; i++)
{
var levelData = new IntegrityVerificationStream(levelInfo[i].Data, Levels[i - 1],
levelInfo[i].BlockSizePower, enableIntegrityChecks);
Levels[i] = new RandomAccessSectorStream(levelData);
}
DataLevel = Levels[Levels.Length - 1];
}
public override void Flush()
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = offset;
break;
case SeekOrigin.Current:
Position += offset;
break;
case SeekOrigin.End:
Position = Length - offset;
break;
}
return Position;
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
return DataLevel.Read(buffer, offset, count);
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override bool CanRead => DataLevel.CanRead;
public override bool CanSeek => DataLevel.CanSeek;
public override bool CanWrite => false;
public override long Length => DataLevel.Length;
public override long Position
{
get => DataLevel.Position;
set => DataLevel.Position = value;
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.IO;
using System.Security.Cryptography;
using LibHac.Streams;
namespace LibHac
{
public class IntegrityVerificationStream : SectorStream
{
private const int DigestSize = 0x20;
private Stream HashStream { get; }
public bool EnableIntegrityChecks { get; }
private readonly byte[] _hashBuffer = new byte[DigestSize];
private readonly SHA256 _hash = SHA256.Create();
public IntegrityVerificationStream(Stream dataStream, Stream hashStream, int blockSizePower, bool enableIntegrityChecks)
: base(dataStream, 1 << blockSizePower)
{
HashStream = hashStream;
EnableIntegrityChecks = enableIntegrityChecks;
}
public override void Flush()
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Begin:
Position = offset;
break;
case SeekOrigin.Current:
Position += offset;
break;
case SeekOrigin.End:
Position = Length - offset;
break;
}
return Position;
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
HashStream.Position = CurrentSector * DigestSize;
HashStream.Read(_hashBuffer, 0, DigestSize);
// If a hash is zero the data for the entire block is zero
if (_hashBuffer.IsEmpty())
{
Array.Clear(buffer, 0, SectorSize);
}
int bytesRead = base.Read(buffer, 0, count);
if (bytesRead < SectorSize)
{
// Pad out unused portion of block
Array.Clear(buffer, bytesRead, SectorSize - bytesRead);
}
if (EnableIntegrityChecks && !Util.ArraysEqual(_hashBuffer, _hash.ComputeHash(buffer)))
{
throw new InvalidDataException("Hash error!");
}
return bytesRead;
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
}
/// <summary>
/// Information for creating an <see cref="IntegrityVerificationStream"/>
/// </summary>
public class IntegrityVerificationInfo
{
public Stream Data { get; set; }
public int BlockSizePower { get; set; }
}
}

View File

@ -61,7 +61,7 @@ namespace LibHac
foreach (var pfsSection in Sections.Where(x => x != null && x.Type == SectionType.Pfs0))
{
var sectionStream = OpenSection(pfsSection.SectionNum, false);
Stream sectionStream = OpenSection(pfsSection.SectionNum, false, false);
if (sectionStream == null) continue;
var pfs = new Pfs(sectionStream);
@ -76,10 +76,10 @@ namespace LibHac
return StreamSource.CreateStream();
}
public Stream OpenSection(int index, bool raw)
private Stream OpenRawSection(int index)
{
if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
var sect = Sections[index];
NcaSection sect = Sections[index];
if (sect == null) throw new ArgumentOutOfRangeException(nameof(index));
if (sect.SuperblockHashValidity == Validity.Invalid) return null;
@ -91,54 +91,87 @@ namespace LibHac
switch (sect.Header.CryptType)
{
case SectionCryptType.None:
break;
return rawStream;
case SectionCryptType.XTS:
break;
throw new NotImplementedException("NCA sections using XTS are not supported");
case SectionCryptType.CTR:
rawStream = new RandomAccessSectorStream(new Aes128CtrStream(rawStream, DecryptedKeys[2], offset, sect.Header.Ctr), false);
break;
return new RandomAccessSectorStream(new Aes128CtrStream(rawStream, DecryptedKeys[2], offset, sect.Header.Ctr), false);
case SectionCryptType.BKTR:
rawStream = new RandomAccessSectorStream(
new BktrCryptoStream(rawStream, DecryptedKeys[2], 0, size, offset, sect.Header.Ctr, sect.Header.Bktr),
false);
if (BaseNca == null)
{
return rawStream;
}
else
{
var baseSect = BaseNca.Sections.FirstOrDefault(x => x.Type == SectionType.Romfs);
if (baseSect == null) throw new InvalidDataException("Base NCA has no RomFS section");
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(baseSect.SectionNum, true, false);
return new Bktr(rawStream, baseStream, sect);
var baseStream = BaseNca.OpenSection(baseSect.SectionNum, true);
rawStream = new Bktr(rawStream, baseStream, sect);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public Stream OpenSection(int index, bool raw, bool enableIntegrityChecks)
{
Stream rawStream = OpenRawSection(index);
NcaSection sect = Sections[index];
if (raw) return rawStream;
switch (sect.Header.Type)
{
case SectionType.Pfs0:
offset = sect.Pfs0.Superblock.Pfs0Offset;
size = sect.Pfs0.Superblock.Pfs0Size;
break;
PfsSuperblock pfs0Superblock = sect.Pfs0.Superblock;
return new SubStream(rawStream, pfs0Superblock.Pfs0Offset, pfs0Superblock.Pfs0Size);
case SectionType.Romfs:
offset = sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].LogicalOffset;
size = sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize;
break;
case SectionType.Bktr:
offset = sect.Header.Bktr.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].LogicalOffset;
size = sect.Header.Bktr.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize;
break;
var romfsStreamSource = new SharedStreamSource(rawStream);
IvfcHeader ivfc;
if (sect.Header.Type == SectionType.Romfs)
{
ivfc = sect.Header.Romfs.IvfcHeader;
}
else
{
ivfc = sect.Header.Bktr.IvfcHeader;
}
return InitIvfcForRomfs(ivfc, romfsStreamSource, enableIntegrityChecks);
default:
throw new ArgumentOutOfRangeException();
}
return new SubStream(rawStream, offset, size);
}
private static HierarchicalIntegrityVerificationStream InitIvfcForRomfs(IvfcHeader ivfc,
SharedStreamSource romfsStreamSource, bool enableIntegrityChecks)
{
var initInfo = new IntegrityVerificationInfo[ivfc.NumLevels];
// Set the master hash
initInfo[0] = new IntegrityVerificationInfo
{
Data = new MemoryStream(ivfc.MasterHash),
BlockSizePower = 0
};
for (int i = 1; i < ivfc.NumLevels; i++)
{
IvfcLevelHeader level = ivfc.LevelHeaders[i - 1];
Stream data = romfsStreamSource.CreateStream(level.LogicalOffset, level.HashDataSize);
initInfo[i] = new IntegrityVerificationInfo
{
Data = data,
BlockSizePower = level.BlockSize
};
}
return new HierarchicalIntegrityVerificationStream(initInfo, enableIntegrityChecks);
}
public void SetBaseNca(Nca baseNca) => BaseNca = baseNca;
@ -264,7 +297,7 @@ namespace LibHac
return;
}
var stream = OpenSection(index, true);
Stream stream = OpenSection(index, true, false);
if (stream == null) return;
if (expected == null) return;
@ -287,7 +320,7 @@ namespace LibHac
{
if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index));
var sect = Sections[index];
var stream = OpenSection(index, true);
var stream = OpenSection(index, true, false);
logger?.LogMessage($"Verifying section {index}...");
switch (sect.Type)
@ -390,12 +423,12 @@ namespace LibHac
public static class NcaExtensions
{
public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, IProgressReport logger = null)
public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, bool verify = false, IProgressReport logger = null)
{
if (index < 0 || index > 3) throw new IndexOutOfRangeException();
if (nca.Sections[index] == null) return;
var section = nca.OpenSection(index, raw);
var section = nca.OpenSection(index, raw, verify);
var dir = Path.GetDirectoryName(filename);
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
@ -405,13 +438,13 @@ namespace LibHac
}
}
public static void ExtractSection(this Nca nca, int index, string outputDir, IProgressReport logger = null)
public static void ExtractSection(this Nca nca, int index, string outputDir, bool verify = false, IProgressReport logger = null)
{
if (index < 0 || index > 3) throw new IndexOutOfRangeException();
if (nca.Sections[index] == null) return;
var section = nca.Sections[index];
var stream = nca.OpenSection(index, false);
var stream = nca.OpenSection(index, false, verify);
switch (section.Type)
{

View File

@ -127,8 +127,7 @@ namespace LibHac.Savefile
{
if (file.BlockIndex < 0)
{
//todo replace
return JournalStreamSource.CreateStream(0, 0);
return Stream.Null;
}
return OpenFatBlock(file.BlockIndex, file.FileSize);

View File

@ -135,7 +135,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);
Stream sect = nca.OpenSection(0, false, true);
var pfs0 = new Pfs(sect);
var file = pfs0.OpenFile(pfs0.Files[0]);
@ -175,7 +175,7 @@ namespace LibHac
{
foreach (var title in Titles.Values.Where(x => x.ControlNca != null))
{
var romfs = new Romfs(title.ControlNca.OpenSection(0, false));
var romfs = new Romfs(title.ControlNca.OpenSection(0, false, true));
var control = romfs.GetFile("/control.nacp");
var reader = new BinaryReader(new MemoryStream(control));

View File

@ -13,6 +13,7 @@ namespace hactoolnet
new CliOption("intype", 't', 1, (o, a) => o.InFileType = ParseFileType(a[0])),
new CliOption("raw", 'r', 0, (o, a) => o.Raw = true),
new CliOption("verify", 'y', 0, (o, a) => o.Validate = true),
new CliOption("enablehash", 'h', 0, (o, a) => o.EnableHash = true),
new CliOption("keyset", 'k', 1, (o, a) => o.Keyfile = a[0]),
new CliOption("titlekeys", 1, (o, a) => o.TitleKeyFile = a[0]),
new CliOption("consolekeys", 1, (o, a) => o.ConsoleKeyFile = a[0]),
@ -145,7 +146,8 @@ namespace hactoolnet
sb.AppendLine("Usage: hactoolnet.exe [options...] <path>");
sb.AppendLine("Options:");
sb.AppendLine(" -r, --raw Keep raw data, don\'t unpack.");
sb.AppendLine(" -y, --verify Verify hashes.");
sb.AppendLine(" -y, --verify Verify all hashes in the input file.");
sb.AppendLine(" -h, --enablehash Enable hash checks when reading the input file.");
sb.AppendLine(" -k, --keyset Load keys from an external file.");
sb.AppendLine(" -t, --intype=type Specify input file type [nca, xci, romfs, pk11, pk21, ini1, kip1, switchfs, save, keygen]");
sb.AppendLine(" --titlekeys <file> Load title keys from an external file.");

View File

@ -9,6 +9,7 @@ namespace hactoolnet
public FileType InFileType = FileType.Nca;
public bool Raw;
public bool Validate;
public bool EnableHash;
public string Keyfile;
public string TitleKeyFile;
public string ConsoleKeyFile;

View File

@ -25,12 +25,12 @@ namespace hactoolnet
{
if (ctx.Options.SectionOut[i] != null)
{
nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Logger);
nca.ExportSection(i, ctx.Options.SectionOut[i], ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
if (ctx.Options.SectionOutDir[i] != null)
{
nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Logger);
nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Options.EnableHash, ctx.Logger);
}
if (ctx.Options.Validate && nca.Sections[i] != null)
@ -41,7 +41,7 @@ namespace hactoolnet
if (ctx.Options.ListRomFs && nca.Sections[1] != null)
{
var romfs = new Romfs(nca.OpenSection(1, false));
var romfs = new Romfs(nca.OpenSection(1, false, ctx.Options.EnableHash));
foreach (var romfsFile in romfs.Files)
{
@ -67,12 +67,12 @@ namespace hactoolnet
if (ctx.Options.RomfsOut != null)
{
nca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Logger);
nca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
if (ctx.Options.RomfsOutDir != null)
{
var romfs = new Romfs(nca.OpenSection(section.SectionNum, false));
var romfs = new Romfs(nca.OpenSection(section.SectionNum, false, ctx.Options.EnableHash));
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
}
@ -89,12 +89,12 @@ namespace hactoolnet
if (ctx.Options.ExefsOut != null)
{
nca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Logger);
nca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
if (ctx.Options.ExefsOutDir != null)
{
nca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Logger);
nca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger);
}
}

View File

@ -54,12 +54,12 @@ namespace hactoolnet
if (ctx.Options.ExefsOutDir != null)
{
title.MainNca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Logger);
title.MainNca.ExtractSection(section.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger);
}
if (ctx.Options.ExefsOut != null)
{
title.MainNca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Logger);
title.MainNca.ExportSection(section.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
}
@ -94,13 +94,13 @@ namespace hactoolnet
if (ctx.Options.RomfsOutDir != null)
{
var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false));
var romfs = new Romfs(title.MainNca.OpenSection(section.SectionNum, false, ctx.Options.EnableHash));
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
if (ctx.Options.RomfsOut != null)
{
title.MainNca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Logger);
title.MainNca.ExportSection(section.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
}

View File

@ -79,12 +79,12 @@ namespace hactoolnet
if (ctx.Options.ExefsOutDir != null)
{
mainNca.ExtractSection(exefsSection.SectionNum, ctx.Options.ExefsOutDir, ctx.Logger);
mainNca.ExtractSection(exefsSection.SectionNum, ctx.Options.ExefsOutDir, ctx.Options.EnableHash, ctx.Logger);
}
if (ctx.Options.ExefsOut != null)
{
mainNca.ExportSection(exefsSection.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Logger);
mainNca.ExportSection(exefsSection.SectionNum, ctx.Options.ExefsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
}
@ -108,13 +108,13 @@ namespace hactoolnet
if (ctx.Options.RomfsOutDir != null)
{
var romfs = new Romfs(mainNca.OpenSection(romfsSection.SectionNum, false));
var romfs = new Romfs(mainNca.OpenSection(romfsSection.SectionNum, false, ctx.Options.EnableHash));
romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger);
}
if (ctx.Options.RomfsOut != null)
{
mainNca.ExportSection(romfsSection.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Logger);
mainNca.ExportSection(romfsSection.SectionNum, ctx.Options.RomfsOut, ctx.Options.Raw, ctx.Options.EnableHash, ctx.Logger);
}
}
}