Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/common/dbpa_local_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
using namespace dbps::external;

namespace {
// should rename to DBPS_ENCRYPTION_METADATA
const std::map<std::string, std::string> DBPS_ENCRYPTION_METADATA = {{"dbps_agent_version", "v0.01_unittest"}};
const std::map<std::string, std::string> DBPS_ENCRYPTION_METADATA = {
{"dbps_agent_version", "v0.01_unittest"},
{"encryption_mode", "per_block"}
};
}

// Test fixture for LocalDataBatchProtectionAgent tests
Expand Down Expand Up @@ -121,8 +123,8 @@ TEST_F(LocalDataBatchProtectionAgentTest, RoundTripEncryptDecrypt) {
// 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
ASSERT_TRUE(encryption_metadata->find("dbps_agent_version") != encryption_metadata->end());
ASSERT_TRUE(encryption_metadata->find("encryption_mode") != encryption_metadata->end());

// Get the ciphertext
auto ciphertext_span = encrypt_result->ciphertext();
Expand Down
9 changes: 5 additions & 4 deletions src/scripts/dbpa_remote_testapp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ template <typename T>
using span = tcb::span<T>;

namespace {
const std::map<std::string, std::string> VALID_ENCRYPTION_METADATA = {{"dbps_agent_version", "v0.01_unittest"}};
const std::map<std::string, std::string> VALID_ENCRYPTION_METADATA = {
{"dbps_agent_version", "v0.01_unittest"}, {"encryption_mode", "per_block"}};
const std::string SEQUENCER_ENCRYPTION_METADATA_VERSION = "v0.01";
}

Expand Down Expand Up @@ -92,7 +93,7 @@ class DBPARemoteTestApp {
Type::UNDEFINED, // datatype
std::nullopt, // datatype_length (not needed for UNDEFINED)
CompressionCodec::UNCOMPRESSED, // compression_type
VALID_ENCRYPTION_METADATA // column_encryption_metadata
VALID_ENCRYPTION_METADATA // column_encryption_metadata -- used only during /decrypt calls
);

std::cout << "OK: Main DBPA agent initialized successfully" << std::endl;
Expand All @@ -119,7 +120,7 @@ class DBPARemoteTestApp {
Type::FLOAT, // datatype
std::nullopt, // datatype_length (not needed for FLOAT)
CompressionCodec::UNCOMPRESSED, // compression_type
VALID_ENCRYPTION_METADATA // column_encryption_metadata
VALID_ENCRYPTION_METADATA // column_encryption_metadata -- used only during /decrypt calls
);

std::cout << "OK: Float DBPA agent initialized successfully" << std::endl;
Expand All @@ -146,7 +147,7 @@ class DBPARemoteTestApp {
Type::FIXED_LEN_BYTE_ARRAY, // datatype
8, // datatype_length (8 bytes per element)
CompressionCodec::SNAPPY, // compression_type (input will be Snappy-compressed)
VALID_ENCRYPTION_METADATA // column_encryption_metadata
VALID_ENCRYPTION_METADATA // column_encryption_metadata -- used only during /decrypt calls
);

std::cout << "OK: Fixed-length DBPA agent initialized successfully" << std::endl;
Expand Down
8 changes: 8 additions & 0 deletions src/server/decoding_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ TypedListValues ParseValueBytesIntoTypedList(
}
}

std::vector<uint8_t> GetTypedListAsValueBytes(
const TypedListValues& list,
Type::type datatype,
const std::optional<int>& datatype_length,
Format::type format) {
throw DBPSUnsupportedException("GetTypedListAsValueBytes not implemented");
}

