From e92c686d77d56cd20547ec29a6394bb4e7a4dc69 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 20 Sep 2018 20:34:40 -0500 Subject: [PATCH] Add save file integrity verification --- ...HierarchicalIntegrityVerificationStream.cs | 3 +- LibHac/IntegrityVerificationStream.cs | 46 ++++++++++++++-- LibHac/Nca.cs | 3 +- LibHac/NcaStructs.cs | 4 +- LibHac/Savefile/Header.cs | 4 ++ LibHac/Savefile/Savefile.cs | 55 ++++++++++++++++++- LibHac/SwitchFs.cs | 2 +- NandReader/Program.cs | 2 +- NandReaderGui/ViewModel/NandViewModel.cs | 3 +- hactoolnet/Program.cs | 4 +- 10 files changed, 108 insertions(+), 18 deletions(-) diff --git a/LibHac/HierarchicalIntegrityVerificationStream.cs b/LibHac/HierarchicalIntegrityVerificationStream.cs index cc70f05f..bdb09c2b 100644 --- a/LibHac/HierarchicalIntegrityVerificationStream.cs +++ b/LibHac/HierarchicalIntegrityVerificationStream.cs @@ -19,8 +19,7 @@ namespace LibHac for (int i = 1; i < Levels.Length; i++) { - var levelData = new IntegrityVerificationStream(levelInfo[i].Data, Levels[i - 1], - levelInfo[i].BlockSizePower, enableIntegrityChecks); + var levelData = new IntegrityVerificationStream(levelInfo[i], Levels[i - 1], enableIntegrityChecks); Levels[i] = new RandomAccessSectorStream(levelData); } diff --git a/LibHac/IntegrityVerificationStream.cs b/LibHac/IntegrityVerificationStream.cs index 4bfd745b..de6ad6fb 100644 --- a/LibHac/IntegrityVerificationStream.cs +++ b/LibHac/IntegrityVerificationStream.cs @@ -12,14 +12,19 @@ namespace LibHac private Stream HashStream { get; } public bool EnableIntegrityChecks { get; } + private byte[] Salt { get; } + private IntegrityStreamType Type { 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) + public IntegrityVerificationStream(IntegrityVerificationInfo info, Stream hashStream, bool enableIntegrityChecks) + : base(info.Data, 1 << info.BlockSizePower) { HashStream = hashStream; EnableIntegrityChecks = enableIntegrityChecks; + Salt = info.Salt; + Type = info.Type; } public override void Flush() @@ -55,21 +60,42 @@ namespace LibHac HashStream.Position = CurrentSector * DigestSize; HashStream.Read(_hashBuffer, 0, DigestSize); + int bytesRead = base.Read(buffer, 0, count); + // If a hash is zero the data for the entire block is zero - if (_hashBuffer.IsEmpty()) + if (Type == IntegrityStreamType.Save && _hashBuffer.IsEmpty()) { Array.Clear(buffer, 0, SectorSize); + return bytesRead; } - 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))) + if (!EnableIntegrityChecks) return bytesRead; + + _hash.Initialize(); + + if (Type == IntegrityStreamType.Save) + { + _hash.TransformBlock(Salt, 0, Salt.Length, null, 0); + } + + _hash.TransformBlock(buffer, 0, SectorSize, null, 0); + _hash.TransformFinalBlock(buffer, 0, 0); + + byte[] hash = _hash.Hash; + + if (Type == IntegrityStreamType.Save) + { + // This bit is set on all save hashes + hash[0x1F] |= 0x80; + } + + if (!Util.ArraysEqual(_hashBuffer, hash)) { throw new InvalidDataException("Hash error!"); } @@ -94,5 +120,13 @@ namespace LibHac { public Stream Data { get; set; } public int BlockSizePower { get; set; } + public byte[] Salt { get; set; } + public IntegrityStreamType Type { get; set; } + } + + public enum IntegrityStreamType + { + Save, + RomFs } } diff --git a/LibHac/Nca.cs b/LibHac/Nca.cs index cd124c1b..00aa9ef0 100644 --- a/LibHac/Nca.cs +++ b/LibHac/Nca.cs @@ -167,7 +167,8 @@ namespace LibHac initInfo[i] = new IntegrityVerificationInfo { Data = data, - BlockSizePower = level.BlockSize + BlockSizePower = level.BlockSize, + Type = IntegrityStreamType.RomFs }; } diff --git a/LibHac/NcaStructs.cs b/LibHac/NcaStructs.cs index c7b3b24d..82de56a9 100644 --- a/LibHac/NcaStructs.cs +++ b/LibHac/NcaStructs.cs @@ -166,9 +166,9 @@ namespace LibHac public uint MasterHashSize; public uint NumLevels; public IvfcLevelHeader[] LevelHeaders = new IvfcLevelHeader[6]; + public byte[] SaltSource; public byte[] MasterHash; - public IvfcHeader(BinaryReader reader) { Magic = reader.ReadAscii(4); @@ -181,7 +181,7 @@ namespace LibHac LevelHeaders[i] = new IvfcLevelHeader(reader); } - reader.BaseStream.Position += 0x20; + SaltSource = reader.ReadBytes(0x20); MasterHash = reader.ReadBytes(0x20); } } diff --git a/LibHac/Savefile/Header.cs b/LibHac/Savefile/Header.cs index e01b9864..5512a55d 100644 --- a/LibHac/Savefile/Header.cs +++ b/LibHac/Savefile/Header.cs @@ -9,6 +9,7 @@ namespace LibHac.Savefile public FsLayout Layout { get; set; } public JournalHeader Journal { get; set; } public DuplexHeader Duplex { get; set; } + public IvfcHeader Ivfc { get; set; } public SaveHeader Save { get; set; } public RemapHeader FileRemap { get; set; } @@ -41,6 +42,9 @@ namespace LibHac.Savefile reader.BaseStream.Position = 0x300; Duplex = new DuplexHeader(reader); + reader.BaseStream.Position = 0x344; + Ivfc = new IvfcHeader(reader); + reader.BaseStream.Position = 0x408; Journal = new JournalHeader(reader); diff --git a/LibHac/Savefile/Savefile.cs b/LibHac/Savefile/Savefile.cs index 6acf567a..d60d6e85 100644 --- a/LibHac/Savefile/Savefile.cs +++ b/LibHac/Savefile/Savefile.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; using System.Text; using LibHac.Streams; @@ -15,6 +16,8 @@ namespace LibHac.Savefile public SharedStreamSource MetaRemapSource { get; } private JournalStream JournalStream { get; } public SharedStreamSource JournalStreamSource { get; } + private HierarchicalIntegrityVerificationStream IvfcStream { get; } + public SharedStreamSource IvfcStreamSource { get; } private AllocationTable AllocationTable { get; } public Stream DuplexL1A { get; } @@ -38,7 +41,7 @@ namespace LibHac.Savefile public DirectoryEntry[] Directories { get; private set; } private Dictionary FileDict { get; } - public Savefile(Keyset keyset, Stream file, IProgressReport logger = null) + public Savefile(Keyset keyset, Stream file, bool enableIntegrityChecks, IProgressReport logger = null) { SavefileSource = new SharedStreamSource(file); @@ -102,6 +105,10 @@ namespace LibHac.Savefile layout.JournalDataSizeB + layout.SizeReservedArea); JournalStream = new JournalStream(journalData, journalMap, (int)Header.Journal.BlockSize); JournalStreamSource = new SharedStreamSource(JournalStream); + + IvfcStream = InitIvfcStream(enableIntegrityChecks); + IvfcStreamSource = new SharedStreamSource(IvfcStream); + ReadFileInfo(); Dictionary dictionary = new Dictionary(); foreach (FileEntry entry in Files) @@ -113,6 +120,40 @@ namespace LibHac.Savefile } } + private HierarchicalIntegrityVerificationStream InitIvfcStream(bool enableIntegrityChecks) + { + IvfcHeader ivfc = Header.Ivfc; + + const int ivfcLevels = 5; + var initInfo = new IntegrityVerificationInfo[ivfcLevels]; + + initInfo[0] = new IntegrityVerificationInfo + { + Data = new MemoryStream(Header.MasterHashA), + BlockSizePower = 0, + Type = IntegrityStreamType.Save + }; + + for (int i = 1; i < ivfcLevels; i++) + { + IvfcLevelHeader level = ivfc.LevelHeaders[i - 1]; + + Stream data = i == ivfcLevels - 1 + ? (Stream)JournalStream + : MetaRemapSource.CreateStream(level.LogicalOffset, level.HashDataSize); + + initInfo[i] = new IntegrityVerificationInfo + { + Data = data, + BlockSizePower = level.BlockSize, + Salt = new HMACSHA256(Encoding.ASCII.GetBytes(SaltSources[i - 1])).ComputeHash(ivfc.SaltSource), + Type = IntegrityStreamType.Save + }; + } + + return new HierarchicalIntegrityVerificationStream(initInfo, enableIntegrityChecks); + } + public Stream OpenFile(string filename) { if (!FileDict.TryGetValue(filename, out FileEntry file)) @@ -135,7 +176,7 @@ namespace LibHac.Savefile private AllocationTableStream OpenFatBlock(int blockIndex, long size) { - return new AllocationTableStream(JournalStreamSource.CreateStream(), AllocationTable, (int)Header.Save.BlockSize, blockIndex, size); + return new AllocationTableStream(IvfcStreamSource.CreateStream(), AllocationTable, (int)Header.Save.BlockSize, blockIndex, size); } public bool FileExists(string filename) => FileDict.ContainsKey(filename); @@ -242,6 +283,16 @@ namespace LibHac.Savefile return true; } + + private string[] SaltSources = + { + "HierarchicalIntegrityVerificationStorage::Master", + "HierarchicalIntegrityVerificationStorage::L1", + "HierarchicalIntegrityVerificationStorage::L2", + "HierarchicalIntegrityVerificationStorage::L3", + "HierarchicalIntegrityVerificationStorage::L4", + "HierarchicalIntegrityVerificationStorage::L5" + }; } public static class SavefileExtensions diff --git a/LibHac/SwitchFs.cs b/LibHac/SwitchFs.cs index 60d86662..7b0dc09b 100644 --- a/LibHac/SwitchFs.cs +++ b/LibHac/SwitchFs.cs @@ -114,7 +114,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); + save = new Savefile.Savefile(Keyset, nax0.Stream, false); } catch (Exception ex) { diff --git a/NandReader/Program.cs b/NandReader/Program.cs index 1cb39fdc..8b9f04cc 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); + var save = new Savefile(keyset, savefile, false); 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 bfcd9639..7e638887 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); + var save = new Savefile(keyset, savefile, false); var ticketList = new BinaryReader(save.OpenFile("/ticket_list.bin")); var ticketFile = new BinaryReader(save.OpenFile("/ticket.bin")); @@ -100,6 +100,7 @@ namespace NandReaderGui.ViewModel return tickets; } + private static Keyset OpenKeyset() { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 8a01d1b3..c9e21ac7 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -104,7 +104,7 @@ namespace hactoolnet { using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.ReadWrite)) { - var save = new Savefile(ctx.Keyset, file, ctx.Logger); + var save = new Savefile(ctx.Keyset, file, ctx.Options.EnableHash, ctx.Logger); if (ctx.Options.OutDir != null) { @@ -136,7 +136,7 @@ namespace hactoolnet save.JournalLayer3Hash.WriteAllBytes(Path.Combine(dir, "L2_6_Layer3Hash"), ctx.Logger); save.JournalFat.WriteAllBytes(Path.Combine(dir, "L2_7_FAT"), ctx.Logger); - save.JournalStreamSource.CreateStream().WriteAllBytes(Path.Combine(dir, "L3_0_SaveData"), ctx.Logger); + save.IvfcStreamSource.CreateStream().WriteAllBytes(Path.Combine(dir, "L3_0_SaveData"), ctx.Logger); } if (ctx.Options.SignSave)