diff --git a/src/client/dbps_api_client.cpp b/src/client/dbps_api_client.cpp index 88a0b4f..52b97f8 100644 --- a/src/client/dbps_api_client.cpp +++ b/src/client/dbps_api_client.cpp @@ -270,7 +270,8 @@ DecryptApiResponse DBPSApiClient::Decrypt( CompressionCodec::type encrypted_compression, const std::string& key_id, const std::string& user_id, - const std::string& application_context + const std::string& application_context, + const std::map& encryption_metadata ) { DecryptJsonRequest json_request; json_request.column_name_ = column_name; @@ -283,6 +284,7 @@ DecryptApiResponse DBPSApiClient::Decrypt( json_request.key_id_ = key_id; json_request.user_id_ = user_id; json_request.application_context_ = application_context; + json_request.encryption_metadata_ = encryption_metadata; json_request.reference_id_ = GenerateReferenceId(); DecryptApiResponse api_response; diff --git a/src/client/dbps_api_client.h b/src/client/dbps_api_client.h index 1e3f755..109437d 100644 --- a/src/client/dbps_api_client.h +++ b/src/client/dbps_api_client.h @@ -218,7 +218,8 @@ class DBPSApiClient { CompressionCodec::type encrypted_compression, const std::string& key_id, const std::string& user_id, - const std::string& application_context + const std::string& application_context, + const std::map& encryption_metadata ); private: diff --git a/src/client/dbps_api_client_test.cpp b/src/client/dbps_api_client_test.cpp index 56f780b..dc3354f 100644 --- a/src/client/dbps_api_client_test.cpp +++ b/src/client/dbps_api_client_test.cpp @@ -365,6 +365,7 @@ TEST(DBPSApiClient, DecryptWithValidData) { "encryption": {"key_id": "test_key_123"}, "access": {"user_id": "test_user_456"}, "application_context": "{\"user_id\": \"test_user_456\"}", + "encryption_metadata": {}, "debug": {"reference_id": "1755831549871"} })"; @@ -410,7 +411,8 @@ TEST(DBPSApiClient, DecryptWithValidData) { CompressionCodec::UNCOMPRESSED, // encrypted_compression "test_key_123", // key_id "test_user_456", // user_id - "{\"user_id\": \"test_user_456\"}" // application_context + "{\"user_id\": \"test_user_456\"}", // application_context + std::map{} // encryption_metadata ); // Verify the response @@ -486,7 +488,8 @@ TEST(DBPSApiClient, DecryptWithInvalidData) { CompressionCodec::UNCOMPRESSED, // encrypted_compression "test_key_123", // key_id "test_user_456", // user_id - "{\"user_id\": \"test_user_456\"}" // application_context + "{\"user_id\": \"test_user_456\"}", // application_context + std::map{} // encryption_metadata ); // Verify the response indicates failure @@ -581,6 +584,7 @@ TEST(DBPSApiClient, DecryptWithInvalidJsonResponse) { "encryption": {"key_id": "test_key_123"}, "access": {"user_id": "test_user_456"}, "application_context": "{\"user_id\": \"test_user_456\"}", + "encryption_metadata": {}, "debug": {"reference_id": "1755831549871"} })"; @@ -616,7 +620,8 @@ TEST(DBPSApiClient, DecryptWithInvalidJsonResponse) { CompressionCodec::UNCOMPRESSED, // encrypted_compression "test_key_123", // key_id "test_user_456", // user_id - "{\"user_id\": \"test_user_456\"}" // application_context + "{\"user_id\": \"test_user_456\"}", // application_context + std::map{} // encryption_metadata ); // Verify the response indicates failure diff --git a/src/common/dbpa_interface.h b/src/common/dbpa_interface.h index 09d1121..3c2b261 100644 --- a/src/common/dbpa_interface.h +++ b/src/common/dbpa_interface.h @@ -63,10 +63,10 @@ class DBPS_EXPORT EncryptionResult { // Success flag; false indicates an error. virtual bool success() const = 0; - //TODO: revisit the default implementation. - virtual const std::optional> encryption_metadata() const { - return std::nullopt; - } + // Encryption metadata (valid when success() == true) + // Map of string key-value pairs containing any extra parameters used during encryption that are needed for decryption, + // for example, the encryption_algorithm_version used. + virtual const std::optional> encryption_metadata() const = 0; // Error details (valid when success() == false). virtual const std::string& error_message() const = 0; @@ -137,7 +137,12 @@ class DBPS_EXPORT DataBatchProtectionAgentInterface { span ciphertext, std::map encoding_attributes) = 0; - virtual const std::optional> EncryptionMetadata() const { + /* Returns the encryption metadata provided during the class init() call. + * The encryption metadata is a map of string key-value pairs and is defined only for Decrypt usage. + * This metadata map is the one returned by the EncryptionResult.encryption_metadata() during the Encrypt call and indicates + * any extra parameters used during encryption that are needed for decryption, for example, the encryption_algorithm_version used. + */ + virtual const std::optional> EncryptionMetadata() const { return column_encryption_metadata_; } diff --git a/src/common/dbpa_interface_test.cpp b/src/common/dbpa_interface_test.cpp index 5a59cb0..585d007 100644 --- a/src/common/dbpa_interface_test.cpp +++ b/src/common/dbpa_interface_test.cpp @@ -40,6 +40,7 @@ class MockEncryptionResult : public EncryptionResult { std::size_t size() const override { return data_.size(); } bool success() const override { return success_; } + const std::optional> encryption_metadata() const override { return std::nullopt; } const std::string& error_message() const override { static std::string empty; return empty; } const std::map& error_fields() const override { static std::map empty; return empty; } }; diff --git a/src/common/dbpa_local.cpp b/src/common/dbpa_local.cpp index fa3758b..289f07d 100644 --- a/src/common/dbpa_local.cpp +++ b/src/common/dbpa_local.cpp @@ -27,8 +27,10 @@ using namespace dbps::enum_utils; // LocalEncryptionResult implementation -LocalEncryptionResult::LocalEncryptionResult(std::vector ciphertext) - : ciphertext_(std::move(ciphertext)), success_(true) { +LocalEncryptionResult::LocalEncryptionResult(std::vector ciphertext, const std::map& encryption_metadata) + : ciphertext_(std::move(ciphertext)), + success_(true), + encryption_metadata_(encryption_metadata) { } LocalEncryptionResult::LocalEncryptionResult(const std::string& error_stage, const std::string& error_message) @@ -55,6 +57,10 @@ bool LocalEncryptionResult::success() const { return success_; } +const std::optional> LocalEncryptionResult::encryption_metadata() const { + return encryption_metadata_.empty() ? std::nullopt : std::optional{encryption_metadata_}; +} + const std::string& LocalEncryptionResult::error_message() const { return error_message_; } @@ -191,7 +197,8 @@ std::unique_ptr LocalDataBatchProtectionAgent::Encrypt( compression_type_, column_key_id_, user_id_, - app_context_ + app_context_, + {} // encryption_metadata, which is empty for the Encryption call. ); // Convert plaintext span to vector for the sequencer @@ -205,8 +212,8 @@ std::unique_ptr LocalDataBatchProtectionAgent::Encrypt( return std::make_unique(sequencer.error_stage_, sequencer.error_message_); } - // Return successful result with encrypted data - return std::make_unique(std::move(sequencer.encrypted_result_)); + // Return successful result with encrypted data and encryption_metadata + return std::make_unique(std::move(sequencer.encrypted_result_), sequencer.encryption_metadata_); } std::unique_ptr LocalDataBatchProtectionAgent::Decrypt( @@ -241,7 +248,8 @@ std::unique_ptr LocalDataBatchProtectionAgent::Decrypt( compression_type_, column_key_id_, user_id_, - app_context_ + app_context_, + column_encryption_metadata_.value_or(std::map{}) ); // Convert ciphertext span to vector for the sequencer diff --git a/src/common/dbpa_local.h b/src/common/dbpa_local.h index a5efed1..8fea72f 100644 --- a/src/common/dbpa_local.h +++ b/src/common/dbpa_local.h @@ -41,7 +41,7 @@ namespace dbps::external { class DBPS_EXPORT LocalEncryptionResult : public EncryptionResult { public: // Constructor for successful encryption - LocalEncryptionResult(std::vector ciphertext); + LocalEncryptionResult(std::vector ciphertext, const std::map& encryption_metadata = {}); // Constructor for failed encryption LocalEncryptionResult(const std::string& error_stage, const std::string& error_message); @@ -50,6 +50,7 @@ class DBPS_EXPORT LocalEncryptionResult : public EncryptionResult { span ciphertext() const override; std::size_t size() const override; bool success() const override; + const std::optional> encryption_metadata() const override; const std::string& error_message() const override; const std::map& error_fields() const override; @@ -58,6 +59,7 @@ class DBPS_EXPORT LocalEncryptionResult : public EncryptionResult { private: std::vector ciphertext_; bool success_; + std::map encryption_metadata_; std::string error_message_; std::map error_fields_; }; diff --git a/src/common/dbpa_local_test.cpp b/src/common/dbpa_local_test.cpp index 74e1dae..7942d04 100644 --- a/src/common/dbpa_local_test.cpp +++ b/src/common/dbpa_local_test.cpp @@ -22,6 +22,11 @@ using namespace dbps::external; +namespace { + // should rename to DBPS_ENCRYPTION_METADATA + const std::map DBPS_ENCRYPTION_METADATA = {{"dbps_agent_version", "v0.01_unittest"}}; +} + // Test fixture for LocalDataBatchProtectionAgent tests class LocalDataBatchProtectionAgentTest : public ::testing::Test { protected: @@ -81,7 +86,7 @@ TEST_F(LocalDataBatchProtectionAgentTest, SuccessfulDecryption) { std::string app_context = R"({"user_id": "test_user"})"; EXPECT_NO_THROW(agent.init("test_column", connection_config, app_context, "test_key", - Type::UNDEFINED, std::nullopt, CompressionCodec::UNCOMPRESSED, std::nullopt)); + Type::UNDEFINED, std::nullopt, CompressionCodec::UNCOMPRESSED, DBPS_ENCRYPTION_METADATA)); std::vector test_data = {1, 2, 3, 4}; std::map encoding_attributes = {{"page_encoding", "PLAIN"}, {"page_type", "DICTIONARY_PAGE"}}; @@ -92,6 +97,56 @@ TEST_F(LocalDataBatchProtectionAgentTest, SuccessfulDecryption) { EXPECT_GT(result->size(), 0); } +// Test roundtrip encryption/decryption +TEST_F(LocalDataBatchProtectionAgentTest, RoundTripEncryptDecrypt) { + LocalDataBatchProtectionAgent encrypt_agent; + + std::map connection_config; + std::string app_context = R"({"user_id": "test_user"})"; + + EXPECT_NO_THROW(encrypt_agent.init("test_column", connection_config, app_context, "test_key", + Type::UNDEFINED, std::nullopt, CompressionCodec::UNCOMPRESSED, std::nullopt)); + + // Original data to encrypt + std::vector original_data = {1, 2, 3, 4, 5}; + std::map encoding_attributes = {{"page_encoding", "PLAIN"}, {"page_type", "DICTIONARY_PAGE"}}; + + // Encrypt the data + auto encrypt_result = encrypt_agent.Encrypt(original_data, encoding_attributes); + + ASSERT_NE(encrypt_result, nullptr); + ASSERT_TRUE(encrypt_result->success()); + EXPECT_GT(encrypt_result->size(), 0); + + // Verify encryption_metadata is present in the result + auto encryption_metadata = encrypt_result->encryption_metadata(); + ASSERT_TRUE(encryption_metadata.has_value()); + ASSERT_EQ(1, encryption_metadata->size()); + ASSERT_TRUE(encryption_metadata->at("dbps_agent_version").length() > 0); // Non-empty string + + // Get the ciphertext + auto ciphertext_span = encrypt_result->ciphertext(); + std::vector ciphertext(ciphertext_span.begin(), ciphertext_span.end()); + + // Create a new agent for decryption with the encryption_metadata from the encryption result + LocalDataBatchProtectionAgent decrypt_agent; + EXPECT_NO_THROW(decrypt_agent.init("test_column", connection_config, app_context, "test_key", + Type::UNDEFINED, std::nullopt, CompressionCodec::UNCOMPRESSED, encryption_metadata)); + + // Decrypt the ciphertext + auto decrypt_result = decrypt_agent.Decrypt(ciphertext, encoding_attributes); + + ASSERT_NE(decrypt_result, nullptr); + ASSERT_TRUE(decrypt_result->success()); + + // Verify the decrypted data matches the original + auto plaintext_span = decrypt_result->plaintext(); + std::vector decrypted_data(plaintext_span.begin(), plaintext_span.end()); + + ASSERT_EQ(original_data.size(), decrypted_data.size()); + EXPECT_EQ(original_data, decrypted_data); +} + // Test encryption without initialization TEST_F(LocalDataBatchProtectionAgentTest, EncryptWithoutInit) { LocalDataBatchProtectionAgent agent; diff --git a/src/common/dbpa_remote.cpp b/src/common/dbpa_remote.cpp index eac68f0..c125bcb 100644 --- a/src/common/dbpa_remote.cpp +++ b/src/common/dbpa_remote.cpp @@ -47,24 +47,39 @@ bool RemoteEncryptionResult::success() const { return response_ && response_->Success(); } +const std::optional> RemoteEncryptionResult::encryption_metadata() const { + if (!parsed_encryption_metadata_.has_value() && response_ && response_->Success()) { + const auto& response_attrs = response_->GetResponseAttributes(); + // For the RemoteEncryptionResult, encryption_metadata_ is forwarded from the API response. + // The attribute in the API response is also named encryption_metadata. + // If the API reponse is empty, the parsed value is set to NULL for compatibility with the class interface. + if (!response_attrs.encryption_metadata_.empty()) { + parsed_encryption_metadata_ = response_attrs.encryption_metadata_; + } else { + parsed_encryption_metadata_ = std::nullopt; + } + } + return parsed_encryption_metadata_; +} + const std::string& RemoteEncryptionResult::error_message() const { - if (cached_error_message_.empty() && response_) { + if (parsed_error_message_.empty() && response_) { if (!response_->Success()) { - cached_error_message_ = response_->ErrorMessage(); + parsed_error_message_ = response_->ErrorMessage(); } else { - cached_error_message_ = "Successful encryption"; + parsed_error_message_ = "Successful encryption"; } } - return cached_error_message_; + return parsed_error_message_; } const std::map& RemoteEncryptionResult::error_fields() const { - if (cached_error_fields_.empty() && response_) { + if (parsed_error_fields_.empty() && response_) { if (!response_->Success()) { - cached_error_fields_ = response_->ErrorFields(); + parsed_error_fields_ = response_->ErrorFields(); } } - return cached_error_fields_; + return parsed_error_fields_; } RemoteDecryptionResult::RemoteDecryptionResult(std::unique_ptr response) @@ -90,23 +105,23 @@ bool RemoteDecryptionResult::success() const { } const std::string& RemoteDecryptionResult::error_message() const { - if (cached_error_message_.empty() && response_) { + if (parsed_error_message_.empty() && response_) { if (!response_->Success()) { - cached_error_message_ = response_->ErrorMessage(); + parsed_error_message_ = response_->ErrorMessage(); } else { - cached_error_message_ = "Successful decryption"; + parsed_error_message_ = "Successful decryption"; } } - return cached_error_message_; + return parsed_error_message_; } const std::map& RemoteDecryptionResult::error_fields() const { - if (cached_error_fields_.empty() && response_) { + if (parsed_error_fields_.empty() && response_) { if (!response_->Success()) { - cached_error_fields_ = response_->ErrorFields(); + parsed_error_fields_ = response_->ErrorFields(); } } - return cached_error_fields_; + return parsed_error_fields_; } // Helper functions for validating that fields of the request <> response match. @@ -318,7 +333,9 @@ std::unique_ptr RemoteDataBatchProtectionAgent::Decrypt(span{}) ); // Validate that response fields match request fields diff --git a/src/common/dbpa_remote.h b/src/common/dbpa_remote.h index 3267ccf..6849b79 100644 --- a/src/common/dbpa_remote.h +++ b/src/common/dbpa_remote.h @@ -49,6 +49,7 @@ class DBPS_EXPORT RemoteEncryptionResult : public EncryptionResult { span ciphertext() const override; std::size_t size() const override; bool success() const override; + const std::optional> encryption_metadata() const override; const std::string& error_message() const override; const std::map& error_fields() const override; @@ -57,11 +58,12 @@ class DBPS_EXPORT RemoteEncryptionResult : public EncryptionResult { private: std::unique_ptr response_; - // Cache error message/fields to avoid repeated parsing of API response. + // Stored error message/fields/encryption_metadata to avoid repeated parsing of API response. // Defined as mutable because it has lazy evaluation (updated only once on the getter methods) // The caching of this is possible because the API response `response_` doesn't change after construction. - mutable std::string cached_error_message_; - mutable std::map cached_error_fields_; + mutable std::string parsed_error_message_; + mutable std::map parsed_error_fields_; + mutable std::optional> parsed_encryption_metadata_; }; /** @@ -84,10 +86,10 @@ class DBPS_EXPORT RemoteDecryptionResult : public DecryptionResult { private: std::unique_ptr response_; - // Cached error message/fields to avoid repeated parsing of API response. + // Stored error message/fields to avoid repeated parsing of API response. // Defined as mutable because it has lazy evaluation (updated only once on the getter methods) - mutable std::string cached_error_message_; - mutable std::map cached_error_fields_; + mutable std::string parsed_error_message_; + mutable std::map parsed_error_fields_; }; /** diff --git a/src/common/json_request.cpp b/src/common/json_request.cpp index e8e47be..3a9b974 100644 --- a/src/common/json_request.cpp +++ b/src/common/json_request.cpp @@ -65,6 +65,16 @@ std::optional SafeParseToInt(const std::string& str) { } } +// Helper function to safely load JSON body and return nullopt if invalid or null +static std::optional SafeLoadJsonBody(const std::string& json_string) { + auto json_body = crow::json::load(json_string); + if (!json_body || json_body.t() == crow::json::type::Null) { + CROW_LOG_ERROR << "Invalid JSON body: " << json_string; + return std::nullopt; + } + return json_body; +} + // Helper function to build validation error message from missing fields static std::string BuildValidationError(const std::vector& missing_fields) { if (missing_fields.empty()) { @@ -122,9 +132,9 @@ std::string EncodeBase64Safe(const std::vector& data) { // JsonRequest implementation void JsonRequest::ParseCommon(const std::string& request_body) { // Load and validate JSON first - auto json_body = crow::json::load(request_body); - - if (!json_body) return; // Stop parsing if JSON is invalid + auto json_body_opt = SafeLoadJsonBody(request_body); + if (!json_body_opt) return; // Stop parsing if JSON is invalid + auto json_body = *json_body_opt; // Extract common required fields if (auto parsed_value = SafeGetFromJsonPath(json_body, {"column_reference", "name"})) { @@ -243,8 +253,9 @@ void EncryptJsonRequest::Parse(const std::string& request_body) { ParseCommon(request_body); // Load JSON for encrypt-specific fields - auto json_body = crow::json::load(request_body); - if (!json_body) return; + auto json_body_opt = SafeLoadJsonBody(request_body); + if (!json_body_opt) return; + auto json_body = *json_body_opt; // Extract encrypt-specific fields if (auto parsed_value = SafeGetFromJsonPath(json_body, {"data_batch", "value"})) { @@ -342,8 +353,9 @@ void DecryptJsonRequest::Parse(const std::string& request_body) { ParseCommon(request_body); // Load JSON for decrypt-specific fields - auto json_body = crow::json::load(request_body); - if (!json_body) return; + auto json_body_opt = SafeLoadJsonBody(request_body); + if (!json_body_opt) return; + auto json_body = *json_body_opt; // Extract decrypt-specific fields if (auto parsed_value = SafeGetFromJsonPath(json_body, {"data_batch_encrypted", "value"})) { @@ -351,6 +363,14 @@ void DecryptJsonRequest::Parse(const std::string& request_body) { encrypted_value_ = *decoded_value; } } + + if (json_body.has("encryption_metadata") && json_body["encryption_metadata"].t() == crow::json::type::Object) { + auto metadata = json_body["encryption_metadata"]; + auto keys = metadata.keys(); + for (const auto& key : keys) { + encryption_metadata_[key] = std::string(metadata[key]); + } + } } bool DecryptJsonRequest::IsValid() const { @@ -431,6 +451,17 @@ std::string DecryptJsonRequest::ToJsonString() const { debug["reference_id"] = reference_id_; json["debug"] = std::move(debug); + // If the encryption_metadata is empty, create an empty object. + if (!encryption_metadata_.empty()) { + crow::json::wvalue metadata; + for (const auto& pair : encryption_metadata_) { + metadata[pair.first] = pair.second; + } + json["encryption_metadata"] = std::move(metadata); + } else { + json["encryption_metadata"] = crow::json::load("{}"); + } + // Converts crow json object to a string with pretty printing return PrettyPrintJson(json.dump()); } @@ -438,9 +469,9 @@ std::string DecryptJsonRequest::ToJsonString() const { // JsonResponse implementations void JsonResponse::Parse(const std::string& response_body) { // Load and validate JSON first - auto json_body = crow::json::load(response_body); - - if (!json_body) return; // Stop parsing if JSON is invalid + auto json_body_opt = SafeLoadJsonBody(response_body); + if (!json_body_opt) return; // Stop parsing if JSON is invalid + auto json_body = *json_body_opt; // Extract common required fields if (auto parsed_value = SafeGetFromJsonPath(json_body, {"access", "user_id"})) { @@ -462,9 +493,10 @@ void EncryptJsonResponse::Parse(const std::string& response_body) { JsonResponse::Parse(response_body); // Load JSON for encrypt-specific fields - auto json_body = crow::json::load(response_body); - if (!json_body) return; - + auto json_body_opt = SafeLoadJsonBody(response_body); + if (!json_body_opt) return; + auto json_body = *json_body_opt; + // Extract encrypt-specific fields if (auto parsed_value = SafeGetFromJsonPath(json_body, {"data_batch_encrypted", "value_format", "compression"})) { if (auto enum_value = to_compression_enum(*parsed_value)) { @@ -476,6 +508,15 @@ void EncryptJsonResponse::Parse(const std::string& response_body) { encrypted_value_ = *decoded_value; } } + + // Parse encryption_metadata if it exists + if (json_body.has("encryption_metadata") && json_body["encryption_metadata"].t() == crow::json::type::Object) { + auto metadata = json_body["encryption_metadata"]; + auto keys = metadata.keys(); + for (const auto& key : keys) { + encryption_metadata_[key] = std::string(metadata[key]); + } + } } void DecryptJsonResponse::Parse(const std::string& response_body) { @@ -483,8 +524,9 @@ void DecryptJsonResponse::Parse(const std::string& response_body) { JsonResponse::Parse(response_body); // Load JSON for decrypt-specific fields - auto json_body = crow::json::load(response_body); - if (!json_body) return; + auto json_body_opt = SafeLoadJsonBody(response_body); + if (!json_body_opt) return; + auto json_body = *json_body_opt; // Extract decrypt-specific fields if (auto parsed_value = SafeGetFromJsonPath(json_body, {"data_batch", "datatype_info", "datatype"})) { @@ -592,6 +634,17 @@ std::string EncryptJsonResponse::ToJsonString() const { debug["reference_id"] = reference_id_; json["debug"] = std::move(debug); + // If the encryption_metadata is empty, create an empty object. + if (!encryption_metadata_.empty()) { + crow::json::wvalue metadata; + for (const auto& pair : encryption_metadata_) { + metadata[pair.first] = pair.second; + } + json["encryption_metadata"] = std::move(metadata); + } else { + json["encryption_metadata"] = crow::json::load("{}"); + } + // Converts crow json object to a string with pretty printing return PrettyPrintJson(json.dump()); } diff --git a/src/common/json_request.h b/src/common/json_request.h index 80b5049..a84dd6e 100644 --- a/src/common/json_request.h +++ b/src/common/json_request.h @@ -150,6 +150,7 @@ class DecryptJsonRequest : public JsonRequest { public: // Decrypt-specific required fields std::vector encrypted_value_; + std::map encryption_metadata_; /** * Default constructor. @@ -247,6 +248,7 @@ class EncryptJsonResponse : public JsonResponse { // Encrypt-specific required fields std::optional encrypted_compression_; std::vector encrypted_value_; + std::map encryption_metadata_; /** * Default constructor. diff --git a/src/common/json_request_test.cpp b/src/common/json_request_test.cpp index fbcce5d..9e66b6a 100644 --- a/src/common/json_request_test.cpp +++ b/src/common/json_request_test.cpp @@ -141,6 +141,9 @@ const std::string VALID_DECRYPT_JSON = R"({ "user_id": "user456" }, "application_context": "{\"user_id\": \"user456\"}", + "encryption_metadata": { + "dbps_agent_version": "v0.01" + }, "debug": { "reference_id": "ref789", "pretty_printed_value": "ENCRYPTED_test@example.com" @@ -353,6 +356,10 @@ TEST(JsonRequest, DecryptJsonRequestValidParse) { // Check decrypt-specific fields ASSERT_EQ(StringToBinary("ENCRYPTED_test@example.com"), request.encrypted_value_); + // Verify encryption_metadata is parsed correctly + ASSERT_EQ(1, request.encryption_metadata_.size()); + ASSERT_EQ("v0.01", request.encryption_metadata_.at("dbps_agent_version")); + ASSERT_TRUE(request.IsValid()); ASSERT_EQ("", request.GetValidationError()); } @@ -570,6 +577,9 @@ TEST(JsonRequest, DecryptJsonRequestToJson) { ASSERT_TRUE(json_string.find("ref789") != std::string::npos); ASSERT_TRUE(json_string.find("key123") != std::string::npos); ASSERT_TRUE(json_string.find("user456") != std::string::npos); + + // Verify encryption_metadata is included in the JSON output + ASSERT_TRUE(json_string.find("encryption_metadata") != std::string::npos); } // Test data for JsonResponse parsing @@ -588,6 +598,9 @@ const std::string VALID_ENCRYPT_RESPONSE_JSON = R"({ "debug": { "reference_id": "ref789", "pretty_printed_value": "ENCRYPTED_test@example.com" + }, + "encryption_metadata": { + "dbps_agent_version": "v0.01" } })"; @@ -625,6 +638,10 @@ TEST(JsonRequest, EncryptJsonResponseValidParse) { ASSERT_EQ(CompressionCodec::GZIP, response.encrypted_compression_.value()); ASSERT_EQ(StringToBinary("ENCRYPTED_test@example.com"), response.encrypted_value_); + // Verify encryption_metadata is parsed correctly + ASSERT_EQ(1, response.encryption_metadata_.size()); + ASSERT_EQ("v0.01", response.encryption_metadata_.at("dbps_agent_version")); + ASSERT_TRUE(response.IsValid()); ASSERT_EQ("", response.GetValidationError()); } @@ -732,6 +749,7 @@ TEST(JsonRequest, EncryptJsonResponseToJson) { response.reference_id_ = "ref456"; response.encrypted_compression_ = CompressionCodec::GZIP; response.encrypted_value_ = StringToBinary("ENCRYPTED_data"); + response.encryption_metadata_["dbps_agent_version"] = "v0.01"; ASSERT_TRUE(response.IsValid()); @@ -742,6 +760,11 @@ TEST(JsonRequest, EncryptJsonResponseToJson) { ASSERT_TRUE(json_string.find("ref456") != std::string::npos); ASSERT_TRUE(json_string.find("RU5DUllQVEVEX2RhdGE=") != std::string::npos); // "ENCRYPTED_data" ASSERT_TRUE(json_string.find("GZIP") != std::string::npos); + + // Verify encryption_metadata is serialized correctly + ASSERT_TRUE(json_string.find("encryption_metadata") != std::string::npos); + ASSERT_TRUE(json_string.find("dbps_agent_version") != std::string::npos); + ASSERT_TRUE(json_string.find("v0.01") != std::string::npos); } TEST(JsonRequest, DecryptJsonResponseToJson) { diff --git a/src/scripts/dbpa_remote_testapp.cpp b/src/scripts/dbpa_remote_testapp.cpp index 8d62d94..9ada009 100644 --- a/src/scripts/dbpa_remote_testapp.cpp +++ b/src/scripts/dbpa_remote_testapp.cpp @@ -36,6 +36,11 @@ using namespace dbps::enum_utils; template using span = tcb::span; +namespace { + const std::map VALID_ENCRYPTION_METADATA = {{"dbps_agent_version", "v0.01_unittest"}}; + const std::string SEQUENCER_ENCRYPTION_METADATA_VERSION = "v0.01"; +} + // Demo application class class DBPARemoteTestApp { private: @@ -85,7 +90,7 @@ class DBPARemoteTestApp { Type::UNDEFINED, // datatype std::nullopt, // datatype_length (not needed for UNDEFINED) CompressionCodec::UNCOMPRESSED, // compression_type - std::nullopt + VALID_ENCRYPTION_METADATA // column_encryption_metadata ); std::cout << "OK: Main DBPA agent initialized successfully" << std::endl; @@ -112,7 +117,7 @@ class DBPARemoteTestApp { Type::FLOAT, // datatype std::nullopt, // datatype_length (not needed for FLOAT) CompressionCodec::UNCOMPRESSED, // compression_type - std::nullopt + VALID_ENCRYPTION_METADATA // column_encryption_metadata ); std::cout << "OK: Float DBPA agent initialized successfully" << std::endl; @@ -139,7 +144,7 @@ class DBPARemoteTestApp { Type::FIXED_LEN_BYTE_ARRAY, // datatype 8, // datatype_length (8 bytes per element) CompressionCodec::UNCOMPRESSED, // compression_type - std::nullopt + VALID_ENCRYPTION_METADATA // column_encryption_metadata ); std::cout << "OK: Fixed-length DBPA agent initialized successfully" << std::endl; @@ -195,6 +200,23 @@ class DBPARemoteTestApp { all_succeeded = false; continue; } + + // Verify encryption metadata + auto encryption_metadata = encrypt_result->encryption_metadata(); + if (!encryption_metadata || encryption_metadata->find("dbps_agent_version") == encryption_metadata->end()) { + std::cout << " ERROR: Encryption metadata verification failed" << std::endl; + all_succeeded = false; + continue; + } + if (encryption_metadata->at("dbps_agent_version") != SEQUENCER_ENCRYPTION_METADATA_VERSION) { + std::cout << " ERROR: Encryption metadata version mismatch" << std::endl; + std::cout << " Expected: " << SEQUENCER_ENCRYPTION_METADATA_VERSION << std::endl; + std::cout << " Got: " << encryption_metadata->at("dbps_agent_version") << std::endl; + all_succeeded = false; + continue; + } + std::cout << " OK: Encryption metadata verified" << std::endl; + std::cout << " dbps_agent_version: " << encryption_metadata->at("dbps_agent_version") << std::endl; std::cout << " OK: Encrypted (" << encrypt_result->size() << " bytes)" << std::endl; std::cout << " OK: Ciphertext size: " << encrypt_result->size() << " bytes" << std::endl; diff --git a/src/server/dbps_api_server.cpp b/src/server/dbps_api_server.cpp index bb574ff..c3664fd 100644 --- a/src/server/dbps_api_server.cpp +++ b/src/server/dbps_api_server.cpp @@ -76,7 +76,8 @@ int main() { request.encrypted_compression_.value(), request.key_id_, request.user_id_, - request.application_context_ + request.application_context_, + {} // encryption_metadata does not exist in the Encryption request. ); try { @@ -88,8 +89,9 @@ int main() { return CreateErrorResponse("Invalid input for encryption: " + std::string(e.what())); } - // Set encrypted value + // Set encrypted value and encryption_metadata response.encrypted_value_ = sequencer.encrypted_result_; + response.encryption_metadata_ = sequencer.encryption_metadata_; // Set common fields of response // TODO: Add role and access control logic based on context-aware access control logic during encryption. @@ -151,7 +153,8 @@ int main() { request.encrypted_compression_.value(), request.key_id_, request.user_id_, - request.application_context_ + request.application_context_, + request.encryption_metadata_ ); try { diff --git a/src/server/encryption_sequencer.cpp b/src/server/encryption_sequencer.cpp index 143160a..0e3bb40 100644 --- a/src/server/encryption_sequencer.cpp +++ b/src/server/encryption_sequencer.cpp @@ -28,6 +28,11 @@ using namespace dbps::external; using namespace dbps::enum_utils; +namespace { + constexpr const char* DBPS_VERSION_KEY = "dbps_agent_version"; + constexpr const char* DBPS_VERSION_VALUE = "v0.01"; +} + // Constructor implementation DataBatchEncryptionSequencer::DataBatchEncryptionSequencer( const std::string& column_name, @@ -39,7 +44,8 @@ DataBatchEncryptionSequencer::DataBatchEncryptionSequencer( CompressionCodec::type encrypted_compression, const std::string& key_id, const std::string& user_id, - const std::string& application_context + const std::string& application_context, + const std::map& encryption_metadata ) : column_name_(column_name), datatype_(datatype), datatype_length_(datatype_length), @@ -49,7 +55,8 @@ DataBatchEncryptionSequencer::DataBatchEncryptionSequencer( encrypted_compression_(encrypted_compression), key_id_(key_id), user_id_(user_id), - application_context_(application_context) {} + application_context_(application_context), + encryption_metadata_(encryption_metadata) {} // TODO: Rename this method so it captures better the flow of decompress/format and encrypt/decrypt operations. bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector& plaintext) { @@ -79,11 +86,13 @@ bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector& error_message_ = "Failed to encrypt data"; return false; } + encryption_metadata_[DBPS_VERSION_KEY] = DBPS_VERSION_VALUE; return true; } catch (const InvalidInputException& e) { // Throw the exception so it can be caught by the caller. throw; } + encryption_metadata_[DBPS_VERSION_KEY] = DBPS_VERSION_VALUE; return true; } @@ -383,6 +392,25 @@ bool DataBatchEncryptionSequencer::ConvertAndDecrypt(const std::vector& return false; } + // Check encryption_metadata for dbps_agent_version + // + // The DBPS server version check during Decrypt is to future-proof against changes on the Encryption process. + // The Encryption process could change due to updates on the payload decoding, updates on fallback encryption methods, or other changes, + // and it is possible that it results on a mismatch with the Decryption implementation. This check helps to catch such mismatches. + auto it = encryption_metadata_.find(DBPS_VERSION_KEY); + if (it == encryption_metadata_.end()) { + std::cerr << "ERROR: EncryptionSequencer - encryption_metadata must contain key '" << DBPS_VERSION_KEY << "'" << std::endl; + error_stage_ = "decrypt_version_check"; + error_message_ = "encryption_metadata must contain key '" + std::string(DBPS_VERSION_KEY) + "'"; + return false; + } else if (it->second.find(DBPS_VERSION_VALUE) != 0) { + std::cerr << "ERROR: EncryptionSequencer - encryption_metadata['" << DBPS_VERSION_KEY << "'] must match '" + << DBPS_VERSION_VALUE << "', but got '" << it->second << "'" << std::endl; + error_stage_ = "decrypt_version_check"; + error_message_ = "encryption_metadata['" + std::string(DBPS_VERSION_KEY) + "'] must match '" + std::string(DBPS_VERSION_VALUE) + "'"; + return false; + } + // Simple XOR decryption (same operation as encryption) decrypted_result_ = DecryptData(ciphertext); if (decrypted_result_.empty()) { diff --git a/src/server/encryption_sequencer.h b/src/server/encryption_sequencer.h index 03b9cf6..c77c2f5 100644 --- a/src/server/encryption_sequencer.h +++ b/src/server/encryption_sequencer.h @@ -77,6 +77,9 @@ class DataBatchEncryptionSequencer { // Result storage std::vector encrypted_result_; std::vector decrypted_result_; + + // Encryption metadata + std::map encryption_metadata_; // Error reporting fields std::string error_stage_; @@ -93,7 +96,8 @@ class DataBatchEncryptionSequencer { CompressionCodec::type encrypted_compression, const std::string& key_id, const std::string& user_id, - const std::string& application_context + const std::string& application_context, + const std::map& encryption_metadata ); // Default constructor diff --git a/src/server/encryption_sequencer_test.cpp b/src/server/encryption_sequencer_test.cpp index 4f79a99..b6377ff 100644 --- a/src/server/encryption_sequencer_test.cpp +++ b/src/server/encryption_sequencer_test.cpp @@ -80,8 +80,9 @@ class TestDataBatchEncryptionSequencer : public DataBatchEncryptionSequencer { CompressionCodec::type encrypted_compression, const std::string& key_id, const std::string& user_id, - const std::string& application_context - ) : DataBatchEncryptionSequencer(column_name, datatype, datatype_length, compression, format, encoding_attributes, encrypted_compression, key_id, user_id, application_context) {} + const std::string& application_context, + const std::map& encryption_metadata + ) : DataBatchEncryptionSequencer(column_name, datatype, datatype_length, compression, format, encoding_attributes, encrypted_compression, key_id, user_id, application_context, encryption_metadata) {} // Public access to protected methods bool TestConvertEncodingAttributesToValues() { @@ -107,7 +108,8 @@ TEST(EncryptionSequencer, EncryptionDecryption) { CompressionCodec::UNCOMPRESSED, // encrypted_compression "test_key_123", // key_id "test_user", // user_id - "{}" // application_context + "{}", // application_context + {} // encryption_metadata ); // Test encryption @@ -118,11 +120,11 @@ TEST(EncryptionSequencer, EncryptionDecryption) { // Test 2: Different key_id produces different encryption { DataBatchEncryptionSequencer sequencer1( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key1", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key1", "test_user", "{}", {} ); DataBatchEncryptionSequencer sequencer2( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key2", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key2", "test_user", "{}", {} ); @@ -136,11 +138,11 @@ TEST(EncryptionSequencer, EncryptionDecryption) { // Test 3: Same key_id produces consistent encryption { DataBatchEncryptionSequencer sequencer1( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "same_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "same_key", "test_user", "{}", {} ); DataBatchEncryptionSequencer sequencer2( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "same_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "same_key", "test_user", "{}", {} ); @@ -154,7 +156,7 @@ TEST(EncryptionSequencer, EncryptionDecryption) { // Test 4: Empty data encryption { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); // This should fail because empty input is rejected @@ -165,7 +167,7 @@ TEST(EncryptionSequencer, EncryptionDecryption) { // Test 5: Binary data encryption { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); // Binary data: 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 @@ -181,7 +183,7 @@ TEST(EncryptionSequencer, ParameterValidation) { // Test 1: Valid parameters, should succeed { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); ASSERT_TRUE(result) << "Valid parameters test failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; @@ -190,7 +192,7 @@ TEST(EncryptionSequencer, ParameterValidation) { // Test 2: Invalid compression (should succeed with warning) { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::GZIP, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::GZIP, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); ASSERT_TRUE(result); @@ -200,7 +202,7 @@ TEST(EncryptionSequencer, ParameterValidation) { // Test 3: Undefined format is supported { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::UNDEFINED, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::UNDEFINED, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); ASSERT_TRUE(result) << "Format UNDEFINED should be supported: " << sequencer.error_message_; @@ -210,7 +212,7 @@ TEST(EncryptionSequencer, ParameterValidation) { // Test 4: All formats now supported (including RLE) { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::RLE, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::RLE, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); ASSERT_TRUE(result) << "Format RLE should now be supported: " << sequencer.error_message_; @@ -224,7 +226,7 @@ TEST(EncryptionSequencer, InputValidation) { // Test 1: Empty plaintext { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(EMPTY_DATA); EXPECT_FALSE(result) << "Empty plaintext test should have failed"; @@ -234,7 +236,7 @@ TEST(EncryptionSequencer, InputValidation) { // Test 2: Empty ciphertext { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndDecrypt(EMPTY_DATA); EXPECT_FALSE(result) << "Empty ciphertext test should have failed"; @@ -244,13 +246,37 @@ TEST(EncryptionSequencer, InputValidation) { // Test 3: Empty key_id { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); EXPECT_FALSE(result) << "Empty key_id test should have failed"; EXPECT_EQ(sequencer.error_stage_, "validation") << "Wrong error stage for empty key_id"; } - + + // Test 4: Missing encryption_metadata + { + DataBatchEncryptionSequencer sequencer( + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", + {} // encryption_metadata, setting it to empty map. + ); + bool result = sequencer.ConvertAndDecrypt(HELLO_WORLD_DATA); + EXPECT_FALSE(result) << "Missing encryption_metadata test should have failed"; + EXPECT_EQ(sequencer.error_stage_, "decrypt_version_check") << "Wrong error stage for missing encryption_metadata"; + EXPECT_TRUE(sequencer.error_message_.find("encryption_metadata must contain key") != std::string::npos) << "Wrong error message for missing encryption_metadata"; + } + + // Test 5: Incorrect encryption_metadata version + { + DataBatchEncryptionSequencer sequencer( + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", + {{"dbps_agent_version", "v0.09"}} // encryption_metadata, setting it to incorrect version. + ); + bool result = sequencer.ConvertAndDecrypt(HELLO_WORLD_DATA); + EXPECT_FALSE(result) << "Incorrect encryption_metadata version test should have failed"; + EXPECT_EQ(sequencer.error_stage_, "decrypt_version_check") << "Wrong error stage for incorrect encryption_metadata version"; + EXPECT_TRUE(sequencer.error_message_.find("must match") != std::string::npos) << "Wrong error message for incorrect encryption_metadata version"; + } + } // Test round-trip encryption/decryption @@ -258,32 +284,34 @@ TEST(EncryptionSequencer, RoundTripEncryption) { // Test 1: Basic round trip - "Hello, World!" { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key_123", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key_123", "test_user", "{}", {} ); // Encrypt bool encrypt_result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); ASSERT_TRUE(encrypt_result) << "Round trip encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; - // Decrypt the encrypted result + // Decrypt the encrypted result - need encryption_metadata with dbps_agent_version + ASSERT_TRUE(sequencer.encryption_metadata_.size() > 0 && sequencer.encryption_metadata_.at("dbps_agent_version").length() > 0); bool decrypt_result = sequencer.ConvertAndDecrypt(sequencer.encrypted_result_); ASSERT_TRUE(decrypt_result) << "Round trip decryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; // Verify the decrypted result matches the original EXPECT_EQ(sequencer.decrypted_result_, HELLO_WORLD_DATA); - } + } // Test 2: Binary data round trip { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "binary_test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "binary_test_key", "test_user", "{}", {} ); // Encrypt bool encrypt_result = sequencer.ConvertAndEncrypt(BINARY_DATA); // Binary data: 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 ASSERT_TRUE(encrypt_result) << "Binary round trip encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; - // Decrypt the encrypted result + // Decrypt the encrypted result - need encryption_metadata with dbps_agent_version + ASSERT_TRUE(sequencer.encryption_metadata_.size() > 0 && sequencer.encryption_metadata_.at("dbps_agent_version").length() > 0); bool decrypt_result = sequencer.ConvertAndDecrypt(sequencer.encrypted_result_); ASSERT_TRUE(decrypt_result) << "Binary round trip decryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; @@ -294,7 +322,7 @@ TEST(EncryptionSequencer, RoundTripEncryption) { // Test 3: Single character round trip { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "single_char_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "single_char_key", "test_user", "{}", {} ); // "A" @@ -303,7 +331,8 @@ TEST(EncryptionSequencer, RoundTripEncryption) { bool encrypt_result = sequencer.ConvertAndEncrypt(SINGLE_CHAR_DATA); ASSERT_TRUE(encrypt_result) << "Single char round trip encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; - // Decrypt the encrypted result + // Decrypt the encrypted result - need encryption_metadata with dbps_agent_version + ASSERT_TRUE(sequencer.encryption_metadata_.size() > 0 && sequencer.encryption_metadata_.at("dbps_agent_version").length() > 0); bool decrypt_result = sequencer.ConvertAndDecrypt(sequencer.encrypted_result_); ASSERT_TRUE(decrypt_result) << "Single char round trip decryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; @@ -314,11 +343,11 @@ TEST(EncryptionSequencer, RoundTripEncryption) { // Test 4: Different keys produce different encrypted results { DataBatchEncryptionSequencer sequencer1( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key1", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key1", "test_user", "{}", {} ); DataBatchEncryptionSequencer sequencer2( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key2", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "key2", "test_user", "{}", {} ); bool result1 = sequencer1.ConvertAndEncrypt(HELLO_WORLD_DATA); @@ -330,7 +359,9 @@ TEST(EncryptionSequencer, RoundTripEncryption) { // Key-aware XOR encryption should produce different results for different keys EXPECT_NE(sequencer1.encrypted_result_, sequencer2.encrypted_result_); - // But both should decrypt back to the same original + // But both should decrypt back to the same original - need encryption_metadata with dbps_agent_version + ASSERT_TRUE(sequencer1.encryption_metadata_.size() > 0 && sequencer1.encryption_metadata_.at("dbps_agent_version").length() > 0); + ASSERT_TRUE(sequencer2.encryption_metadata_.size() > 0 && sequencer2.encryption_metadata_.at("dbps_agent_version").length() > 0); bool decrypt1 = sequencer1.ConvertAndDecrypt(sequencer1.encrypted_result_); bool decrypt2 = sequencer2.ConvertAndDecrypt(sequencer2.encrypted_result_); @@ -347,7 +378,7 @@ TEST(EncryptionSequencer, ResultStorage) { // Test 1: Verify encrypted result is stored { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); @@ -362,14 +393,15 @@ TEST(EncryptionSequencer, ResultStorage) { // Test 2: Verify decrypted result is stored { DataBatchEncryptionSequencer sequencer( - "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}" + "test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {} ); // First encrypt something bool encrypt_result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); ASSERT_TRUE(encrypt_result) << "Result storage decryption test failed during encryption"; - // Then decrypt it + // Then decrypt it - need encryption_metadata with dbps_agent_version + ASSERT_TRUE(sequencer.encryption_metadata_.size() > 0 && sequencer.encryption_metadata_.at("dbps_agent_version").length() > 0); bool decrypt_result = sequencer.ConvertAndDecrypt(sequencer.encrypted_result_); ASSERT_TRUE(decrypt_result) << "Result storage decryption test failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; @@ -387,7 +419,7 @@ TEST(EncryptionSequencer, FixedLenByteArrayValidation) { // Helper function to test validation failure auto testValidationFailure = [&](const std::optional& datatype_length, const std::string& expected_msg) -> bool { DataBatchEncryptionSequencer sequencer( - "test_column", Type::FIXED_LEN_BYTE_ARRAY, datatype_length, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key_123", "test_user", "{}" + "test_column", Type::FIXED_LEN_BYTE_ARRAY, datatype_length, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key_123", "test_user", "{}", {} ); bool result = sequencer.ConvertAndEncrypt(HELLO_WORLD_DATA); @@ -411,7 +443,7 @@ TEST(EncryptionSequencer, FixedLenByteArrayValidation) { EXPECT_TRUE(testValidationFailure(0, "FIXED_LEN_BYTE_ARRAY datatype_length must be positive")); // Test valid case (should pass parameter validation) - DataBatchEncryptionSequencer sequencer("test_column", Type::FIXED_LEN_BYTE_ARRAY, 16, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key_123", "test_user", "{}"); + DataBatchEncryptionSequencer sequencer("test_column", Type::FIXED_LEN_BYTE_ARRAY, 16, CompressionCodec::UNCOMPRESSED, Format::PLAIN, {{"page_type", "DICTIONARY_PAGE"}}, CompressionCodec::UNCOMPRESSED, "test_key_123", "test_user", "{}", {}); bool result = sequencer.ConvertAndEncrypt(FIXED_LEN_BYTE_ARRAY_DATA); if (!result && sequencer.error_stage_ == "parameter_validation") { @@ -452,7 +484,7 @@ TEST(EncryptionSequencer, ConvertEncodingAttributesToValues_Positive) { {"page_v2_is_compressed", "true"} }; - TestDataBatchEncryptionSequencer sequencer_v2("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, attribs_v2, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}"); + TestDataBatchEncryptionSequencer sequencer_v2("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, attribs_v2, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {}); ASSERT_TRUE(sequencer_v2.TestConvertEncodingAttributesToValues()) << "DATA_PAGE_V2 conversion failed: " << sequencer_v2.error_stage_ << " - " << sequencer_v2.error_message_; @@ -472,7 +504,7 @@ TEST(EncryptionSequencer, ConvertEncodingAttributesToValues_Positive) { {"page_v1_repetition_level_encoding", "BIT_PACKED"} }; - TestDataBatchEncryptionSequencer sequencer_v1("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, attribs_v1, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}"); + TestDataBatchEncryptionSequencer sequencer_v1("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, attribs_v1, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {}); ASSERT_TRUE(sequencer_v1.TestConvertEncodingAttributesToValues()) << "DATA_PAGE_V1 conversion failed: " << sequencer_v1.error_stage_ << " - " << sequencer_v1.error_message_; @@ -488,7 +520,7 @@ TEST(EncryptionSequencer, ConvertEncodingAttributesToValues_Negative) { // Test missing page_type std::map empty_attribs; - TestDataBatchEncryptionSequencer sequencer1("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, empty_attribs, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}"); + TestDataBatchEncryptionSequencer sequencer1("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, empty_attribs, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {}); EXPECT_FALSE(sequencer1.TestConvertEncodingAttributesToValues()); EXPECT_EQ(sequencer1.error_stage_, "encoding_attribute_validation"); @@ -504,7 +536,7 @@ TEST(EncryptionSequencer, ConvertEncodingAttributesToValues_Negative) { {"page_v2_is_compressed", "true"} }; - TestDataBatchEncryptionSequencer sequencer2("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, invalid_int, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}"); + TestDataBatchEncryptionSequencer sequencer2("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, invalid_int, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {}); EXPECT_FALSE(sequencer2.TestConvertEncodingAttributesToValues()); EXPECT_EQ(sequencer2.error_stage_, "encoding_attribute_conversion"); @@ -520,7 +552,7 @@ TEST(EncryptionSequencer, ConvertEncodingAttributesToValues_Negative) { {"page_v2_is_compressed", "maybe"} }; - TestDataBatchEncryptionSequencer sequencer3("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, invalid_bool, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}"); + TestDataBatchEncryptionSequencer sequencer3("test_column", Type::BYTE_ARRAY, std::nullopt, CompressionCodec::UNCOMPRESSED, Format::PLAIN, invalid_bool, CompressionCodec::UNCOMPRESSED, "test_key", "test_user", "{}", {}); EXPECT_FALSE(sequencer3.TestConvertEncodingAttributesToValues()); EXPECT_EQ(sequencer3.error_stage_, "encoding_attribute_conversion"); }