Add secure m2p loader with runtime key enforcement
Some checks failed
build / Windows Build (push) Has been cancelled

This commit is contained in:
server
2026-04-14 12:12:23 +02:00
parent 0c2d6c7c9c
commit 229c809b96
8 changed files with 719 additions and 6 deletions

275
src/PackLib/M2Pack.cpp Normal file
View File

@@ -0,0 +1,275 @@
#include "M2Pack.h"
#include "M2PackRuntimeKeyProvider.h"
#include <cstring>
#include <zstd.h>
#include "EterBase/Debug.h"
#include "EterLib/BufferPool.h"
namespace
{
thread_local ZSTD_DCtx* g_m2pack_zstd_dctx = nullptr;
ZSTD_DCtx* GetThreadLocalZstdContext()
{
if (!g_m2pack_zstd_dctx)
{
g_m2pack_zstd_dctx = ZSTD_createDCtx();
}
return g_m2pack_zstd_dctx;
}
template <typename T>
bool ReadPod(const uint8_t* bytes, std::size_t size, std::size_t& offset, T& out)
{
if (offset + sizeof(T) > size)
{
return false;
}
memcpy(&out, bytes + offset, sizeof(T));
offset += sizeof(T);
return true;
}
}
bool CM2Pack::Load(const std::string& path)
{
if (!HasM2PackRuntimeKeysForArchiveLoad())
{
TraceError("CM2Pack::Load: runtime master key required for '%s'", path.c_str());
return false;
}
std::error_code ec;
m_file.map(path, ec);
if (ec)
{
TraceError("CM2Pack::Load: map failed for '%s': %s", path.c_str(), ec.message().c_str());
return false;
}
if (m_file.size() < sizeof(TM2PackHeader))
{
TraceError("CM2Pack::Load: file too small '%s'", path.c_str());
return false;
}
memcpy(&m_header, m_file.data(), sizeof(TM2PackHeader));
static constexpr char kMagic[M2PACK_MAGIC_SIZE] = {'M', '2', 'P', 'A', 'C', 'K', '2', '\0'};
if (memcmp(m_header.magic, kMagic, sizeof(kMagic)) != 0)
{
TraceError("CM2Pack::Load: invalid magic in '%s'", path.c_str());
return false;
}
if (m_header.version != 1)
{
TraceError("CM2Pack::Load: unsupported version %u in '%s'", m_header.version, path.c_str());
return false;
}
if (m_header.manifest_offset + m_header.manifest_size > m_file.size())
{
TraceError("CM2Pack::Load: manifest out of bounds in '%s'", path.c_str());
return false;
}
m_manifest_bytes.assign(
m_file.data() + m_header.manifest_offset,
m_file.data() + m_header.manifest_offset + m_header.manifest_size);
if (!ValidateManifest())
{
TraceError("CM2Pack::Load: manifest validation failed for '%s'", path.c_str());
return false;
}
return true;
}
bool CM2Pack::ValidateManifest()
{
std::array<uint8_t, M2PACK_HASH_SIZE> manifest_hash {};
crypto_generichash(
manifest_hash.data(),
manifest_hash.size(),
m_manifest_bytes.data(),
m_manifest_bytes.size(),
nullptr,
0);
if (memcmp(manifest_hash.data(), m_header.manifest_hash, manifest_hash.size()) != 0)
{
TraceError("CM2Pack::ValidateManifest: manifest hash mismatch");
return false;
}
if (crypto_sign_verify_detached(
m_header.manifest_signature,
m_manifest_bytes.data(),
m_manifest_bytes.size(),
GetM2PackActivePublicKey().data()) != 0)
{
TraceError("CM2Pack::ValidateManifest: manifest signature mismatch");
return false;
}
std::size_t offset = 0;
TM2PackManifestHeader manifest_header {};
if (!ReadPod(m_manifest_bytes.data(), m_manifest_bytes.size(), offset, manifest_header))
{
return false;
}
m_index.clear();
m_index.reserve(manifest_header.entry_count);
for (uint32_t i = 0; i < manifest_header.entry_count; ++i)
{
TM2PackManifestEntryFixed fixed {};
if (!ReadPod(m_manifest_bytes.data(), m_manifest_bytes.size(), offset, fixed))
{
return false;
}
if (offset + fixed.path_size > m_manifest_bytes.size())
{
return false;
}
TM2PackEntry entry;
entry.path.assign(reinterpret_cast<const char*>(m_manifest_bytes.data() + offset), fixed.path_size);
offset += fixed.path_size;
entry.compression = fixed.compression;
entry.data_offset = fixed.data_offset;
entry.original_size = fixed.original_size;
entry.stored_size = fixed.stored_size;
memcpy(entry.nonce.data(), fixed.nonce, entry.nonce.size());
memcpy(entry.plaintext_hash.data(), fixed.plaintext_hash, entry.plaintext_hash.size());
const uint64_t payload_begin = sizeof(TM2PackHeader);
const uint64_t payload_end = m_header.manifest_offset;
const uint64_t begin = payload_begin + entry.data_offset;
const uint64_t end = begin + entry.stored_size;
if (entry.path.empty() || entry.path.find("..") != std::string::npos || entry.path[0] == '/')
{
TraceError("CM2Pack::ValidateManifest: invalid path '%s'", entry.path.c_str());
return false;
}
if (begin > payload_end || end > payload_end || end < begin)
{
TraceError("CM2Pack::ValidateManifest: invalid entry bounds '%s'", entry.path.c_str());
return false;
}
m_index.push_back(std::move(entry));
}
return true;
}
bool CM2Pack::DecryptEntryPayload(const TM2PackEntry& entry, std::vector<uint8_t>& decrypted, CBufferPool* pPool)
{
const uint64_t begin = sizeof(TM2PackHeader) + entry.data_offset;
const auto* ciphertext = reinterpret_cast<const uint8_t*>(m_file.data() + begin);
std::vector<uint8_t> ciphertext_copy;
if (pPool)
{
ciphertext_copy = pPool->Acquire(entry.stored_size);
}
ciphertext_copy.resize(entry.stored_size);
memcpy(ciphertext_copy.data(), ciphertext, entry.stored_size);
decrypted.resize(entry.stored_size);
unsigned long long written = 0;
if (crypto_aead_xchacha20poly1305_ietf_decrypt(
decrypted.data(),
&written,
nullptr,
ciphertext_copy.data(),
ciphertext_copy.size(),
reinterpret_cast<const unsigned char*>(entry.path.data()),
entry.path.size(),
entry.nonce.data(),
GetM2PackActiveMasterKey().data()) != 0)
{
if (pPool)
{
pPool->Release(std::move(ciphertext_copy));
}
return false;
}
decrypted.resize(static_cast<std::size_t>(written));
if (pPool)
{
pPool->Release(std::move(ciphertext_copy));
}
return true;
}
bool CM2Pack::GetFile(const TM2PackEntry& entry, std::vector<uint8_t>& result)
{
return GetFileWithPool(entry, result, nullptr);
}
bool CM2Pack::GetFileWithPool(const TM2PackEntry& entry, std::vector<uint8_t>& result, CBufferPool* pPool)
{
std::vector<uint8_t> compressed;
if (!DecryptEntryPayload(entry, compressed, pPool))
{
TraceError("CM2Pack::GetFileWithPool: decrypt failed for '%s'", entry.path.c_str());
return false;
}
switch (entry.compression)
{
case 0:
result = std::move(compressed);
break;
case 1:
{
result.resize(entry.original_size);
ZSTD_DCtx* dctx = GetThreadLocalZstdContext();
size_t written = ZSTD_decompressDCtx(dctx, result.data(), result.size(), compressed.data(), compressed.size());
if (ZSTD_isError(written) || written != entry.original_size)
{
TraceError("CM2Pack::GetFileWithPool: zstd failed for '%s'", entry.path.c_str());
return false;
}
break;
}
default:
TraceError("CM2Pack::GetFileWithPool: unsupported compression %u for '%s'", entry.compression, entry.path.c_str());
return false;
}
std::array<uint8_t, M2PACK_HASH_SIZE> plain_hash {};
crypto_generichash(
plain_hash.data(),
plain_hash.size(),
result.data(),
result.size(),
nullptr,
0);
if (memcmp(plain_hash.data(), entry.plaintext_hash.data(), plain_hash.size()) != 0)
{
TraceError("CM2Pack::GetFileWithPool: plaintext hash mismatch for '%s'", entry.path.c_str());
return false;
}
return true;
}