Add verification option to save files

This commit is contained in:
Alex Barney 2018-10-18 17:54:24 -05:00
parent f8e7c00ef4
commit f83a284b96
11 changed files with 127 additions and 80 deletions

View File

@ -105,11 +105,6 @@ namespace LibHac
Counter[8] = (byte)((Counter[8] & 0xF0) | (int)(off & 0x0F));
}
public override void Flush()
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)

View File

@ -130,4 +130,32 @@ namespace LibHac
set => DataLevel.Position = value;
}
}
public static class HierarchicalIntegrityVerificationStreamExtensions
{
internal static void SetLevelValidities(this HierarchicalIntegrityVerificationStream stream, IvfcHeader header)
{
for (int i = 0; i < stream.Levels.Length - 1; i++)
{
Validity[] level = stream.LevelValidities[i];
var levelValidity = Validity.Valid;
foreach (Validity block in level)
{
if (block == Validity.Invalid)
{
levelValidity = Validity.Invalid;
break;
}
if (block == Validity.Unchecked && levelValidity != Validity.Invalid)
{
levelValidity = Validity.Unchecked;
}
}
header.LevelHeaders[i].HashValidity = levelValidity;
}
}
}
}

View File

@ -71,6 +71,7 @@ namespace LibHac
if (Type == IntegrityStreamType.Save && _hashBuffer.IsEmpty())
{
Array.Clear(buffer, offset, SectorSize);
BlockValidities[blockNum] = Validity.Valid;
return bytesRead;
}

View File

@ -381,7 +381,6 @@ namespace LibHac
stream.Read(hashTable, 0, hashTable.Length);
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 Dispose()
@ -400,7 +399,21 @@ namespace LibHac
public int SectionNum { get; set; }
public long Offset { get; set; }
public long Size { get; set; }
public Validity MasterHashValidity { get; set; }
public Validity MasterHashValidity
{
get
{
if (Header.HashType == NcaHashType.Ivfc) return Header.IvfcInfo.LevelHeaders[0].HashValidity;
if (Header.HashType == NcaHashType.Sha256) return Header.Sha256Info.MasterHashValidity;
return Validity.Unchecked;
}
set
{
if (Header.HashType == NcaHashType.Ivfc) Header.IvfcInfo.LevelHeaders[0].HashValidity = value;
if (Header.HashType == NcaHashType.Sha256) Header.Sha256Info.MasterHashValidity = value;
}
}
public byte[] GetMasterHash()
{
@ -493,7 +506,7 @@ namespace LibHac
if (hashType == NcaHashType.Ivfc)
{
SetIvfcLevelValidities(stream, sect.Header.IvfcInfo);
stream.SetLevelValidities(sect.Header.IvfcInfo);
}
else if (hashType == NcaHashType.Sha256)
{
@ -502,30 +515,5 @@ namespace LibHac
return validity;
}
private static void SetIvfcLevelValidities(HierarchicalIntegrityVerificationStream stream, IvfcHeader header)
{
for (int i = 0; i < stream.Levels.Length - 1; i++)
{
Validity[] level = stream.LevelValidities[i];
var levelValidity = Validity.Valid;
foreach (Validity block in level)
{
if (block == Validity.Invalid)
{
levelValidity = Validity.Invalid;
break;
}
if (block == Validity.Unchecked && levelValidity != Validity.Invalid)
{
levelValidity = Validity.Unchecked;
}
}
header.LevelHeaders[i].HashValidity = levelValidity;
}
}
}
}

View File

