From a71c045d697f5dff486b64dc69e5d1567e988c7a Mon Sep 17 00:00:00 2001 From: bt <99887872+caatge@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:16:15 +0200 Subject: [PATCH] vpkpp: add encrypted GCF support --- include/vpkpp/format/GCF.h | 30 ++++++++++++- src/vpkpp/format/GCF.cpp | 89 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/include/vpkpp/format/GCF.h b/include/vpkpp/format/GCF.h index c618cacde..28286d0ae 100644 --- a/include/vpkpp/format/GCF.h +++ b/include/vpkpp/format/GCF.h @@ -37,13 +37,39 @@ class GCF : public PackFileReadOnly { }; struct Block { - uint32_t entry_type; + enum class CompressionType : uint32_t { + UNCOMPRESSED = 0, + COMPRESSED, // compressed on the server, client automatically extracts it so not really + COMPRESSED_AND_ENCRYPTED, + ENCRYPTED, + }; + + uint16_t flags; + uint16_t open; uint32_t file_data_offset; uint32_t file_data_size; uint32_t first_data_block_index; uint32_t next_block_entry_index; uint32_t prev_block_entry_index; uint32_t dir_index; + + [[nodiscard]] CompressionType getCompressionType() const { + static constexpr std::array compressionTypeLUT = { + CompressionType::COMPRESSED, + CompressionType::COMPRESSED_AND_ENCRYPTED, + CompressionType::UNCOMPRESSED, + CompressionType::ENCRYPTED, + }; + const auto idx = (static_cast(flags) & 7) - 1; + if (idx <= 3) { + return compressionTypeLUT[idx]; + } + return CompressionType::UNCOMPRESSED; + } + + [[nodiscard]] bool isEncrypted() const { + return static_cast(this->getCompressionType()) - 2 <= 1; + } }; struct DirectoryHeader { @@ -153,7 +179,7 @@ class GCF : public PackFileReadOnly { DataBlockHeader datablockheader{}; std::vector chksum_map{}; std::vector checksums{}; - + std::array decryption_key; private: VPKPP_REGISTER_PACKFILE_OPEN(GCF_EXTENSION, &GCF::open); }; diff --git a/src/vpkpp/format/GCF.cpp b/src/vpkpp/format/GCF.cpp index 3effcc411..dc49a2f4b 100644 --- a/src/vpkpp/format/GCF.cpp +++ b/src/vpkpp/format/GCF.cpp @@ -3,7 +3,11 @@ #include #include +#include +#include +#include #include +#include #include #include @@ -60,9 +64,20 @@ std::unique_ptr GCF::open(const std::string& path, const EntryCallback } // block headers!!!!!! + bool requestKey = false; for (int i = 0; i < gcf->header.blockcount; i++) { Block& block = gcf->blockdata.emplace_back(); reader.read(block); + if (block.isEncrypted()) { + requestKey = true; + } + } + if (requestKey) { + const auto key = requestProperty(gcf, OpenProperty::DECRYPTION_KEY); + if (key.size() != gcf->decryption_key.size()) { + return nullptr; + } + std::ranges::copy(key, gcf->decryption_key.begin()); } // Fragmentation Map header @@ -232,7 +247,7 @@ std::optional> GCF::readEntry(const std::string& path_) c std::vector toread; for (const auto& v : this->blockdata) { - if (v.dir_index == dir_index) { + if (v.dir_index == dir_index && (v.flags & 0x8000)) { // need to check for 0x8000 here because valve dumps uninitialized memory toread.push_back(v); } } @@ -251,8 +266,9 @@ std::optional> GCF::readEntry(const std::string& path_) c } uint64_t remaining = entry->length; - - for (const auto& block : toread) { + bool needs_decrypt = false; + auto filemode = Block::CompressionType::UNCOMPRESSED; + for (auto& block : toread) { uint32_t currindex = block.first_data_block_index; while (currindex <= this->blockheader.count) { uint64_t curfilepos = static_cast(this->datablockheader.firstblockoffset) + (static_cast(0x2000) * static_cast(currindex)); @@ -265,8 +281,73 @@ std::optional> GCF::readEntry(const std::string& path_) c currindex = this->fragmap[currindex]; //printf("curridx now: %lu\n", currindex); } + needs_decrypt = block.isEncrypted(); + filemode = block.getCompressionType(); + } + if (needs_decrypt) { + CryptoPP::byte iv[16] = {}; + switch (filemode) { + using enum Block::CompressionType; + case UNCOMPRESSED: + case COMPRESSED: + break; + case ENCRYPTED: { + auto real_size = filedata.size(); + if (filedata.size() % 10 != 0) { + filedata.resize(filedata.size() + (0x10 - (filedata.size() % 10)), {}); + } + auto remaining_encrypted = filedata.size(); + auto offset = 0; + while (remaining_encrypted) { + CryptoPP::CFB_Mode::Decryption d; + d.SetKeyWithIV(reinterpret_cast(this->decryption_key.data()), 16, iv); + auto current_batch = std::min(remaining_encrypted, static_cast(0x8000)); + d.ProcessData(reinterpret_cast(filedata.data() + offset), reinterpret_cast(filedata.data() + offset), current_batch); + remaining_encrypted -= current_batch; + offset += static_cast(current_batch); + } + filedata.resize(real_size); + break; + } + case COMPRESSED_AND_ENCRYPTED: { + std::vector processed_data; + BufferStream s(filedata.data(), filedata.size()); + while (s.size() != s.tell()) { + static constexpr auto allocate_block = [](std::vector& vec, size_t x) { + const size_t old = vec.size(); + vec.resize(old + x); + return vec.data() + old; + }; + + auto encrypted_size = s.read(); + mz_ulong decompressed_size = s.read(); + + auto buffer = s.read_bytes(encrypted_size); + auto real_size = buffer.size(); + if (buffer.size() % 10 != 0) { + buffer.resize(buffer.size() + (0x10 - (buffer.size() % 10)), {}); + } + CryptoPP::CFB_Mode::Decryption d; + d.SetKeyWithIV(reinterpret_cast(this->decryption_key.data()), 16, iv); + d.ProcessData(reinterpret_cast(buffer.data()), reinterpret_cast(buffer.data()), buffer.size()); + buffer.resize(real_size); + + auto start = allocate_block(processed_data, decompressed_size); + if (mz_uncompress(reinterpret_cast(start), &decompressed_size, reinterpret_cast(buffer.data()), buffer.size()) != MZ_OK) { + return std::nullopt; + } + + //auto remaining = s.size() - s.tell(); + if (entry->length == processed_data.size()) { + break; + } + s.seek_u(s.tell() + (0x8000 - s.tell() % 0x8000)); + } + filedata = processed_data; + break; + } + } } - return filedata; }