diff --git a/SteamKit2/SteamKit2/Steam/CDN/DepotChunk.cs b/SteamKit2/SteamKit2/Steam/CDN/DepotChunk.cs index 4dd743691..7a8f533c3 100644 --- a/SteamKit2/SteamKit2/Steam/CDN/DepotChunk.cs +++ b/SteamKit2/SteamKit2/Steam/CDN/DepotChunk.cs @@ -53,16 +53,30 @@ public static int Process( DepotManifest.ChunkData info, ReadOnlySpan data try { var written = aes.DecryptCbc( data[ iv.Length.. ], iv, buffer, PaddingMode.PKCS7 ); - var decryptedStream = new MemoryStream( buffer, 0, written ); - if ( buffer.Length > 1 && buffer[ 0 ] == 'V' && buffer[ 1 ] == 'Z' ) + if ( buffer.Length < 16 ) { + throw new InvalidDataException( $"Not enough data in the decrypted depot chunk (was {buffer.Length} bytes)." ); + } + + if ( buffer[ 0 ] == 'V' && buffer[ 1 ] == 'S' && buffer[ 2 ] == 'Z' && buffer[ 3 ] == 'a' ) // Zstd + { + throw new NotImplementedException( "Zstd compressed chunks are not yet implemented in SteamKit." ); + } + else if ( buffer[ 0 ] == 'V' && buffer[ 1 ] == 'Z' && buffer[ 2 ] == 'a' ) // LZMA + { + using var decryptedStream = new MemoryStream( buffer, 0, written ); writtenDecompressed = VZipUtil.Decompress( decryptedStream, destination, verifyChecksum: false ); } - else + else if ( buffer[ 0 ] == 'P' && buffer[ 1 ] == 'K' && buffer[ 2 ] == 0x03 && buffer[ 3 ] == 0x04 ) // PKzip { + using var decryptedStream = new MemoryStream( buffer, 0, written ); writtenDecompressed = ZipUtil.Decompress( decryptedStream, destination, verifyChecksum: false ); } + else + { + throw new InvalidDataException( $"Unexpected depot chunk compression (first four bytes are {Convert.ToHexString( buffer.AsSpan( 0, 4 ) )})." ); + } } finally { diff --git a/SteamKit2/SteamKit2/Util/VZipUtil.cs b/SteamKit2/SteamKit2/Util/VZipUtil.cs index dd2e2dea2..0f9ad6423 100644 --- a/SteamKit2/SteamKit2/Util/VZipUtil.cs +++ b/SteamKit2/SteamKit2/Util/VZipUtil.cs @@ -12,19 +12,19 @@ static class VZipUtil private const int HeaderLength = 7; // magic + version + timestamp/crc private const int FooterLength = 10; // crc + decompressed size + magic - private const char Version = 'a'; + private const byte Version = (byte)'a'; public static int Decompress( MemoryStream ms, byte[] destination, bool verifyChecksum = true ) { using BinaryReader reader = new BinaryReader( ms ); if ( reader.ReadUInt16() != VZipHeader ) { - throw new Exception( "Expecting VZipHeader at start of stream" ); + throw new InvalidDataException( "Expecting VZipHeader at start of stream" ); } - if ( reader.ReadChar() != Version ) + if ( reader.ReadByte() != Version ) { - throw new Exception( "Expecting VZip version 'a'" ); + throw new InvalidDataException( "Expecting VZip version 'a'" ); } // Sometimes this is a creation timestamp (e.g. for Steam Client VZips). @@ -45,7 +45,7 @@ public static int Decompress( MemoryStream ms, byte[] destination, bool verifyCh if ( reader.ReadUInt16() != VZipFooter ) { - throw new Exception( "Expecting VZipFooter at end of stream" ); + throw new InvalidDataException( "Expecting VZipFooter at end of stream" ); } if ( destination.Length < sizeDecompressed ) diff --git a/SteamKit2/SteamKit2/Util/ZipUtil.cs b/SteamKit2/SteamKit2/Util/ZipUtil.cs index f579f5e13..59cb9ab72 100644 --- a/SteamKit2/SteamKit2/Util/ZipUtil.cs +++ b/SteamKit2/SteamKit2/Util/ZipUtil.cs @@ -35,7 +35,7 @@ public static int Decompress( MemoryStream ms, byte[] destination, bool verifyCh if ( verifyChecksum && Crc32.HashToUInt32( destination.AsSpan()[ ..sizeDecompressed ] ) != entry.Crc32 ) { - throw new Exception( "Checksum validation failed for decompressed file" ); + throw new InvalidDataException( "Checksum validation failed for decompressed file" ); } return sizeDecompressed; diff --git a/SteamKit2/Tests/DepotChunkFacts.cs b/SteamKit2/Tests/DepotChunkFacts.cs new file mode 100644 index 000000000..6f49a6df1 --- /dev/null +++ b/SteamKit2/Tests/DepotChunkFacts.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using System.IO.Hashing; +using System.Reflection; +using System.Security.Cryptography; +using SteamKit2; +using SteamKit2.CDN; +using Xunit; + +namespace Tests +{ + public class DepotChunkFacts + { + [Fact] + public void DecryptsAndDecompressesDepotChunkPKZip() + { + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream( "Tests.Files.depot_440_chunk_bac8e2657470b2eb70d6ddcd6c07004be8738697.bin" ); + using var ms = new MemoryStream(); + stream.CopyTo( ms ); + var chunkData = ms.ToArray(); + + var chunk = new DepotManifest.ChunkData( + id: [], // id is not needed here + checksum: 2130218374, + offset: 0, + comp_length: 320, + uncomp_length: 544 + ); + + var destination = new byte[ chunk.UncompressedLength ]; + var writtenLength = DepotChunk.Process( chunk, chunkData, destination, [ + 0x44, 0xCE, 0x5C, 0x52, 0x97, 0xA4, 0x15, 0xA1, + 0xA6, 0xF6, 0x9C, 0x85, 0x60, 0x37, 0xA5, 0xA2, + 0xFD, 0xD8, 0x2C, 0xD4, 0x74, 0xFA, 0x65, 0x9E, + 0xDF, 0xB4, 0xD5, 0x9B, 0x2A, 0xBC, 0x55, 0xFC + ] ); + + Assert.Equal( chunk.CompressedLength, ( uint )chunkData.Length ); + Assert.Equal( chunk.UncompressedLength, ( uint )writtenLength ); + + var hash = Convert.ToHexString( SHA1.HashData( destination ) ); + Assert.Equal( "BAC8E2657470B2EB70D6DDCD6C07004BE8738697", hash ); + } + + [Fact] + public void DecryptsAndDecompressesDepotChunkVZip() + { + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream( "Tests.Files.depot_232250_chunk_7b8567d9b3c09295cdbf4978c32b348d8e76c750.bin" ); + using var ms = new MemoryStream(); + stream.CopyTo( ms ); + var chunkData = ms.ToArray(); + + var chunk = new DepotManifest.ChunkData( + id: [], // id is not needed here + checksum: 2894626744, + offset: 0, + comp_length: 304, + uncomp_length: 798 + ); + + var destination = new byte[ chunk.UncompressedLength ]; + var writtenLength = DepotChunk.Process( chunk, chunkData, destination, [ + 0xE5, 0xF6, 0xAE, 0xD5, 0x5E, 0x9E, 0xCE, 0x42, + 0x9E, 0x56, 0xB8, 0x13, 0xFB, 0xF6, 0xBF, 0xE9, + 0x24, 0xF3, 0xCF, 0x72, 0x97, 0x2F, 0xDB, 0xD0, + 0x57, 0x1F, 0xFC, 0xAD, 0x9F, 0x2F, 0x7D, 0xAA, + ] ); + + Assert.Equal( chunk.CompressedLength, ( uint )chunkData.Length ); + Assert.Equal( chunk.UncompressedLength, ( uint )writtenLength ); + + var hash = Convert.ToHexString( SHA1.HashData( destination ) ); + Assert.Equal( "7B8567D9B3C09295CDBF4978C32B348D8E76C750", hash ); + } + } +} diff --git a/SteamKit2/Tests/Files/.gitattributes b/SteamKit2/Tests/Files/.gitattributes new file mode 100644 index 000000000..b4cd26280 --- /dev/null +++ b/SteamKit2/Tests/Files/.gitattributes @@ -0,0 +1,2 @@ +*.bin binary +*.manifest binary diff --git a/SteamKit2/Tests/Files/depot_232250_chunk_7b8567d9b3c09295cdbf4978c32b348d8e76c750.bin b/SteamKit2/Tests/Files/depot_232250_chunk_7b8567d9b3c09295cdbf4978c32b348d8e76c750.bin new file mode 100644 index 000000000..bfce45b0a --- /dev/null +++ b/SteamKit2/Tests/Files/depot_232250_chunk_7b8567d9b3c09295cdbf4978c32b348d8e76c750.bin @@ -0,0 +1,2 @@ +ȔY%OY.i֐ϭԐŎō +c*, N>nTsOآ˝ ުXlo`,4߱pE/[ˆ# T=<yӃR"# 7$^9t?Y/_YUu;sPbw8 wު) \Q|A ܋-" Cι1ZWP$ \ No newline at end of file diff --git a/SteamKit2/Tests/Files/depot_440_chunk_bac8e2657470b2eb70d6ddcd6c07004be8738697.bin b/SteamKit2/Tests/Files/depot_440_chunk_bac8e2657470b2eb70d6ddcd6c07004be8738697.bin new file mode 100644 index 000000000..9885d969b Binary files /dev/null and b/SteamKit2/Tests/Files/depot_440_chunk_bac8e2657470b2eb70d6ddcd6c07004be8738697.bin differ