@ -158,8 +158,8 @@ namespace LibHac
{
public string Magic;
public int Version;
public uint MasterHashSize;
public uint NumLevels;
public int MasterHashSize;
public int NumLevels;
public IvfcLevelHeader[] LevelHeaders = new IvfcLevelHeader[6];
public byte[] SaltSource;
public byte[] MasterHash;
@ -167,10 +167,10 @@ namespace LibHac
public IvfcHeader(BinaryReader reader)
{
Magic = reader.ReadAscii(4);
Version = reader.ReadInt16();
reader.BaseStream.Position += 2;
MasterHashSize = reader.ReadUInt32();
NumLevels = reader.ReadUInt32();
Version = reader.ReadInt16();
MasterHashSize = reader.ReadInt32();
NumLevels = reader.ReadInt32();
for (int i = 0; i < LevelHeaders.Length; i++)
{
@ -210,6 +210,7 @@ namespace LibHac
public long DataOffset;
public long DataSize;
public Validity MasterHashValidity = Validity.Unchecked;
public Validity HashValidity = Validity.Unchecked;
public Sha256Info(BinaryReader reader)

View File

@ -151,6 +151,14 @@ namespace LibHac.Save
return true;
}
public Validity Verify(IProgressReport logger = null)
{
Validity validity = IvfcStream.Validate(true, logger);
IvfcStream.SetLevelValidities(Header.Ivfc);
return validity;
}
private string[] SaltSources =
{
"HierarchicalIntegrityVerificationStorage::Master",

View File

@ -44,6 +44,7 @@ namespace hactoolnet
new CliOption("listapps", 0, (o, a) => o.ListApps = true),
new CliOption("listtitles", 0, (o, a) => o.ListTitles = true),
new CliOption("listromfs", 0, (o, a) => o.ListRomFs = true),
new CliOption("listfiles", 0, (o, a) => o.ListFiles = true),
new CliOption("sign", 0, (o, a) => o.SignSave = true),
new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])),
};
@ -203,6 +204,7 @@ namespace hactoolnet
sb.AppendLine(" --outdir <dir> Specify directory path to save contents to.");
sb.AppendLine(" --debugoutdir <dir> Specify directory path to save intermediate data to for debugging.");
sb.AppendLine(" --sign Sign the save file. (Requires device_key in key file)");
sb.AppendLine(" --listfiles List files in save file.");
sb.AppendLine("Keygen options:");
sb.AppendLine(" --outdir <dir> Specify directory path to save key files to.");

View File

@ -34,11 +34,19 @@ namespace hactoolnet
public bool ListApps;
public bool ListTitles;
public bool ListRomFs;
public bool ListFiles;
public bool SignSave;
public ulong TitleId;
public IntegrityCheckLevel IntegrityLevel =>
EnableHash ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
public IntegrityCheckLevel IntegrityLevel
{
get
{
if (Validate) return IntegrityCheckLevel.IgnoreOnInvalid;
if (EnableHash) return IntegrityCheckLevel.ErrorOnInvalid;
return IntegrityCheckLevel.None;
}
}
}
internal enum FileType

View File

@ -1,4 +1,5 @@
using System.Text;
using System;
using System.Text;
using LibHac;
namespace hactoolnet
@ -26,5 +27,42 @@ namespace hactoolnet
default: return string.Empty;
}
}
public static void PrintIvfcHash(StringBuilder sb, int colLen, int indentSize, IvfcHeader ivfcInfo, IntegrityStreamType type)
{
string prefix = new string(' ', indentSize);
string prefix2 = new string(' ', indentSize + 4);
if (type == IntegrityStreamType.RomFs)
PrintItem(sb, colLen, $"{prefix}Master Hash{ivfcInfo.LevelHeaders[0].HashValidity.GetValidityString()}:", ivfcInfo.MasterHash);
PrintItem(sb, colLen, $"{prefix}Magic:", ivfcInfo.Magic);
PrintItem(sb, colLen, $"{prefix}Version:", ivfcInfo.Version);
if (type == IntegrityStreamType.Save)
PrintItem(sb, colLen, $"{prefix}Salt Seed:", ivfcInfo.SaltSource);
int levelCount = Math.Max(ivfcInfo.NumLevels - 1, 0);
if (type == IntegrityStreamType.Save) levelCount = 4;
int offsetLen = type == IntegrityStreamType.Save ? 16 : 12;
for (int i = 0; i < levelCount; i++)
{
IvfcLevelHeader level = ivfcInfo.LevelHeaders[i];
long hashOffset = 0;
if (i != 0)
{
hashOffset = ivfcInfo.LevelHeaders[i - 1].LogicalOffset;
}
sb.AppendLine($"{prefix}Level {i}{level.HashValidity.GetValidityString()}:");
PrintItem(sb, colLen, $"{prefix2}Data Offset:", $"0x{level.LogicalOffset.ToString($"x{offsetLen}")}");
PrintItem(sb, colLen, $"{prefix2}Data Size:", $"0x{level.HashDataSize.ToString($"x{offsetLen}")}");
PrintItem(sb, colLen, $"{prefix2}Hash Offset:", $"0x{hashOffset.ToString($"x{offsetLen}")}");
PrintItem(sb, colLen, $"{prefix2}Hash BlockSize:", $"0x{1 << level.BlockSizePower:x8}");
}
}
}
}

