Skip to content

Commit

Permalink
Add VerifyFileChecksums
Browse files Browse the repository at this point in the history
  • Loading branch information
xPaw committed Sep 8, 2023
1 parent 9e5e869 commit 147b0a8
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 32 deletions.
17 changes: 17 additions & 0 deletions ValvePak/ValvePak.Test/PackageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,23 @@ public void ExtractDirVPK()
TestVPKExtraction(path);
}

[Test]
public void TestFileChecksums()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "broken_dir.vpk");

using var package = new Package();
package.Read(path);
Assert.DoesNotThrow(() => package.VerifyFileChecksums());

var file = package.FindEntry("UpperCaseFolder/UpperCaseFile.txt");
Assert.AreEqual(0x32CFF012, file.CRC32);

file.CRC32 = 0xDEADBEEF;

Assert.Throws<InvalidDataException>(() => package.VerifyFileChecksums());
}

[Test]
public void ParsesCS2VPKWithInvalidSignature()
{
Expand Down
99 changes: 67 additions & 32 deletions ValvePak/ValvePak/Package.Verify.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;

namespace SteamDatabase.ValvePak
Expand Down Expand Up @@ -71,50 +72,62 @@ public void VerifyHashes()
}

using var md5 = MD5.Create();
var subStream = new SubStream(Reader.BaseStream, 0, FileSizeBeforeWholeFileHash);
var subStream = new SubStream(Reader.BaseStream, HeaderSize, (int)TreeSize);
var hash = md5.ComputeHash(subStream);

if (!HashEquals(hash, WholeFileChecksum))
if (!hash.SequenceEqual(TreeChecksum))
{
throw new InvalidDataException($"Package checksum mismatch ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(WholeFileChecksum)})");
throw new InvalidDataException($"File tree checksum mismatch ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(TreeChecksum)})");
}

subStream = new SubStream(Reader.BaseStream, HeaderSize, (int)TreeSize);
subStream = new SubStream(Reader.BaseStream, FileSizeBeforeArchiveMD5Entries, (int)ArchiveMD5SectionSize);
hash = md5.ComputeHash(subStream);

if (!HashEquals(hash, TreeChecksum))
if (!hash.SequenceEqual(ArchiveMD5EntriesChecksum))
{
throw new InvalidDataException($"File tree checksum mismatch ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(TreeChecksum)})");
throw new InvalidDataException($"Archive MD5 entries checksum mismatch ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(ArchiveMD5EntriesChecksum)})");
}

subStream = new SubStream(Reader.BaseStream, FileSizeBeforeArchiveMD5Entries, (int)ArchiveMD5SectionSize);
subStream = new SubStream(Reader.BaseStream, 0, FileSizeBeforeWholeFileHash);
hash = md5.ComputeHash(subStream);

if (!HashEquals(hash, ArchiveMD5EntriesChecksum))
if (!hash.SequenceEqual(WholeFileChecksum))
{
throw new InvalidDataException($"Archive MD5 entries checksum mismatch ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(ArchiveMD5EntriesChecksum)})");
throw new InvalidDataException($"Package checksum mismatch ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(WholeFileChecksum)})");
}
}

/// <summary>
/// Verify MD5 hashes of individual chunk files provided in the VPK.
/// </summary>
public void VerifyChunkHashes()
/// <param name="progressReporter">If provided, will report a string with the current verification progress.</param>
public void VerifyChunkHashes(IProgress<string> progressReporter = null)
{
using var md5 = MD5.Create();
Stream stream = null;
var lastArchiveIndex = uint.MaxValue;

// When created by Valve, entries are sorted, and are 1MB chunks
var allEntries = ArchiveMD5Entries
.OrderBy(file => file.Offset)
.GroupBy(file => file.ArchiveIndex)
.OrderBy(x => x.Key)
.SelectMany(x => x);

try
{
// TODO: When created by Valve, entries are sorted, and are 1MB chunks
foreach (var entry in ArchiveMD5Entries)
foreach (var entry in allEntries)
{
if (entry.ArchiveIndex > short.MaxValue)
{
throw new InvalidDataException("Unexpected archive index");
}

if (progressReporter != null)
{
progressReporter.Report($"Verifying MD5 hash at offset {entry.Offset} in archive {entry.ArchiveIndex}.");
}

if (lastArchiveIndex != entry.ArchiveIndex)
{
if (lastArchiveIndex != 0x7FFF)
Expand All @@ -134,11 +147,52 @@ public void VerifyChunkHashes()
var subStream = new SubStream(stream, stream.Position + entry.Offset, entry.Length);
var hash = md5.ComputeHash(subStream);

if (!HashEquals(hash, entry.Checksum))
if (!hash.SequenceEqual(entry.Checksum))
{
throw new InvalidDataException($"Package checksum mismatch in archive {entry.ArchiveIndex} at {entry.Offset} ({BitConverter.ToString(hash)} != expected {BitConverter.ToString(entry.Checksum)})");
}
}

progressReporter?.Report("Successfully verified archive MD5 hashes.");
}
finally
{
if (lastArchiveIndex != 0x7FFF)
{
stream?.Close();
}
}
}

/// <summary>
/// Verify CRC32 checksums of all files in the package.
/// </summary>
/// <param name="progressReporter">If provided, will report a string with the current verification progress.</param>
public void VerifyFileChecksums(IProgress<string> progressReporter = null)
{
Stream stream = null;
var lastArchiveIndex = uint.MaxValue;

var allEntries = Entries
.SelectMany(file => file.Value)
.OrderBy(file => file.Offset)
.GroupBy(file => file.ArchiveIndex)
.OrderBy(x => x.Key)
.SelectMany(x => x);

try
{
foreach (var entry in allEntries)
{
if (progressReporter != null)
{
progressReporter.Report($"Verifying CRC32 checksum for '{entry.GetFullPath()}' in archive {entry.ArchiveIndex}.");
}

ReadEntry(entry, out var _, validateCrc: true);
}

progressReporter?.Report("Successfully verified file CRC32 checksums.");
}
finally
{
Expand Down Expand Up @@ -167,24 +221,5 @@ public bool IsSignatureValid()

return rsa.VerifyData(subStream, Signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}

private static bool HashEquals(byte[] a, byte[] b)
{
if (a.Length != b.Length)
{
return false;
}

for (int i = 0; i < a.Length; i++)
{
if (a[i] != b[i])
{
return false;
}
}

return true;
}

}
}

0 comments on commit 147b0a8

Please sign in to comment.