template<typename T>
const char* GetTypeName() {
if constexpr (std::is_same_v<T, std::vector<int32_t>>) return "INT32";
Expand Down
18 changes: 18 additions & 0 deletions src/server/decoding_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ TypedListValues ParseValueBytesIntoTypedList(
const std::optional<int>& datatype_length,
Format::type format);

/**
* Convert a typed list back into value bytes based on the data type and format.
* This is the reverse operation of ParseValueBytesIntoTypedList.
*
* @param list The typed list to convert
* @param datatype The data type of the values
* @param datatype_length Optional length for fixed-length types (required for FIXED_LEN_BYTE_ARRAY)
* @param format The format of the data (currently only PLAIN is supported)
* @return std::vector<uint8_t> containing the serialized value bytes
* @throws DBPSUnsupportedException if format or datatype is unsupported
* @throws InvalidInputException if the data is invalid or malformed
*/
std::vector<uint8_t> GetTypedListAsValueBytes(
const TypedListValues& list,
Type::type datatype,
const std::optional<int>& datatype_length,
Format::type format);

/**
* Print a typed list in a human-readable format.
*
Expand Down
186 changes: 132 additions & 54 deletions src/server/encryption_sequencer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "enum_utils.h"
#include "decoding_utils.h"
#include "compression_utils.h"
#include "exceptions.h"
#include <functional>
#include <iomanip>
#include <iostream>
Expand All @@ -33,7 +34,10 @@ using namespace dbps::compression;

namespace {
constexpr const char* DBPS_VERSION_KEY = "dbps_agent_version";
constexpr const char* DBPS_VERSION_VALUE = "v0.01";
constexpr const char* DBPS_VERSION = "v0.01";
constexpr const char* ENCRYPTION_MODE = "encryption_mode";
constexpr const char* ENCRYPTION_PER_BLOCK = "per_block";
constexpr const char* ENCRYPTION_PER_VALUE = "per_value";
}

// Constructor implementation
Expand Down Expand Up @@ -61,6 +65,8 @@ DataBatchEncryptionSequencer::DataBatchEncryptionSequencer(
application_context_(application_context),
encryption_metadata_(encryption_metadata) {}

// Top level encryption/decryption methods.

// TODO: Rename this method so it captures better the flow of decompress/format and encrypt/decrypt operations.
bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector<uint8_t>& plaintext) {
// Validate all parameters and key_id
Expand All @@ -76,32 +82,106 @@ bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector<uint8_t>&
}

try {
auto level_and_value_bytes = DecompressAndSplit(plaintext);
// Decompress and split plaintext into level and value bytes
auto [level_bytes, value_bytes] = DecompressAndSplit(plaintext);

// Parse value bytes into typed list
auto typed_list = ParseValueBytesIntoTypedList(
level_and_value_bytes.value_bytes, datatype_, datatype_length_, format_);
auto encrypted_bytes = EncryptTypedList(typed_list, level_and_value_bytes.level_bytes);
value_bytes, datatype_, datatype_length_, format_);

// Encrypt the typed list and level bytes
auto encrypted_bytes = EncryptTypedList(typed_list, level_bytes);

// Compress the encrypted bytes
encrypted_result_ = Compress(encrypted_bytes, encrypted_compression_);

} catch (const DBPSUnsupportedException& e) {
// If any stage is as of yet unsupported, default to whole payload
// (as opposed to per-value) XOR encryption
// If any stage is as of yet unsupported, default to whole payload (per-block) encryption
// (as opposed to per-value)
encrypted_result_ = EncryptData(plaintext);
if (encrypted_result_.empty()) {
error_stage_ = "encryption";
error_message_ = "Failed to encrypt data";
return false;
}
encryption_metadata_[DBPS_VERSION_KEY] = DBPS_VERSION_VALUE;
// If the sequence was interrupted by a DBPSUnsupportedException at any point, use per block encryption.
// Set the encryption type to per block.
encryption_metadata_[DBPS_VERSION_KEY] = DBPS_VERSION;
encryption_metadata_[ENCRYPTION_MODE] = ENCRYPTION_PER_BLOCK;
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;
// If the sequencer got here, it means the encryption of the values in the typed list finished successfully.
// Set the encryption type to per value
encryption_metadata_[DBPS_VERSION_KEY] = DBPS_VERSION;
encryption_metadata_[ENCRYPTION_MODE] = ENCRYPTION_PER_VALUE;
return true;
}

// TODO: Rename this method so it captures better the flow of decompress/format and encrypt/decrypt operations.
bool DataBatchEncryptionSequencer::ConvertAndDecrypt(const std::vector<uint8_t>& ciphertext) {
// Validate all parameters and key_id
if (!ValidateParameters()) {
return false;
}

// Check that ciphertext is not null and not empty
if (ciphertext.empty()) {
error_stage_ = "validation";
error_message_ = "ciphertext cannot be null or empty";
return false;
}

// Check encryption_metadata for dbps_agent_version
std::string version_error = ValidateDecryptionVersion();
if (!version_error.empty()) {
error_stage_ = "decrypt_version_check";
error_message_ = version_error;
return false;
}

// Get encryption_mode from encryption_metadata
std::string encryption_mode = SafeGetEncryptionMode();
if (encryption_mode.empty()) {
error_stage_ = "decrypt_encryption_mode_validation";
error_message_ = "Failed to get encryption_mode from encryption_metadata";
return false;
}

// Per-value encryption
if (encryption_mode == ENCRYPTION_PER_VALUE) {
// Decompress the encrypted bytes
auto decompressed_encrypted_bytes = Decompress(ciphertext, encrypted_compression_);

// Decrypt the typed list and level bytes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically we need the Type of the column in here (so we know how to make sense of the decrypted bytes for each value) - but that can wait for a later iteration. This is good for demo/discussion purposes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this will be passed along the rest of the "context" to the implementation (column_name, user_id, key_id, application_context). This DecryptTypedList is a helper method inside the class that assembles the call to the encrypt/decrypt with all the params.

auto [typed_list, level_bytes] = DecryptTypedList(decompressed_encrypted_bytes);

// Convert typed list back to value bytes
auto value_bytes = GetTypedListAsValueBytes(typed_list, datatype_, datatype_length_, format_);

// Merge level and value bytes and compress to get plaintext
decrypted_result_ = CompressAndMerge(level_bytes, value_bytes);
}

// Per-block encryption
else if (encryption_mode == ENCRYPTION_PER_BLOCK) {
// Simple XOR decryption (same operation as encryption) for per-block encryption
decrypted_result_ = DecryptData(ciphertext);
if (decrypted_result_.empty()) {
error_stage_ = "decryption";
error_message_ = "Failed to decrypt data";
return false;
}
}

return true;
}

// Main processing methods
// Functions to pack/unpack the plaintext into level and value bytes.

// Used during Encryption. Decompresses and splits the plaintext into level and value bytes.
LevelAndValueBytes DataBatchEncryptionSequencer::DecompressAndSplit(
const std::vector<uint8_t>& plaintext) {
LevelAndValueBytes result;
Expand Down Expand Up @@ -144,14 +224,20 @@ LevelAndValueBytes DataBatchEncryptionSequencer::DecompressAndSplit(
throw InvalidInputException("Unexpected page type: " + page_type);
}

// This is the primary integration point for Protegrity to encrypt individual items from a typed list.
// Used during Decryption. Merges level and value bytes and compresses them back into the output format.
std::vector<uint8_t> DataBatchEncryptionSequencer::CompressAndMerge(
const std::vector<uint8_t>& level_bytes, const std::vector<uint8_t>& value_bytes) {
throw DBPSUnsupportedException("CompressAndMerge not implemented");
}

// This is the primary integration point for Protegrity to encrypt individual values from a typed list.
//
// Also the context rich encryptor can use these additional parameters: column_name, user_id, key_id, application_context.
//
// The current implementation prints out the list of items in the typed list and throws the DBPSUnsupportedException
// The current implementation prints out the list of values in the typed list and throws the DBPSUnsupportedException
// so that the default encryption method is used.
//
// Both the level_bytes AND elements need to be encrypted and combined as a single encrypted vector of bytes.
// Both the level_bytes AND list of values need to be encrypted and combined as a single encrypted vector of bytes.
//
std::vector<uint8_t> DataBatchEncryptionSequencer::EncryptTypedList(
const TypedListValues& typed_list, const std::vector<uint8_t>& level_bytes) {
Expand All @@ -176,50 +262,13 @@ std::vector<uint8_t> DataBatchEncryptionSequencer::EncryptTypedList(
throw DBPSUnsupportedException("EncryptTypedList not implemented");
}

// TODO: Rename this method so it captures better the flow of decompress/format and encrypt/decrypt operations.
bool DataBatchEncryptionSequencer::ConvertAndDecrypt(const std::vector<uint8_t>& ciphertext) {
// Validate all parameters and key_id
if (!ValidateParameters()) {
return false;
}

// Check that ciphertext is not null and not empty
if (ciphertext.empty()) {
error_stage_ = "validation";
error_message_ = "ciphertext cannot be null or empty";
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()) {
error_stage_ = "decryption";
error_message_ = "Failed to decrypt data";
return false;
}

return true;
std::pair<TypedListValues, std::vector<uint8_t>> DataBatchEncryptionSequencer::DecryptTypedList(
const std::vector<uint8_t>& encrypted_bytes) {
throw DBPSUnsupportedException("DecryptTypedList not implemented");
}

// Helper methods to validate and basic parameter reading.

bool DataBatchEncryptionSequencer::ConvertEncodingAttributesToValues() {
// Helper to find key and return value or null
auto FindKey = [this](const std::string& key) -> const std::string* {
Expand Down Expand Up @@ -332,6 +381,35 @@ bool DataBatchEncryptionSequencer::ValidateParameters() {
return true;
}

std::string DataBatchEncryptionSequencer::ValidateDecryptionVersion() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/question: I remember seeing this code in another PR - let's make sure it's not duplicate.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was moved from ConvertAndDecrypt to here for readabilty.

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;
return "encryption_metadata must contain key '" + std::string(DBPS_VERSION_KEY) + "'";
} else if (it->second.find(DBPS_VERSION) != 0) {
std::cerr << "ERROR: EncryptionSequencer - encryption_metadata['" << DBPS_VERSION_KEY << "'] must match '"
<< DBPS_VERSION << "', but got '" << it->second << "'" << std::endl;
return "encryption_metadata['" + std::string(DBPS_VERSION_KEY) + "'] must match '" + std::string(DBPS_VERSION) + "'";
}
return "";
}

std::string DataBatchEncryptionSequencer::SafeGetEncryptionMode() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we consider using an optional, rather than an empty string to signal 'error'?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks.

auto it = encryption_metadata_.find(ENCRYPTION_MODE);
if (it == encryption_metadata_.end()) {
// The metadata key for encryption mode is missing.
return "";
}
const std::string& encryption_mode = it->second;
if (encryption_mode != ENCRYPTION_PER_BLOCK && encryption_mode != ENCRYPTION_PER_VALUE) {
// The value for encryption mode is not valid.
return "";
}
return encryption_mode;
}

// Simple encryption/decryption functions using XOR with key_id hash

std::vector<uint8_t> DataBatchEncryptionSequencer::EncryptData(const std::vector<uint8_t>& data) {
if (data.empty()) {
return std::vector<uint8_t>();
Expand Down
Loading