View File

@ -174,7 +174,7 @@ namespace hactoolnet
PrintSha256Hash(sect);
break;
case NcaHashType.Ivfc:
PrintIvfcHash(sect);
PrintIvfcHash(sb, colLen, 8, sect.Header.IvfcInfo, IntegrityStreamType.RomFs);
break;
default:
sb.AppendLine(" Unknown/invalid superblock!");
@ -196,32 +196,6 @@ namespace hactoolnet
PrintItem(sb, colLen, " PFS0 Offset:", $"0x{hashInfo.DataOffset:x12}");
PrintItem(sb, colLen, " PFS0 Size:", $"0x{hashInfo.DataSize:x12}");
}
void PrintIvfcHash(NcaSection sect)
{
IvfcHeader ivfcInfo = sect.Header.IvfcInfo;
PrintItem(sb, colLen, $" Master Hash{sect.MasterHashValidity.GetValidityString()}:", ivfcInfo.MasterHash);
PrintItem(sb, colLen, " Magic:", ivfcInfo.Magic);
PrintItem(sb, colLen, " Version:", $"{ivfcInfo.Version:x8}");
for (int i = 0; i < Romfs.IvfcMaxLevel; i++)
{
IvfcLevelHeader level = ivfcInfo.LevelHeaders[i];
long hashOffset = 0;
if (i != 0)
{
hashOffset = ivfcInfo.LevelHeaders[i - 1].LogicalOffset;
}
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}");
PrintItem(sb, colLen, " Hash BlockSize:", $"0x{1 << level.BlockSizePower:x8}");
}
}
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using LibHac;
using LibHac.Save;
@ -16,6 +15,11 @@ namespace hactoolnet
{
var save = new Savefile(ctx.Keyset, file, ctx.Options.IntegrityLevel);
if (ctx.Options.Validate)
{
save.Verify(ctx.Logger);
}
if (ctx.Options.OutDir != null)
{
save.Extract(ctx.Options.OutDir, ctx.Logger);
@ -79,6 +83,14 @@ namespace hactoolnet
}
}
if (ctx.Options.ListFiles)
{
foreach (FileEntry fileEntry in save.Files)
{
ctx.Logger.LogMessage(fileEntry.FullPath);
}
}
ctx.Logger.LogMessage(save.Print());
}
}
@ -100,17 +112,9 @@ namespace hactoolnet
PrintItem(sb, colLen, "Save Data Size:", $"0x{save.Header.ExtraData.DataSize:x16} ({Util.GetBytesReadable(save.Header.ExtraData.DataSize)})");
PrintItem(sb, colLen, "Journal Size:", $"0x{save.Header.ExtraData.JournalSize:x16} ({Util.GetBytesReadable(save.Header.ExtraData.JournalSize)})");
PrintItem(sb, colLen, $"Header Hash{save.Header.HeaderHashValidity.GetValidityString()}:", save.Header.Layout.Hash);
PrintItem(sb, colLen, "IVFC Salt Seed:", save.Header.Ivfc.SaltSource);
PrintItem(sb, colLen, "Number of Files:", save.Files.Length);
if (save.Files.Length > 0 && save.Files.Length < 100)
{
sb.AppendLine("Files:");
foreach (FileEntry file in save.Files.OrderBy(x => x.FullPath))
{
sb.AppendLine(file.FullPath);
}
}
PrintIvfcHash(sb, colLen, 4, save.Header.Ivfc, IntegrityStreamType.Save);
return sb.ToString();
}