|
14 | 14 | #include <boost/test/unit_test.hpp>
|
15 | 15 |
|
16 | 16 | using namespace std::string_literals;
|
| 17 | +using namespace util::hex_literals; |
17 | 18 |
|
18 | 19 | BOOST_FIXTURE_TEST_SUITE(streams_tests, BasicTestingSetup)
|
19 | 20 |
|
| 21 | +// Test that obfuscation can be properly reverted even with random chunk sizes. |
| 22 | +BOOST_AUTO_TEST_CASE(xor_roundtrip_random_chunks) |
| 23 | +{ |
| 24 | + auto apply_random_xor_chunks{[&](std::span<std::byte> target, std::span<const std::byte, Obfuscation::KEY_SIZE> obfuscation) { |
| 25 | + for (size_t offset{0}; offset < target.size();) { |
| 26 | + const size_t chunk_size{1 + m_rng.randrange(target.size() - offset)}; |
| 27 | + util::Xor(target.subspan(offset, chunk_size), obfuscation, offset); |
| 28 | + offset += chunk_size; |
| 29 | + } |
| 30 | + }}; |
| 31 | + |
| 32 | + for (size_t test{0}; test < 100; ++test) { |
| 33 | + const size_t write_size{1 + m_rng.randrange(100U)}; |
| 34 | + const std::vector original{m_rng.randbytes<std::byte>(write_size)}; |
| 35 | + std::vector roundtrip{original}; |
| 36 | + |
| 37 | + const auto key_bytes{m_rng.randbool() ? m_rng.randbytes<Obfuscation::KEY_SIZE>() : std::array<std::byte, Obfuscation::KEY_SIZE>{}}; |
| 38 | + apply_random_xor_chunks(roundtrip, key_bytes); |
| 39 | + |
| 40 | + const bool key_all_zeros{std::ranges::all_of( |
| 41 | + std::span{key_bytes}.first(std::min(write_size, Obfuscation::KEY_SIZE)), [](auto b) { return b == std::byte{0}; })}; |
| 42 | + BOOST_CHECK(key_all_zeros ? original == roundtrip : original != roundtrip); |
| 43 | + |
| 44 | + apply_random_xor_chunks(roundtrip, key_bytes); |
| 45 | + BOOST_CHECK(original == roundtrip); |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +// Compares optimized obfuscation against a trivial, byte-by-byte reference implementation |
| 50 | +// with random offsets to ensure proper handling of key wrapping. |
| 51 | +BOOST_AUTO_TEST_CASE(xor_bytes_reference) |
| 52 | +{ |
| 53 | + auto expected_xor{[](std::span<std::byte> target, std::span<const std::byte, Obfuscation::KEY_SIZE> obfuscation, size_t key_offset) { |
| 54 | + for (auto& b : target) { |
| 55 | + b ^= obfuscation[key_offset++ % obfuscation.size()]; |
| 56 | + } |
| 57 | + }}; |
| 58 | + |
| 59 | + for (size_t test{0}; test < 100; ++test) { |
| 60 | + const size_t write_size{1 + m_rng.randrange(100U)}; |
| 61 | + const size_t key_offset{m_rng.randrange(3 * Obfuscation::KEY_SIZE)}; // Make sure the key can wrap around |
| 62 | + const size_t write_offset{std::min(write_size, m_rng.randrange(Obfuscation::KEY_SIZE * 2))}; // Write unaligned data |
| 63 | + |
| 64 | + const auto key_bytes{m_rng.randbool() ? m_rng.randbytes<Obfuscation::KEY_SIZE>() : std::array<std::byte, Obfuscation::KEY_SIZE>{}}; |
| 65 | + const std::vector obfuscation{key_bytes.begin(), key_bytes.end()}; |
| 66 | + std::vector expected{m_rng.randbytes<std::byte>(write_size)}; |
| 67 | + std::vector actual{expected}; |
| 68 | + |
| 69 | + expected_xor(std::span{expected}.subspan(write_offset), key_bytes, key_offset); |
| 70 | + util::Xor(std::span{actual}.subspan(write_offset), key_bytes, key_offset); |
| 71 | + |
| 72 | + BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), actual.begin(), actual.end()); |
| 73 | + } |
| 74 | +} |
| 75 | + |
20 | 76 | BOOST_AUTO_TEST_CASE(xor_file)
|
21 | 77 | {
|
22 | 78 | fs::path xor_path{m_args.GetDataDirBase() / "test_xor.bin"};
|
23 | 79 | auto raw_file{[&](const auto& mode) { return fsbridge::fopen(xor_path, mode); }};
|
24 | 80 | const std::vector<uint8_t> test1{1, 2, 3};
|
25 | 81 | const std::vector<uint8_t> test2{4, 5};
|
26 |
| - const std::vector<std::byte> xor_pat{std::byte{0xff}, std::byte{0x00}}; |
| 82 | + const auto xor_pat{"ff00ff00ff00ff00"_hex_v}; |
| 83 | + |
27 | 84 | {
|
28 | 85 | // Check errors for missing file
|
29 | 86 | AutoFile xor_file{raw_file("rb"), xor_pat};
|
30 |
| - BOOST_CHECK_EXCEPTION(xor_file << std::byte{}, std::ios_base::failure, HasReason{"AutoFile::write: file handle is nullpt"}); |
31 |
| - BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: file handle is nullpt"}); |
32 |
| - BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullpt"}); |
| 87 | + BOOST_CHECK_EXCEPTION(xor_file << std::byte{}, std::ios_base::failure, HasReason{"AutoFile::write: file handle is nullptr"}); |
| 88 | + BOOST_CHECK_EXCEPTION(xor_file >> std::byte{}, std::ios_base::failure, HasReason{"AutoFile::read: file handle is nullptr"}); |
| 89 | + BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullptr"}); |
33 | 90 | }
|
34 | 91 | {
|
35 | 92 | #ifdef __MINGW64__
|
@@ -77,7 +134,7 @@ BOOST_AUTO_TEST_CASE(streams_vector_writer)
|
77 | 134 | {
|
78 | 135 | unsigned char a(1);
|
79 | 136 | unsigned char b(2);
|
80 |
| - unsigned char bytes[] = { 3, 4, 5, 6 }; |
| 137 | + unsigned char bytes[] = {3, 4, 5, 6}; |
81 | 138 | std::vector<unsigned char> vch;
|
82 | 139 |
|
83 | 140 | // Each test runs twice. Serializing a second time at the same starting
|
@@ -224,34 +281,26 @@ BOOST_AUTO_TEST_CASE(bitstream_reader_writer)
|
224 | 281 |
|
225 | 282 | BOOST_AUTO_TEST_CASE(streams_serializedata_xor)
|
226 | 283 | {
|
227 |
| - std::vector<std::byte> in; |
228 |
| - |
229 | 284 | // Degenerate case
|
230 | 285 | {
|
231 |
| - DataStream ds{in}; |
232 |
| - ds.Xor({0x00, 0x00}); |
| 286 | + DataStream ds{}; |
| 287 | + ds.Xor("0000000000000000"_hex_v_u8); |
233 | 288 | BOOST_CHECK_EQUAL(""s, ds.str());
|
234 | 289 | }
|
235 | 290 |
|
236 |
| - in.push_back(std::byte{0x0f}); |
237 |
| - in.push_back(std::byte{0xf0}); |
238 |
| - |
239 |
| - // Single character key |
240 | 291 | {
|
241 |
| - DataStream ds{in}; |
242 |
| - ds.Xor({0xff}); |
| 292 | + const auto obfuscation{"ffffffffffffffff"_hex_v_u8}; |
| 293 | + |
| 294 | + DataStream ds{"0ff0"_hex}; |
| 295 | + ds.Xor(obfuscation); |
243 | 296 | BOOST_CHECK_EQUAL("\xf0\x0f"s, ds.str());
|
244 | 297 | }
|
245 | 298 |
|
246 |
| - // Multi character key |
247 |
| - |
248 |
| - in.clear(); |
249 |
| - in.push_back(std::byte{0xf0}); |
250 |
| - in.push_back(std::byte{0x0f}); |
251 |
| - |
252 | 299 | {
|
253 |
| - DataStream ds{in}; |
254 |
| - ds.Xor({0xff, 0x0f}); |
| 300 | + const auto obfuscation{"ff0fff0fff0fff0f"_hex_v_u8}; |
| 301 | + |
| 302 | + DataStream ds{"f00f"_hex}; |
| 303 | + ds.Xor(obfuscation); |
255 | 304 | BOOST_CHECK_EQUAL("\x0f\x00"s, ds.str());
|
256 | 305 | }
|
257 | 306 | }
|
@@ -564,7 +613,7 @@ BOOST_AUTO_TEST_CASE(buffered_reader_matches_autofile_random_content)
|
564 | 613 | const FlatFilePos pos{0, 0};
|
565 | 614 |
|
566 | 615 | const FlatFileSeq test_file{m_args.GetDataDirBase(), "buffered_file_test_random", node::BLOCKFILE_CHUNK_SIZE};
|
567 |
| - const std::vector obfuscation{m_rng.randbytes<std::byte>(Obfuscation::KEY_SIZE)}; |
| 616 | + const auto obfuscation{m_rng.randbytes<std::byte>(Obfuscation::KEY_SIZE)}; |
568 | 617 |
|
569 | 618 | // Write out the file with random content
|
570 | 619 | {
|
@@ -619,7 +668,7 @@ BOOST_AUTO_TEST_CASE(buffered_writer_matches_autofile_random_content)
|
619 | 668 |
|
620 | 669 | const FlatFileSeq test_buffered{m_args.GetDataDirBase(), "buffered_write_test", node::BLOCKFILE_CHUNK_SIZE};
|
621 | 670 | const FlatFileSeq test_direct{m_args.GetDataDirBase(), "direct_write_test", node::BLOCKFILE_CHUNK_SIZE};
|
622 |
| - const std::vector obfuscation{m_rng.randbytes<std::byte>(Obfuscation::KEY_SIZE)}; |
| 671 | + const auto obfuscation{m_rng.randbytes<std::byte>(Obfuscation::KEY_SIZE)}; |
623 | 672 |
|
624 | 673 | {
|
625 | 674 | DataBuffer test_data{m_rng.randbytes<std::byte>(file_size)};
|
|
0 commit comments