diff --git a/support/src/utils/encryption.cpp b/support/src/utils/encryption.cpp index f7b281bd..96990b08 100644 --- a/support/src/utils/encryption.cpp +++ b/support/src/utils/encryption.cpp @@ -140,14 +140,51 @@ auto decrypt_file_name(std::string_view encryption_token, const kdf_config &cfg, file_name); } +constexpr auto resize_by(std::span &data, std::size_t size) + -> std::span & { + return data; +} + +static auto resize_by(data_buffer &data, std::size_t size) -> data_buffer & { + data.resize(data.size() + size); + return data; +} + template -auto read_encrypted_range(const http_range &range, - const utils::hash::hash_256_t &key, - reader_func_t reader_func, std::uint64_t total_size, - data_t &data, std::uint8_t file_header_size, - std::size_t &bytes_read) -> bool { +[[nodiscard]] auto +read_encrypted_range(http_range range, const utils::hash::hash_256_t &key, + reader_func_t reader_func, std::uint64_t total_size, + data_t &data, std::uint8_t file_header_size, + std::size_t &bytes_read) -> bool { bytes_read = 0U; + { + if (total_size == 0U) { + return true; + } + + std::uint64_t begin = range.begin; + std::uint64_t end = range.end; + + if (begin >= total_size) { + return true; + } + + std::uint64_t last = total_size - 1U; + if (end > last) { + end = last; + } + + if (end < begin) { + return true; + } + + range = http_range{ + .begin = begin, + .end = end, + }; + } + auto encrypted_chunk_size = utils::encryption::encrypting_reader::get_encrypted_chunk_size(); auto data_chunk_size = @@ -159,25 +196,26 @@ auto read_encrypted_range(const http_range &range, auto source_offset = static_cast(range.begin % data_chunk_size); for (std::size_t chunk = start_chunk; chunk <= end_chunk; chunk++) { - data_buffer cypher; + data_buffer cipher; auto start_offset = (chunk * encrypted_chunk_size) + file_header_size; auto end_offset = std::min( start_offset + (total_size - (chunk * data_chunk_size)) + encryption_header_size - 1U, static_cast(start_offset + encrypted_chunk_size - 1U)); - if (not reader_func(cypher, start_offset, end_offset)) { + if (not reader_func(cipher, start_offset, end_offset)) { return false; } data_buffer source_buffer; - if (not utils::encryption::decrypt_data(key, cypher, source_buffer)) { + if (not utils::encryption::decrypt_data(key, cipher, source_buffer)) { return false; } - cypher.clear(); + cipher.clear(); auto data_size = static_cast(std::min( remain, static_cast(data_chunk_size - source_offset))); + resize_by(data, data_size); std::copy(std::next(source_buffer.begin(), static_cast(source_offset)), std::next(source_buffer.begin(), diff --git a/support/test/src/utils/encryption_read_encrypted_range_test.cpp b/support/test/src/utils/encryption_read_encrypted_range_test.cpp new file mode 100644 index 00000000..3cae2289 --- /dev/null +++ b/support/test/src/utils/encryption_read_encrypted_range_test.cpp @@ -0,0 +1,445 @@ +/* + Copyright <2018-2025> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include "test.hpp" + +#if defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST) + +namespace { +[[nodiscard]] auto make_random_plain(std::size_t size) + -> std::vector { + std::vector ret; + ret.resize(size); + + constexpr std::size_t chunk_size = 4096U; + using buf_t = std::array; + + std::size_t written = 0U; + while (written < size) { + auto block = repertory::utils::generate_secure_random(); + auto to_copy = std::min(chunk_size, size - written); + std::memcpy(ret.data() + written, block.data(), to_copy); + written += to_copy; + } + + return ret; +} + +[[nodiscard]] auto +build_encrypted_blob(const std::vector &plain, + const repertory::utils::hash::hash_256_t &key, + bool with_kdf, + repertory::utils::encryption::kdf_config &kdf) + -> std::pair { + repertory::data_buffer blob; + + if (with_kdf) { + auto hdr = kdf.to_header(); + blob.insert(blob.end(), hdr.begin(), hdr.end()); + } + + auto data_chunk = + repertory::utils::encryption::encrypting_reader::get_data_chunk_size(); + std::size_t offset = 0U; + + while (offset < plain.size()) { + auto take = std::min(data_chunk, plain.size() - offset); + repertory::data_buffer buffer; + repertory::utils::encryption::encrypt_data(key, plain.data() + offset, take, + buffer); + blob.insert(blob.end(), buffer.begin(), buffer.end()); + offset += take; + } + + return {std::move(blob), static_cast(plain.size())}; +} + +[[nodiscard]] auto make_reader(const repertory::data_buffer &cipher_blob) + -> repertory::utils::encryption::reader_func_t { + return [&cipher_blob](repertory::data_buffer &out, std::uint64_t start, + std::uint64_t end) -> bool { + if (end < start) { + return false; + } + + if (end >= static_cast(cipher_blob.size())) { + return false; + } + + auto len = static_cast(end - start + 1U); + out.assign( + std::next(cipher_blob.begin(), static_cast(start)), + std::next(cipher_blob.begin(), + static_cast(start + len))); + return true; + }; +} +} // namespace + +namespace repertory { +class utils_encryption_read_encrypted_range_fixture + : public ::testing::Test, + public ::testing::WithParamInterface { +protected: + bool uses_kdf{}; + utils::hash::hash_256_t key{}; + utils::encryption::kdf_config kdf{}; + std::size_t chunk{}; + std::size_t plain_sz{}; + std::vector plain; + data_buffer cipher_blob; + std::uint64_t total_size{}; + utils::encryption::reader_func_t reader; + + void SetUp() override { + uses_kdf = GetParam(); + + key = + uses_kdf + ? utils::encryption::generate_key("moose", + kdf) + : utils::encryption::generate_key("moose"); + chunk = utils::encryption::encrypting_reader::get_data_chunk_size(); + + plain_sz = (2U * chunk) + (chunk / 2U); + + plain = make_random_plain(plain_sz); + std::tie(cipher_blob, total_size) = + build_encrypted_blob(plain, key, uses_kdf, kdf); + reader = make_reader(cipher_blob); + } +}; + +TEST_P(utils_encryption_read_encrypted_range_fixture, + within_chunk_data_buffer) { + std::uint64_t end_cap = chunk ? static_cast(chunk) - 1U : 0U; + std::uint64_t begin = 123U; + std::uint64_t end = 4567U; + if (end > end_cap) { + end = end_cap; + } + + if (end < begin) { + begin = end; + } + + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want(plain.begin() + begin, + plain.begin() + end + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + cross_chunk_boundary_data_buffer) { + std::uint64_t begin = static_cast(chunk) - 512U; + std::uint64_t end = begin + 1024U - 1U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want(plain.begin() + begin, + plain.begin() + end + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + multi_chunk_span_data_buffer) { + std::uint64_t begin = static_cast(chunk) - 10U; + std::uint64_t end = static_cast(2U * chunk) + 19U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want(plain.begin() + begin, + plain.begin() + end + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + tail_of_file_data_buffer) { + std::uint64_t begin = static_cast(plain_sz) - 200U; + std::uint64_t end = static_cast(plain_sz) - 1U; + ASSERT_GE(end, begin); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::vector want(plain.begin() + begin, + plain.begin() + end + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, whole_file_data_buffer) { + std::uint64_t begin = 0U; + std::uint64_t end = static_cast(plain_sz - 1U); + ASSERT_GE(end, begin); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + EXPECT_EQ(out, plain); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + pointer_sink_cross_chunk_with_array) { + std::uint64_t begin = static_cast(chunk) - 256U; + constexpr std::size_t data_len = 2048U; + std::uint64_t end = begin + data_len - 1U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + + http_range range{begin, end}; + + std::array sink{}; + std::size_t bytes_read = 0U; + + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, sink.data(), sink.size(), + bytes_read)); + EXPECT_EQ(bytes_read, sink.size()); + + std::vector want(plain.begin() + begin, + plain.begin() + end + 1U); + EXPECT_TRUE(std::equal(sink.begin(), sink.end(), want.begin(), want.end())); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + reader_failure_for_both_overloads) { + std::size_t call_count = 0U; + auto flaky_reader = [this, &call_count](data_buffer &out, std::uint64_t start, + std::uint64_t end) -> bool { + if (++call_count == 1U) { + return false; + } + auto len = static_cast(end - start + 1U); + out.assign( + std::next(cipher_blob.begin(), static_cast(start)), + std::next(cipher_blob.begin(), + static_cast(start + len))); + return true; + }; + + std::uint64_t begin = 0U; + constexpr std::size_t data_len = 1024U; + std::uint64_t end = begin + data_len - 1U; + http_range range{begin, end}; + + { + data_buffer out; + EXPECT_FALSE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, flaky_reader, total_size, out)); + EXPECT_TRUE(out.empty()); + } + + call_count = 0U; + { + std::array buf{}; + std::size_t bytes_read = 0U; + EXPECT_FALSE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, flaky_reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 0U); + } +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + invalid_range_end_before_begin) { + std::uint64_t begin = 100U; + std::uint64_t end = 99U; + http_range range{begin, end}; + + { + data_buffer out; + EXPECT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, out)); + EXPECT_TRUE(out.empty()); + } + + { + std::array buf{}; + std::size_t bytes_read = 0U; + EXPECT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 0U); + } +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, single_byte_read) { + std::uint64_t pos = 777U; + if (pos >= plain_sz) { + pos = plain_sz ? static_cast(plain_sz) - 1U : 0U; + } + + http_range range{pos, pos}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + ASSERT_EQ(out.size(), 1U); + EXPECT_EQ(out[0], plain[pos]); + + std::array buf{}; + std::size_t bytes_read = 0U; + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 1U); + EXPECT_EQ(buf[0], plain[pos]); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + begin_at_exact_chunk_boundary) { + if (chunk == 0U) { + GTEST_SKIP() << "chunk size is zero (unexpected)"; + } + + std::uint64_t begin = static_cast(chunk); + std::uint64_t end = begin + 1024U - 1U; + if (end >= plain_sz) + end = (std::uint64_t)plain_sz - 1U; + ASSERT_GE(end, begin); + + http_range range{begin, end}; + data_buffer out; + + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + std::vector want(plain.begin() + begin, + plain.begin() + end + 1U); + EXPECT_EQ(out, want); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, last_byte_only) { + std::uint64_t pos = plain_sz ? static_cast(plain_sz) - 1U : 0U; + http_range range{pos, pos}; + data_buffer out; + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + ASSERT_EQ(out.size(), 1U); + EXPECT_EQ(out[0], plain[pos]); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, tiny_file_whole_read) { + plain = make_random_plain(37U); + std::tie(cipher_blob, total_size) = + build_encrypted_blob(plain, key, uses_kdf, kdf); + reader = make_reader(cipher_blob); + + http_range range{0U, static_cast(plain.size() - 1U)}; + data_buffer out; + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + EXPECT_EQ(out, plain); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + pointer_sink_exact_small_window) { + std::uint64_t begin = 5U; + std::uint64_t end = begin + 7U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + http_range range{begin, end}; + + std::array sink{}; + std::size_t bytes_read = 0U; + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, sink.data(), sink.size(), + bytes_read)); + EXPECT_EQ(bytes_read, sink.size()); + EXPECT_TRUE(std::equal(sink.begin(), sink.end(), plain.begin() + begin)); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + range_past_eof_truncates) { + std::uint64_t begin = static_cast(plain_sz) - 10U; + std::uint64_t end = static_cast(plain_sz); + http_range range{begin, end}; + + data_buffer out; + ASSERT_TRUE(utils::encryption::read_encrypted_range(range, key, uses_kdf, + reader, total_size, out)); + + std::size_t expected_len = + static_cast(static_cast(plain_sz) - begin); + std::vector want(plain.begin() + begin, + plain.begin() + plain_sz); + ASSERT_EQ(out.size(), expected_len); + EXPECT_EQ(out, want); + + std::array buf{}; + std::size_t bytes_read = 0U; + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, std::min(buf.size(), expected_len)); + EXPECT_TRUE( + std::equal(buf.begin(), buf.begin() + bytes_read, plain.begin() + begin)); +} + +TEST_P(utils_encryption_read_encrypted_range_fixture, + pointer_sink_larger_buffer) { + std::uint64_t begin = 42U; + std::uint64_t end = begin + 63U; + ASSERT_GE(end, begin); + ASSERT_LT(end, plain_sz); + http_range range{begin, end}; + + std::array buf{}; + std::size_t bytes_read = 0U; + + ASSERT_TRUE(utils::encryption::read_encrypted_range( + range, key, uses_kdf, reader, total_size, buf.data(), buf.size(), + bytes_read)); + EXPECT_EQ(bytes_read, 64U); + EXPECT_TRUE( + std::equal(buf.begin(), buf.begin() + 64U, plain.begin() + begin)); +} + +INSTANTIATE_TEST_SUITE_P(no_kdf_and_kdf, + utils_encryption_read_encrypted_range_fixture, + ::testing::Values(false, true)); +} // namespace repertory + +#endif // defined(PROJECT_ENABLE_LIBSODIUM) && defined(PROJECT_ENABLE_BOOST)