Add secure m2p loader with runtime key enforcement
Some checks failed
build / Windows Build (push) Has been cancelled
Some checks failed
build / Windows Build (push) Has been cancelled
This commit is contained in:
275
src/PackLib/M2Pack.cpp
Normal file
275
src/PackLib/M2Pack.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user