This commit is contained in:
l0rinc 2025-04-29 11:49:37 +02:00 committed by GitHub
commit 52fd242710
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 350 additions and 175 deletions

View file

@ -6,19 +6,24 @@
#include <random.h>
#include <span.h>
#include <streams.h>
#include <util/byte_units.h>
#include <cstddef>
#include <vector>
static void Xor(benchmark::Bench& bench)
static void ObfuscationBench(benchmark::Bench& bench)
{
FastRandomContext frc{/*fDeterministic=*/true};
auto data{frc.randbytes<std::byte>(1024)};
auto key{frc.randbytes<std::byte>(31)};
FastRandomContext rng{/*fDeterministic=*/true};
constexpr size_t bytes{10_MiB};
auto test_data{rng.randbytes<std::byte>(bytes)};
bench.batch(data.size()).unit("byte").run([&] {
util::Xor(data, key);
const Obfuscation obfuscation{rng.rand64()};
size_t offset{0};
bench.batch(bytes / 1_MiB).unit("MiB").run([&] {
obfuscation(test_data, offset++);
ankerl::nanobench::doNotOptimizeAway(test_data);
});
}
BENCHMARK(Xor, benchmark::PriorityLevel::HIGH);
BENCHMARK(ObfuscationBench, benchmark::PriorityLevel::HIGH);

View file

@ -173,7 +173,7 @@ void CDBBatch::Clear()
void CDBBatch::WriteImpl(std::span<const std::byte> key, DataStream& ssValue)
{
leveldb::Slice slKey(CharCast(key.data()), key.size());
ssValue.Xor(dbwrapper_private::GetObfuscateKey(parent));
dbwrapper_private::GetObfuscation(parent)(ssValue);
leveldb::Slice slValue(CharCast(ssValue.data()), ssValue.size());
m_impl_batch->batch.Put(slKey, slValue);
}
@ -248,24 +248,24 @@ CDBWrapper::CDBWrapper(const DBParams& params)
LogPrintf("Finished database compaction of %s\n", fs::PathToString(params.path));
}
// The base-case obfuscation key, which is a noop.
obfuscate_key = std::vector<unsigned char>(OBFUSCATE_KEY_NUM_BYTES, '\000');
{
assert(m_obfuscation == 0); // Needed for unobfuscated Read() below
std::vector<uint8_t> obfuscation_key_vector(Obfuscation::SIZE_BYTES, '\000');
const bool key_missing{!Read(OBFUSCATION_KEY, obfuscation_key_vector)};
if (key_missing && params.obfuscate && IsEmpty()) {
// Initialize non-degenerate obfuscation if it won't upset existing, non-obfuscated data.
std::vector<uint8_t> new_key(Obfuscation::SIZE_BYTES);
GetRandBytes(new_key);
bool key_exists = Read(OBFUSCATE_KEY_KEY, obfuscate_key);
// Write `new_key` so we don't obfuscate the key with itself
Write(OBFUSCATION_KEY, new_key);
obfuscation_key_vector = std::move(new_key);
if (!key_exists && params.obfuscate && IsEmpty()) {
// Initialize non-degenerate obfuscation if it won't upset
// existing, non-obfuscated data.
std::vector<unsigned char> new_key = CreateObfuscateKey();
// Write `new_key` so we don't obfuscate the key with itself
Write(OBFUSCATE_KEY_KEY, new_key);
obfuscate_key = new_key;
LogPrintf("Wrote new obfuscate key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key));
LogPrintf("Wrote new obfuscate key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscation_key_vector));
}
LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscation_key_vector));
m_obfuscation = obfuscation_key_vector;
}
LogPrintf("Using obfuscation key for %s: %s\n", fs::PathToString(params.path), HexStr(obfuscate_key));
}
CDBWrapper::~CDBWrapper()
@ -310,25 +310,6 @@ size_t CDBWrapper::DynamicMemoryUsage() const
return parsed.value();
}
// Prefixed with null character to avoid collisions with other keys
//
// We must use a string constructor which specifies length so that we copy
// past the null-terminator.
const std::string CDBWrapper::OBFUSCATE_KEY_KEY("\000obfuscate_key", 14);
const unsigned int CDBWrapper::OBFUSCATE_KEY_NUM_BYTES = 8;
/**
* Returns a string (consisting of 8 random bytes) suitable for use as an
* obfuscating XOR key.
*/
std::vector<unsigned char> CDBWrapper::CreateObfuscateKey() const
{
std::vector<uint8_t> ret(OBFUSCATE_KEY_NUM_BYTES);
GetRandBytes(ret);
return ret;
}
std::optional<std::string> CDBWrapper::ReadImpl(std::span<const std::byte> key) const
{
leveldb::Slice slKey(CharCast(key.data()), key.size());
@ -412,9 +393,6 @@ void CDBIterator::Next() { m_impl_iter->iter->Next(); }
namespace dbwrapper_private {
const std::vector<unsigned char>& GetObfuscateKey(const CDBWrapper &w)
{
return w.obfuscate_key;
}
Obfuscation GetObfuscation(const CDBWrapper& w) { return w.m_obfuscation; }
} // namespace dbwrapper_private

View file

@ -18,7 +18,6 @@
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
static const size_t DBWRAPPER_PREALLOC_KEY_SIZE = 64;
static const size_t DBWRAPPER_PREALLOC_VALUE_SIZE = 1024;
@ -63,8 +62,7 @@ namespace dbwrapper_private {
* Database obfuscation should be considered an implementation detail of the
* specific database.
*/
const std::vector<unsigned char>& GetObfuscateKey(const CDBWrapper &w);
Obfuscation GetObfuscation(const CDBWrapper&);
}; // namespace dbwrapper_private
bool DestroyDB(const std::string& path_str);
@ -166,7 +164,7 @@ public:
template<typename V> bool GetValue(V& value) {
try {
DataStream ssValue{GetValueImpl()};
ssValue.Xor(dbwrapper_private::GetObfuscateKey(parent));
dbwrapper_private::GetObfuscation(parent)(ssValue);
ssValue >> value;
} catch (const std::exception&) {
return false;
@ -179,7 +177,7 @@ struct LevelDBContext;
class CDBWrapper
{
friend const std::vector<unsigned char>& dbwrapper_private::GetObfuscateKey(const CDBWrapper &w);
friend Obfuscation dbwrapper_private::GetObfuscation(const CDBWrapper&);
private:
//! holds all leveldb-specific fields of this class
std::unique_ptr<LevelDBContext> m_db_context;
@ -187,16 +185,8 @@ private:
//! the name of this database
std::string m_name;
//! a key used for optional XOR-obfuscation of the database
std::vector<unsigned char> obfuscate_key;
//! the key under which the obfuscation key is stored
static const std::string OBFUSCATE_KEY_KEY;
//! the length of the obfuscate key in number of bytes
static const unsigned int OBFUSCATE_KEY_NUM_BYTES;
std::vector<unsigned char> CreateObfuscateKey() const;
//! optional XOR-obfuscation of the database
Obfuscation m_obfuscation{0};
//! path to filesystem storage
const fs::path m_path;
@ -210,6 +200,11 @@ private:
auto& DBContext() const LIFETIMEBOUND { return *Assert(m_db_context); }
public:
// Prefixed with null character to avoid collisions with other keys
//
// We must use a string constructor which specifies length so that we copy past the null-terminator.
inline static const std::string OBFUSCATION_KEY{"\000obfuscate_key", 14};
CDBWrapper(const DBParams& params);
~CDBWrapper();
@ -228,7 +223,7 @@ public:
}
try {
DataStream ssValue{MakeByteSpan(*strValue)};
ssValue.Xor(obfuscate_key);
m_obfuscation(ssValue);
ssValue >> value;
} catch (const std::exception&) {
return false;

View file

@ -777,13 +777,13 @@ void BlockManager::UnlinkPrunedFiles(const std::set<int>& setFilesToPrune) const
AutoFile BlockManager::OpenBlockFile(const FlatFilePos& pos, bool fReadOnly) const
{
return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_xor_key};
return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_obfuscation};
}
/** Open an undo file (rev?????.dat) */
AutoFile BlockManager::OpenUndoFile(const FlatFilePos& pos, bool fReadOnly) const
{
return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_xor_key};
return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_obfuscation};
}
fs::path BlockManager::GetBlockPosFilename(const FlatFilePos& pos) const
@ -1107,7 +1107,7 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts)
{
// Bytes are serialized without length indicator, so this is also the exact
// size of the XOR-key file.
std::array<std::byte, 8> xor_key{};
std::array<std::byte, Obfuscation::SIZE_BYTES> xor_key{};
// Consider this to be the first run if the blocksdir contains only hidden
// files (those which start with a .). Checking for a fully-empty dir would
@ -1152,12 +1152,12 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts)
};
}
LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(xor_key));
return std::vector<std::byte>{xor_key.begin(), xor_key.end()};
return Obfuscation{xor_key};
}
BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts)
: m_prune_mode{opts.prune_target > 0},
m_xor_key{InitBlocksdirXorKey(opts)},
m_obfuscation{InitBlocksdirXorKey(opts)},
m_opts{std::move(opts)},
m_block_file_seq{FlatFileSeq{m_opts.blocks_dir, "blk", m_opts.fast_prune ? 0x4000 /* 16kB */ : BLOCKFILE_CHUNK_SIZE}},
m_undo_file_seq{FlatFileSeq{m_opts.blocks_dir, "rev", UNDOFILE_CHUNK_SIZE}},

