test: compare util::Xor with randomized inputs against simple impl

Since production code only uses keys of length 8, we're not testing with other values anymore
This commit is contained in:
Lőrinc 2024-10-24 08:37:57 +02:00
parent 971952588d
commit b9c54ccd8c
2 changed files with 124 additions and 36 deletions

View file

@ -30,18 +30,51 @@ 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)));
std::vector<uint8_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(obfuscate != is_null_key(dbwrapper_private::GetObfuscateKey(dbw)));
obfuscation_key = dbwrapper_private::GetObfuscateKey(dbw);
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(obfuscation_key == dbwrapper_private::GetObfuscateKey(dbw));
}
// Read back the values
{
CDBWrapper dbw{{.path = path, .cache_bytes = CACHE_SIZE, .obfuscate = obfuscate}};
// Ensure obfuscation is read back correctly
BOOST_CHECK(obfuscate != is_null_key(dbwrapper_private::GetObfuscateKey(dbw)));
BOOST_CHECK(obfuscation_key == dbwrapper_private::GetObfuscateKey(dbw));
// 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());
}
}
}
}

View file

@ -13,19 +13,82 @@
#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 std::span<std::byte> obfuscation) {
for (size_t offset{0}; offset < target.size();) {
const size_t chunk_size{1 + m_rng.randrange(target.size() - offset)};
util::Xor(target.subspan(offset, chunk_size), obfuscation, 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};
auto key_bytes{m_rng.randbool() ? std::vector(sizeof(uint64_t), std::byte{0}) : m_rng.randbytes<std::byte>(sizeof(uint64_t))};
uint64_t obfuscation;
std::memcpy(&obfuscation, key_bytes.data(), sizeof(obfuscation));
apply_random_xor_chunks(roundtrip, key_bytes);
// Verify intermediate state differs from original unless the key is all zeros
const bool all_zeros{(obfuscation == 0) || std::ranges::all_of(
std::span{key_bytes}.first(std::min(write_size, key_bytes.size())), [](auto b) { return b == std::byte{0}; })};
BOOST_CHECK_EQUAL(original != roundtrip, !all_zeros);
apply_random_xor_chunks(roundtrip, key_bytes);
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
std::vector key_bytes{m_rng.randbytes<std::byte>(sizeof(uint64_t))};
uint64_t obfuscation;
std::memcpy(&obfuscation, key_bytes.data(), sizeof(obfuscation));
std::vector expected{m_rng.randbytes<std::byte>(write_size)};
std::vector actual{expected};
expected_xor(expected, key_bytes, key_offset);
util::Xor(actual, key_bytes, key_offset);
BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), actual.begin(), actual.end());
}
}
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_v};
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 +100,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), key_bytes};
xor_file << test1 << test2;
}
{
@ -50,7 +113,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"), key_bytes};
std::vector<std::byte> read1, read2;
xor_file >> read1 >> read2;
BOOST_CHECK_EQUAL(HexStr(read1), HexStr(test1));
@ -59,7 +122,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"), key_bytes};
std::vector<std::byte> read2;
// Check that ignore works
xor_file.ignore(4);
@ -75,7 +138,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 +285,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{};
ds.Xor("0000000000000000"_hex_v_u8);
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 auto key_bytes{"ffffffffffffffff"_hex_v_u8};
DataStream ds{"0ff0"_hex};
ds.Xor(key_bytes);
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 auto key_bytes{"ff0fff0fff0fff0f"_hex_v_u8};
DataStream ds{"f00f"_hex};
ds.Xor(key_bytes);
BOOST_CHECK_EQUAL("\x0f\x00"s, ds.str());
}
}
@ -272,7 +327,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 +416,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());