View file

@ -235,7 +235,7 @@ private:
const bool m_prune_mode;
const std::vector<std::byte> m_xor_key;
const Obfuscation m_obfuscation;
/** Dirty block index entries. */
std::set<CBlockIndex*> m_dirty_blockindex;

View file

@ -58,15 +58,17 @@ bool LoadMempool(CTxMemPool& pool, const fs::path& load_path, Chainstate& active
try {
uint64_t version;
file >> version;
std::vector<std::byte> xor_key;
if (version == MEMPOOL_DUMP_VERSION_NO_XOR_KEY) {
// Leave XOR-key empty
file.SetObfuscation(0);
} else if (version == MEMPOOL_DUMP_VERSION) {
file >> xor_key;
Obfuscation obfuscation{0};
file >> obfuscation;
file.SetObfuscation(obfuscation);
} else {
return false;
}
file.SetXor(xor_key);
uint64_t total_txns_to_load;
file >> total_txns_to_load;
uint64_t txns_tried = 0;
@ -177,12 +179,13 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock
const uint64_t version{pool.m_opts.persist_v1_dat ? MEMPOOL_DUMP_VERSION_NO_XOR_KEY : MEMPOOL_DUMP_VERSION};
file << version;
std::vector<std::byte> xor_key(8);
if (!pool.m_opts.persist_v1_dat) {
FastRandomContext{}.fillrand(xor_key);
file << xor_key;
const Obfuscation obfuscation{FastRandomContext{}.rand64()};
file << obfuscation;
file.SetObfuscation(obfuscation);
} else {
file.SetObfuscation(0);
}
file.SetXor(xor_key);
uint64_t mempool_transactions_to_write(vinfo.size());
file << mempool_transactions_to_write;

86
src/obfuscation.h Normal file
View file

@ -0,0 +1,86 @@
// Copyright (c) 2025-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_OBFUSCATION_H
#define BITCOIN_OBFUSCATION_H
#include <span.h>
#include <tinyformat.h>
#include <array>
#include <bit>
#include <climits>
#include <stdexcept>
class Obfuscation
{
public:
static constexpr size_t SIZE_BYTES{sizeof(uint64_t)};
Obfuscation(const uint64_t key) { SetRotations(key); }
Obfuscation(const std::span<const std::byte, SIZE_BYTES> key_span) : Obfuscation(ToUint64(key_span)) {}
Obfuscation(const std::vector<uint8_t>& key_vec) : Obfuscation(MakeByteSpan(key_vec).first<SIZE_BYTES>()) {}
Obfuscation(const std::vector<std::byte>& key_vec) : Obfuscation(std::span(key_vec).first<SIZE_BYTES>()) {}
uint64_t Key() const { return m_rotations[0]; }
operator bool() const { return Key() != 0; }
void operator()(std::span<std::byte> target, const size_t key_offset_bytes = 0) const
{
if (!*this) return;
const uint64_t rot_key{m_rotations[key_offset_bytes % SIZE_BYTES]}; // Continue obfuscation from where we left off
for (; target.size() >= SIZE_BYTES; target = target.subspan(SIZE_BYTES)) { // Process multiple bytes at a time
Xor(target, rot_key, SIZE_BYTES);
}
Xor(target, rot_key, target.size());
}
template <typename Stream>
void Serialize(Stream& s) const
{
// Use vector serialization for convenient compact size prefix.
std::vector<std::byte> bytes(SIZE_BYTES);
std::memcpy(bytes.data(), &m_rotations[0], SIZE_BYTES);
s << bytes;
}
template <typename Stream>
void Unserialize(Stream& s)
{
std::vector<std::byte> bytes(SIZE_BYTES);
s >> bytes;
if (bytes.size() != SIZE_BYTES) throw std::logic_error(strprintf("Obfuscation key size should be exactly %s bytes long", SIZE_BYTES));
SetRotations(ToUint64(std::span<std::byte, SIZE_BYTES>(bytes)));
}
private:
// Cached key rotations for different offsets.
std::array<uint64_t, SIZE_BYTES> m_rotations;
void SetRotations(const uint64_t key)
{
for (size_t i{0}; i < SIZE_BYTES; ++i) {
size_t key_rotation_bits{CHAR_BIT * i};
if constexpr (std::endian::native == std::endian::big) key_rotation_bits *= -1;
m_rotations[i] = std::rotr(key, key_rotation_bits);
}
}
static uint64_t ToUint64(const std::span<const std::byte, SIZE_BYTES> key_span)
{
uint64_t key{};
std::memcpy(&key, key_span.data(), SIZE_BYTES);
return key;
}
static void Xor(std::span<std::byte> target, const uint64_t key, const size_t size)
{
assert(size <= target.size());
uint64_t raw{};
std::memcpy(&raw, target.data(), size);
raw ^= key;
std::memcpy(target.data(), &raw, size);
}
};
#endif // BITCOIN_OBFUSCATION_H

View file

@ -9,8 +9,7 @@
#include <array>
AutoFile::AutoFile(std::FILE* file, std::vector<std::byte> data_xor)
: m_file{file}, m_xor{std::move(data_xor)}
AutoFile::AutoFile(std::FILE* file, const Obfuscation& obfuscation) : m_file{file}, m_obfuscation{obfuscation}
{
if (!IsNull()) {
auto pos{std::ftell(m_file)};
@ -21,12 +20,12 @@ AutoFile::AutoFile(std::FILE* file, std::vector<std::byte> data_xor)
std::size_t AutoFile::detail_fread(std::span<std::byte> dst)
{
if (!m_file) throw std::ios_base::failure("AutoFile::read: file handle is nullptr");
size_t ret = std::fread(dst.data(), 1, dst.size(), m_file);
if (!m_xor.empty()) {
if (!m_position.has_value()) throw std::ios_base::failure("AutoFile::read: position unknown");
util::Xor(dst.subspan(0, ret), m_xor, *m_position);
const size_t ret = std::fread(dst.data(), 1, dst.size(), m_file);
if (m_obfuscation) {
if (!m_position) throw std::ios_base::failure("AutoFile::read: position unknown");
m_obfuscation(dst, *m_position);
}
if (m_position.has_value()) *m_position += ret;
if (m_position) *m_position += ret;
return ret;
}
@ -81,7 +80,7 @@ void AutoFile::ignore(size_t nSize)
void AutoFile::write(std::span<const std::byte> src)
{
if (!m_file) throw std::ios_base::failure("AutoFile::write: file handle is nullptr");
if (m_xor.empty()) {
if (!m_obfuscation) {
if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) {
throw std::ios_base::failure("AutoFile::write: write failed");
}
@ -100,9 +99,9 @@ void AutoFile::write(std::span<const std::byte> src)
void AutoFile::write_buffer(std::span<std::byte> src)
{
if (!m_file) throw std::ios_base::failure("AutoFile::write_buffer: file handle is nullptr");
if (m_xor.size()) {
if (m_obfuscation) {
if (!m_position) throw std::ios_base::failure("AutoFile::write_buffer: obfuscation position unknown");
util::Xor(src, m_xor, *m_position); // obfuscate in-place
m_obfuscation(src, *m_position); // obfuscate in-place
}
if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) {
throw std::ios_base::failure("AutoFile::write_buffer: write failed");

View file

@ -6,6 +6,7 @@
#ifndef BITCOIN_STREAMS_H
#define BITCOIN_STREAMS_H
#include <obfuscation.h>
#include <serialize.h>
#include <span.h>
#include <support/allocators/zeroafterfree.h>
@ -21,30 +22,8 @@
#include <stdint.h>
#include <string.h>
#include <string>
#include <utility>
#include <vector>
namespace util {
inline void Xor(std::span<std::byte> write, std::span<const std::byte> key, size_t key_offset = 0)
{
if (key.size() == 0) {
return;
}
key_offset %= key.size();
for (size_t i = 0, j = key_offset; i != write.size(); i++) {
write[i] ^= key[j++];
// This potentially acts on very many bytes of data, so it's
// important that we calculate `j`, i.e. the `key` index in this
// way instead of doing a %, which would effectively be a division
// for each byte Xor'd -- much slower than need be.
if (j == key.size())
j = 0;
}
}
} // namespace util
/* Minimal stream for overwriting and/or appending to an existing byte vector
*
* The referenced vector will grow as necessary
@ -261,21 +240,16 @@ public:
return (*this);
}
template<typename T>
template <typename T>
DataStream& operator>>(T&& obj)
{
::Unserialize(*this, obj);
return (*this);
}
/**
* XOR the contents of this stream with a certain key.
*
* @param[in] key The key used to XOR the data in this stream.
*/
void Xor(const std::vector<unsigned char>& key)
void Obfuscate(const Obfuscation& obfuscation)
{
util::Xor(MakeWritableByteSpan(*this), MakeByteSpan(key));
if (obfuscation) obfuscation(MakeWritableByteSpan(*this));
}
/** Compute total memory usage of this object (own memory + any dynamic memory). */
@ -392,11 +366,11 @@ class AutoFile
{
protected:
std::FILE* m_file;
std::vector<std::byte> m_xor;
Obfuscation m_obfuscation;
std::optional<int64_t> m_position;
public:
explicit AutoFile(std::FILE* file, std::vector<std::byte> data_xor={});
explicit AutoFile(std::FILE* file, const Obfuscation& obfuscation = 0);
~AutoFile() { fclose(); }
@ -428,7 +402,7 @@ public:
bool IsNull() const { return m_file == nullptr; }
/** Continue with a different XOR key */
void SetXor(std::vector<std::byte> data_xor) { m_xor = data_xor; }
void SetObfuscation(const Obfuscation& obfuscation) { m_obfuscation = obfuscation; }
/** Implementation detail, only used internally. */
std::size_t detail_fread(std::span<std::byte> dst);

View file

@ -9,39 +9,63 @@
#include <util/string.h>
#include <memory>
#include <ranges>
#include <boost/test/unit_test.hpp>
using util::ToString;
// Test if a string consists entirely of null characters
static bool is_null_key(const std::vector<unsigned char>& key) {
bool isnull = true;
for (unsigned int i = 0; i < key.size(); i++)
isnull &= (key[i] == '\x00');
return isnull;
}
BOOST_FIXTURE_TEST_SUITE(dbwrapper_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(dbwrapper)
{
// Perform tests both obfuscated and non-obfuscated.
for (const bool obfuscate : {false, true}) {
fs::path ph = m_args.GetDataDirBase() / (obfuscate ? "dbwrapper_obfuscate_true" : "dbwrapper_obfuscate_false");
CDBWrapper dbw({.path = ph, .cache_bytes = 1 << 20, .memory_only = true, .wipe_data = false, .obfuscate = obfuscate});
uint8_t key{'k'};
uint256 in = m_rng.rand256();
uint256 res;
constexpr size_t CACHE_SIZE{1_MiB};
const fs::path path{m_args.GetDataDirBase() / "dbwrapper"};
// Ensure that we're doing real obfuscation when obfuscate=true
BOOST_CHECK(obfuscate != is_null_key(dbwrapper_private::GetObfuscateKey(dbw)));
uint64_t obfuscation_key{};
std::vector<std::pair<uint8_t, uint256>> key_values{};
BOOST_CHECK(dbw.Write(key, in));
BOOST_CHECK(dbw.Read(key, res));
BOOST_CHECK_EQUAL(res.ToString(), in.ToString());
// Write values
{
CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .wipe_data = true, .obfuscate = obfuscate}};
BOOST_CHECK_EQUAL(obfuscate, !dbw.IsEmpty());
// Ensure that we're doing real obfuscation when obfuscate=true
BOOST_CHECK_EQUAL(obfuscate, dbwrapper_private::GetObfuscation(dbw));
obfuscation_key = dbwrapper_private::GetObfuscation(dbw).Key();
for (uint8_t k{0}; k < 10; ++k) {
uint8_t key{k};
uint256 value{m_rng.rand256()};
BOOST_CHECK(dbw.Write(key, value));
key_values.emplace_back(key, value);
}
}
// Verify that the obfuscation key is never obfuscated
{
CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = false}};
BOOST_CHECK_EQUAL(obfuscation_key, dbwrapper_private::GetObfuscation(dbw).Key());
}
// Read back the values
{
CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = obfuscate}};
// Ensure obfuscation is read back correctly
BOOST_CHECK_EQUAL(obfuscate, dbwrapper_private::GetObfuscation(dbw));
BOOST_CHECK_EQUAL(obfuscation_key, dbwrapper_private::GetObfuscation(dbw).Key());
// Verify all written values
for (const auto& [key, expected_value] : key_values) {
uint256 read_value{};
BOOST_CHECK(dbw.Read(key, read_value));
BOOST_CHECK_EQUAL(read_value.ToString(), expected_value.ToString());
}
}
}
}
@ -57,7 +81,7 @@ BOOST_AUTO_TEST_CASE(dbwrapper_basic_data)
bool res_bool;
// Ensure that we're doing real obfuscation when obfuscate=true
BOOST_CHECK(obfuscate != is_null_key(dbwrapper_private::GetObfuscateKey(dbw)));
BOOST_CHECK_EQUAL(obfuscate, dbwrapper_private::GetObfuscation(dbw));
//Simulate block raw data - "b + block hash"
std::string key_block = "b" + m_rng.rand256().ToString();
@ -116,13 +140,13 @@ BOOST_AUTO_TEST_CASE(dbwrapper_basic_data)
std::string file_option_tag = "F";
uint8_t filename_length = m_rng.randbits(8);
std::string filename = "randomfilename";
std::string key_file_option = strprintf("%s%01x%s", file_option_tag,filename_length,filename);
std::string key_file_option = strprintf("%s%01x%s", file_option_tag, filename_length, filename);
bool in_file_bool = m_rng.randbool();
BOOST_CHECK(dbw.Write(key_file_option, in_file_bool));
BOOST_CHECK(dbw.Read(key_file_option, res_bool));
BOOST_CHECK_EQUAL(res_bool, in_file_bool);
}
}
}
// Test batch operations
@ -231,8 +255,8 @@ BOOST_AUTO_TEST_CASE(existing_data_no_obfuscate)
BOOST_CHECK(odbw.Read(key, res2));
BOOST_CHECK_EQUAL(res2.ToString(), in.ToString());
BOOST_CHECK(!odbw.IsEmpty()); // There should be existing data
BOOST_CHECK(is_null_key(dbwrapper_private::GetObfuscateKey(odbw))); // The key should be an empty string
BOOST_CHECK(!odbw.IsEmpty()); // There should be existing data
BOOST_CHECK(!dbwrapper_private::GetObfuscation(odbw)); // The key should be an empty string
uint256 in2 = m_rng.rand256();
uint256 res3;
@ -269,7 +293,7 @@ BOOST_AUTO_TEST_CASE(existing_data_reindex)
// Check that the key/val we wrote with unobfuscated wrapper doesn't exist
uint256 res2;
BOOST_CHECK(!odbw.Read(key, res2));
BOOST_CHECK(!is_null_key(dbwrapper_private::GetObfuscateKey(odbw)));
BOOST_CHECK(dbwrapper_private::GetObfuscation(odbw));
uint256 in2 = m_rng.rand256();
uint256 res3;

View file

@ -20,7 +20,7 @@ FUZZ_TARGET(autofile)
FuzzedFileProvider fuzzed_file_provider{fuzzed_data_provider};
AutoFile auto_file{
fuzzed_file_provider.open(),
ConsumeRandomLengthByteVector<std::byte>(fuzzed_data_provider),
fuzzed_data_provider.ConsumeIntegral<uint64_t>()
};
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100)
{

View file

@ -22,7 +22,7 @@ FUZZ_TARGET(buffered_file)
std::optional<BufferedFile> opt_buffered_file;
AutoFile fuzzed_file{
fuzzed_file_provider.open(),
ConsumeRandomLengthByteVector<std::byte>(fuzzed_data_provider),
fuzzed_data_provider.ConsumeIntegral<uint64_t>()
};
try {
auto n_buf_size = fuzzed_data_provider.ConsumeIntegralInRange<uint64_t>(0, 4096);

View file

@ -13,19 +13,138 @@
#include <boost/test/unit_test.hpp>
using namespace std::string_literals;
using namespace util::hex_literals;
BOOST_FIXTURE_TEST_SUITE(streams_tests, BasicTestingSetup)
// Test that obfuscation can be properly reverted even with random chunk sizes.
BOOST_AUTO_TEST_CASE(xor_roundtrip_random_chunks)
{
auto apply_random_xor_chunks{[&](std::span<std::byte> target, const Obfuscation& obfuscation) {
for (size_t offset{0}; offset < target.size();) {
const size_t chunk_size{1 + m_rng.randrange(target.size() - offset)};
obfuscation(target.subspan(offset, chunk_size), offset);
offset += chunk_size;
}
}};
for (size_t test{0}; test < 100; ++test) {
const size_t write_size{1 + m_rng.randrange(100U)};
const std::vector original{m_rng.randbytes<std::byte>(write_size)};
std::vector roundtrip{original};
const auto key_bytes{m_rng.randbool() ? std::vector(Obfuscation::SIZE_BYTES, std::byte{0}) : m_rng.randbytes<std::byte>(Obfuscation::SIZE_BYTES)};
const Obfuscation obfuscation{key_bytes};
apply_random_xor_chunks(roundtrip, obfuscation);
// Verify intermediate state differs from original unless the key is all zeros
const bool all_zeros{!obfuscation || std::ranges::all_of(
std::span{key_bytes}.first(std::min(write_size, Obfuscation::SIZE_BYTES)), [](auto b) { return b == std::byte{0}; })};
BOOST_CHECK_EQUAL(original != roundtrip, !all_zeros);
apply_random_xor_chunks(roundtrip, obfuscation);
BOOST_CHECK(original == roundtrip);
}
}
// Compares optimized obfuscation against a trivial byte-by-byte reference implementation
// with random offsets to ensure proper handling of key wrapping.
BOOST_AUTO_TEST_CASE(xor_bytes_reference)
{
auto expected_xor{[](std::span<std::byte> target, const std::span<const std::byte> obfuscation, size_t key_offset) {
for (auto& b : target) {
b ^= obfuscation[key_offset++ % obfuscation.size()];
}
}};
for (size_t test{0}; test < 100; ++test) {
const size_t write_size{1 + m_rng.randrange(100U)};
const size_t key_offset{m_rng.randrange(3 * 8U)}; // Should wrap around
const auto key_bytes{m_rng.randbytes<std::byte>(Obfuscation::SIZE_BYTES)};
const Obfuscation obfuscation{key_bytes};
std::vector expected{m_rng.randbytes<std::byte>(write_size)};
std::vector actual{expected};
expected_xor(expected, key_bytes, key_offset);
obfuscation(actual, key_offset);
BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), actual.begin(), actual.end());
}
}
BOOST_AUTO_TEST_CASE(obfuscation_constructors)
{
constexpr uint64_t test_key{0x0123456789ABCDEF};
// Direct uint64_t constructor
{
const Obfuscation obfuscation{test_key};
BOOST_CHECK_EQUAL(obfuscation.Key(), test_key);
}
// std::span constructor
{
std::array<std::byte, Obfuscation::SIZE_BYTES> key_bytes{};
std::memcpy(key_bytes.data(), &test_key, Obfuscation::SIZE_BYTES);
const Obfuscation obfuscation{std::span{key_bytes}};
BOOST_CHECK_EQUAL(obfuscation.Key(), test_key);
}
// std::vector<uint8_t> constructor
{
std::vector<uint8_t> uint8_key(Obfuscation::SIZE_BYTES);
std::memcpy(uint8_key.data(), &test_key, uint8_key.size());
const Obfuscation obfuscation{uint8_key};
BOOST_CHECK_EQUAL(obfuscation.Key(), test_key);
}
// std::vector<std::byte> constructor
{
std::vector<std::byte> byte_vector_key(Obfuscation::SIZE_BYTES);
std::memcpy(byte_vector_key.data(), &test_key, byte_vector_key.size());
const Obfuscation obfuscation{byte_vector_key};
BOOST_CHECK_EQUAL(obfuscation.Key(), test_key);
}
}
BOOST_AUTO_TEST_CASE(obfuscation_serialize)
{
const Obfuscation original{0xDEADBEEF};
// Serialize
DataStream ds;
ds << original;
BOOST_CHECK_EQUAL(ds.size(), 1 + Obfuscation::SIZE_BYTES); // serialized as a vector
// Deserialize
Obfuscation recovered{0};
ds >> recovered;
BOOST_CHECK_EQUAL(recovered.Key(), original.Key());
}
BOOST_AUTO_TEST_CASE(obfuscation_empty)
{
const Obfuscation null_obf{0};
BOOST_CHECK(!null_obf);
}
BOOST_AUTO_TEST_CASE(xor_file)
{
fs::path xor_path{m_args.GetDataDirBase() / "test_xor.bin"};
auto raw_file{[&](const auto& mode) { return fsbridge::fopen(xor_path, mode); }};
const std::vector<uint8_t> test1{1, 2, 3};
const std::vector<uint8_t> test2{4, 5};
const std::vector<std::byte> xor_pat{std::byte{0xff}, std::byte{0x00}};
auto key_bytes{"ff00ff00ff00ff00"_hex};
uint64_t xor_key;
std::memcpy(&xor_key, key_bytes.data(), sizeof(xor_key));
{
// Check errors for missing file
AutoFile xor_file{raw_file("rb"), xor_pat};
AutoFile xor_file{raw_file("rb"), {key_bytes}};
BOOST_CHECK_EXCEPTION(xor_file << std::byte{}, std::ios_base::failure, HasReason{"AutoFile::write: file handle is nullpt"});
BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: file handle is nullpt"});
BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullpt"});
@ -37,7 +156,7 @@ BOOST_AUTO_TEST_CASE(xor_file)
#else
const char* mode = "wbx";
#endif
AutoFile xor_file{raw_file(mode), xor_pat};
AutoFile xor_file{raw_file(mode), xor_key};
xor_file << test1 << test2;
}
{
@ -50,7 +169,7 @@ BOOST_AUTO_TEST_CASE(xor_file)
BOOST_CHECK_EXCEPTION(non_xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: end of file"});
}
{
AutoFile xor_file{raw_file("rb"), xor_pat};
AutoFile xor_file{raw_file("rb"), xor_key};
std::vector<std::byte> read1, read2;
xor_file >> read1 >> read2;
BOOST_CHECK_EQUAL(HexStr(read1), HexStr(test1));
@ -59,7 +178,7 @@ BOOST_AUTO_TEST_CASE(xor_file)
BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: end of file"});
}
{
AutoFile xor_file{raw_file("rb"), xor_pat};
AutoFile xor_file{raw_file("rb"), xor_key};
std::vector<std::byte> read2;
// Check that ignore works
xor_file.ignore(4);
@ -75,7 +194,7 @@ BOOST_AUTO_TEST_CASE(streams_vector_writer)
{
unsigned char a(1);
unsigned char b(2);
unsigned char bytes[] = { 3, 4, 5, 6 };
unsigned char bytes[] = {3, 4, 5, 6};
std::vector<unsigned char> vch;
// Each test runs twice. Serializing a second time at the same starting
@ -222,34 +341,26 @@ BOOST_AUTO_TEST_CASE(bitstream_reader_writer)
BOOST_AUTO_TEST_CASE(streams_serializedata_xor)
{
std::vector<std::byte> in;
// Degenerate case
{
DataStream ds{in};
ds.Xor({0x00, 0x00});
DataStream ds{};
Obfuscation{0}(ds);
BOOST_CHECK_EQUAL(""s, ds.str());
}
in.push_back(std::byte{0x0f});
in.push_back(std::byte{0xf0});
// Single character key
{
DataStream ds{in};
ds.Xor({0xff});
const Obfuscation obfuscation{"ffffffffffffffff"_hex};
DataStream ds{"0ff0"_hex};
obfuscation(ds);
BOOST_CHECK_EQUAL("\xf0\x0f"s, ds.str());
}
// Multi character key
in.clear();
in.push_back(std::byte{0xf0});
in.push_back(std::byte{0x0f});
{
DataStream ds{in};
ds.Xor({0xff, 0x0f});
const Obfuscation obfuscation{"ff0fff0fff0fff0f"_hex};
DataStream ds{"f00f"_hex};
obfuscation(ds);
BOOST_CHECK_EQUAL("\x0f\x00"s, ds.str());
}
}
@ -272,7 +383,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
BOOST_CHECK(false);
} catch (const std::exception& e) {
BOOST_CHECK(strstr(e.what(),
"Rewind limit must be less than buffer size") != nullptr);
"Rewind limit must be less than buffer size") != nullptr);
}
// The buffer is 25 bytes, allow rewinding 10 bytes.
@ -361,7 +472,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
BOOST_CHECK(false);
} catch (const std::exception& e) {
BOOST_CHECK(strstr(e.what(),
"BufferedFile::Fill: end of file") != nullptr);
"BufferedFile::Fill: end of file") != nullptr);
}
// Attempting to read beyond the end sets the EOF indicator.
BOOST_CHECK(bf.eof());
@ -562,7 +673,7 @@ BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content)
const FlatFilePos pos{0, 0};
const FlatFileSeq test_file{m_args.GetDataDirBase(), "buffered_file_test_random", node::BLOCKFILE_CHUNK_SIZE};
const std::vector obfuscation{m_rng.randbytes<std::byte>(8)};
const Obfuscation obfuscation{m_rng.rand64()};
// Write out the file with random content
{
@ -615,7 +726,7 @@ BOOST_AUTO_TEST_CASE(buffered_writer_matches_autofile_random_content)
const FlatFileSeq test_buffered{m_args.GetDataDirBase(), "buffered_write_test", node::BLOCKFILE_CHUNK_SIZE};
const FlatFileSeq test_direct{m_args.GetDataDirBase(), "direct_write_test", node::BLOCKFILE_CHUNK_SIZE};
const std::vector obfuscation{m_rng.randbytes<std::byte>(8)};
const Obfuscation obfuscation{m_rng.rand64()};
{
DataBuffer test_data{m_rng.randbytes<std::byte>(file_size)};